From 31e35cd9aef71b656522e3ffe94c5d3d8b0fb3e6 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 3 Feb 2026 23:07:27 -0800 Subject: [PATCH 01/40] Store worker_instance_key in ActivityInfo --- api/persistence/v1/executions.pb.go | 18 +++++++++++---- go.mod | 1 + go.sum | 8 ++----- .../api/persistence/v1/executions.proto | 3 +++ .../api/recordactivitytaskstarted/api.go | 1 + .../api/respondactivitytaskcompleted/api.go | 23 ++++++++++--------- .../workflow_task_completed_handler.go | 1 + service/history/history_engine_test.go | 1 + service/history/interfaces/mutable_state.go | 1 + .../history/workflow/mutable_state_impl.go | 4 ++++ ...utable_state_impl_restart_activity_test.go | 1 + .../workflow/mutable_state_impl_test.go | 2 ++ 12 files changed, 43 insertions(+), 21 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index 2000a2538d1..da2dbced4e6 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -2521,8 +2521,10 @@ type ActivityInfo struct { // set to true if reset heartbeat flag was set with an activity reset ResetHeartbeats bool `protobuf:"varint,48,opt,name=reset_heartbeats,json=resetHeartbeats,proto3" json:"reset_heartbeats,omitempty"` StartVersion int64 `protobuf:"varint,50,opt,name=start_version,json=startVersion,proto3" json:"start_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Worker instance key of the worker executing this activity. + WorkerInstanceKey string `protobuf:"bytes,51,opt,name=worker_instance_key,json=workerInstanceKey,proto3" json:"worker_instance_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ActivityInfo) Reset() { @@ -2895,6 +2897,13 @@ func (x *ActivityInfo) GetStartVersion() int64 { return 0 } +func (x *ActivityInfo) GetWorkerInstanceKey() string { + if x != nil { + return x.WorkerInstanceKey + } + return "" +} + type isActivityInfo_BuildIdInfo interface { isActivityInfo_BuildIdInfo() } @@ -4840,7 +4849,7 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x17NexusInvocationTaskInfo\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"4\n" + "\x18NexusCancelationTaskInfo\x12\x18\n" + - "\aattempt\x18\x01 \x01(\x05R\aattempt\"\x9e\x1b\n" + + "\aattempt\x18\x01 \x01(\x05R\aattempt\"\xce\x1b\n" + "\fActivityInfo\x12\x18\n" + "\aversion\x18\x01 \x01(\x03R\aversion\x127\n" + "\x18scheduled_event_batch_id\x18\x02 \x01(\x03R\x15scheduledEventBatchId\x12A\n" + @@ -4893,7 +4902,8 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "pause_info\x18. \x01(\v2:.temporal.server.api.persistence.v1.ActivityInfo.PauseInfoR\tpauseInfo\x12%\n" + "\x0eactivity_reset\x18/ \x01(\bR\ractivityReset\x12)\n" + "\x10reset_heartbeats\x180 \x01(\bR\x0fresetHeartbeats\x12#\n" + - "\rstart_version\x182 \x01(\x03R\fstartVersion\x1ay\n" + + "\rstart_version\x182 \x01(\x03R\fstartVersion\x12.\n" + + "\x13worker_instance_key\x183 \x01(\tR\x11workerInstanceKey\x1ay\n" + "\x16UseWorkflowBuildIdInfo\x12+\n" + "\x12last_used_build_id\x18\x01 \x01(\tR\x0flastUsedBuildId\x122\n" + "\x15last_redirect_counter\x18\x02 \x01(\x03R\x13lastRedirectCounter\x1a\x89\x02\n" + diff --git a/go.mod b/go.mod index 60ad1aa45ad..d9bc2d26b66 100644 --- a/go.mod +++ b/go.mod @@ -173,3 +173,4 @@ require ( modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) + diff --git a/go.sum b/go.sum index 2cd1258aafd..a4c698ef6a3 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nexus-rpc/sdk-go v0.5.2-0.20260211051645-26b0b4c584e5 h1:Van9KGGs8lcDgxzSNFbDhEMNeJ80TbBxwZ45f9iBk9U= -github.com/nexus-rpc/sdk-go v0.5.2-0.20260211051645-26b0b4c584e5/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= +github.com/nexus-rpc/sdk-go v0.5.1 h1:UFYYfoHlQc+Pn9gQpmn9QE7xluewAn2AO1OSkAh7YFU= +github.com/nexus-rpc/sdk-go v0.5.1/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= @@ -317,8 +317,6 @@ github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143 github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b/go.mod h1:c+V9Z/ZgkzAdyGvHrvC5AsXgN+M9Qwey04cBdKYzV7U= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 h1:sEJGhmDo+0FaPWM6f0v8Tjia0H5pR6/Baj6+kS78B+M= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938/go.mod h1:ezRQRwu9KQXy8Wuuv1aaFFxoCNz5CeNbVOOkh3xctbY= -github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= -github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-common/bark v1.0.0/go.mod h1:g0ZuPcD7XiExKHynr93Q742G/sbrdVQkghrqLGOoFuY= @@ -375,8 +373,6 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.62.2 h1:jFhIzlqNyJsJZTiCRQmTIMv6OTQ5BZ57z8gbgLGMaoo= -go.temporal.io/api v1.62.2/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.38.0 h1:4Bok5LEdED7YKpsSjIa3dDqram5VOq+ydBf4pyx0Wo4= go.temporal.io/sdk v1.38.0/go.mod h1:a+R2Ej28ObvHoILbHaxMyind7M6D+W0L7edt5UJF4SE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 0a025649ae0..bbf99aa9189 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -631,6 +631,9 @@ message ActivityInfo { bool reset_heartbeats = 48; int64 start_version = 50; + + // Worker instance key of the worker executing this activity. + string worker_instance_key = 51; } // timer_map column diff --git a/service/history/api/recordactivitytaskstarted/api.go b/service/history/api/recordactivitytaskstarted/api.go index 05c65076b79..381cbf94b50 100644 --- a/service/history/api/recordactivitytaskstarted/api.go +++ b/service/history/api/recordactivitytaskstarted/api.go @@ -242,6 +242,7 @@ func recordActivityTaskStarted( if _, err := mutableState.AddActivityTaskStartedEvent( ai, scheduledEventID, requestID, request.PollRequest.GetIdentity(), versioningStamp, pollerDeployment, request.GetBuildIdRedirectInfo(), + request.PollRequest.GetWorkerInstanceKey(), ); err != nil { return nil, rejectCodeUndefined, err } diff --git a/service/history/api/respondactivitytaskcompleted/api.go b/service/history/api/respondactivitytaskcompleted/api.go index 69764f9fd31..2186d0c2ba6 100644 --- a/service/history/api/respondactivitytaskcompleted/api.go +++ b/service/history/api/respondactivitytaskcompleted/api.go @@ -88,17 +88,18 @@ func Invoke( // we need to force complete an activity fabricateStartedEvent = ai.StartedEventId == common.EmptyEventID if fabricateStartedEvent { - _, err := mutableState.AddActivityTaskStartedEvent( - ai, - scheduledEventID, - "", - req.GetCompleteRequest().GetIdentity(), - nil, - nil, - // TODO (shahab): do we need to do anything with wf redirect in this case or any - // other case where an activity starts? - nil, - ) + _, err := mutableState.AddActivityTaskStartedEvent( + ai, + scheduledEventID, + "", + req.GetCompleteRequest().GetIdentity(), + nil, + nil, + // TODO (shahab): do we need to do anything with wf redirect in this case or any + // other case where an activity starts? + nil, + "", // workerInstanceKey not available for force complete + ) if err != nil { return nil, err } diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 0b526b4c7fe..615f0ee092e 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -559,6 +559,7 @@ func (handler *workflowTaskCompletedHandler) handlePostCommandEagerExecuteActivi stamp, nil, nil, + "", // workerInstanceKey not available for eager dispatch ); err != nil { return nil, err } diff --git a/service/history/history_engine_test.go b/service/history/history_engine_test.go index 8b888ca4e1c..165782b3ef7 100644 --- a/service/history/history_engine_test.go +++ b/service/history/history_engine_test.go @@ -6679,6 +6679,7 @@ func addActivityTaskStartedEvent(ms historyi.MutableState, scheduledEventID int6 nil, nil, nil, + "", ) return event } diff --git a/service/history/interfaces/mutable_state.go b/service/history/interfaces/mutable_state.go index 68aedfd2ac9..ae6c59d2769 100644 --- a/service/history/interfaces/mutable_state.go +++ b/service/history/interfaces/mutable_state.go @@ -57,6 +57,7 @@ type ( *commonpb.WorkerVersionStamp, *deploymentpb.Deployment, *taskqueuespb.BuildIdRedirectInfo, + string, // workerInstanceKey ) (*historypb.HistoryEvent, error) AddActivityTaskTimedOutEvent(int64, int64, *failurepb.Failure, enumspb.RetryState) (*historypb.HistoryEvent, error) AddChildWorkflowExecutionCanceledEvent(int64, *commonpb.WorkflowExecution, *historypb.WorkflowExecutionCanceledEventAttributes) (*historypb.HistoryEvent, error) diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index e0b21c83426..6d49ad34b6e 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -4057,6 +4057,7 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( versioningStamp *commonpb.WorkerVersionStamp, deployment *deploymentpb.Deployment, redirectInfo *taskqueuespb.BuildIdRedirectInfo, + workerInstanceKey string, ) (*historypb.HistoryEvent, error) { opTag := tag.WorkflowActionActivityTaskStarted err := ms.checkMutability(opTag) @@ -4085,6 +4086,8 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( ai.LastDeploymentVersion = worker_versioning.ExternalWorkerDeploymentVersionFromDeployment(deployment) } + ai.WorkerInstanceKey = workerInstanceKey + if !ai.HasRetryPolicy { event := ms.hBuilder.AddActivityTaskStartedEvent( scheduledEventID, @@ -4116,6 +4119,7 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( activityInfo.RequestId = requestID activityInfo.StartedTime = timestamppb.New(ms.timeSource.Now()) activityInfo.StartedIdentity = identity + activityInfo.WorkerInstanceKey = workerInstanceKey return nil }); err != nil { return nil, err diff --git a/service/history/workflow/mutable_state_impl_restart_activity_test.go b/service/history/workflow/mutable_state_impl_restart_activity_test.go index 9337a98fff9..3cb91b7fbd3 100644 --- a/service/history/workflow/mutable_state_impl_restart_activity_test.go +++ b/service/history/workflow/mutable_state_impl_restart_activity_test.go @@ -419,6 +419,7 @@ func (s *retryActivitySuite) makeActivityAndPutIntoFailingState() *persistencesp nil, nil, nil, + "", ) s.NoError(err) diff --git a/service/history/workflow/mutable_state_impl_test.go b/service/history/workflow/mutable_state_impl_test.go index 2b302fd7daa..f5a205e7156 100644 --- a/service/history/workflow/mutable_state_impl_test.go +++ b/service/history/workflow/mutable_state_impl_test.go @@ -2878,6 +2878,7 @@ func (s *mutableStateSuite) TestRetryActivity_TruncateRetryableFailure() { nil, nil, nil, + "", ) s.NoError(err) @@ -2943,6 +2944,7 @@ func (s *mutableStateSuite) TestRetryActivity_PausedIncrementsStamp() { nil, nil, nil, + "", ) s.NoError(err) From 2874f4e17e45069745fb490cf9b7d873e9d15f70 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Thu, 5 Feb 2026 09:53:22 -0800 Subject: [PATCH 02/40] Add unit test --- api/persistence/v1/executions.pb.go | 2 +- go.mod | 1 - go.sum | 2 + .../api/persistence/v1/executions.proto | 2 +- .../history/interfaces/mutable_state_mock.go | 8 +- .../workflow/mutable_state_impl_test.go | 73 +++++++++++++++++++ 6 files changed, 81 insertions(+), 7 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index da2dbced4e6..2866fc4f98a 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -2521,7 +2521,7 @@ type ActivityInfo struct { // set to true if reset heartbeat flag was set with an activity reset ResetHeartbeats bool `protobuf:"varint,48,opt,name=reset_heartbeats,json=resetHeartbeats,proto3" json:"reset_heartbeats,omitempty"` StartVersion int64 `protobuf:"varint,50,opt,name=start_version,json=startVersion,proto3" json:"start_version,omitempty"` - // Worker instance key of the worker executing this activity. + // Unique identifier of the worker that is this activity. WorkerInstanceKey string `protobuf:"bytes,51,opt,name=worker_instance_key,json=workerInstanceKey,proto3" json:"worker_instance_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/go.mod b/go.mod index d9bc2d26b66..60ad1aa45ad 100644 --- a/go.mod +++ b/go.mod @@ -173,4 +173,3 @@ require ( modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) - diff --git a/go.sum b/go.sum index a4c698ef6a3..e8cf371965d 100644 --- a/go.sum +++ b/go.sum @@ -373,6 +373,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.temporal.io/api v1.61.1-0.20260128230845-c246540cf2ed h1:g3CgsK5BXL2rQy0ZIJVRpNUDdtPM1y4bGv5ZoKsqR74= +go.temporal.io/api v1.61.1-0.20260128230845-c246540cf2ed/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.38.0 h1:4Bok5LEdED7YKpsSjIa3dDqram5VOq+ydBf4pyx0Wo4= go.temporal.io/sdk v1.38.0/go.mod h1:a+R2Ej28ObvHoILbHaxMyind7M6D+W0L7edt5UJF4SE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index bbf99aa9189..7912db4a1a9 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -632,7 +632,7 @@ message ActivityInfo { int64 start_version = 50; - // Worker instance key of the worker executing this activity. + // Unique identifier of the worker that is this activity. string worker_instance_key = 51; } diff --git a/service/history/interfaces/mutable_state_mock.go b/service/history/interfaces/mutable_state_mock.go index 103a53840e6..9b89e383594 100644 --- a/service/history/interfaces/mutable_state_mock.go +++ b/service/history/interfaces/mutable_state_mock.go @@ -148,18 +148,18 @@ func (mr *MockMutableStateMockRecorder) AddActivityTaskScheduledEvent(arg0, arg1 } // AddActivityTaskStartedEvent mocks base method. -func (m *MockMutableState) AddActivityTaskStartedEvent(arg0 *persistence.ActivityInfo, arg1 int64, arg2, arg3 string, arg4 *common.WorkerVersionStamp, arg5 *deployment.Deployment, arg6 *taskqueue0.BuildIdRedirectInfo) (*history.HistoryEvent, error) { +func (m *MockMutableState) AddActivityTaskStartedEvent(arg0 *persistence.ActivityInfo, arg1 int64, arg2, arg3 string, arg4 *common.WorkerVersionStamp, arg5 *deployment.Deployment, arg6 *taskqueue0.BuildIdRedirectInfo, arg7 string) (*history.HistoryEvent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddActivityTaskStartedEvent", arg0, arg1, arg2, arg3, arg4, arg5, arg6) + ret := m.ctrl.Call(m, "AddActivityTaskStartedEvent", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) ret0, _ := ret[0].(*history.HistoryEvent) ret1, _ := ret[1].(error) return ret0, ret1 } // AddActivityTaskStartedEvent indicates an expected call of AddActivityTaskStartedEvent. -func (mr *MockMutableStateMockRecorder) AddActivityTaskStartedEvent(arg0, arg1, arg2, arg3, arg4, arg5, arg6 any) *gomock.Call { +func (mr *MockMutableStateMockRecorder) AddActivityTaskStartedEvent(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityTaskStartedEvent", reflect.TypeOf((*MockMutableState)(nil).AddActivityTaskStartedEvent), arg0, arg1, arg2, arg3, arg4, arg5, arg6) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityTaskStartedEvent", reflect.TypeOf((*MockMutableState)(nil).AddActivityTaskStartedEvent), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) } // AddActivityTaskTimedOutEvent mocks base method. diff --git a/service/history/workflow/mutable_state_impl_test.go b/service/history/workflow/mutable_state_impl_test.go index f5a205e7156..18d77f0a418 100644 --- a/service/history/workflow/mutable_state_impl_test.go +++ b/service/history/workflow/mutable_state_impl_test.go @@ -6152,3 +6152,76 @@ func (s *mutableStateSuite) TestSetContextMetadata() { s.True(ok) s.Equal(taskQueue, tq) } + +func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerInstanceKey() { + s.mockEventsCache.EXPECT().PutEvent(gomock.Any(), gomock.Any()).AnyTimes() + + // Setup workflow execution + _, err := s.mutableState.AddWorkflowExecutionStartedEvent( + &commonpb.WorkflowExecution{WorkflowId: tests.WorkflowID, RunId: tests.RunID}, + &historyservice.StartWorkflowExecutionRequest{ + NamespaceId: tests.NamespaceID.String(), + StartRequest: &workflowservice.StartWorkflowExecutionRequest{ + WorkflowType: &commonpb.WorkflowType{Name: "workflow-type"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: "task-queue"}, + WorkflowRunTimeout: durationpb.New(200 * time.Second), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), + }, + }, + ) + s.NoError(err) + + di, err := s.mutableState.AddWorkflowTaskScheduledEvent(false, enumsspb.WORKFLOW_TASK_TYPE_NORMAL) + s.NoError(err) + _, _, err = s.mutableState.AddWorkflowTaskStartedEvent( + di.ScheduledEventID, + di.RequestID, + di.TaskQueue, + "identity", + nil, + nil, + nil, + false, + nil, + ) + s.NoError(err) + _, err = s.mutableState.AddWorkflowTaskCompletedEvent( + di, + &workflowservice.RespondWorkflowTaskCompletedRequest{Identity: "identity"}, + workflowTaskCompletionLimits, + ) + s.NoError(err) + + // Schedule activity + workflowTaskCompletedEventID := int64(4) + _, activityInfo, err := s.mutableState.AddActivityTaskScheduledEvent( + workflowTaskCompletedEventID, + &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: "test-activity-1", + ActivityType: &commonpb.ActivityType{Name: "test-activity-type"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: "test-task-queue"}, + }, + false, + ) + s.NoError(err) + s.Empty(activityInfo.WorkerInstanceKey, "WorkerInstanceKey should be empty before activity starts") + + // Start activity with workerInstanceKey + expectedWorkerInstanceKey := "test-worker-instance-key-12345" + _, err = s.mutableState.AddActivityTaskStartedEvent( + activityInfo, + activityInfo.ScheduledEventId, + uuid.NewString(), + "worker-identity", + nil, + nil, + nil, + expectedWorkerInstanceKey, + ) + s.NoError(err) + + // Verify workerInstanceKey is stored + updatedActivityInfo, ok := s.mutableState.GetActivityInfo(activityInfo.ScheduledEventId) + s.True(ok) + s.Equal(expectedWorkerInstanceKey, updatedActivityInfo.WorkerInstanceKey) +} From ad8c4809150c73fe46d3026e35b01b1ce580b8e3 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Thu, 5 Feb 2026 13:16:54 -0800 Subject: [PATCH 03/40] Fix lint --- .../api/respondactivitytaskcompleted/api.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/service/history/api/respondactivitytaskcompleted/api.go b/service/history/api/respondactivitytaskcompleted/api.go index 2186d0c2ba6..d9c5a8d6657 100644 --- a/service/history/api/respondactivitytaskcompleted/api.go +++ b/service/history/api/respondactivitytaskcompleted/api.go @@ -88,18 +88,18 @@ func Invoke( // we need to force complete an activity fabricateStartedEvent = ai.StartedEventId == common.EmptyEventID if fabricateStartedEvent { - _, err := mutableState.AddActivityTaskStartedEvent( - ai, - scheduledEventID, - "", - req.GetCompleteRequest().GetIdentity(), - nil, - nil, - // TODO (shahab): do we need to do anything with wf redirect in this case or any - // other case where an activity starts? - nil, - "", // workerInstanceKey not available for force complete - ) + _, err := mutableState.AddActivityTaskStartedEvent( + ai, + scheduledEventID, + "", + req.GetCompleteRequest().GetIdentity(), + nil, + nil, + // TODO (shahab): do we need to do anything with wf redirect in this case or any + // other case where an activity starts? + nil, + "", // workerInstanceKey not available for force complete + ) if err != nil { return nil, err } From 986e9a140d607ae770d8066a478589a8bed2e145 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Mon, 9 Feb 2026 11:46:16 -0800 Subject: [PATCH 04/40] Remove redundant WorkerInstanceKey assignment in UpdateActivity callback --- service/history/workflow/mutable_state_impl.go | 1 - 1 file changed, 1 deletion(-) diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index 6d49ad34b6e..7e2fb318ff7 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -4119,7 +4119,6 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( activityInfo.RequestId = requestID activityInfo.StartedTime = timestamppb.New(ms.timeSource.Now()) activityInfo.StartedIdentity = identity - activityInfo.WorkerInstanceKey = workerInstanceKey return nil }); err != nil { return nil, err From fb3066c0c968967d47b79bee9827039262ba9208 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 11:37:02 -0800 Subject: [PATCH 05/40] Store worker_control_task_queue in ActivityInfo Add workerControlTaskQueue parameter to AddActivityTaskStartedEvent and persist it in ActivityInfo when an activity starts. This enables routing activity cancellation requests to the correct worker's control queue via Nexus. Changes: - Add worker_control_task_queue field to ActivityInfo proto - Update MutableState interface and implementation - Pass workerControlTaskQueue from poll request for regular activities - Pass from RespondWorkflowTaskCompleted request for eager activities - Update all test call sites --- api/persistence/v1/executions.pb.go | 18 ++++++++++++++---- .../server/api/persistence/v1/executions.proto | 3 +++ .../api/recordactivitytaskstarted/api.go | 1 + .../api/respondactivitytaskcompleted/api.go | 1 + .../api/respondworkflowtaskcompleted/api.go | 2 ++ .../workflow_task_completed_handler.go | 11 +++++++++-- .../workflow_task_completed_handler_test.go | 2 ++ service/history/history_engine_test.go | 1 + service/history/interfaces/mutable_state.go | 1 + .../history/interfaces/mutable_state_mock.go | 8 ++++---- service/history/workflow/mutable_state_impl.go | 2 ++ ...mutable_state_impl_restart_activity_test.go | 1 + .../workflow/mutable_state_impl_test.go | 9 +++++++-- 13 files changed, 48 insertions(+), 12 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index 2866fc4f98a..1fc0ca3b796 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -2523,8 +2523,10 @@ type ActivityInfo struct { StartVersion int64 `protobuf:"varint,50,opt,name=start_version,json=startVersion,proto3" json:"start_version,omitempty"` // Unique identifier of the worker that is this activity. WorkerInstanceKey string `protobuf:"bytes,51,opt,name=worker_instance_key,json=workerInstanceKey,proto3" json:"worker_instance_key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The task queue on which the server will send control tasks to the worker running this activity. + WorkerControlTaskQueue string `protobuf:"bytes,52,opt,name=worker_control_task_queue,json=workerControlTaskQueue,proto3" json:"worker_control_task_queue,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ActivityInfo) Reset() { @@ -2904,6 +2906,13 @@ func (x *ActivityInfo) GetWorkerInstanceKey() string { return "" } +func (x *ActivityInfo) GetWorkerControlTaskQueue() string { + if x != nil { + return x.WorkerControlTaskQueue + } + return "" +} + type isActivityInfo_BuildIdInfo interface { isActivityInfo_BuildIdInfo() } @@ -4849,7 +4858,7 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x17NexusInvocationTaskInfo\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"4\n" + "\x18NexusCancelationTaskInfo\x12\x18\n" + - "\aattempt\x18\x01 \x01(\x05R\aattempt\"\xce\x1b\n" + + "\aattempt\x18\x01 \x01(\x05R\aattempt\"\x89\x1c\n" + "\fActivityInfo\x12\x18\n" + "\aversion\x18\x01 \x01(\x03R\aversion\x127\n" + "\x18scheduled_event_batch_id\x18\x02 \x01(\x03R\x15scheduledEventBatchId\x12A\n" + @@ -4903,7 +4912,8 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x0eactivity_reset\x18/ \x01(\bR\ractivityReset\x12)\n" + "\x10reset_heartbeats\x180 \x01(\bR\x0fresetHeartbeats\x12#\n" + "\rstart_version\x182 \x01(\x03R\fstartVersion\x12.\n" + - "\x13worker_instance_key\x183 \x01(\tR\x11workerInstanceKey\x1ay\n" + + "\x13worker_instance_key\x183 \x01(\tR\x11workerInstanceKey\x129\n" + + "\x19worker_control_task_queue\x184 \x01(\tR\x16workerControlTaskQueue\x1ay\n" + "\x16UseWorkflowBuildIdInfo\x12+\n" + "\x12last_used_build_id\x18\x01 \x01(\tR\x0flastUsedBuildId\x122\n" + "\x15last_redirect_counter\x18\x02 \x01(\x03R\x13lastRedirectCounter\x1a\x89\x02\n" + diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 7912db4a1a9..8401ceb8582 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -634,6 +634,9 @@ message ActivityInfo { // Unique identifier of the worker that is this activity. string worker_instance_key = 51; + + // The task queue on which the server will send control tasks to the worker running this activity. + string worker_control_task_queue = 52; } // timer_map column diff --git a/service/history/api/recordactivitytaskstarted/api.go b/service/history/api/recordactivitytaskstarted/api.go index 381cbf94b50..976c5e11a09 100644 --- a/service/history/api/recordactivitytaskstarted/api.go +++ b/service/history/api/recordactivitytaskstarted/api.go @@ -243,6 +243,7 @@ func recordActivityTaskStarted( ai, scheduledEventID, requestID, request.PollRequest.GetIdentity(), versioningStamp, pollerDeployment, request.GetBuildIdRedirectInfo(), request.PollRequest.GetWorkerInstanceKey(), + request.PollRequest.GetWorkerControlTaskQueue(), ); err != nil { return nil, rejectCodeUndefined, err } diff --git a/service/history/api/respondactivitytaskcompleted/api.go b/service/history/api/respondactivitytaskcompleted/api.go index d9c5a8d6657..a641f09edc0 100644 --- a/service/history/api/respondactivitytaskcompleted/api.go +++ b/service/history/api/respondactivitytaskcompleted/api.go @@ -99,6 +99,7 @@ func Invoke( // other case where an activity starts? nil, "", // workerInstanceKey not available for force complete + "", // workerControlTaskQueue not available for force complete ) if err != nil { return nil, err diff --git a/service/history/api/respondworkflowtaskcompleted/api.go b/service/history/api/respondworkflowtaskcompleted/api.go index 310dc4c1ae6..0fd22aa980b 100644 --- a/service/history/api/respondworkflowtaskcompleted/api.go +++ b/service/history/api/respondworkflowtaskcompleted/api.go @@ -389,6 +389,8 @@ func (handler *WorkflowTaskCompletedHandler) Invoke( workflowTaskHandler := newWorkflowTaskCompletedHandler( request.GetIdentity(), + request.GetWorkerInstanceKey(), + request.GetWorkerControlTaskQueue(), completedEvent.GetEventId(), // If completedEvent is nil, then GetEventId() returns 0 and this value shouldn't be used in workflowTaskHandler. ms, updateRegistry, diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 615f0ee092e..8685aae95d8 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -53,6 +53,8 @@ type ( workflowTaskCompletedHandler struct { identity string + workerInstanceKey string + workerControlTaskQueue string workflowTaskCompletedID int64 // internal state @@ -104,6 +106,8 @@ type ( func newWorkflowTaskCompletedHandler( identity string, + workerInstanceKey string, + workerControlTaskQueue string, workflowTaskCompletedID int64, mutableState historyi.MutableState, updateRegistry update.Registry, @@ -122,7 +126,9 @@ func newWorkflowTaskCompletedHandler( versionMembershipCache worker_versioning.VersionMembershipCache, ) *workflowTaskCompletedHandler { return &workflowTaskCompletedHandler{ - identity: identity, + identity: identity, + workerInstanceKey: workerInstanceKey, + workerControlTaskQueue: workerControlTaskQueue, workflowTaskCompletedID: workflowTaskCompletedID, // internal state @@ -559,7 +565,8 @@ func (handler *workflowTaskCompletedHandler) handlePostCommandEagerExecuteActivi stamp, nil, nil, - "", // workerInstanceKey not available for eager dispatch + handler.workerInstanceKey, + handler.workerControlTaskQueue, ); err != nil { return nil, err } diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go index 0018795df3e..4cdaf1f7080 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go @@ -84,6 +84,8 @@ func TestCommandProtocolMessage(t *testing.T) { ) out.handler = newWorkflowTaskCompletedHandler( // 😲 t.Name(), // identity + "", // workerInstanceKey + "", // workerControlTaskQueue 123, // workflowTaskCompletedID out.ms, out.updates, diff --git a/service/history/history_engine_test.go b/service/history/history_engine_test.go index 165782b3ef7..76a9821f5d9 100644 --- a/service/history/history_engine_test.go +++ b/service/history/history_engine_test.go @@ -6680,6 +6680,7 @@ func addActivityTaskStartedEvent(ms historyi.MutableState, scheduledEventID int6 nil, nil, "", + "", ) return event } diff --git a/service/history/interfaces/mutable_state.go b/service/history/interfaces/mutable_state.go index ae6c59d2769..e1f7b48cd27 100644 --- a/service/history/interfaces/mutable_state.go +++ b/service/history/interfaces/mutable_state.go @@ -58,6 +58,7 @@ type ( *deploymentpb.Deployment, *taskqueuespb.BuildIdRedirectInfo, string, // workerInstanceKey + string, // workerControlTaskQueue ) (*historypb.HistoryEvent, error) AddActivityTaskTimedOutEvent(int64, int64, *failurepb.Failure, enumspb.RetryState) (*historypb.HistoryEvent, error) AddChildWorkflowExecutionCanceledEvent(int64, *commonpb.WorkflowExecution, *historypb.WorkflowExecutionCanceledEventAttributes) (*historypb.HistoryEvent, error) diff --git a/service/history/interfaces/mutable_state_mock.go b/service/history/interfaces/mutable_state_mock.go index 9b89e383594..9c249569a37 100644 --- a/service/history/interfaces/mutable_state_mock.go +++ b/service/history/interfaces/mutable_state_mock.go @@ -148,18 +148,18 @@ func (mr *MockMutableStateMockRecorder) AddActivityTaskScheduledEvent(arg0, arg1 } // AddActivityTaskStartedEvent mocks base method. -func (m *MockMutableState) AddActivityTaskStartedEvent(arg0 *persistence.ActivityInfo, arg1 int64, arg2, arg3 string, arg4 *common.WorkerVersionStamp, arg5 *deployment.Deployment, arg6 *taskqueue0.BuildIdRedirectInfo, arg7 string) (*history.HistoryEvent, error) { +func (m *MockMutableState) AddActivityTaskStartedEvent(arg0 *persistence.ActivityInfo, arg1 int64, arg2, arg3 string, arg4 *common.WorkerVersionStamp, arg5 *deployment.Deployment, arg6 *taskqueue0.BuildIdRedirectInfo, arg7, arg8 string) (*history.HistoryEvent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddActivityTaskStartedEvent", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) + ret := m.ctrl.Call(m, "AddActivityTaskStartedEvent", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) ret0, _ := ret[0].(*history.HistoryEvent) ret1, _ := ret[1].(error) return ret0, ret1 } // AddActivityTaskStartedEvent indicates an expected call of AddActivityTaskStartedEvent. -func (mr *MockMutableStateMockRecorder) AddActivityTaskStartedEvent(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7 any) *gomock.Call { +func (mr *MockMutableStateMockRecorder) AddActivityTaskStartedEvent(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityTaskStartedEvent", reflect.TypeOf((*MockMutableState)(nil).AddActivityTaskStartedEvent), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityTaskStartedEvent", reflect.TypeOf((*MockMutableState)(nil).AddActivityTaskStartedEvent), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) } // AddActivityTaskTimedOutEvent mocks base method. diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index 7e2fb318ff7..a944703e8c3 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -4058,6 +4058,7 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( deployment *deploymentpb.Deployment, redirectInfo *taskqueuespb.BuildIdRedirectInfo, workerInstanceKey string, + workerControlTaskQueue string, ) (*historypb.HistoryEvent, error) { opTag := tag.WorkflowActionActivityTaskStarted err := ms.checkMutability(opTag) @@ -4087,6 +4088,7 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( } ai.WorkerInstanceKey = workerInstanceKey + ai.WorkerControlTaskQueue = workerControlTaskQueue if !ai.HasRetryPolicy { event := ms.hBuilder.AddActivityTaskStartedEvent( diff --git a/service/history/workflow/mutable_state_impl_restart_activity_test.go b/service/history/workflow/mutable_state_impl_restart_activity_test.go index 3cb91b7fbd3..a47174e731a 100644 --- a/service/history/workflow/mutable_state_impl_restart_activity_test.go +++ b/service/history/workflow/mutable_state_impl_restart_activity_test.go @@ -420,6 +420,7 @@ func (s *retryActivitySuite) makeActivityAndPutIntoFailingState() *persistencesp nil, nil, "", + "", ) s.NoError(err) diff --git a/service/history/workflow/mutable_state_impl_test.go b/service/history/workflow/mutable_state_impl_test.go index 18d77f0a418..1392f8ac5ed 100644 --- a/service/history/workflow/mutable_state_impl_test.go +++ b/service/history/workflow/mutable_state_impl_test.go @@ -2879,6 +2879,7 @@ func (s *mutableStateSuite) TestRetryActivity_TruncateRetryableFailure() { nil, nil, "", + "", ) s.NoError(err) @@ -2945,6 +2946,7 @@ func (s *mutableStateSuite) TestRetryActivity_PausedIncrementsStamp() { nil, nil, "", + "", ) s.NoError(err) @@ -6206,8 +6208,9 @@ func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerInstanceK s.NoError(err) s.Empty(activityInfo.WorkerInstanceKey, "WorkerInstanceKey should be empty before activity starts") - // Start activity with workerInstanceKey + // Start activity with workerInstanceKey and workerControlTaskQueue expectedWorkerInstanceKey := "test-worker-instance-key-12345" + expectedWorkerControlTaskQueue := "test-control-queue" _, err = s.mutableState.AddActivityTaskStartedEvent( activityInfo, activityInfo.ScheduledEventId, @@ -6217,11 +6220,13 @@ func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerInstanceK nil, nil, expectedWorkerInstanceKey, + expectedWorkerControlTaskQueue, ) s.NoError(err) - // Verify workerInstanceKey is stored + // Verify workerInstanceKey and workerControlTaskQueue are stored updatedActivityInfo, ok := s.mutableState.GetActivityInfo(activityInfo.ScheduledEventId) s.True(ok) s.Equal(expectedWorkerInstanceKey, updatedActivityInfo.WorkerInstanceKey) + s.Equal(expectedWorkerControlTaskQueue, updatedActivityInfo.WorkerControlTaskQueue) } From 6ef264022a060ac4e43c01e232689b8f90e45a0b Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 13:04:06 -0800 Subject: [PATCH 06/40] Update go.temporal.io/api to include worker_instance_key and worker_control_task_queue fields --- go.mod | 2 ++ go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 60ad1aa45ad..72ac5d39f8b 100644 --- a/go.mod +++ b/go.mod @@ -173,3 +173,5 @@ require ( modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) + +replace go.temporal.io/api => /Users/krajah/Code/api-go diff --git a/go.sum b/go.sum index e8cf371965d..2c188359f12 100644 --- a/go.sum +++ b/go.sum @@ -317,6 +317,8 @@ github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143 github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b/go.mod h1:c+V9Z/ZgkzAdyGvHrvC5AsXgN+M9Qwey04cBdKYzV7U= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 h1:sEJGhmDo+0FaPWM6f0v8Tjia0H5pR6/Baj6+kS78B+M= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938/go.mod h1:ezRQRwu9KQXy8Wuuv1aaFFxoCNz5CeNbVOOkh3xctbY= +github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= +github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-common/bark v1.0.0/go.mod h1:g0ZuPcD7XiExKHynr93Q742G/sbrdVQkghrqLGOoFuY= @@ -373,8 +375,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.61.1-0.20260128230845-c246540cf2ed h1:g3CgsK5BXL2rQy0ZIJVRpNUDdtPM1y4bGv5ZoKsqR74= -go.temporal.io/api v1.61.1-0.20260128230845-c246540cf2ed/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.2-0.20260211210638-ce51fa5547d4 h1:4IFoNlk70wmZ7TtOxNOW+n8YTxrsp3UaCnxKnP+AqmU= +go.temporal.io/api v1.62.2-0.20260211210638-ce51fa5547d4/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.38.0 h1:4Bok5LEdED7YKpsSjIa3dDqram5VOq+ydBf4pyx0Wo4= go.temporal.io/sdk v1.38.0/go.mod h1:a+R2Ej28ObvHoILbHaxMyind7M6D+W0L7edt5UJF4SE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= From eab94b026fb04c067ba203be10b576048e42363d Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 13:14:21 -0800 Subject: [PATCH 07/40] Fix lint --- go.sum | 4 ++-- .../workflow_task_completed_handler.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.sum b/go.sum index 2c188359f12..8c5dd481b9e 100644 --- a/go.sum +++ b/go.sum @@ -375,8 +375,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.62.2-0.20260211210638-ce51fa5547d4 h1:4IFoNlk70wmZ7TtOxNOW+n8YTxrsp3UaCnxKnP+AqmU= -go.temporal.io/api v1.62.2-0.20260211210638-ce51fa5547d4/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.2-0.20260211212044-12e31472fd35 h1:5Ig1D6s0oQVnj5blVPwoMqG/jvVaHcWWHXO9Y4B9n/M= +go.temporal.io/api v1.62.2-0.20260211212044-12e31472fd35/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.38.0 h1:4Bok5LEdED7YKpsSjIa3dDqram5VOq+ydBf4pyx0Wo4= go.temporal.io/sdk v1.38.0/go.mod h1:a+R2Ej28ObvHoILbHaxMyind7M6D+W0L7edt5UJF4SE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 8685aae95d8..945a4522755 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -126,9 +126,9 @@ func newWorkflowTaskCompletedHandler( versionMembershipCache worker_versioning.VersionMembershipCache, ) *workflowTaskCompletedHandler { return &workflowTaskCompletedHandler{ - identity: identity, - workerInstanceKey: workerInstanceKey, - workerControlTaskQueue: workerControlTaskQueue, + identity: identity, + workerInstanceKey: workerInstanceKey, + workerControlTaskQueue: workerControlTaskQueue, workflowTaskCompletedID: workflowTaskCompletedID, // internal state From 0f8e48e73f6cb2a93bbb38470ac368c656ee6eea Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 16:40:46 -0800 Subject: [PATCH 08/40] Remove worker_instance_key as it is not needed --- api/persistence/v1/executions.pb.go | 18 ++++-------------- .../server/api/persistence/v1/executions.proto | 5 +---- .../api/recordactivitytaskstarted/api.go | 1 - .../api/respondactivitytaskcompleted/api.go | 1 - .../api/respondworkflowtaskcompleted/api.go | 1 - .../workflow_task_completed_handler.go | 4 ---- .../workflow_task_completed_handler_test.go | 1 - service/history/history_engine_test.go | 1 - service/history/interfaces/mutable_state.go | 1 - .../history/interfaces/mutable_state_mock.go | 8 ++++---- service/history/workflow/mutable_state_impl.go | 2 -- ...mutable_state_impl_restart_activity_test.go | 1 - .../workflow/mutable_state_impl_test.go | 13 ++++--------- 13 files changed, 13 insertions(+), 44 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index 1fc0ca3b796..d14e49bf16c 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -2521,10 +2521,8 @@ type ActivityInfo struct { // set to true if reset heartbeat flag was set with an activity reset ResetHeartbeats bool `protobuf:"varint,48,opt,name=reset_heartbeats,json=resetHeartbeats,proto3" json:"reset_heartbeats,omitempty"` StartVersion int64 `protobuf:"varint,50,opt,name=start_version,json=startVersion,proto3" json:"start_version,omitempty"` - // Unique identifier of the worker that is this activity. - WorkerInstanceKey string `protobuf:"bytes,51,opt,name=worker_instance_key,json=workerInstanceKey,proto3" json:"worker_instance_key,omitempty"` // The task queue on which the server will send control tasks to the worker running this activity. - WorkerControlTaskQueue string `protobuf:"bytes,52,opt,name=worker_control_task_queue,json=workerControlTaskQueue,proto3" json:"worker_control_task_queue,omitempty"` + WorkerControlTaskQueue string `protobuf:"bytes,51,opt,name=worker_control_task_queue,json=workerControlTaskQueue,proto3" json:"worker_control_task_queue,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2899,13 +2897,6 @@ func (x *ActivityInfo) GetStartVersion() int64 { return 0 } -func (x *ActivityInfo) GetWorkerInstanceKey() string { - if x != nil { - return x.WorkerInstanceKey - } - return "" -} - func (x *ActivityInfo) GetWorkerControlTaskQueue() string { if x != nil { return x.WorkerControlTaskQueue @@ -4858,7 +4849,7 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x17NexusInvocationTaskInfo\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"4\n" + "\x18NexusCancelationTaskInfo\x12\x18\n" + - "\aattempt\x18\x01 \x01(\x05R\aattempt\"\x89\x1c\n" + + "\aattempt\x18\x01 \x01(\x05R\aattempt\"\xd9\x1b\n" + "\fActivityInfo\x12\x18\n" + "\aversion\x18\x01 \x01(\x03R\aversion\x127\n" + "\x18scheduled_event_batch_id\x18\x02 \x01(\x03R\x15scheduledEventBatchId\x12A\n" + @@ -4911,9 +4902,8 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "pause_info\x18. \x01(\v2:.temporal.server.api.persistence.v1.ActivityInfo.PauseInfoR\tpauseInfo\x12%\n" + "\x0eactivity_reset\x18/ \x01(\bR\ractivityReset\x12)\n" + "\x10reset_heartbeats\x180 \x01(\bR\x0fresetHeartbeats\x12#\n" + - "\rstart_version\x182 \x01(\x03R\fstartVersion\x12.\n" + - "\x13worker_instance_key\x183 \x01(\tR\x11workerInstanceKey\x129\n" + - "\x19worker_control_task_queue\x184 \x01(\tR\x16workerControlTaskQueue\x1ay\n" + + "\rstart_version\x182 \x01(\x03R\fstartVersion\x129\n" + + "\x19worker_control_task_queue\x183 \x01(\tR\x16workerControlTaskQueue\x1ay\n" + "\x16UseWorkflowBuildIdInfo\x12+\n" + "\x12last_used_build_id\x18\x01 \x01(\tR\x0flastUsedBuildId\x122\n" + "\x15last_redirect_counter\x18\x02 \x01(\x03R\x13lastRedirectCounter\x1a\x89\x02\n" + diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 8401ceb8582..fc3cf5b2204 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -632,11 +632,8 @@ message ActivityInfo { int64 start_version = 50; - // Unique identifier of the worker that is this activity. - string worker_instance_key = 51; - // The task queue on which the server will send control tasks to the worker running this activity. - string worker_control_task_queue = 52; + string worker_control_task_queue = 51; } // timer_map column diff --git a/service/history/api/recordactivitytaskstarted/api.go b/service/history/api/recordactivitytaskstarted/api.go index 976c5e11a09..bb8fd4c56c1 100644 --- a/service/history/api/recordactivitytaskstarted/api.go +++ b/service/history/api/recordactivitytaskstarted/api.go @@ -242,7 +242,6 @@ func recordActivityTaskStarted( if _, err := mutableState.AddActivityTaskStartedEvent( ai, scheduledEventID, requestID, request.PollRequest.GetIdentity(), versioningStamp, pollerDeployment, request.GetBuildIdRedirectInfo(), - request.PollRequest.GetWorkerInstanceKey(), request.PollRequest.GetWorkerControlTaskQueue(), ); err != nil { return nil, rejectCodeUndefined, err diff --git a/service/history/api/respondactivitytaskcompleted/api.go b/service/history/api/respondactivitytaskcompleted/api.go index a641f09edc0..99675b340a2 100644 --- a/service/history/api/respondactivitytaskcompleted/api.go +++ b/service/history/api/respondactivitytaskcompleted/api.go @@ -98,7 +98,6 @@ func Invoke( // TODO (shahab): do we need to do anything with wf redirect in this case or any // other case where an activity starts? nil, - "", // workerInstanceKey not available for force complete "", // workerControlTaskQueue not available for force complete ) if err != nil { diff --git a/service/history/api/respondworkflowtaskcompleted/api.go b/service/history/api/respondworkflowtaskcompleted/api.go index 0fd22aa980b..39e79877459 100644 --- a/service/history/api/respondworkflowtaskcompleted/api.go +++ b/service/history/api/respondworkflowtaskcompleted/api.go @@ -389,7 +389,6 @@ func (handler *WorkflowTaskCompletedHandler) Invoke( workflowTaskHandler := newWorkflowTaskCompletedHandler( request.GetIdentity(), - request.GetWorkerInstanceKey(), request.GetWorkerControlTaskQueue(), completedEvent.GetEventId(), // If completedEvent is nil, then GetEventId() returns 0 and this value shouldn't be used in workflowTaskHandler. ms, diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 945a4522755..ee23a768939 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -53,7 +53,6 @@ type ( workflowTaskCompletedHandler struct { identity string - workerInstanceKey string workerControlTaskQueue string workflowTaskCompletedID int64 @@ -106,7 +105,6 @@ type ( func newWorkflowTaskCompletedHandler( identity string, - workerInstanceKey string, workerControlTaskQueue string, workflowTaskCompletedID int64, mutableState historyi.MutableState, @@ -127,7 +125,6 @@ func newWorkflowTaskCompletedHandler( ) *workflowTaskCompletedHandler { return &workflowTaskCompletedHandler{ identity: identity, - workerInstanceKey: workerInstanceKey, workerControlTaskQueue: workerControlTaskQueue, workflowTaskCompletedID: workflowTaskCompletedID, @@ -565,7 +562,6 @@ func (handler *workflowTaskCompletedHandler) handlePostCommandEagerExecuteActivi stamp, nil, nil, - handler.workerInstanceKey, handler.workerControlTaskQueue, ); err != nil { return nil, err diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go index 4cdaf1f7080..c68b8d1478f 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go @@ -84,7 +84,6 @@ func TestCommandProtocolMessage(t *testing.T) { ) out.handler = newWorkflowTaskCompletedHandler( // 😲 t.Name(), // identity - "", // workerInstanceKey "", // workerControlTaskQueue 123, // workflowTaskCompletedID out.ms, diff --git a/service/history/history_engine_test.go b/service/history/history_engine_test.go index 76a9821f5d9..165782b3ef7 100644 --- a/service/history/history_engine_test.go +++ b/service/history/history_engine_test.go @@ -6680,7 +6680,6 @@ func addActivityTaskStartedEvent(ms historyi.MutableState, scheduledEventID int6 nil, nil, "", - "", ) return event } diff --git a/service/history/interfaces/mutable_state.go b/service/history/interfaces/mutable_state.go index e1f7b48cd27..cd07f9ac698 100644 --- a/service/history/interfaces/mutable_state.go +++ b/service/history/interfaces/mutable_state.go @@ -57,7 +57,6 @@ type ( *commonpb.WorkerVersionStamp, *deploymentpb.Deployment, *taskqueuespb.BuildIdRedirectInfo, - string, // workerInstanceKey string, // workerControlTaskQueue ) (*historypb.HistoryEvent, error) AddActivityTaskTimedOutEvent(int64, int64, *failurepb.Failure, enumspb.RetryState) (*historypb.HistoryEvent, error) diff --git a/service/history/interfaces/mutable_state_mock.go b/service/history/interfaces/mutable_state_mock.go index 9c249569a37..9b89e383594 100644 --- a/service/history/interfaces/mutable_state_mock.go +++ b/service/history/interfaces/mutable_state_mock.go @@ -148,18 +148,18 @@ func (mr *MockMutableStateMockRecorder) AddActivityTaskScheduledEvent(arg0, arg1 } // AddActivityTaskStartedEvent mocks base method. -func (m *MockMutableState) AddActivityTaskStartedEvent(arg0 *persistence.ActivityInfo, arg1 int64, arg2, arg3 string, arg4 *common.WorkerVersionStamp, arg5 *deployment.Deployment, arg6 *taskqueue0.BuildIdRedirectInfo, arg7, arg8 string) (*history.HistoryEvent, error) { +func (m *MockMutableState) AddActivityTaskStartedEvent(arg0 *persistence.ActivityInfo, arg1 int64, arg2, arg3 string, arg4 *common.WorkerVersionStamp, arg5 *deployment.Deployment, arg6 *taskqueue0.BuildIdRedirectInfo, arg7 string) (*history.HistoryEvent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddActivityTaskStartedEvent", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) + ret := m.ctrl.Call(m, "AddActivityTaskStartedEvent", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) ret0, _ := ret[0].(*history.HistoryEvent) ret1, _ := ret[1].(error) return ret0, ret1 } // AddActivityTaskStartedEvent indicates an expected call of AddActivityTaskStartedEvent. -func (mr *MockMutableStateMockRecorder) AddActivityTaskStartedEvent(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 any) *gomock.Call { +func (mr *MockMutableStateMockRecorder) AddActivityTaskStartedEvent(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityTaskStartedEvent", reflect.TypeOf((*MockMutableState)(nil).AddActivityTaskStartedEvent), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityTaskStartedEvent", reflect.TypeOf((*MockMutableState)(nil).AddActivityTaskStartedEvent), arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) } // AddActivityTaskTimedOutEvent mocks base method. diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index a944703e8c3..0b92eb7ddc2 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -4057,7 +4057,6 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( versioningStamp *commonpb.WorkerVersionStamp, deployment *deploymentpb.Deployment, redirectInfo *taskqueuespb.BuildIdRedirectInfo, - workerInstanceKey string, workerControlTaskQueue string, ) (*historypb.HistoryEvent, error) { opTag := tag.WorkflowActionActivityTaskStarted @@ -4087,7 +4086,6 @@ func (ms *MutableStateImpl) AddActivityTaskStartedEvent( ai.LastDeploymentVersion = worker_versioning.ExternalWorkerDeploymentVersionFromDeployment(deployment) } - ai.WorkerInstanceKey = workerInstanceKey ai.WorkerControlTaskQueue = workerControlTaskQueue if !ai.HasRetryPolicy { diff --git a/service/history/workflow/mutable_state_impl_restart_activity_test.go b/service/history/workflow/mutable_state_impl_restart_activity_test.go index a47174e731a..3cb91b7fbd3 100644 --- a/service/history/workflow/mutable_state_impl_restart_activity_test.go +++ b/service/history/workflow/mutable_state_impl_restart_activity_test.go @@ -420,7 +420,6 @@ func (s *retryActivitySuite) makeActivityAndPutIntoFailingState() *persistencesp nil, nil, "", - "", ) s.NoError(err) diff --git a/service/history/workflow/mutable_state_impl_test.go b/service/history/workflow/mutable_state_impl_test.go index 1392f8ac5ed..62e48a43ed3 100644 --- a/service/history/workflow/mutable_state_impl_test.go +++ b/service/history/workflow/mutable_state_impl_test.go @@ -2879,7 +2879,6 @@ func (s *mutableStateSuite) TestRetryActivity_TruncateRetryableFailure() { nil, nil, "", - "", ) s.NoError(err) @@ -2946,7 +2945,6 @@ func (s *mutableStateSuite) TestRetryActivity_PausedIncrementsStamp() { nil, nil, "", - "", ) s.NoError(err) @@ -6155,7 +6153,7 @@ func (s *mutableStateSuite) TestSetContextMetadata() { s.Equal(taskQueue, tq) } -func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerInstanceKey() { +func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerControlTaskQueue() { s.mockEventsCache.EXPECT().PutEvent(gomock.Any(), gomock.Any()).AnyTimes() // Setup workflow execution @@ -6206,10 +6204,9 @@ func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerInstanceK false, ) s.NoError(err) - s.Empty(activityInfo.WorkerInstanceKey, "WorkerInstanceKey should be empty before activity starts") + s.Empty(activityInfo.WorkerControlTaskQueue, "WorkerControlTaskQueue should be empty before activity starts") - // Start activity with workerInstanceKey and workerControlTaskQueue - expectedWorkerInstanceKey := "test-worker-instance-key-12345" + // Start activity with workerControlTaskQueue expectedWorkerControlTaskQueue := "test-control-queue" _, err = s.mutableState.AddActivityTaskStartedEvent( activityInfo, @@ -6219,14 +6216,12 @@ func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerInstanceK nil, nil, nil, - expectedWorkerInstanceKey, expectedWorkerControlTaskQueue, ) s.NoError(err) - // Verify workerInstanceKey and workerControlTaskQueue are stored + // Verify workerControlTaskQueue is stored updatedActivityInfo, ok := s.mutableState.GetActivityInfo(activityInfo.ScheduledEventId) s.True(ok) - s.Equal(expectedWorkerInstanceKey, updatedActivityInfo.WorkerInstanceKey) s.Equal(expectedWorkerControlTaskQueue, updatedActivityInfo.WorkerControlTaskQueue) } From 539075959119c94776004ca3ae3e09c9d5902af9 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 17 Feb 2026 18:47:51 -0800 Subject: [PATCH 09/40] Forward WorkerControlTaskQueue through matching service partitions --- go.mod | 2 +- go.sum | 8 ++++---- service/matching/forwarder.go | 2 ++ service/matching/matching_engine.go | 3 +++ service/matching/pri_forwarder.go | 2 ++ 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 72ac5d39f8b..92b96de1dd7 100644 --- a/go.mod +++ b/go.mod @@ -174,4 +174,4 @@ require ( modernc.org/memory v1.11.0 // indirect ) -replace go.temporal.io/api => /Users/krajah/Code/api-go +replace go.temporal.io/api => github.com/temporalio/api-go v1.62.2-0.20260217225453-e6a9241288b9 diff --git a/go.sum b/go.sum index 8c5dd481b9e..204f76e6e49 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nexus-rpc/sdk-go v0.5.1 h1:UFYYfoHlQc+Pn9gQpmn9QE7xluewAn2AO1OSkAh7YFU= -github.com/nexus-rpc/sdk-go v0.5.1/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= +github.com/nexus-rpc/sdk-go v0.5.2-0.20260211051645-26b0b4c584e5 h1:Van9KGGs8lcDgxzSNFbDhEMNeJ80TbBxwZ45f9iBk9U= +github.com/nexus-rpc/sdk-go v0.5.2-0.20260211051645-26b0b4c584e5/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= @@ -310,6 +310,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/temporalio/api-go v1.62.2-0.20260217225453-e6a9241288b9 h1:nv1DjGfsfM/ITt5ehoHodVzCtTxv+mM+sMqM9Zgx0Tc= +github.com/temporalio/api-go v1.62.2-0.20260217225453-e6a9241288b9/go.mod h1:oewVgOWEx67DlpbXkEJl5PlcpDPXjR8h9+raDfl0fpo= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4= github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04= @@ -375,8 +377,6 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.62.2-0.20260211212044-12e31472fd35 h1:5Ig1D6s0oQVnj5blVPwoMqG/jvVaHcWWHXO9Y4B9n/M= -go.temporal.io/api v1.62.2-0.20260211212044-12e31472fd35/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.38.0 h1:4Bok5LEdED7YKpsSjIa3dDqram5VOq+ydBf4pyx0Wo4= go.temporal.io/sdk v1.38.0/go.mod h1:a+R2Ej28ObvHoILbHaxMyind7M6D+W0L7edt5UJF4SE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/service/matching/forwarder.go b/service/matching/forwarder.go index 018aeffca1d..6b893db2aa0 100644 --- a/service/matching/forwarder.go +++ b/service/matching/forwarder.go @@ -249,6 +249,7 @@ func (fwdr *Forwarder) ForwardPoll(ctx context.Context, pollMetadata *pollMetada WorkerVersionCapabilities: pollMetadata.workerVersionCapabilities, DeploymentOptions: pollMetadata.deploymentOptions, WorkerInstanceKey: pollMetadata.workerInstanceKey, + WorkerControlTaskQueue: pollMetadata.workerControlTaskQueue, }, ForwardedSource: fwdr.partition.RpcName(), Conditions: pollMetadata.conditions, @@ -273,6 +274,7 @@ func (fwdr *Forwarder) ForwardPoll(ctx context.Context, pollMetadata *pollMetada WorkerVersionCapabilities: pollMetadata.workerVersionCapabilities, DeploymentOptions: pollMetadata.deploymentOptions, WorkerInstanceKey: pollMetadata.workerInstanceKey, + WorkerControlTaskQueue: pollMetadata.workerControlTaskQueue, }, ForwardedSource: fwdr.partition.RpcName(), Conditions: pollMetadata.conditions, diff --git a/service/matching/matching_engine.go b/service/matching/matching_engine.go index b09c10338c0..0195923910d 100644 --- a/service/matching/matching_engine.go +++ b/service/matching/matching_engine.go @@ -95,6 +95,7 @@ type ( forwardedFrom string localPollStartTime time.Time workerInstanceKey string + workerControlTaskQueue string } userDataUpdate struct { @@ -679,6 +680,7 @@ pollLoop: forwardedFrom: req.ForwardedSource, conditions: req.Conditions, workerInstanceKey: request.WorkerInstanceKey, + workerControlTaskQueue: request.WorkerControlTaskQueue, } task, versionSetUsed, err := e.pollTask(pollerCtx, partition, pollMetadata) if err != nil { @@ -984,6 +986,7 @@ pollLoop: forwardedFrom: req.ForwardedSource, conditions: req.Conditions, workerInstanceKey: request.WorkerInstanceKey, + workerControlTaskQueue: request.WorkerControlTaskQueue, } task, versionSetUsed, err := e.pollTask(pollerCtx, partition, pollMetadata) if err != nil { diff --git a/service/matching/pri_forwarder.go b/service/matching/pri_forwarder.go index ca1e1f6f5c0..dd2ab8c8c38 100644 --- a/service/matching/pri_forwarder.go +++ b/service/matching/pri_forwarder.go @@ -219,6 +219,7 @@ func ForwardPollWithTarget( WorkerVersionCapabilities: pollMetadata.workerVersionCapabilities, DeploymentOptions: pollMetadata.deploymentOptions, WorkerInstanceKey: pollMetadata.workerInstanceKey, + WorkerControlTaskQueue: pollMetadata.workerControlTaskQueue, }, ForwardedSource: source.RpcName(), Conditions: pollMetadata.conditions, @@ -243,6 +244,7 @@ func ForwardPollWithTarget( WorkerVersionCapabilities: pollMetadata.workerVersionCapabilities, DeploymentOptions: pollMetadata.deploymentOptions, WorkerInstanceKey: pollMetadata.workerInstanceKey, + WorkerControlTaskQueue: pollMetadata.workerControlTaskQueue, }, ForwardedSource: source.RpcName(), Conditions: pollMetadata.conditions, From ac7be081bc31d7a7bcb255ac87f114e5c6340089 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 3 Feb 2026 12:06:21 -0800 Subject: [PATCH 10/40] Define CancelActivityNexusTask transfer task type --- api/enums/v1/task.go-helpers.pb.go | 1 + api/enums/v1/task.pb.go | 11 +- api/persistence/v1/executions.pb.go | 499 ++++++++++-------- common/dynamicconfig/constants.go | 5 + .../serialization/task_serializers.go | 38 ++ .../serialization/task_serializers_test.go | 13 + .../temporal/server/api/enums/v1/task.proto | 3 + .../api/persistence/v1/executions.proto | 10 + .../workflow_task_completed_handler.go | 8 + service/history/configs/config.go | 22 +- .../tasks/cancel_activity_nexus_task.go | 71 +++ service/history/tasks/utils.go | 6 + .../transfer_queue_active_task_executor.go | 3 + .../transfer_queue_standby_task_executor.go | 3 + 14 files changed, 470 insertions(+), 223 deletions(-) create mode 100644 service/history/tasks/cancel_activity_nexus_task.go diff --git a/api/enums/v1/task.go-helpers.pb.go b/api/enums/v1/task.go-helpers.pb.go index fdf9ecdb978..4a171504f00 100644 --- a/api/enums/v1/task.go-helpers.pb.go +++ b/api/enums/v1/task.go-helpers.pb.go @@ -57,6 +57,7 @@ var ( "ReplicationSyncVersionedTransition": 31, "ChasmPure": 32, "Chasm": 33, + "TransferCancelActivityNexus": 34, } ) diff --git a/api/enums/v1/task.pb.go b/api/enums/v1/task.pb.go index 2b902a95b2f..0d382d7c910 100644 --- a/api/enums/v1/task.pb.go +++ b/api/enums/v1/task.pb.go @@ -126,6 +126,8 @@ const ( TASK_TYPE_CHASM_PURE TaskType = 32 // A task with side effects generated by a CHASM component. TASK_TYPE_CHASM TaskType = 33 + // A task to cancel a running activity via Nexus control queue. + TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS TaskType = 34 ) // Enum value maps for TaskType. @@ -162,6 +164,7 @@ var ( 31: "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION", 32: "TASK_TYPE_CHASM_PURE", 33: "TASK_TYPE_CHASM", + 34: "TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS", } TaskType_value = map[string]int32{ "TASK_TYPE_UNSPECIFIED": 0, @@ -195,6 +198,7 @@ var ( "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION": 31, "TASK_TYPE_CHASM_PURE": 32, "TASK_TYPE_CHASM": 33, + "TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS": 34, } ) @@ -278,6 +282,8 @@ func (x TaskType) String() string { return "ChasmPure" case TASK_TYPE_CHASM: return "Chasm" + case TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS: + return "TransferCancelActivityNexus" default: return strconv.Itoa(int(x)) } @@ -367,7 +373,7 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "TaskSource\x12\x1b\n" + "\x17TASK_SOURCE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13TASK_SOURCE_HISTORY\x10\x01\x12\x1a\n" + - "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\xb6\t\n" + + "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\xe4\t\n" + "\bTaskType\x12\x19\n" + "\x15TASK_TYPE_UNSPECIFIED\x10\x00\x12!\n" + "\x1dTASK_TYPE_REPLICATION_HISTORY\x10\x01\x12'\n" + @@ -400,7 +406,8 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "\x1eTASK_TYPE_REPLICATION_SYNC_HSM\x10\x1e\x123\n" + "/TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION\x10\x1f\x12\x18\n" + "\x14TASK_TYPE_CHASM_PURE\x10 \x12\x13\n" + - "\x0fTASK_TYPE_CHASM\x10!\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*\\\n" + + "\x0fTASK_TYPE_CHASM\x10!\x12,\n" + + "(TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS\x10\"\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*\\\n" + "\fTaskPriority\x12\x1d\n" + "\x19TASK_PRIORITY_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12TASK_PRIORITY_HIGH\x10\x01\x12\x15\n" + diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index d14e49bf16c..346fe898fa4 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -1366,6 +1366,7 @@ type TransferTaskInfo struct { // // *TransferTaskInfo_CloseExecutionTaskDetails_ // *TransferTaskInfo_ChasmTaskInfo + // *TransferTaskInfo_CancelActivityNexusTaskDetails_ TaskDetails isTransferTaskInfo_TaskDetails `protobuf_oneof:"task_details"` // Stamp represents the "version" of the entity's internal state for which the transfer task was created. // It increases monotonically when the entity's options are modified. @@ -1528,6 +1529,15 @@ func (x *TransferTaskInfo) GetChasmTaskInfo() *ChasmTaskInfo { return nil } +func (x *TransferTaskInfo) GetCancelActivityNexusTaskDetails() *TransferTaskInfo_CancelActivityNexusTaskDetails { + if x != nil { + if x, ok := x.TaskDetails.(*TransferTaskInfo_CancelActivityNexusTaskDetails_); ok { + return x.CancelActivityNexusTaskDetails + } + } + return nil +} + func (x *TransferTaskInfo) GetStamp() int32 { if x != nil { return x.Stamp @@ -1548,10 +1558,16 @@ type TransferTaskInfo_ChasmTaskInfo struct { ChasmTaskInfo *ChasmTaskInfo `protobuf:"bytes,18,opt,name=chasm_task_info,json=chasmTaskInfo,proto3,oneof"` } +type TransferTaskInfo_CancelActivityNexusTaskDetails_ struct { + CancelActivityNexusTaskDetails *TransferTaskInfo_CancelActivityNexusTaskDetails `protobuf:"bytes,19,opt,name=cancel_activity_nexus_task_details,json=cancelActivityNexusTaskDetails,proto3,oneof"` +} + func (*TransferTaskInfo_CloseExecutionTaskDetails_) isTransferTaskInfo_TaskDetails() {} func (*TransferTaskInfo_ChasmTaskInfo) isTransferTaskInfo_TaskDetails() {} +func (*TransferTaskInfo_CancelActivityNexusTaskDetails_) isTransferTaskInfo_TaskDetails() {} + // replication column type ReplicationTaskInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4122,6 +4138,59 @@ func (x *TransferTaskInfo_CloseExecutionTaskDetails) GetCanSkipVisibilityArchiva return false } +type TransferTaskInfo_CancelActivityNexusTaskDetails struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Scheduled event IDs of activities to cancel (batched by worker). + ScheduledEventIds []int64 `protobuf:"varint,1,rep,packed,name=scheduled_event_ids,json=scheduledEventIds,proto3" json:"scheduled_event_ids,omitempty"` + WorkerInstanceKey string `protobuf:"bytes,2,opt,name=worker_instance_key,json=workerInstanceKey,proto3" json:"worker_instance_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) Reset() { + *x = TransferTaskInfo_CancelActivityNexusTaskDetails{} + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransferTaskInfo_CancelActivityNexusTaskDetails) ProtoMessage() {} + +func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransferTaskInfo_CancelActivityNexusTaskDetails.ProtoReflect.Descriptor instead. +func (*TransferTaskInfo_CancelActivityNexusTaskDetails) Descriptor() ([]byte, []int) { + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{5, 1} +} + +func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetScheduledEventIds() []int64 { + if x != nil { + return x.ScheduledEventIds + } + return nil +} + +func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetWorkerInstanceKey() string { + if x != nil { + return x.WorkerInstanceKey + } + return "" +} + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] type ActivityInfo_UseWorkflowBuildIdInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4136,7 +4205,7 @@ type ActivityInfo_UseWorkflowBuildIdInfo struct { func (x *ActivityInfo_UseWorkflowBuildIdInfo) Reset() { *x = ActivityInfo_UseWorkflowBuildIdInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4148,7 +4217,7 @@ func (x *ActivityInfo_UseWorkflowBuildIdInfo) String() string { func (*ActivityInfo_UseWorkflowBuildIdInfo) ProtoMessage() {} func (x *ActivityInfo_UseWorkflowBuildIdInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4193,7 +4262,7 @@ type ActivityInfo_PauseInfo struct { func (x *ActivityInfo_PauseInfo) Reset() { *x = ActivityInfo_PauseInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4205,7 +4274,7 @@ func (x *ActivityInfo_PauseInfo) String() string { func (*ActivityInfo_PauseInfo) ProtoMessage() {} func (x *ActivityInfo_PauseInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4283,7 +4352,7 @@ type ActivityInfo_PauseInfo_Manual struct { func (x *ActivityInfo_PauseInfo_Manual) Reset() { *x = ActivityInfo_PauseInfo_Manual{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4295,7 +4364,7 @@ func (x *ActivityInfo_PauseInfo_Manual) String() string { func (*ActivityInfo_PauseInfo_Manual) ProtoMessage() {} func (x *ActivityInfo_PauseInfo_Manual) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4340,7 +4409,7 @@ type Callback_Nexus struct { func (x *Callback_Nexus) Reset() { *x = Callback_Nexus{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4352,7 +4421,7 @@ func (x *Callback_Nexus) String() string { func (*Callback_Nexus) ProtoMessage() {} func (x *Callback_Nexus) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4402,7 +4471,7 @@ type Callback_HSM struct { func (x *Callback_HSM) Reset() { *x = Callback_HSM{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4414,7 +4483,7 @@ func (x *Callback_HSM) String() string { func (*Callback_HSM) ProtoMessage() {} func (x *Callback_HSM) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4474,7 +4543,7 @@ type CallbackInfo_WorkflowClosed struct { func (x *CallbackInfo_WorkflowClosed) Reset() { *x = CallbackInfo_WorkflowClosed{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[41] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4486,7 +4555,7 @@ func (x *CallbackInfo_WorkflowClosed) String() string { func (*CallbackInfo_WorkflowClosed) ProtoMessage() {} func (x *CallbackInfo_WorkflowClosed) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[41] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4514,7 +4583,7 @@ type CallbackInfo_Trigger struct { func (x *CallbackInfo_Trigger) Reset() { *x = CallbackInfo_Trigger{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4526,7 +4595,7 @@ func (x *CallbackInfo_Trigger) String() string { func (*CallbackInfo_Trigger) ProtoMessage() {} func (x *CallbackInfo_Trigger) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4736,7 +4805,8 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\rRequestIDInfo\x12?\n" + "\n" + "event_type\x18\x01 \x01(\x0e2 .temporal.api.enums.v1.EventTypeR\teventType\x12\x19\n" + - "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\xdf\a\n" + + "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\x86\n" + + "\n" + "\x10TransferTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + @@ -4756,10 +4826,14 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x0fvisibility_time\x18\r \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\x12,\n" + "\x12delete_after_close\x18\x0f \x01(\bR\x10deleteAfterClose\x12\x91\x01\n" + "\x1cclose_execution_task_details\x18\x10 \x01(\v2N.temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetailsH\x00R\x19closeExecutionTaskDetails\x12[\n" + - "\x0fchasm_task_info\x18\x12 \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12\x14\n" + + "\x0fchasm_task_info\x18\x12 \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12\xa1\x01\n" + + "\"cancel_activity_nexus_task_details\x18\x13 \x01(\v2S.temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetailsH\x00R\x1ecancelActivityNexusTaskDetails\x12\x14\n" + "\x05stamp\x18\x11 \x01(\x05R\x05stamp\x1a\\\n" + "\x19CloseExecutionTaskDetails\x12?\n" + - "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchivalB\x0e\n" + + "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchival\x1a\x80\x01\n" + + "\x1eCancelActivityNexusTaskDetails\x12.\n" + + "\x13scheduled_event_ids\x18\x01 \x03(\x03R\x11scheduledEventIds\x12.\n" + + "\x13worker_instance_key\x18\x02 \x01(\tR\x11workerInstanceKeyB\x0e\n" + "\ftask_detailsJ\x04\b\x0e\x10\x0f\"\xd8\b\n" + "\x13ReplicationTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + @@ -5051,7 +5125,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP() []by return file_temporal_server_api_persistence_v1_executions_proto_rawDescData } -var file_temporal_server_api_persistence_v1_executions_proto_msgTypes = make([]protoimpl.MessageInfo, 43) +var file_temporal_server_api_persistence_v1_executions_proto_msgTypes = make([]protoimpl.MessageInfo, 44) var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ (*ShardInfo)(nil), // 0: temporal.server.api.persistence.v1.ShardInfo (*WorkflowExecutionInfo)(nil), // 1: temporal.server.api.persistence.v1.WorkflowExecutionInfo @@ -5087,209 +5161,211 @@ var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ nil, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry nil, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry nil, // 33: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry - (*TransferTaskInfo_CloseExecutionTaskDetails)(nil), // 34: temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 35: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - (*ActivityInfo_PauseInfo)(nil), // 36: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - (*ActivityInfo_PauseInfo_Manual)(nil), // 37: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - (*Callback_Nexus)(nil), // 38: temporal.server.api.persistence.v1.Callback.Nexus - (*Callback_HSM)(nil), // 39: temporal.server.api.persistence.v1.Callback.HSM - nil, // 40: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - (*CallbackInfo_WorkflowClosed)(nil), // 41: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - (*CallbackInfo_Trigger)(nil), // 42: temporal.server.api.persistence.v1.CallbackInfo.Trigger - (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 44: google.protobuf.Duration - (v1.WorkflowTaskType)(0), // 45: temporal.server.api.enums.v1.WorkflowTaskType - (v11.SuggestContinueAsNewReason)(0), // 46: temporal.api.enums.v1.SuggestContinueAsNewReason - (*v12.ResetPoints)(nil), // 47: temporal.api.workflow.v1.ResetPoints - (*v14.VersionHistories)(nil), // 48: temporal.server.api.history.v1.VersionHistories - (*v15.VectorClock)(nil), // 49: temporal.server.api.clock.v1.VectorClock - (*v16.BaseExecutionInfo)(nil), // 50: temporal.server.api.workflow.v1.BaseExecutionInfo - (*v13.WorkerVersionStamp)(nil), // 51: temporal.api.common.v1.WorkerVersionStamp - (*VersionedTransition)(nil), // 52: temporal.server.api.persistence.v1.VersionedTransition - (*StateMachineTimerGroup)(nil), // 53: temporal.server.api.persistence.v1.StateMachineTimerGroup - (*StateMachineTombstoneBatch)(nil), // 54: temporal.server.api.persistence.v1.StateMachineTombstoneBatch - (*v12.WorkflowExecutionVersioningInfo)(nil), // 55: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - (*v13.Priority)(nil), // 56: temporal.api.common.v1.Priority - (v11.WorkflowTaskFailedCause)(0), // 57: temporal.api.enums.v1.WorkflowTaskFailedCause - (v11.TimeoutType)(0), // 58: temporal.api.enums.v1.TimeoutType - (v1.WorkflowExecutionState)(0), // 59: temporal.server.api.enums.v1.WorkflowExecutionState - (v11.WorkflowExecutionStatus)(0), // 60: temporal.api.enums.v1.WorkflowExecutionStatus - (v11.EventType)(0), // 61: temporal.api.enums.v1.EventType - (v1.TaskType)(0), // 62: temporal.server.api.enums.v1.TaskType - (*ChasmTaskInfo)(nil), // 63: temporal.server.api.persistence.v1.ChasmTaskInfo - (v1.TaskPriority)(0), // 64: temporal.server.api.enums.v1.TaskPriority - (*v14.VersionHistoryItem)(nil), // 65: temporal.server.api.history.v1.VersionHistoryItem - (v1.WorkflowBackoffType)(0), // 66: temporal.server.api.enums.v1.WorkflowBackoffType - (*StateMachineTaskInfo)(nil), // 67: temporal.server.api.persistence.v1.StateMachineTaskInfo - (*v17.Failure)(nil), // 68: temporal.api.failure.v1.Failure - (*v13.Payloads)(nil), // 69: temporal.api.common.v1.Payloads - (*v13.ActivityType)(nil), // 70: temporal.api.common.v1.ActivityType - (*v18.Deployment)(nil), // 71: temporal.api.deployment.v1.Deployment - (*v18.WorkerDeploymentVersion)(nil), // 72: temporal.api.deployment.v1.WorkerDeploymentVersion - (v11.ParentClosePolicy)(0), // 73: temporal.api.enums.v1.ParentClosePolicy - (v1.ChecksumFlavor)(0), // 74: temporal.server.api.enums.v1.ChecksumFlavor - (*v13.Link)(nil), // 75: temporal.api.common.v1.Link - (*v19.HistoryEvent)(nil), // 76: temporal.api.history.v1.HistoryEvent - (v1.CallbackState)(0), // 77: temporal.server.api.enums.v1.CallbackState - (v1.NexusOperationState)(0), // 78: temporal.server.api.enums.v1.NexusOperationState - (v11.NexusOperationCancellationState)(0), // 79: temporal.api.enums.v1.NexusOperationCancellationState - (*QueueState)(nil), // 80: temporal.server.api.persistence.v1.QueueState - (*v13.Payload)(nil), // 81: temporal.api.common.v1.Payload - (*UpdateInfo)(nil), // 82: temporal.server.api.persistence.v1.UpdateInfo - (*StateMachineMap)(nil), // 83: temporal.server.api.persistence.v1.StateMachineMap - (*StateMachineRef)(nil), // 84: temporal.server.api.persistence.v1.StateMachineRef + (*TransferTaskInfo_CloseExecutionTaskDetails)(nil), // 34: temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails + (*TransferTaskInfo_CancelActivityNexusTaskDetails)(nil), // 35: temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetails + (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 36: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + (*ActivityInfo_PauseInfo)(nil), // 37: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + (*ActivityInfo_PauseInfo_Manual)(nil), // 38: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + (*Callback_Nexus)(nil), // 39: temporal.server.api.persistence.v1.Callback.Nexus + (*Callback_HSM)(nil), // 40: temporal.server.api.persistence.v1.Callback.HSM + nil, // 41: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + (*CallbackInfo_WorkflowClosed)(nil), // 42: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + (*CallbackInfo_Trigger)(nil), // 43: temporal.server.api.persistence.v1.CallbackInfo.Trigger + (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 45: google.protobuf.Duration + (v1.WorkflowTaskType)(0), // 46: temporal.server.api.enums.v1.WorkflowTaskType + (v11.SuggestContinueAsNewReason)(0), // 47: temporal.api.enums.v1.SuggestContinueAsNewReason + (*v12.ResetPoints)(nil), // 48: temporal.api.workflow.v1.ResetPoints + (*v14.VersionHistories)(nil), // 49: temporal.server.api.history.v1.VersionHistories + (*v15.VectorClock)(nil), // 50: temporal.server.api.clock.v1.VectorClock + (*v16.BaseExecutionInfo)(nil), // 51: temporal.server.api.workflow.v1.BaseExecutionInfo + (*v13.WorkerVersionStamp)(nil), // 52: temporal.api.common.v1.WorkerVersionStamp + (*VersionedTransition)(nil), // 53: temporal.server.api.persistence.v1.VersionedTransition + (*StateMachineTimerGroup)(nil), // 54: temporal.server.api.persistence.v1.StateMachineTimerGroup + (*StateMachineTombstoneBatch)(nil), // 55: temporal.server.api.persistence.v1.StateMachineTombstoneBatch + (*v12.WorkflowExecutionVersioningInfo)(nil), // 56: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + (*v13.Priority)(nil), // 57: temporal.api.common.v1.Priority + (v11.WorkflowTaskFailedCause)(0), // 58: temporal.api.enums.v1.WorkflowTaskFailedCause + (v11.TimeoutType)(0), // 59: temporal.api.enums.v1.TimeoutType + (v1.WorkflowExecutionState)(0), // 60: temporal.server.api.enums.v1.WorkflowExecutionState + (v11.WorkflowExecutionStatus)(0), // 61: temporal.api.enums.v1.WorkflowExecutionStatus + (v11.EventType)(0), // 62: temporal.api.enums.v1.EventType + (v1.TaskType)(0), // 63: temporal.server.api.enums.v1.TaskType + (*ChasmTaskInfo)(nil), // 64: temporal.server.api.persistence.v1.ChasmTaskInfo + (v1.TaskPriority)(0), // 65: temporal.server.api.enums.v1.TaskPriority + (*v14.VersionHistoryItem)(nil), // 66: temporal.server.api.history.v1.VersionHistoryItem + (v1.WorkflowBackoffType)(0), // 67: temporal.server.api.enums.v1.WorkflowBackoffType + (*StateMachineTaskInfo)(nil), // 68: temporal.server.api.persistence.v1.StateMachineTaskInfo + (*v17.Failure)(nil), // 69: temporal.api.failure.v1.Failure + (*v13.Payloads)(nil), // 70: temporal.api.common.v1.Payloads + (*v13.ActivityType)(nil), // 71: temporal.api.common.v1.ActivityType + (*v18.Deployment)(nil), // 72: temporal.api.deployment.v1.Deployment + (*v18.WorkerDeploymentVersion)(nil), // 73: temporal.api.deployment.v1.WorkerDeploymentVersion + (v11.ParentClosePolicy)(0), // 74: temporal.api.enums.v1.ParentClosePolicy + (v1.ChecksumFlavor)(0), // 75: temporal.server.api.enums.v1.ChecksumFlavor + (*v13.Link)(nil), // 76: temporal.api.common.v1.Link + (*v19.HistoryEvent)(nil), // 77: temporal.api.history.v1.HistoryEvent + (v1.CallbackState)(0), // 78: temporal.server.api.enums.v1.CallbackState + (v1.NexusOperationState)(0), // 79: temporal.server.api.enums.v1.NexusOperationState + (v11.NexusOperationCancellationState)(0), // 80: temporal.api.enums.v1.NexusOperationCancellationState + (*QueueState)(nil), // 81: temporal.server.api.persistence.v1.QueueState + (*v13.Payload)(nil), // 82: temporal.api.common.v1.Payload + (*UpdateInfo)(nil), // 83: temporal.server.api.persistence.v1.UpdateInfo + (*StateMachineMap)(nil), // 84: temporal.server.api.persistence.v1.StateMachineMap + (*StateMachineRef)(nil), // 85: temporal.server.api.persistence.v1.StateMachineRef } var file_temporal_server_api_persistence_v1_executions_proto_depIdxs = []int32{ - 43, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp + 44, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp 26, // 1: temporal.server.api.persistence.v1.ShardInfo.replication_dlq_ack_level:type_name -> temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry 27, // 2: temporal.server.api.persistence.v1.ShardInfo.queue_states:type_name -> temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry - 44, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration - 44, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration - 44, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration - 43, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp - 43, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp - 44, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration - 43, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp - 43, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp - 43, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp - 45, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType - 46, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason - 44, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration - 44, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 44, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 43, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp - 47, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints + 45, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration + 45, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration + 45, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration + 44, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp + 44, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp + 45, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration + 44, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp + 44, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp + 46, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType + 47, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason + 45, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 45, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 44, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp + 48, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints 28, // 19: temporal.server.api.persistence.v1.WorkflowExecutionInfo.search_attributes:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry 29, // 20: temporal.server.api.persistence.v1.WorkflowExecutionInfo.memo:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry - 48, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 49, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories 2, // 22: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_stats:type_name -> temporal.server.api.persistence.v1.ExecutionStats - 43, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp - 43, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp - 49, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock - 43, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp - 50, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo - 51, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 44, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp + 44, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp + 50, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock + 44, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp + 51, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo + 52, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp 30, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.update_infos:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry - 52, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition 31, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machines_by_type:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry - 53, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup - 52, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 52, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 52, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 54, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch - 55, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - 52, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 52, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 54, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup + 53, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 55, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch + 56, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + 53, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition 32, // 40: temporal.server.api.persistence.v1.WorkflowExecutionInfo.children_initialized_post_reset_point:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry - 56, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 57, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority 25, // 42: temporal.server.api.persistence.v1.WorkflowExecutionInfo.pause_info:type_name -> temporal.server.api.persistence.v1.WorkflowPauseInfo - 57, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause - 58, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType - 59, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState - 60, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus - 52, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 43, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp + 58, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause + 59, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType + 60, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState + 61, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus + 53, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 44, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp 33, // 49: temporal.server.api.persistence.v1.WorkflowExecutionState.request_ids:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry - 61, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType - 62, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 43, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 62, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType + 63, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp 34, // 53: temporal.server.api.persistence.v1.TransferTaskInfo.close_execution_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - 63, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 62, // 55: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 43, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 64, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority - 52, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 6, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo - 65, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 62, // 61: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 43, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 43, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp - 63, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 62, // 65: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 58, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType - 66, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType - 43, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 63, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 62, // 70: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 43, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 62, // 72: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 43, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 67, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo - 63, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 43, // 76: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 43, // 77: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp - 44, // 78: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 44, // 79: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 80: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 81: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration - 44, // 82: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 44, // 83: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 43, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp - 68, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure - 69, // 86: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads - 43, // 87: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp - 70, // 88: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType - 35, // 89: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - 51, // 90: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 52, // 91: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 43, // 92: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp - 43, // 93: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 71, // 94: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment - 72, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion - 56, // 96: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority - 36, // 97: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - 43, // 98: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp - 52, // 99: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 73, // 100: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy - 49, // 101: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 52, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 56, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority - 52, // 104: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 52, // 105: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 74, // 106: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor - 38, // 107: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus - 39, // 108: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM - 75, // 109: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link - 76, // 110: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent - 19, // 111: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback - 42, // 112: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger - 43, // 113: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp - 77, // 114: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState - 43, // 115: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 68, // 116: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 43, // 117: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 44, // 118: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 43, // 119: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 78, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState - 43, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 68, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 43, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 44, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 44, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 43, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp - 43, // 127: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp - 79, // 128: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState - 43, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 68, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 43, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 43, // 132: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 80, // 133: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState - 81, // 134: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload - 81, // 135: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload - 82, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo - 83, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap - 24, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo - 4, // 139: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo - 43, // 140: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 37, // 141: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - 40, // 142: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - 84, // 143: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef - 41, // 144: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - 145, // [145:145] is the sub-list for method output_type - 145, // [145:145] is the sub-list for method input_type - 145, // [145:145] is the sub-list for extension type_name - 145, // [145:145] is the sub-list for extension extendee - 0, // [0:145] is the sub-list for field type_name + 64, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 35, // 55: temporal.server.api.persistence.v1.TransferTaskInfo.cancel_activity_nexus_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetails + 63, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 65, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority + 53, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 6, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo + 66, // 61: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 63, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 44, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp + 64, // 65: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 63, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 59, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType + 67, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType + 44, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 64, // 70: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 63, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 72: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 63, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 68, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo + 64, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 44, // 77: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 78: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp + 45, // 79: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 81: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 82: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration + 45, // 83: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 45, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 44, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp + 69, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure + 70, // 87: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads + 44, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp + 71, // 89: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType + 36, // 90: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + 52, // 91: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 53, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 44, // 93: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 94: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 72, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment + 73, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 57, // 97: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority + 37, // 98: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + 44, // 99: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp + 53, // 100: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 74, // 101: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy + 50, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 53, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 57, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 53, // 105: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 106: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 75, // 107: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor + 39, // 108: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus + 40, // 109: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM + 76, // 110: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link + 77, // 111: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent + 19, // 112: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback + 43, // 113: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger + 44, // 114: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp + 78, // 115: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState + 44, // 116: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 69, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 118: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 45, // 119: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 44, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 79, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState + 44, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 69, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 45, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 44, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp + 44, // 128: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp + 80, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState + 44, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 69, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 44, // 133: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 81, // 134: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState + 82, // 135: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload + 82, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload + 83, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo + 84, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap + 24, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo + 4, // 140: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo + 44, // 141: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 38, // 142: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + 41, // 143: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + 85, // 144: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef + 42, // 145: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + 146, // [146:146] is the sub-list for method output_type + 146, // [146:146] is the sub-list for method input_type + 146, // [146:146] is the sub-list for extension type_name + 146, // [146:146] is the sub-list for extension extendee + 0, // [0:146] is the sub-list for field type_name } func init() { file_temporal_server_api_persistence_v1_executions_proto_init() } @@ -5308,6 +5384,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { file_temporal_server_api_persistence_v1_executions_proto_msgTypes[5].OneofWrappers = []any{ (*TransferTaskInfo_CloseExecutionTaskDetails_)(nil), (*TransferTaskInfo_ChasmTaskInfo)(nil), + (*TransferTaskInfo_CancelActivityNexusTaskDetails_)(nil), } file_temporal_server_api_persistence_v1_executions_proto_msgTypes[7].OneofWrappers = []any{ (*VisibilityTaskInfo_ChasmTaskInfo)(nil), @@ -5327,11 +5404,11 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { (*Callback_Nexus_)(nil), (*Callback_Hsm)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37].OneofWrappers = []any{ (*ActivityInfo_PauseInfo_Manual_)(nil), (*ActivityInfo_PauseInfo_RuleId)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43].OneofWrappers = []any{ (*CallbackInfo_Trigger_WorkflowClosed)(nil), } type x struct{} @@ -5340,7 +5417,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_persistence_v1_executions_proto_rawDesc), len(file_temporal_server_api_persistence_v1_executions_proto_rawDesc)), NumEnums: 0, - NumMessages: 43, + NumMessages: 44, NumExtensions: 0, NumServices: 0, }, diff --git a/common/dynamicconfig/constants.go b/common/dynamicconfig/constants.go index 81b276f1189..b85f052b861 100644 --- a/common/dynamicconfig/constants.go +++ b/common/dynamicconfig/constants.go @@ -189,6 +189,11 @@ config as the other services.`, false, `EnableActivityEagerExecution indicates if activity eager execution is enabled per namespace`, ) + EnableActivityCancellationNexusTask = NewGlobalBoolSetting( + "system.enableActivityCancellationNexusTask", + false, + `EnableActivityCancellationNexusTask enables pushing activity cancellation to workers via Nexus task`, + ) NamespaceMinRetentionGlobal = NewGlobalDurationSetting( "system.namespaceMinRetentionGlobal", 24*time.Hour, diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index 598cd194dd8..13e6c2846bb 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -38,6 +38,8 @@ func serializeTransferTask( transferTask = transferDeleteExecutionTaskToProto(task) case *tasks.ChasmTask: transferTask = transferChasmTaskToProto(task) + case *tasks.CancelActivityNexusTask: + transferTask = transferCancelActivityNexusTaskToProto(task) default: return nil, serviceerror.NewInternalf("Unknown transfer task type: %v", task) } @@ -86,6 +88,8 @@ func deserializeTransferTask( task = transferDeleteExecutionTaskFromProto(transferTask) case enumsspb.TASK_TYPE_CHASM: task = transferChasmTaskFromProto(transferTask) + case enumsspb.TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS: + task = transferCancelActivityNexusTaskFromProto(transferTask) default: return nil, serviceerror.NewInternalf("Unknown transfer task type: %v", transferTask.TaskType) } @@ -106,6 +110,40 @@ func transferChasmTaskFromProto(task *persistencespb.TransferTaskInfo) tasks.Tas } } +func transferCancelActivityNexusTaskToProto(task *tasks.CancelActivityNexusTask) *persistencespb.TransferTaskInfo { + return &persistencespb.TransferTaskInfo{ + NamespaceId: task.WorkflowKey.NamespaceID, + WorkflowId: task.WorkflowKey.WorkflowID, + RunId: task.WorkflowKey.RunID, + TaskId: task.TaskID, + TaskType: task.GetType(), + Version: task.Version, + VisibilityTime: timestamppb.New(task.VisibilityTimestamp), + TaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails_{ + CancelActivityNexusTaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails{ + ScheduledEventIds: task.ScheduledEventIDs, + WorkerInstanceKey: task.WorkerInstanceKey, + }, + }, + } +} + +func transferCancelActivityNexusTaskFromProto(task *persistencespb.TransferTaskInfo) tasks.Task { + details := task.GetCancelActivityNexusTaskDetails() + return &tasks.CancelActivityNexusTask{ + WorkflowKey: definition.NewWorkflowKey( + task.NamespaceId, + task.WorkflowId, + task.RunId, + ), + VisibilityTimestamp: task.VisibilityTime.AsTime(), + TaskID: task.TaskId, + Version: task.Version, + ScheduledEventIDs: details.GetScheduledEventIds(), + WorkerInstanceKey: details.GetWorkerInstanceKey(), + } +} + func serializeTimerTask( encoder Encoder, task tasks.Task, diff --git a/common/persistence/serialization/task_serializers_test.go b/common/persistence/serialization/task_serializers_test.go index e3ac399c6c8..dd037e430f5 100644 --- a/common/persistence/serialization/task_serializers_test.go +++ b/common/persistence/serialization/task_serializers_test.go @@ -169,6 +169,19 @@ func (s *taskSerializerSuite) TestTransferResetTask() { s.assertEqualTasks(resetTask) } +func (s *taskSerializerSuite) TestTransferCancelActivityNexusTask() { + cancelActivityNexusTask := &tasks.CancelActivityNexusTask{ + WorkflowKey: s.workflowKey, + VisibilityTimestamp: time.Unix(0, rand.Int63()).UTC(), + TaskID: rand.Int63(), + Version: rand.Int63(), + ScheduledEventIDs: []int64{rand.Int63(), rand.Int63(), rand.Int63()}, + WorkerInstanceKey: "test-worker-instance-key", + } + + s.assertEqualTasks(cancelActivityNexusTask) +} + func (s *taskSerializerSuite) TestTimerWorkflowTask() { workflowTaskTimer := &tasks.WorkflowTaskTimeoutTask{ WorkflowKey: s.workflowKey, diff --git a/proto/internal/temporal/server/api/enums/v1/task.proto b/proto/internal/temporal/server/api/enums/v1/task.proto index 34e026271ae..c0150d2abd3 100644 --- a/proto/internal/temporal/server/api/enums/v1/task.proto +++ b/proto/internal/temporal/server/api/enums/v1/task.proto @@ -59,6 +59,9 @@ enum TaskType { // A task with side effects generated by a CHASM component. TASK_TYPE_CHASM = 33; + + // A task to cancel a running activity via Nexus control queue. + TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS = 34; } // TaskPriority is only used for replication task as of May 2024 diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index fc3cf5b2204..ad8c90c99f9 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -350,11 +350,21 @@ message TransferTaskInfo { // by some other task, so this task doesn't need to worry about it. bool can_skip_visibility_archival = 1; } + + // Details for a Nexus task that cancels activities belonging to a specific worker. + message CancelActivityNexusTaskDetails { + // Scheduled event IDs of activities to cancel. + repeated int64 scheduled_event_ids = 1; + string worker_instance_key = 2; + } + oneof task_details { CloseExecutionTaskDetails close_execution_task_details = 16; // If the task addresses a CHASM component, this field will be set. ChasmTaskInfo chasm_task_info = 18; + + CancelActivityNexusTaskDetails cancel_activity_nexus_task_details = 19; } // Stamp represents the "version" of the entity's internal state for which the transfer task was created. // It increases monotonically when the entity's options are modified. diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index ee23a768939..03277b70331 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -39,6 +39,7 @@ import ( "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/configs" historyi "go.temporal.io/server/service/history/interfaces" + "go.temporal.io/server/service/history/tasks" "go.temporal.io/server/service/history/workflow" "go.temporal.io/server/service/history/workflow/update" "google.golang.org/protobuf/proto" @@ -664,6 +665,13 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( return nil, err } handler.activityNotStartedCancelled = true + } else if ai.WorkerInstanceKey != "" && handler.config.EnableActivityCancellationNexusTask() { + // Activity has started and worker supports Nexus tasks - create cancel task. + handler.mutableState.AddTasks(&tasks.CancelActivityNexusTask{ + WorkflowKey: handler.mutableState.GetWorkflowKey(), + ScheduledEventIDs: []int64{ai.ScheduledEventId}, + WorkerInstanceKey: ai.WorkerInstanceKey, + }) } } return actCancelReqEvent, nil diff --git a/service/history/configs/config.go b/service/history/configs/config.go index 1f614adabff..99a6ddb7ead 100644 --- a/service/history/configs/config.go +++ b/service/history/configs/config.go @@ -354,11 +354,12 @@ type Config struct { ESProcessorFlushInterval dynamicconfig.DurationPropertyFn ESProcessorAckTimeout dynamicconfig.DurationPropertyFn - EnableCrossNamespaceCommands dynamicconfig.BoolPropertyFn - EnableActivityEagerExecution dynamicconfig.BoolPropertyFnWithNamespaceFilter - EnableActivityRetryStampIncrement dynamicconfig.BoolPropertyFn - EnableEagerWorkflowStart dynamicconfig.BoolPropertyFnWithNamespaceFilter - NamespaceCacheRefreshInterval dynamicconfig.DurationPropertyFn + EnableCrossNamespaceCommands dynamicconfig.BoolPropertyFn + EnableActivityEagerExecution dynamicconfig.BoolPropertyFnWithNamespaceFilter + EnableActivityRetryStampIncrement dynamicconfig.BoolPropertyFn + EnableActivityCancellationNexusTask dynamicconfig.BoolPropertyFn + EnableEagerWorkflowStart dynamicconfig.BoolPropertyFnWithNamespaceFilter + NamespaceCacheRefreshInterval dynamicconfig.DurationPropertyFn // ArchivalQueueProcessor settings ArchivalProcessorSchedulerWorkerCount dynamicconfig.TypedSubscribable[int] @@ -726,11 +727,12 @@ func NewConfig( ESProcessorFlushInterval: dynamicconfig.WorkerESProcessorFlushInterval.Get(dc), ESProcessorAckTimeout: dynamicconfig.WorkerESProcessorAckTimeout.Get(dc), - EnableCrossNamespaceCommands: dynamicconfig.EnableCrossNamespaceCommands.Get(dc), - EnableActivityEagerExecution: dynamicconfig.EnableActivityEagerExecution.Get(dc), - EnableActivityRetryStampIncrement: dynamicconfig.EnableActivityRetryStampIncrement.Get(dc), - EnableEagerWorkflowStart: dynamicconfig.EnableEagerWorkflowStart.Get(dc), - NamespaceCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc), + EnableCrossNamespaceCommands: dynamicconfig.EnableCrossNamespaceCommands.Get(dc), + EnableActivityEagerExecution: dynamicconfig.EnableActivityEagerExecution.Get(dc), + EnableActivityRetryStampIncrement: dynamicconfig.EnableActivityRetryStampIncrement.Get(dc), + EnableActivityCancellationNexusTask: dynamicconfig.EnableActivityCancellationNexusTask.Get(dc), + EnableEagerWorkflowStart: dynamicconfig.EnableEagerWorkflowStart.Get(dc), + NamespaceCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc), // Archival related ArchivalTaskBatchSize: dynamicconfig.ArchivalTaskBatchSize.Get(dc), diff --git a/service/history/tasks/cancel_activity_nexus_task.go b/service/history/tasks/cancel_activity_nexus_task.go new file mode 100644 index 00000000000..05382dd45a8 --- /dev/null +++ b/service/history/tasks/cancel_activity_nexus_task.go @@ -0,0 +1,71 @@ +package tasks + +import ( + "fmt" + "time" + + enumsspb "go.temporal.io/server/api/enums/v1" + "go.temporal.io/server/common/definition" +) + +var _ Task = (*CancelActivityNexusTask)(nil) + +type ( + CancelActivityNexusTask struct { + definition.WorkflowKey + VisibilityTimestamp time.Time + TaskID int64 + Version int64 + + // ScheduledEventIDs of activities to cancel (batched by worker). + ScheduledEventIDs []int64 + WorkerInstanceKey string + } +) + +func (t *CancelActivityNexusTask) GetKey() Key { + return NewImmediateKey(t.TaskID) +} + +func (t *CancelActivityNexusTask) GetVersion() int64 { + return t.Version +} + +func (t *CancelActivityNexusTask) SetVersion(version int64) { + t.Version = version +} + +func (t *CancelActivityNexusTask) GetTaskID() int64 { + return t.TaskID +} + +func (t *CancelActivityNexusTask) SetTaskID(id int64) { + t.TaskID = id +} + +func (t *CancelActivityNexusTask) GetVisibilityTime() time.Time { + return t.VisibilityTimestamp +} + +func (t *CancelActivityNexusTask) SetVisibilityTime(timestamp time.Time) { + t.VisibilityTimestamp = timestamp +} + +func (t *CancelActivityNexusTask) GetCategory() Category { + return CategoryTransfer +} + +func (t *CancelActivityNexusTask) GetType() enumsspb.TaskType { + return enumsspb.TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS +} + +func (t *CancelActivityNexusTask) String() string { + return fmt.Sprintf("CancelActivityNexusTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, ScheduledEventIDs: %v, WorkerInstanceKey: %v, Version: %v}", + t.WorkflowKey.String(), + t.VisibilityTimestamp, + t.TaskID, + t.ScheduledEventIDs, + t.WorkerInstanceKey, + t.Version, + ) +} diff --git a/service/history/tasks/utils.go b/service/history/tasks/utils.go index 3c446342b6d..6532528162c 100644 --- a/service/history/tasks/utils.go +++ b/service/history/tasks/utils.go @@ -78,6 +78,12 @@ func GetTransferTaskEventID( eventID = common.FirstEventID case *ChasmTask: return getChasmTaskEventID() + case *CancelActivityNexusTask: + if len(task.ScheduledEventIDs) > 0 { + eventID = task.ScheduledEventIDs[0] + } else { + eventID = common.FirstEventID + } case *FakeTask: // no-op default: diff --git a/service/history/transfer_queue_active_task_executor.go b/service/history/transfer_queue_active_task_executor.go index 3fba82a3e86..4fda52b25ae 100644 --- a/service/history/transfer_queue_active_task_executor.go +++ b/service/history/transfer_queue_active_task_executor.go @@ -145,6 +145,9 @@ func (t *transferQueueActiveTaskExecutor) Execute( err = t.processDeleteExecutionTask(ctx, task) case *tasks.ChasmTask: err = t.executeChasmSideEffectTransferTask(ctx, task) + case *tasks.CancelActivityNexusTask: + // TODO: Implement dispatch to worker control queue + err = nil default: err = errUnknownTransferTask } diff --git a/service/history/transfer_queue_standby_task_executor.go b/service/history/transfer_queue_standby_task_executor.go index 4e61c43aafd..a9738cab2d1 100644 --- a/service/history/transfer_queue_standby_task_executor.go +++ b/service/history/transfer_queue_standby_task_executor.go @@ -108,6 +108,9 @@ func (t *transferQueueStandbyTaskExecutor) Execute( err = t.processDeleteExecutionTask(ctx, task, false) case *tasks.ChasmTask: err = t.executeChasmSideEffectTransferTask(ctx, task) + case *tasks.CancelActivityNexusTask: + // Cancel activity nexus task is best-effort and only processed in active cluster + err = nil default: err = errUnknownTransferTask } From 113ca15bb0750d160d3f689c70defb939e0d36ce Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Thu, 5 Feb 2026 17:26:55 -0800 Subject: [PATCH 11/40] Fix lint errors --- .../serialization/task_serializers.go | 6 +++--- service/history/configs/config.go | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index 13e6c2846bb..fc2ff762aff 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -112,9 +112,9 @@ func transferChasmTaskFromProto(task *persistencespb.TransferTaskInfo) tasks.Tas func transferCancelActivityNexusTaskToProto(task *tasks.CancelActivityNexusTask) *persistencespb.TransferTaskInfo { return &persistencespb.TransferTaskInfo{ - NamespaceId: task.WorkflowKey.NamespaceID, - WorkflowId: task.WorkflowKey.WorkflowID, - RunId: task.WorkflowKey.RunID, + NamespaceId: task.NamespaceID, + WorkflowId: task.WorkflowID, + RunId: task.RunID, TaskId: task.TaskID, TaskType: task.GetType(), Version: task.Version, diff --git a/service/history/configs/config.go b/service/history/configs/config.go index 99a6ddb7ead..5e7a8d84032 100644 --- a/service/history/configs/config.go +++ b/service/history/configs/config.go @@ -354,12 +354,12 @@ type Config struct { ESProcessorFlushInterval dynamicconfig.DurationPropertyFn ESProcessorAckTimeout dynamicconfig.DurationPropertyFn - EnableCrossNamespaceCommands dynamicconfig.BoolPropertyFn - EnableActivityEagerExecution dynamicconfig.BoolPropertyFnWithNamespaceFilter - EnableActivityRetryStampIncrement dynamicconfig.BoolPropertyFn + EnableCrossNamespaceCommands dynamicconfig.BoolPropertyFn + EnableActivityEagerExecution dynamicconfig.BoolPropertyFnWithNamespaceFilter + EnableActivityRetryStampIncrement dynamicconfig.BoolPropertyFn EnableActivityCancellationNexusTask dynamicconfig.BoolPropertyFn - EnableEagerWorkflowStart dynamicconfig.BoolPropertyFnWithNamespaceFilter - NamespaceCacheRefreshInterval dynamicconfig.DurationPropertyFn + EnableEagerWorkflowStart dynamicconfig.BoolPropertyFnWithNamespaceFilter + NamespaceCacheRefreshInterval dynamicconfig.DurationPropertyFn // ArchivalQueueProcessor settings ArchivalProcessorSchedulerWorkerCount dynamicconfig.TypedSubscribable[int] @@ -727,12 +727,12 @@ func NewConfig( ESProcessorFlushInterval: dynamicconfig.WorkerESProcessorFlushInterval.Get(dc), ESProcessorAckTimeout: dynamicconfig.WorkerESProcessorAckTimeout.Get(dc), - EnableCrossNamespaceCommands: dynamicconfig.EnableCrossNamespaceCommands.Get(dc), - EnableActivityEagerExecution: dynamicconfig.EnableActivityEagerExecution.Get(dc), - EnableActivityRetryStampIncrement: dynamicconfig.EnableActivityRetryStampIncrement.Get(dc), + EnableCrossNamespaceCommands: dynamicconfig.EnableCrossNamespaceCommands.Get(dc), + EnableActivityEagerExecution: dynamicconfig.EnableActivityEagerExecution.Get(dc), + EnableActivityRetryStampIncrement: dynamicconfig.EnableActivityRetryStampIncrement.Get(dc), EnableActivityCancellationNexusTask: dynamicconfig.EnableActivityCancellationNexusTask.Get(dc), - EnableEagerWorkflowStart: dynamicconfig.EnableEagerWorkflowStart.Get(dc), - NamespaceCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc), + EnableEagerWorkflowStart: dynamicconfig.EnableEagerWorkflowStart.Get(dc), + NamespaceCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc), // Archival related ArchivalTaskBatchSize: dynamicconfig.ArchivalTaskBatchSize.Get(dc), From 5ad9aeb27bafcf22ff4ef6fab74dea129f33754f Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 13:29:30 -0800 Subject: [PATCH 12/40] Regen proto --- api/persistence/v1/executions.pb.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index 346fe898fa4..f4d61affb7e 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -4138,9 +4138,10 @@ func (x *TransferTaskInfo_CloseExecutionTaskDetails) GetCanSkipVisibilityArchiva return false } +// Details for a Nexus task that cancels activities belonging to a specific worker. type TransferTaskInfo_CancelActivityNexusTaskDetails struct { state protoimpl.MessageState `protogen:"open.v1"` - // Scheduled event IDs of activities to cancel (batched by worker). + // Scheduled event IDs of activities to cancel. ScheduledEventIds []int64 `protobuf:"varint,1,rep,packed,name=scheduled_event_ids,json=scheduledEventIds,proto3" json:"scheduled_event_ids,omitempty"` WorkerInstanceKey string `protobuf:"bytes,2,opt,name=worker_instance_key,json=workerInstanceKey,proto3" json:"worker_instance_key,omitempty"` unknownFields protoimpl.UnknownFields From 05adf1a5b01793d4db5fc4578306b11afec9e409 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 16:43:10 -0800 Subject: [PATCH 13/40] Remove worker_instance_key as it is not needed --- api/persistence/v1/executions.pb.go | 18 ++++-------------- .../serialization/task_serializers.go | 2 -- .../serialization/task_serializers_test.go | 1 - .../server/api/persistence/v1/executions.proto | 3 +-- .../workflow_task_completed_handler.go | 3 +-- .../tasks/cancel_activity_nexus_task.go | 4 +--- 6 files changed, 7 insertions(+), 24 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index f4d61affb7e..da802fecc52 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -4138,12 +4138,11 @@ func (x *TransferTaskInfo_CloseExecutionTaskDetails) GetCanSkipVisibilityArchiva return false } -// Details for a Nexus task that cancels activities belonging to a specific worker. +// Details for a Nexus task that cancels activities. type TransferTaskInfo_CancelActivityNexusTaskDetails struct { state protoimpl.MessageState `protogen:"open.v1"` // Scheduled event IDs of activities to cancel. ScheduledEventIds []int64 `protobuf:"varint,1,rep,packed,name=scheduled_event_ids,json=scheduledEventIds,proto3" json:"scheduled_event_ids,omitempty"` - WorkerInstanceKey string `protobuf:"bytes,2,opt,name=worker_instance_key,json=workerInstanceKey,proto3" json:"worker_instance_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4185,13 +4184,6 @@ func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetScheduledEventIds() return nil } -func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetWorkerInstanceKey() string { - if x != nil { - return x.WorkerInstanceKey - } - return "" -} - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] type ActivityInfo_UseWorkflowBuildIdInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4806,8 +4798,7 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\rRequestIDInfo\x12?\n" + "\n" + "event_type\x18\x01 \x01(\x0e2 .temporal.api.enums.v1.EventTypeR\teventType\x12\x19\n" + - "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\x86\n" + - "\n" + + "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\xd5\t\n" + "\x10TransferTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + @@ -4831,10 +4822,9 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\"cancel_activity_nexus_task_details\x18\x13 \x01(\v2S.temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetailsH\x00R\x1ecancelActivityNexusTaskDetails\x12\x14\n" + "\x05stamp\x18\x11 \x01(\x05R\x05stamp\x1a\\\n" + "\x19CloseExecutionTaskDetails\x12?\n" + - "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchival\x1a\x80\x01\n" + + "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchival\x1aP\n" + "\x1eCancelActivityNexusTaskDetails\x12.\n" + - "\x13scheduled_event_ids\x18\x01 \x03(\x03R\x11scheduledEventIds\x12.\n" + - "\x13worker_instance_key\x18\x02 \x01(\tR\x11workerInstanceKeyB\x0e\n" + + "\x13scheduled_event_ids\x18\x01 \x03(\x03R\x11scheduledEventIdsB\x0e\n" + "\ftask_detailsJ\x04\b\x0e\x10\x0f\"\xd8\b\n" + "\x13ReplicationTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index fc2ff762aff..6b8dff8b24c 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -122,7 +122,6 @@ func transferCancelActivityNexusTaskToProto(task *tasks.CancelActivityNexusTask) TaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails_{ CancelActivityNexusTaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails{ ScheduledEventIds: task.ScheduledEventIDs, - WorkerInstanceKey: task.WorkerInstanceKey, }, }, } @@ -140,7 +139,6 @@ func transferCancelActivityNexusTaskFromProto(task *persistencespb.TransferTaskI TaskID: task.TaskId, Version: task.Version, ScheduledEventIDs: details.GetScheduledEventIds(), - WorkerInstanceKey: details.GetWorkerInstanceKey(), } } diff --git a/common/persistence/serialization/task_serializers_test.go b/common/persistence/serialization/task_serializers_test.go index dd037e430f5..af4e86382a7 100644 --- a/common/persistence/serialization/task_serializers_test.go +++ b/common/persistence/serialization/task_serializers_test.go @@ -176,7 +176,6 @@ func (s *taskSerializerSuite) TestTransferCancelActivityNexusTask() { TaskID: rand.Int63(), Version: rand.Int63(), ScheduledEventIDs: []int64{rand.Int63(), rand.Int63(), rand.Int63()}, - WorkerInstanceKey: "test-worker-instance-key", } s.assertEqualTasks(cancelActivityNexusTask) diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index ad8c90c99f9..9c3d567c9a5 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -351,11 +351,10 @@ message TransferTaskInfo { bool can_skip_visibility_archival = 1; } - // Details for a Nexus task that cancels activities belonging to a specific worker. + // Details for a Nexus task that cancels activities. message CancelActivityNexusTaskDetails { // Scheduled event IDs of activities to cancel. repeated int64 scheduled_event_ids = 1; - string worker_instance_key = 2; } oneof task_details { diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 03277b70331..ea9a3dce89a 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -665,12 +665,11 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( return nil, err } handler.activityNotStartedCancelled = true - } else if ai.WorkerInstanceKey != "" && handler.config.EnableActivityCancellationNexusTask() { + } else if ai.WorkerControlTaskQueue != "" && handler.config.EnableActivityCancellationNexusTask() { // Activity has started and worker supports Nexus tasks - create cancel task. handler.mutableState.AddTasks(&tasks.CancelActivityNexusTask{ WorkflowKey: handler.mutableState.GetWorkflowKey(), ScheduledEventIDs: []int64{ai.ScheduledEventId}, - WorkerInstanceKey: ai.WorkerInstanceKey, }) } } diff --git a/service/history/tasks/cancel_activity_nexus_task.go b/service/history/tasks/cancel_activity_nexus_task.go index 05382dd45a8..824571467b7 100644 --- a/service/history/tasks/cancel_activity_nexus_task.go +++ b/service/history/tasks/cancel_activity_nexus_task.go @@ -19,7 +19,6 @@ type ( // ScheduledEventIDs of activities to cancel (batched by worker). ScheduledEventIDs []int64 - WorkerInstanceKey string } ) @@ -60,12 +59,11 @@ func (t *CancelActivityNexusTask) GetType() enumsspb.TaskType { } func (t *CancelActivityNexusTask) String() string { - return fmt.Sprintf("CancelActivityNexusTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, ScheduledEventIDs: %v, WorkerInstanceKey: %v, Version: %v}", + return fmt.Sprintf("CancelActivityNexusTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, ScheduledEventIDs: %v, Version: %v}", t.WorkflowKey.String(), t.VisibilityTimestamp, t.TaskID, t.ScheduledEventIDs, - t.WorkerInstanceKey, t.Version, ) } From 39c1759cc9660c4ab876b7e6741d43c676dfcaf9 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 19:17:49 -0800 Subject: [PATCH 14/40] Move CancelActivityNexusTask creation to task_generator --- .../workflow_task_completed_handler.go | 12 +++++------- service/history/interfaces/mutable_state.go | 1 + service/history/workflow/mutable_state_impl.go | 4 ++++ service/history/workflow/task_generator.go | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index ea9a3dce89a..b19b6a9c799 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -39,7 +39,6 @@ import ( "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/configs" historyi "go.temporal.io/server/service/history/interfaces" - "go.temporal.io/server/service/history/tasks" "go.temporal.io/server/service/history/workflow" "go.temporal.io/server/service/history/workflow/update" "google.golang.org/protobuf/proto" @@ -665,12 +664,11 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( return nil, err } handler.activityNotStartedCancelled = true - } else if ai.WorkerControlTaskQueue != "" && handler.config.EnableActivityCancellationNexusTask() { - // Activity has started and worker supports Nexus tasks - create cancel task. - handler.mutableState.AddTasks(&tasks.CancelActivityNexusTask{ - WorkflowKey: handler.mutableState.GetWorkflowKey(), - ScheduledEventIDs: []int64{ai.ScheduledEventId}, - }) + } else if ai.StartedEventId != common.EmptyEventID { + // Activity has started - create cancel task and send to worker via Nexus. + if err := handler.mutableState.AddCancelActivityNexusTasks(ai.ScheduledEventId); err != nil { + return nil, err + } } } return actCancelReqEvent, nil diff --git a/service/history/interfaces/mutable_state.go b/service/history/interfaces/mutable_state.go index cd07f9ac698..ebf16ab351c 100644 --- a/service/history/interfaces/mutable_state.go +++ b/service/history/interfaces/mutable_state.go @@ -46,6 +46,7 @@ type ( AddActivityTaskCancelRequestedEvent(int64, int64, string) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) AddActivityTaskCanceledEvent(int64, int64, int64, *commonpb.Payloads, string) (*historypb.HistoryEvent, error) + AddCancelActivityNexusTasks(int64) error AddActivityTaskCompletedEvent(int64, int64, *workflowservice.RespondActivityTaskCompletedRequest) (*historypb.HistoryEvent, error) AddActivityTaskFailedEvent(int64, int64, *failurepb.Failure, enumspb.RetryState, string, *commonpb.WorkerVersionStamp) (*historypb.HistoryEvent, error) AddActivityTaskScheduledEvent(int64, *commandpb.ScheduleActivityTaskCommandAttributes, bool) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index 0b92eb7ddc2..759102c90e5 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -4369,6 +4369,10 @@ func (ms *MutableStateImpl) AddActivityTaskCancelRequestedEvent( return actCancelReqEvent, ai, nil } +func (ms *MutableStateImpl) AddCancelActivityNexusTasks(scheduledEventID int64) error { + return ms.taskGenerator.GenerateCancelActivityNexusTasks(scheduledEventID) +} + func (ms *MutableStateImpl) ApplyActivityTaskCancelRequestedEvent( event *historypb.HistoryEvent, ) error { diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index c06d5563b2e..e9af62b359f 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -63,6 +63,7 @@ type ( activityScheduledEventID int64, ) error GenerateActivityRetryTasks(activityInfo *persistencespb.ActivityInfo) error + GenerateCancelActivityNexusTasks(scheduledEventID int64) error GenerateChildWorkflowTasks( childInitiatedEventId int64, ) error @@ -583,6 +584,23 @@ func (r *TaskGeneratorImpl) GenerateActivityRetryTasks(activityInfo *persistence return nil } +func (r *TaskGeneratorImpl) GenerateCancelActivityNexusTasks(scheduledEventID int64) error { + if !r.config.EnableActivityCancellationNexusTask() { + return nil + } + + ai, ok := r.mutableState.GetActivityInfo(scheduledEventID) + if !ok || ai.WorkerControlTaskQueue == "" { + return nil + } + + r.mutableState.AddTasks(&tasks.CancelActivityNexusTask{ + WorkflowKey: r.mutableState.GetWorkflowKey(), + ScheduledEventIDs: []int64{scheduledEventID}, + }) + return nil +} + func (r *TaskGeneratorImpl) GenerateChildWorkflowTasks( childInitiatedEventId int64, ) error { From 131c278582540cb519dd77c19e3fd9a8f459fb5f Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 19:27:32 -0800 Subject: [PATCH 15/40] Add WorkerControlTaskQueue to CancelActivityNexusTask --- api/persistence/v1/executions.pb.go | 21 ++++++++++++++----- .../serialization/task_serializers.go | 12 ++++++----- .../serialization/task_serializers_test.go | 11 +++++----- .../api/persistence/v1/executions.proto | 2 ++ .../tasks/cancel_activity_nexus_task.go | 2 ++ service/history/workflow/task_generator.go | 5 +++-- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index da802fecc52..e029f0f093d 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -4143,8 +4143,10 @@ type TransferTaskInfo_CancelActivityNexusTaskDetails struct { state protoimpl.MessageState `protogen:"open.v1"` // Scheduled event IDs of activities to cancel. ScheduledEventIds []int64 `protobuf:"varint,1,rep,packed,name=scheduled_event_ids,json=scheduledEventIds,proto3" json:"scheduled_event_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The Nexus queue to dispatch the cancel request to. + WorkerControlTaskQueue string `protobuf:"bytes,2,opt,name=worker_control_task_queue,json=workerControlTaskQueue,proto3" json:"worker_control_task_queue,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) Reset() { @@ -4184,6 +4186,13 @@ func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetScheduledEventIds() return nil } +func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetWorkerControlTaskQueue() string { + if x != nil { + return x.WorkerControlTaskQueue + } + return "" +} + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] type ActivityInfo_UseWorkflowBuildIdInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4798,7 +4807,8 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\rRequestIDInfo\x12?\n" + "\n" + "event_type\x18\x01 \x01(\x0e2 .temporal.api.enums.v1.EventTypeR\teventType\x12\x19\n" + - "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\xd5\t\n" + + "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\x91\n" + + "\n" + "\x10TransferTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + @@ -4822,9 +4832,10 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\"cancel_activity_nexus_task_details\x18\x13 \x01(\v2S.temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetailsH\x00R\x1ecancelActivityNexusTaskDetails\x12\x14\n" + "\x05stamp\x18\x11 \x01(\x05R\x05stamp\x1a\\\n" + "\x19CloseExecutionTaskDetails\x12?\n" + - "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchival\x1aP\n" + + "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchival\x1a\x8b\x01\n" + "\x1eCancelActivityNexusTaskDetails\x12.\n" + - "\x13scheduled_event_ids\x18\x01 \x03(\x03R\x11scheduledEventIdsB\x0e\n" + + "\x13scheduled_event_ids\x18\x01 \x03(\x03R\x11scheduledEventIds\x129\n" + + "\x19worker_control_task_queue\x18\x02 \x01(\tR\x16workerControlTaskQueueB\x0e\n" + "\ftask_detailsJ\x04\b\x0e\x10\x0f\"\xd8\b\n" + "\x13ReplicationTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index 6b8dff8b24c..62b2fc343a2 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -121,7 +121,8 @@ func transferCancelActivityNexusTaskToProto(task *tasks.CancelActivityNexusTask) VisibilityTime: timestamppb.New(task.VisibilityTimestamp), TaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails_{ CancelActivityNexusTaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails{ - ScheduledEventIds: task.ScheduledEventIDs, + ScheduledEventIds: task.ScheduledEventIDs, + WorkerControlTaskQueue: task.WorkerControlTaskQueue, }, }, } @@ -135,10 +136,11 @@ func transferCancelActivityNexusTaskFromProto(task *persistencespb.TransferTaskI task.WorkflowId, task.RunId, ), - VisibilityTimestamp: task.VisibilityTime.AsTime(), - TaskID: task.TaskId, - Version: task.Version, - ScheduledEventIDs: details.GetScheduledEventIds(), + VisibilityTimestamp: task.VisibilityTime.AsTime(), + TaskID: task.TaskId, + Version: task.Version, + ScheduledEventIDs: details.GetScheduledEventIds(), + WorkerControlTaskQueue: details.GetWorkerControlTaskQueue(), } } diff --git a/common/persistence/serialization/task_serializers_test.go b/common/persistence/serialization/task_serializers_test.go index af4e86382a7..6e7ad603d9c 100644 --- a/common/persistence/serialization/task_serializers_test.go +++ b/common/persistence/serialization/task_serializers_test.go @@ -171,11 +171,12 @@ func (s *taskSerializerSuite) TestTransferResetTask() { func (s *taskSerializerSuite) TestTransferCancelActivityNexusTask() { cancelActivityNexusTask := &tasks.CancelActivityNexusTask{ - WorkflowKey: s.workflowKey, - VisibilityTimestamp: time.Unix(0, rand.Int63()).UTC(), - TaskID: rand.Int63(), - Version: rand.Int63(), - ScheduledEventIDs: []int64{rand.Int63(), rand.Int63(), rand.Int63()}, + WorkflowKey: s.workflowKey, + VisibilityTimestamp: time.Unix(0, rand.Int63()).UTC(), + TaskID: rand.Int63(), + Version: rand.Int63(), + ScheduledEventIDs: []int64{rand.Int63(), rand.Int63(), rand.Int63()}, + WorkerControlTaskQueue: "test-control-queue", } s.assertEqualTasks(cancelActivityNexusTask) diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 9c3d567c9a5..00fb34de5ff 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -355,6 +355,8 @@ message TransferTaskInfo { message CancelActivityNexusTaskDetails { // Scheduled event IDs of activities to cancel. repeated int64 scheduled_event_ids = 1; + // The Nexus queue to dispatch the cancel request to. + string worker_control_task_queue = 2; } oneof task_details { diff --git a/service/history/tasks/cancel_activity_nexus_task.go b/service/history/tasks/cancel_activity_nexus_task.go index 824571467b7..0ed3112daa0 100644 --- a/service/history/tasks/cancel_activity_nexus_task.go +++ b/service/history/tasks/cancel_activity_nexus_task.go @@ -19,6 +19,8 @@ type ( // ScheduledEventIDs of activities to cancel (batched by worker). ScheduledEventIDs []int64 + // WorkerControlTaskQueue is the Nexus queue to dispatch the cancel request to. + WorkerControlTaskQueue string } ) diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index e9af62b359f..5b604cd640f 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -595,8 +595,9 @@ func (r *TaskGeneratorImpl) GenerateCancelActivityNexusTasks(scheduledEventID in } r.mutableState.AddTasks(&tasks.CancelActivityNexusTask{ - WorkflowKey: r.mutableState.GetWorkflowKey(), - ScheduledEventIDs: []int64{scheduledEventID}, + WorkflowKey: r.mutableState.GetWorkflowKey(), + ScheduledEventIDs: []int64{scheduledEventID}, + WorkerControlTaskQueue: ai.WorkerControlTaskQueue, }) return nil } From 7bc8adaae92e25e4d4d25f33fc9a89bfc6f8e50d Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 19:36:31 -0800 Subject: [PATCH 16/40] Add metrics tag and low priority for CancelActivityNexusTask --- common/metrics/metric_defs.go | 1 + service/history/queues/metrics.go | 2 ++ service/history/queues/priority_assigner.go | 1 + 3 files changed, 4 insertions(+) diff --git a/common/metrics/metric_defs.go b/common/metrics/metric_defs.go index 08c99e689dc..f77fb50d785 100644 --- a/common/metrics/metric_defs.go +++ b/common/metrics/metric_defs.go @@ -567,6 +567,7 @@ const ( TaskTypeTransferActiveTaskStartChildExecution = "TransferActiveTaskStartChildExecution" TaskTypeTransferActiveTaskResetWorkflow = "TransferActiveTaskResetWorkflow" TaskTypeTransferActiveTaskDeleteExecution = "TransferActiveTaskDeleteExecution" + TaskTypeTransferActiveTaskCancelActivityNexus = "TransferActiveTaskCancelActivityNexus" TaskTypeTransferStandbyTaskActivity = "TransferStandbyTaskActivity" TaskTypeTransferStandbyTaskWorkflowTask = "TransferStandbyTaskWorkflowTask" TaskTypeTransferStandbyTaskCloseExecution = "TransferStandbyTaskCloseExecution" diff --git a/service/history/queues/metrics.go b/service/history/queues/metrics.go index c836dc5451b..c2b581eb617 100644 --- a/service/history/queues/metrics.go +++ b/service/history/queues/metrics.go @@ -48,6 +48,8 @@ func GetActiveTransferTaskTypeTagValue( return metrics.TaskTypeTransferActiveTaskResetWorkflow case *tasks.DeleteExecutionTask: return metrics.TaskTypeTransferActiveTaskDeleteExecution + case *tasks.CancelActivityNexusTask: + return metrics.TaskTypeTransferActiveTaskCancelActivityNexus case *tasks.ChasmTask: return prefix + "." + getCHASMTaskTypeTagValue(t, chasmRegistry) default: diff --git a/service/history/queues/priority_assigner.go b/service/history/queues/priority_assigner.go index cbaf7d4b951..e8ee618f3e4 100644 --- a/service/history/queues/priority_assigner.go +++ b/service/history/queues/priority_assigner.go @@ -49,6 +49,7 @@ func (a *priorityAssignerImpl) Assign(executable Executable) tasks.Priority { enumsspb.TASK_TYPE_TRANSFER_DELETE_EXECUTION, enumsspb.TASK_TYPE_VISIBILITY_DELETE_EXECUTION, enumsspb.TASK_TYPE_ARCHIVAL_ARCHIVE_EXECUTION, + enumsspb.TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS, enumsspb.TASK_TYPE_UNSPECIFIED: // add more task types here if we believe it's ok to delay those tasks // and assign them the same priority as throttled tasks From 01e48221b6f9799bf7e3b67369e32afc739a70d7 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 22:49:14 -0800 Subject: [PATCH 17/40] Update comment for standby executor --- .../workflow_task_completed_handler.go | 1 + service/history/transfer_queue_standby_task_executor.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index b19b6a9c799..31c03d70e9e 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -666,6 +666,7 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( handler.activityNotStartedCancelled = true } else if ai.StartedEventId != common.EmptyEventID { // Activity has started - create cancel task and send to worker via Nexus. + // TODO: Batch tasks for the same control queue. if err := handler.mutableState.AddCancelActivityNexusTasks(ai.ScheduledEventId); err != nil { return nil, err } diff --git a/service/history/transfer_queue_standby_task_executor.go b/service/history/transfer_queue_standby_task_executor.go index a9738cab2d1..03839817966 100644 --- a/service/history/transfer_queue_standby_task_executor.go +++ b/service/history/transfer_queue_standby_task_executor.go @@ -109,7 +109,8 @@ func (t *transferQueueStandbyTaskExecutor) Execute( case *tasks.ChasmTask: err = t.executeChasmSideEffectTransferTask(ctx, task) case *tasks.CancelActivityNexusTask: - // Cancel activity nexus task is best-effort and only processed in active cluster + // Nexus operation is synchronous. So if the failover happens waiting for the Nexus response, + // the task will be retried in standby. err = nil default: err = errUnknownTransferTask From e9b1fac93fcf124515576a05767bbb63e4cfaa62 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 22:59:13 -0800 Subject: [PATCH 18/40] Add Version field to CancelActivityNexusTask for multi-cluster support --- service/history/workflow/task_generator.go | 1 + 1 file changed, 1 insertion(+) diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index 5b604cd640f..6162f6287fc 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -598,6 +598,7 @@ func (r *TaskGeneratorImpl) GenerateCancelActivityNexusTasks(scheduledEventID in WorkflowKey: r.mutableState.GetWorkflowKey(), ScheduledEventIDs: []int64{scheduledEventID}, WorkerControlTaskQueue: ai.WorkerControlTaskQueue, + Version: ai.Version, }) return nil } From faff7eeb5a87045b70fe79064a8008c95ff67929 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 11 Feb 2026 23:08:03 -0800 Subject: [PATCH 19/40] Add comment --- service/history/tasks/utils.go | 1 + service/history/workflow/task_generator.go | 1 + 2 files changed, 2 insertions(+) diff --git a/service/history/tasks/utils.go b/service/history/tasks/utils.go index 6532528162c..1784b608bdd 100644 --- a/service/history/tasks/utils.go +++ b/service/history/tasks/utils.go @@ -82,6 +82,7 @@ func GetTransferTaskEventID( if len(task.ScheduledEventIDs) > 0 { eventID = task.ScheduledEventIDs[0] } else { + // Should never happen. eventID = common.FirstEventID } case *FakeTask: diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index 6162f6287fc..ef1073d819d 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -590,6 +590,7 @@ func (r *TaskGeneratorImpl) GenerateCancelActivityNexusTasks(scheduledEventID in } ai, ok := r.mutableState.GetActivityInfo(scheduledEventID) + // If control queue is not set, it means the worker that this activity belongs to does not support Nexus tasks. if !ok || ai.WorkerControlTaskQueue == "" { return nil } From d6af322a9d50f0effd6cb1b4f06cf691326c1c91 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 17 Feb 2026 18:27:45 -0800 Subject: [PATCH 20/40] Add ActivityCommandTask for outbound activity commands - Define ActivityCommandTask in tasks package - Add serialization/deserialization for ActivityCommandTask - Add task generation in TaskGenerator - Add batching logic in workflow task completed handler - Forward WorkerControlTaskQueue through matching service - Add unit tests for task generation and batching - Add metrics support with command type --- api/enums/v1/task.go-helpers.pb.go | 20 +- api/enums/v1/task.pb.go | 108 ++- .../v1/executions.go-helpers.pb.go | 37 + api/persistence/v1/executions.pb.go | 643 +++++++++--------- common/metrics/metric_defs.go | 1 - .../serialization/task_serializers.go | 68 +- .../serialization/task_serializers_test.go | 18 +- common/testing/testvars/test_vars.go | 9 + .../temporal/server/api/enums/v1/task.proto | 10 +- .../api/persistence/v1/executions.proto | 21 +- .../workflow_task_completed_handler.go | 52 +- .../workflow_task_completed_handler_test.go | 69 ++ service/history/interfaces/mutable_state.go | 2 +- .../history/interfaces/mutable_state_mock.go | 14 + service/history/queues/metrics.go | 4 +- service/history/queues/priority_assigner.go | 1 - .../history/tasks/activity_command_task.go | 72 ++ .../tasks/cancel_activity_nexus_task.go | 71 -- service/history/tasks/utils.go | 7 - .../transfer_queue_active_task_executor.go | 3 - .../transfer_queue_standby_task_executor.go | 4 - .../history/workflow/mutable_state_impl.go | 4 +- service/history/workflow/task_generator.go | 18 +- .../history/workflow/task_generator_mock.go | 15 + .../history/workflow/task_generator_test.go | 77 +++ 25 files changed, 827 insertions(+), 521 deletions(-) create mode 100644 service/history/tasks/activity_command_task.go delete mode 100644 service/history/tasks/cancel_activity_nexus_task.go diff --git a/api/enums/v1/task.go-helpers.pb.go b/api/enums/v1/task.go-helpers.pb.go index 4a171504f00..6953cba3fcc 100644 --- a/api/enums/v1/task.go-helpers.pb.go +++ b/api/enums/v1/task.go-helpers.pb.go @@ -57,7 +57,7 @@ var ( "ReplicationSyncVersionedTransition": 31, "ChasmPure": 32, "Chasm": 33, - "TransferCancelActivityNexus": 34, + "ActivityCommand": 34, } ) @@ -72,6 +72,24 @@ func TaskTypeFromString(s string) (TaskType, error) { return TaskType(0), fmt.Errorf("%s is not a valid TaskType", s) } +var ( + ActivityCommandType_shorthandValue = map[string]int32{ + "Unspecified": 0, + "Cancel": 1, + } +) + +// ActivityCommandTypeFromString parses a ActivityCommandType value from either the protojson +// canonical SCREAMING_CASE enum or the traditional temporal PascalCase enum to ActivityCommandType +func ActivityCommandTypeFromString(s string) (ActivityCommandType, error) { + if v, ok := ActivityCommandType_value[s]; ok { + return ActivityCommandType(v), nil + } else if v, ok := ActivityCommandType_shorthandValue[s]; ok { + return ActivityCommandType(v), nil + } + return ActivityCommandType(0), fmt.Errorf("%s is not a valid ActivityCommandType", s) +} + var ( TaskPriority_shorthandValue = map[string]int32{ "Unspecified": 0, diff --git a/api/enums/v1/task.pb.go b/api/enums/v1/task.pb.go index 0d382d7c910..bac5034329f 100644 --- a/api/enums/v1/task.pb.go +++ b/api/enums/v1/task.pb.go @@ -126,8 +126,8 @@ const ( TASK_TYPE_CHASM_PURE TaskType = 32 // A task with side effects generated by a CHASM component. TASK_TYPE_CHASM TaskType = 33 - // A task to cancel a running activity via Nexus control queue. - TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS TaskType = 34 + // A task to send commands to activities via Nexus. Also see ActivityCommandType. + TASK_TYPE_ACTIVITY_COMMAND TaskType = 34 ) // Enum value maps for TaskType. @@ -164,7 +164,7 @@ var ( 31: "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION", 32: "TASK_TYPE_CHASM_PURE", 33: "TASK_TYPE_CHASM", - 34: "TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS", + 34: "TASK_TYPE_ACTIVITY_COMMAND", } TaskType_value = map[string]int32{ "TASK_TYPE_UNSPECIFIED": 0, @@ -198,7 +198,7 @@ var ( "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION": 31, "TASK_TYPE_CHASM_PURE": 32, "TASK_TYPE_CHASM": 33, - "TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS": 34, + "TASK_TYPE_ACTIVITY_COMMAND": 34, } ) @@ -232,7 +232,7 @@ func (x TaskType) String() string { return "TransferSignalExecution" case TASK_TYPE_TRANSFER_RESET_WORKFLOW: - // TaskPriority is only used for replication task as of May 2024 + // ActivityCommandType specifies the type of command to send to activities. return "TransferResetWorkflow" case TASK_TYPE_WORKFLOW_TASK_TIMEOUT: return "WorkflowTaskTimeout" @@ -240,14 +240,12 @@ func (x TaskType) String() string { return "ActivityTimeout" case TASK_TYPE_USER_TIMER: return "UserTimer" - - // gap between index can be used for future priority levels if needed case TASK_TYPE_WORKFLOW_RUN_TIMEOUT: return "WorkflowRunTimeout" + + // Enum value maps for ActivityCommandType. case TASK_TYPE_DELETE_HISTORY_EVENT: return "DeleteHistoryEvent" - - // Enum value maps for TaskPriority. case TASK_TYPE_ACTIVITY_RETRY_TIMER: return "ActivityRetryTimer" case TASK_TYPE_WORKFLOW_BACKOFF_TIMER: @@ -273,17 +271,19 @@ func (x TaskType) String() string { case TASK_TYPE_WORKFLOW_EXECUTION_TIMEOUT: return "WorkflowExecutionTimeout" case TASK_TYPE_REPLICATION_SYNC_HSM: - return "ReplicationSyncHsm" - // Deprecated: Use TaskPriority.Descriptor instead. + // Deprecated: Use ActivityCommandType.Descriptor instead. + return "ReplicationSyncHsm" case TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION: return "ReplicationSyncVersionedTransition" case TASK_TYPE_CHASM_PURE: return "ChasmPure" case TASK_TYPE_CHASM: return "Chasm" - case TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS: - return "TransferCancelActivityNexus" + + // TaskPriority is only used for replication task as of May 2024 + case TASK_TYPE_ACTIVITY_COMMAND: + return "ActivityCommand" default: return strconv.Itoa(int(x)) } @@ -306,15 +306,68 @@ func (TaskType) EnumDescriptor() ([]byte, []int) { return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{1} } +type ActivityCommandType int32 + +const ( + ACTIVITY_COMMAND_TYPE_UNSPECIFIED ActivityCommandType = 0 + ACTIVITY_COMMAND_TYPE_CANCEL ActivityCommandType = 1 +) + +var ( + ActivityCommandType_name = map[int32]string{ + 0: "ACTIVITY_COMMAND_TYPE_UNSPECIFIED", + 1: "ACTIVITY_COMMAND_TYPE_CANCEL", + } + ActivityCommandType_value = map[string]int32{ + "ACTIVITY_COMMAND_TYPE_UNSPECIFIED": 0, + "ACTIVITY_COMMAND_TYPE_CANCEL": 1, + } +) + +func (x ActivityCommandType) Enum() *ActivityCommandType { + p := new(ActivityCommandType) + *p = x + return p +} + +func (x ActivityCommandType) String() string { + switch x { + case ACTIVITY_COMMAND_TYPE_UNSPECIFIED: + return "Unspecified" + case ACTIVITY_COMMAND_TYPE_CANCEL: + return "Cancel" + default: + return strconv.Itoa(int(x)) + } + +} + +func (ActivityCommandType) Descriptor() protoreflect.EnumDescriptor { + return file_temporal_server_api_enums_v1_task_proto_enumTypes[2].Descriptor() +} + +func (ActivityCommandType) Type() protoreflect.EnumType { + return &file_temporal_server_api_enums_v1_task_proto_enumTypes[2] +} + +func (x ActivityCommandType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +func (ActivityCommandType) EnumDescriptor() ([]byte, []int) { + return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{2} +} + type TaskPriority int32 const ( TASK_PRIORITY_UNSPECIFIED TaskPriority = 0 TASK_PRIORITY_HIGH TaskPriority = 1 - + // gap between index can be used for future priority levels if needed TASK_PRIORITY_LOW TaskPriority = 10 ) +// Enum value maps for TaskPriority. var ( TaskPriority_name = map[int32]string{ 0: "TASK_PRIORITY_UNSPECIFIED", @@ -349,19 +402,20 @@ func (x TaskPriority) String() string { } func (TaskPriority) Descriptor() protoreflect.EnumDescriptor { - return file_temporal_server_api_enums_v1_task_proto_enumTypes[2].Descriptor() + return file_temporal_server_api_enums_v1_task_proto_enumTypes[3].Descriptor() } func (TaskPriority) Type() protoreflect.EnumType { - return &file_temporal_server_api_enums_v1_task_proto_enumTypes[2] + return &file_temporal_server_api_enums_v1_task_proto_enumTypes[3] } func (x TaskPriority) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } +// Deprecated: Use TaskPriority.Descriptor instead. func (TaskPriority) EnumDescriptor() ([]byte, []int) { - return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{2} + return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{3} } var File_temporal_server_api_enums_v1_task_proto protoreflect.FileDescriptor @@ -373,7 +427,7 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "TaskSource\x12\x1b\n" + "\x17TASK_SOURCE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13TASK_SOURCE_HISTORY\x10\x01\x12\x1a\n" + - "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\xe4\t\n" + + "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\xd6\t\n" + "\bTaskType\x12\x19\n" + "\x15TASK_TYPE_UNSPECIFIED\x10\x00\x12!\n" + "\x1dTASK_TYPE_REPLICATION_HISTORY\x10\x01\x12'\n" + @@ -406,8 +460,11 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "\x1eTASK_TYPE_REPLICATION_SYNC_HSM\x10\x1e\x123\n" + "/TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION\x10\x1f\x12\x18\n" + "\x14TASK_TYPE_CHASM_PURE\x10 \x12\x13\n" + - "\x0fTASK_TYPE_CHASM\x10!\x12,\n" + - "(TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS\x10\"\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*\\\n" + + "\x0fTASK_TYPE_CHASM\x10!\x12\x1e\n" + + "\x1aTASK_TYPE_ACTIVITY_COMMAND\x10\"\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*^\n" + + "\x13ActivityCommandType\x12%\n" + + "!ACTIVITY_COMMAND_TYPE_UNSPECIFIED\x10\x00\x12 \n" + + "\x1cACTIVITY_COMMAND_TYPE_CANCEL\x10\x01*\\\n" + "\fTaskPriority\x12\x1d\n" + "\x19TASK_PRIORITY_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12TASK_PRIORITY_HIGH\x10\x01\x12\x15\n" + @@ -426,11 +483,12 @@ func file_temporal_server_api_enums_v1_task_proto_rawDescGZIP() []byte { return file_temporal_server_api_enums_v1_task_proto_rawDescData } -var file_temporal_server_api_enums_v1_task_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_temporal_server_api_enums_v1_task_proto_enumTypes = make([]protoimpl.EnumInfo, 4) var file_temporal_server_api_enums_v1_task_proto_goTypes = []any{ - (TaskSource)(0), // 0: temporal.server.api.enums.v1.TaskSource - (TaskType)(0), // 1: temporal.server.api.enums.v1.TaskType - (TaskPriority)(0), // 2: temporal.server.api.enums.v1.TaskPriority + (TaskSource)(0), // 0: temporal.server.api.enums.v1.TaskSource + (TaskType)(0), // 1: temporal.server.api.enums.v1.TaskType + (ActivityCommandType)(0), // 2: temporal.server.api.enums.v1.ActivityCommandType + (TaskPriority)(0), // 3: temporal.server.api.enums.v1.TaskPriority } var file_temporal_server_api_enums_v1_task_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -450,7 +508,7 @@ func file_temporal_server_api_enums_v1_task_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_enums_v1_task_proto_rawDesc), len(file_temporal_server_api_enums_v1_task_proto_rawDesc)), - NumEnums: 3, + NumEnums: 4, NumMessages: 0, NumExtensions: 0, NumServices: 0, diff --git a/api/persistence/v1/executions.go-helpers.pb.go b/api/persistence/v1/executions.go-helpers.pb.go index b6e6a0dcd10..965f8080bb0 100644 --- a/api/persistence/v1/executions.go-helpers.pb.go +++ b/api/persistence/v1/executions.go-helpers.pb.go @@ -412,6 +412,43 @@ func (this *OutboundTaskInfo) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type ActivityCommandTaskInfo to the protobuf v3 wire format +func (val *ActivityCommandTaskInfo) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ActivityCommandTaskInfo from the protobuf v3 wire format +func (val *ActivityCommandTaskInfo) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ActivityCommandTaskInfo) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ActivityCommandTaskInfo values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ActivityCommandTaskInfo) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ActivityCommandTaskInfo + switch t := that.(type) { + case *ActivityCommandTaskInfo: + that1 = t + case ActivityCommandTaskInfo: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type NexusInvocationTaskInfo to the protobuf v3 wire format func (val *NexusInvocationTaskInfo) Marshal() ([]byte, error) { return proto.Marshal(val) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index e029f0f093d..fea1986e34a 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -1366,7 +1366,6 @@ type TransferTaskInfo struct { // // *TransferTaskInfo_CloseExecutionTaskDetails_ // *TransferTaskInfo_ChasmTaskInfo - // *TransferTaskInfo_CancelActivityNexusTaskDetails_ TaskDetails isTransferTaskInfo_TaskDetails `protobuf_oneof:"task_details"` // Stamp represents the "version" of the entity's internal state for which the transfer task was created. // It increases monotonically when the entity's options are modified. @@ -1529,15 +1528,6 @@ func (x *TransferTaskInfo) GetChasmTaskInfo() *ChasmTaskInfo { return nil } -func (x *TransferTaskInfo) GetCancelActivityNexusTaskDetails() *TransferTaskInfo_CancelActivityNexusTaskDetails { - if x != nil { - if x, ok := x.TaskDetails.(*TransferTaskInfo_CancelActivityNexusTaskDetails_); ok { - return x.CancelActivityNexusTaskDetails - } - } - return nil -} - func (x *TransferTaskInfo) GetStamp() int32 { if x != nil { return x.Stamp @@ -1558,16 +1548,10 @@ type TransferTaskInfo_ChasmTaskInfo struct { ChasmTaskInfo *ChasmTaskInfo `protobuf:"bytes,18,opt,name=chasm_task_info,json=chasmTaskInfo,proto3,oneof"` } -type TransferTaskInfo_CancelActivityNexusTaskDetails_ struct { - CancelActivityNexusTaskDetails *TransferTaskInfo_CancelActivityNexusTaskDetails `protobuf:"bytes,19,opt,name=cancel_activity_nexus_task_details,json=cancelActivityNexusTaskDetails,proto3,oneof"` -} - func (*TransferTaskInfo_CloseExecutionTaskDetails_) isTransferTaskInfo_TaskDetails() {} func (*TransferTaskInfo_ChasmTaskInfo) isTransferTaskInfo_TaskDetails() {} -func (*TransferTaskInfo_CancelActivityNexusTaskDetails_) isTransferTaskInfo_TaskDetails() {} - // replication column type ReplicationTaskInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2230,6 +2214,7 @@ type OutboundTaskInfo struct { // // *OutboundTaskInfo_StateMachineInfo // *OutboundTaskInfo_ChasmTaskInfo + // *OutboundTaskInfo_ActivityCommandInfo TaskDetails isOutboundTaskInfo_TaskDetails `protobuf_oneof:"task_details"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2339,6 +2324,15 @@ func (x *OutboundTaskInfo) GetChasmTaskInfo() *ChasmTaskInfo { return nil } +func (x *OutboundTaskInfo) GetActivityCommandInfo() *ActivityCommandTaskInfo { + if x != nil { + if x, ok := x.TaskDetails.(*OutboundTaskInfo_ActivityCommandInfo); ok { + return x.ActivityCommandInfo + } + } + return nil +} + type isOutboundTaskInfo_TaskDetails interface { isOutboundTaskInfo_TaskDetails() } @@ -2353,10 +2347,72 @@ type OutboundTaskInfo_ChasmTaskInfo struct { ChasmTaskInfo *ChasmTaskInfo `protobuf:"bytes,9,opt,name=chasm_task_info,json=chasmTaskInfo,proto3,oneof"` } +type OutboundTaskInfo_ActivityCommandInfo struct { + // If the task is an activity command task. + ActivityCommandInfo *ActivityCommandTaskInfo `protobuf:"bytes,10,opt,name=activity_command_info,json=activityCommandInfo,proto3,oneof"` +} + func (*OutboundTaskInfo_StateMachineInfo) isOutboundTaskInfo_TaskDetails() {} func (*OutboundTaskInfo_ChasmTaskInfo) isOutboundTaskInfo_TaskDetails() {} +func (*OutboundTaskInfo_ActivityCommandInfo) isOutboundTaskInfo_TaskDetails() {} + +// ActivityCommandTaskInfo contains details for activity command operations. +type ActivityCommandTaskInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Type of command to send. + CommandType v1.ActivityCommandType `protobuf:"varint,1,opt,name=command_type,json=commandType,proto3,enum=temporal.server.api.enums.v1.ActivityCommandType" json:"command_type,omitempty"` + // Scheduled event IDs of activities to send command to. + ScheduledEventIds []int64 `protobuf:"varint,2,rep,packed,name=scheduled_event_ids,json=scheduledEventIds,proto3" json:"scheduled_event_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActivityCommandTaskInfo) Reset() { + *x = ActivityCommandTaskInfo{} + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActivityCommandTaskInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActivityCommandTaskInfo) ProtoMessage() {} + +func (x *ActivityCommandTaskInfo) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActivityCommandTaskInfo.ProtoReflect.Descriptor instead. +func (*ActivityCommandTaskInfo) Descriptor() ([]byte, []int) { + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11} +} + +func (x *ActivityCommandTaskInfo) GetCommandType() v1.ActivityCommandType { + if x != nil { + return x.CommandType + } + return v1.ActivityCommandType(0) +} + +func (x *ActivityCommandTaskInfo) GetScheduledEventIds() []int64 { + if x != nil { + return x.ScheduledEventIds + } + return nil +} + type NexusInvocationTaskInfo struct { state protoimpl.MessageState `protogen:"open.v1"` Attempt int32 `protobuf:"varint,1,opt,name=attempt,proto3" json:"attempt,omitempty"` @@ -2366,7 +2422,7 @@ type NexusInvocationTaskInfo struct { func (x *NexusInvocationTaskInfo) Reset() { *x = NexusInvocationTaskInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[11] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2378,7 +2434,7 @@ func (x *NexusInvocationTaskInfo) String() string { func (*NexusInvocationTaskInfo) ProtoMessage() {} func (x *NexusInvocationTaskInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[11] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2391,7 +2447,7 @@ func (x *NexusInvocationTaskInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use NexusInvocationTaskInfo.ProtoReflect.Descriptor instead. func (*NexusInvocationTaskInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{12} } func (x *NexusInvocationTaskInfo) GetAttempt() int32 { @@ -2410,7 +2466,7 @@ type NexusCancelationTaskInfo struct { func (x *NexusCancelationTaskInfo) Reset() { *x = NexusCancelationTaskInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[12] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2422,7 +2478,7 @@ func (x *NexusCancelationTaskInfo) String() string { func (*NexusCancelationTaskInfo) ProtoMessage() {} func (x *NexusCancelationTaskInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[12] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2435,7 +2491,7 @@ func (x *NexusCancelationTaskInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use NexusCancelationTaskInfo.ProtoReflect.Descriptor instead. func (*NexusCancelationTaskInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{12} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{13} } func (x *NexusCancelationTaskInfo) GetAttempt() int32 { @@ -2545,7 +2601,7 @@ type ActivityInfo struct { func (x *ActivityInfo) Reset() { *x = ActivityInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[13] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2557,7 +2613,7 @@ func (x *ActivityInfo) String() string { func (*ActivityInfo) ProtoMessage() {} func (x *ActivityInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[13] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2570,7 +2626,7 @@ func (x *ActivityInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ActivityInfo.ProtoReflect.Descriptor instead. func (*ActivityInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{13} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{14} } func (x *ActivityInfo) GetVersion() int64 { @@ -2959,7 +3015,7 @@ type TimerInfo struct { func (x *TimerInfo) Reset() { *x = TimerInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[14] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2971,7 +3027,7 @@ func (x *TimerInfo) String() string { func (*TimerInfo) ProtoMessage() {} func (x *TimerInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[14] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2984,7 +3040,7 @@ func (x *TimerInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use TimerInfo.ProtoReflect.Descriptor instead. func (*TimerInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{14} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{15} } func (x *TimerInfo) GetVersion() int64 { @@ -3052,7 +3108,7 @@ type ChildExecutionInfo struct { func (x *ChildExecutionInfo) Reset() { *x = ChildExecutionInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[15] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3064,7 +3120,7 @@ func (x *ChildExecutionInfo) String() string { func (*ChildExecutionInfo) ProtoMessage() {} func (x *ChildExecutionInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[15] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3077,7 +3133,7 @@ func (x *ChildExecutionInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ChildExecutionInfo.ProtoReflect.Descriptor instead. func (*ChildExecutionInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{15} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{16} } func (x *ChildExecutionInfo) GetVersion() int64 { @@ -3192,7 +3248,7 @@ type RequestCancelInfo struct { func (x *RequestCancelInfo) Reset() { *x = RequestCancelInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[16] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3204,7 +3260,7 @@ func (x *RequestCancelInfo) String() string { func (*RequestCancelInfo) ProtoMessage() {} func (x *RequestCancelInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[16] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3217,7 +3273,7 @@ func (x *RequestCancelInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestCancelInfo.ProtoReflect.Descriptor instead. func (*RequestCancelInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{16} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{17} } func (x *RequestCancelInfo) GetVersion() int64 { @@ -3269,7 +3325,7 @@ type SignalInfo struct { func (x *SignalInfo) Reset() { *x = SignalInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[17] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3281,7 +3337,7 @@ func (x *SignalInfo) String() string { func (*SignalInfo) ProtoMessage() {} func (x *SignalInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[17] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3294,7 +3350,7 @@ func (x *SignalInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use SignalInfo.ProtoReflect.Descriptor instead. func (*SignalInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{17} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{18} } func (x *SignalInfo) GetVersion() int64 { @@ -3344,7 +3400,7 @@ type Checksum struct { func (x *Checksum) Reset() { *x = Checksum{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[18] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3356,7 +3412,7 @@ func (x *Checksum) String() string { func (*Checksum) ProtoMessage() {} func (x *Checksum) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[18] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3369,7 +3425,7 @@ func (x *Checksum) ProtoReflect() protoreflect.Message { // Deprecated: Use Checksum.ProtoReflect.Descriptor instead. func (*Checksum) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{18} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{19} } func (x *Checksum) GetVersion() int32 { @@ -3407,7 +3463,7 @@ type Callback struct { func (x *Callback) Reset() { *x = Callback{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[19] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3419,7 +3475,7 @@ func (x *Callback) String() string { func (*Callback) ProtoMessage() {} func (x *Callback) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[19] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3432,7 +3488,7 @@ func (x *Callback) ProtoReflect() protoreflect.Message { // Deprecated: Use Callback.ProtoReflect.Descriptor instead. func (*Callback) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{19} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{20} } func (x *Callback) GetVariant() isCallback_Variant { @@ -3499,7 +3555,7 @@ type HSMCompletionCallbackArg struct { func (x *HSMCompletionCallbackArg) Reset() { *x = HSMCompletionCallbackArg{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[20] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3511,7 +3567,7 @@ func (x *HSMCompletionCallbackArg) String() string { func (*HSMCompletionCallbackArg) ProtoMessage() {} func (x *HSMCompletionCallbackArg) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[20] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3524,7 +3580,7 @@ func (x *HSMCompletionCallbackArg) ProtoReflect() protoreflect.Message { // Deprecated: Use HSMCompletionCallbackArg.ProtoReflect.Descriptor instead. func (*HSMCompletionCallbackArg) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{20} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{21} } func (x *HSMCompletionCallbackArg) GetNamespaceId() string { @@ -3581,7 +3637,7 @@ type CallbackInfo struct { func (x *CallbackInfo) Reset() { *x = CallbackInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[21] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3593,7 +3649,7 @@ func (x *CallbackInfo) String() string { func (*CallbackInfo) ProtoMessage() {} func (x *CallbackInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[21] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3606,7 +3662,7 @@ func (x *CallbackInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use CallbackInfo.ProtoReflect.Descriptor instead. func (*CallbackInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{21} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{22} } func (x *CallbackInfo) GetCallback() *Callback { @@ -3727,7 +3783,7 @@ type NexusOperationInfo struct { func (x *NexusOperationInfo) Reset() { *x = NexusOperationInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[22] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3739,7 +3795,7 @@ func (x *NexusOperationInfo) String() string { func (*NexusOperationInfo) ProtoMessage() {} func (x *NexusOperationInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[22] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3752,7 +3808,7 @@ func (x *NexusOperationInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use NexusOperationInfo.ProtoReflect.Descriptor instead. func (*NexusOperationInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{22} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{23} } func (x *NexusOperationInfo) GetEndpoint() string { @@ -3897,7 +3953,7 @@ type NexusOperationCancellationInfo struct { func (x *NexusOperationCancellationInfo) Reset() { *x = NexusOperationCancellationInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[23] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3909,7 +3965,7 @@ func (x *NexusOperationCancellationInfo) String() string { func (*NexusOperationCancellationInfo) ProtoMessage() {} func (x *NexusOperationCancellationInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[23] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3922,7 +3978,7 @@ func (x *NexusOperationCancellationInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use NexusOperationCancellationInfo.ProtoReflect.Descriptor instead. func (*NexusOperationCancellationInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{23} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{24} } func (x *NexusOperationCancellationInfo) GetRequestedTime() *timestamppb.Timestamp { @@ -3985,7 +4041,7 @@ type ResetChildInfo struct { func (x *ResetChildInfo) Reset() { *x = ResetChildInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[24] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3997,7 +4053,7 @@ func (x *ResetChildInfo) String() string { func (*ResetChildInfo) ProtoMessage() {} func (x *ResetChildInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[24] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4010,7 +4066,7 @@ func (x *ResetChildInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ResetChildInfo.ProtoReflect.Descriptor instead. func (*ResetChildInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{24} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{25} } func (x *ResetChildInfo) GetShouldTerminateAndStart() bool { @@ -4036,7 +4092,7 @@ type WorkflowPauseInfo struct { func (x *WorkflowPauseInfo) Reset() { *x = WorkflowPauseInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[25] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4048,7 +4104,7 @@ func (x *WorkflowPauseInfo) String() string { func (*WorkflowPauseInfo) ProtoMessage() {} func (x *WorkflowPauseInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[25] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4061,7 +4117,7 @@ func (x *WorkflowPauseInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkflowPauseInfo.ProtoReflect.Descriptor instead. func (*WorkflowPauseInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{25} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{26} } func (x *WorkflowPauseInfo) GetPauseTime() *timestamppb.Timestamp { @@ -4103,7 +4159,7 @@ type TransferTaskInfo_CloseExecutionTaskDetails struct { func (x *TransferTaskInfo_CloseExecutionTaskDetails) Reset() { *x = TransferTaskInfo_CloseExecutionTaskDetails{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[34] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4115,7 +4171,7 @@ func (x *TransferTaskInfo_CloseExecutionTaskDetails) String() string { func (*TransferTaskInfo_CloseExecutionTaskDetails) ProtoMessage() {} func (x *TransferTaskInfo_CloseExecutionTaskDetails) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[34] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4138,61 +4194,6 @@ func (x *TransferTaskInfo_CloseExecutionTaskDetails) GetCanSkipVisibilityArchiva return false } -// Details for a Nexus task that cancels activities. -type TransferTaskInfo_CancelActivityNexusTaskDetails struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Scheduled event IDs of activities to cancel. - ScheduledEventIds []int64 `protobuf:"varint,1,rep,packed,name=scheduled_event_ids,json=scheduledEventIds,proto3" json:"scheduled_event_ids,omitempty"` - // The Nexus queue to dispatch the cancel request to. - WorkerControlTaskQueue string `protobuf:"bytes,2,opt,name=worker_control_task_queue,json=workerControlTaskQueue,proto3" json:"worker_control_task_queue,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) Reset() { - *x = TransferTaskInfo_CancelActivityNexusTaskDetails{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TransferTaskInfo_CancelActivityNexusTaskDetails) ProtoMessage() {} - -func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[35] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TransferTaskInfo_CancelActivityNexusTaskDetails.ProtoReflect.Descriptor instead. -func (*TransferTaskInfo_CancelActivityNexusTaskDetails) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{5, 1} -} - -func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetScheduledEventIds() []int64 { - if x != nil { - return x.ScheduledEventIds - } - return nil -} - -func (x *TransferTaskInfo_CancelActivityNexusTaskDetails) GetWorkerControlTaskQueue() string { - if x != nil { - return x.WorkerControlTaskQueue - } - return "" -} - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] type ActivityInfo_UseWorkflowBuildIdInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4232,7 +4233,7 @@ func (x *ActivityInfo_UseWorkflowBuildIdInfo) ProtoReflect() protoreflect.Messag // Deprecated: Use ActivityInfo_UseWorkflowBuildIdInfo.ProtoReflect.Descriptor instead. func (*ActivityInfo_UseWorkflowBuildIdInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{13, 0} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{14, 0} } func (x *ActivityInfo_UseWorkflowBuildIdInfo) GetLastUsedBuildId() string { @@ -4289,7 +4290,7 @@ func (x *ActivityInfo_PauseInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ActivityInfo_PauseInfo.ProtoReflect.Descriptor instead. func (*ActivityInfo_PauseInfo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{13, 1} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{14, 1} } func (x *ActivityInfo_PauseInfo) GetPauseTime() *timestamppb.Timestamp { @@ -4379,7 +4380,7 @@ func (x *ActivityInfo_PauseInfo_Manual) ProtoReflect() protoreflect.Message { // Deprecated: Use ActivityInfo_PauseInfo_Manual.ProtoReflect.Descriptor instead. func (*ActivityInfo_PauseInfo_Manual) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{13, 1, 0} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{14, 1, 0} } func (x *ActivityInfo_PauseInfo_Manual) GetIdentity() string { @@ -4436,7 +4437,7 @@ func (x *Callback_Nexus) ProtoReflect() protoreflect.Message { // Deprecated: Use Callback_Nexus.ProtoReflect.Descriptor instead. func (*Callback_Nexus) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{19, 0} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{20, 0} } func (x *Callback_Nexus) GetUrl() string { @@ -4498,7 +4499,7 @@ func (x *Callback_HSM) ProtoReflect() protoreflect.Message { // Deprecated: Use Callback_HSM.ProtoReflect.Descriptor instead. func (*Callback_HSM) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{19, 1} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{20, 1} } func (x *Callback_HSM) GetNamespaceId() string { @@ -4570,7 +4571,7 @@ func (x *CallbackInfo_WorkflowClosed) ProtoReflect() protoreflect.Message { // Deprecated: Use CallbackInfo_WorkflowClosed.ProtoReflect.Descriptor instead. func (*CallbackInfo_WorkflowClosed) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{21, 0} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{22, 0} } type CallbackInfo_Trigger struct { @@ -4610,7 +4611,7 @@ func (x *CallbackInfo_Trigger) ProtoReflect() protoreflect.Message { // Deprecated: Use CallbackInfo_Trigger.ProtoReflect.Descriptor instead. func (*CallbackInfo_Trigger) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{21, 1} + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{22, 1} } func (x *CallbackInfo_Trigger) GetVariant() isCallbackInfo_Trigger_Variant { @@ -4807,8 +4808,7 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\rRequestIDInfo\x12?\n" + "\n" + "event_type\x18\x01 \x01(\x0e2 .temporal.api.enums.v1.EventTypeR\teventType\x12\x19\n" + - "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\x91\n" + - "\n" + + "\bevent_id\x18\x02 \x01(\x03R\aeventId\"\xdf\a\n" + "\x10TransferTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + @@ -4828,14 +4828,10 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x0fvisibility_time\x18\r \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\x12,\n" + "\x12delete_after_close\x18\x0f \x01(\bR\x10deleteAfterClose\x12\x91\x01\n" + "\x1cclose_execution_task_details\x18\x10 \x01(\v2N.temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetailsH\x00R\x19closeExecutionTaskDetails\x12[\n" + - "\x0fchasm_task_info\x18\x12 \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12\xa1\x01\n" + - "\"cancel_activity_nexus_task_details\x18\x13 \x01(\v2S.temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetailsH\x00R\x1ecancelActivityNexusTaskDetails\x12\x14\n" + + "\x0fchasm_task_info\x18\x12 \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12\x14\n" + "\x05stamp\x18\x11 \x01(\x05R\x05stamp\x1a\\\n" + "\x19CloseExecutionTaskDetails\x12?\n" + - "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchival\x1a\x8b\x01\n" + - "\x1eCancelActivityNexusTaskDetails\x12.\n" + - "\x13scheduled_event_ids\x18\x01 \x03(\x03R\x11scheduledEventIds\x129\n" + - "\x19worker_control_task_queue\x18\x02 \x01(\tR\x16workerControlTaskQueueB\x0e\n" + + "\x1ccan_skip_visibility_archival\x18\x01 \x01(\bR\x19canSkipVisibilityArchivalB\x0e\n" + "\ftask_detailsJ\x04\b\x0e\x10\x0f\"\xd8\b\n" + "\x13ReplicationTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + @@ -4909,7 +4905,7 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x06run_id\x18\x04 \x01(\tR\x05runId\x12C\n" + "\ttask_type\x18\x05 \x01(\x0e2&.temporal.server.api.enums.v1.TaskTypeR\btaskType\x12\x18\n" + "\aversion\x18\x06 \x01(\x03R\aversion\x12C\n" + - "\x0fvisibility_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\"\x89\x04\n" + + "\x0fvisibility_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\"\xfc\x04\n" + "\x10OutboundTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + @@ -4920,8 +4916,13 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x0fvisibility_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\x12 \n" + "\vdestination\x18\a \x01(\tR\vdestination\x12h\n" + "\x12state_machine_info\x18\b \x01(\v28.temporal.server.api.persistence.v1.StateMachineTaskInfoH\x00R\x10stateMachineInfo\x12[\n" + - "\x0fchasm_task_info\x18\t \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfoB\x0e\n" + - "\ftask_details\"3\n" + + "\x0fchasm_task_info\x18\t \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12q\n" + + "\x15activity_command_info\x18\n" + + " \x01(\v2;.temporal.server.api.persistence.v1.ActivityCommandTaskInfoH\x00R\x13activityCommandInfoB\x0e\n" + + "\ftask_details\"\x9f\x01\n" + + "\x17ActivityCommandTaskInfo\x12T\n" + + "\fcommand_type\x18\x01 \x01(\x0e21.temporal.server.api.enums.v1.ActivityCommandTypeR\vcommandType\x12.\n" + + "\x13scheduled_event_ids\x18\x02 \x03(\x03R\x11scheduledEventIds\"3\n" + "\x17NexusInvocationTaskInfo\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"4\n" + "\x18NexusCancelationTaskInfo\x12\x18\n" + @@ -5140,86 +5141,87 @@ var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ (*TimerTaskInfo)(nil), // 8: temporal.server.api.persistence.v1.TimerTaskInfo (*ArchivalTaskInfo)(nil), // 9: temporal.server.api.persistence.v1.ArchivalTaskInfo (*OutboundTaskInfo)(nil), // 10: temporal.server.api.persistence.v1.OutboundTaskInfo - (*NexusInvocationTaskInfo)(nil), // 11: temporal.server.api.persistence.v1.NexusInvocationTaskInfo - (*NexusCancelationTaskInfo)(nil), // 12: temporal.server.api.persistence.v1.NexusCancelationTaskInfo - (*ActivityInfo)(nil), // 13: temporal.server.api.persistence.v1.ActivityInfo - (*TimerInfo)(nil), // 14: temporal.server.api.persistence.v1.TimerInfo - (*ChildExecutionInfo)(nil), // 15: temporal.server.api.persistence.v1.ChildExecutionInfo - (*RequestCancelInfo)(nil), // 16: temporal.server.api.persistence.v1.RequestCancelInfo - (*SignalInfo)(nil), // 17: temporal.server.api.persistence.v1.SignalInfo - (*Checksum)(nil), // 18: temporal.server.api.persistence.v1.Checksum - (*Callback)(nil), // 19: temporal.server.api.persistence.v1.Callback - (*HSMCompletionCallbackArg)(nil), // 20: temporal.server.api.persistence.v1.HSMCompletionCallbackArg - (*CallbackInfo)(nil), // 21: temporal.server.api.persistence.v1.CallbackInfo - (*NexusOperationInfo)(nil), // 22: temporal.server.api.persistence.v1.NexusOperationInfo - (*NexusOperationCancellationInfo)(nil), // 23: temporal.server.api.persistence.v1.NexusOperationCancellationInfo - (*ResetChildInfo)(nil), // 24: temporal.server.api.persistence.v1.ResetChildInfo - (*WorkflowPauseInfo)(nil), // 25: temporal.server.api.persistence.v1.WorkflowPauseInfo - nil, // 26: temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry - nil, // 27: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry - nil, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry - nil, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry - nil, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry - nil, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry - nil, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry - nil, // 33: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry - (*TransferTaskInfo_CloseExecutionTaskDetails)(nil), // 34: temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - (*TransferTaskInfo_CancelActivityNexusTaskDetails)(nil), // 35: temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetails - (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 36: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - (*ActivityInfo_PauseInfo)(nil), // 37: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - (*ActivityInfo_PauseInfo_Manual)(nil), // 38: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - (*Callback_Nexus)(nil), // 39: temporal.server.api.persistence.v1.Callback.Nexus - (*Callback_HSM)(nil), // 40: temporal.server.api.persistence.v1.Callback.HSM - nil, // 41: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - (*CallbackInfo_WorkflowClosed)(nil), // 42: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - (*CallbackInfo_Trigger)(nil), // 43: temporal.server.api.persistence.v1.CallbackInfo.Trigger - (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 45: google.protobuf.Duration - (v1.WorkflowTaskType)(0), // 46: temporal.server.api.enums.v1.WorkflowTaskType - (v11.SuggestContinueAsNewReason)(0), // 47: temporal.api.enums.v1.SuggestContinueAsNewReason - (*v12.ResetPoints)(nil), // 48: temporal.api.workflow.v1.ResetPoints - (*v14.VersionHistories)(nil), // 49: temporal.server.api.history.v1.VersionHistories - (*v15.VectorClock)(nil), // 50: temporal.server.api.clock.v1.VectorClock - (*v16.BaseExecutionInfo)(nil), // 51: temporal.server.api.workflow.v1.BaseExecutionInfo - (*v13.WorkerVersionStamp)(nil), // 52: temporal.api.common.v1.WorkerVersionStamp - (*VersionedTransition)(nil), // 53: temporal.server.api.persistence.v1.VersionedTransition - (*StateMachineTimerGroup)(nil), // 54: temporal.server.api.persistence.v1.StateMachineTimerGroup - (*StateMachineTombstoneBatch)(nil), // 55: temporal.server.api.persistence.v1.StateMachineTombstoneBatch - (*v12.WorkflowExecutionVersioningInfo)(nil), // 56: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - (*v13.Priority)(nil), // 57: temporal.api.common.v1.Priority - (v11.WorkflowTaskFailedCause)(0), // 58: temporal.api.enums.v1.WorkflowTaskFailedCause - (v11.TimeoutType)(0), // 59: temporal.api.enums.v1.TimeoutType - (v1.WorkflowExecutionState)(0), // 60: temporal.server.api.enums.v1.WorkflowExecutionState - (v11.WorkflowExecutionStatus)(0), // 61: temporal.api.enums.v1.WorkflowExecutionStatus - (v11.EventType)(0), // 62: temporal.api.enums.v1.EventType - (v1.TaskType)(0), // 63: temporal.server.api.enums.v1.TaskType - (*ChasmTaskInfo)(nil), // 64: temporal.server.api.persistence.v1.ChasmTaskInfo - (v1.TaskPriority)(0), // 65: temporal.server.api.enums.v1.TaskPriority - (*v14.VersionHistoryItem)(nil), // 66: temporal.server.api.history.v1.VersionHistoryItem - (v1.WorkflowBackoffType)(0), // 67: temporal.server.api.enums.v1.WorkflowBackoffType - (*StateMachineTaskInfo)(nil), // 68: temporal.server.api.persistence.v1.StateMachineTaskInfo - (*v17.Failure)(nil), // 69: temporal.api.failure.v1.Failure - (*v13.Payloads)(nil), // 70: temporal.api.common.v1.Payloads - (*v13.ActivityType)(nil), // 71: temporal.api.common.v1.ActivityType - (*v18.Deployment)(nil), // 72: temporal.api.deployment.v1.Deployment - (*v18.WorkerDeploymentVersion)(nil), // 73: temporal.api.deployment.v1.WorkerDeploymentVersion - (v11.ParentClosePolicy)(0), // 74: temporal.api.enums.v1.ParentClosePolicy - (v1.ChecksumFlavor)(0), // 75: temporal.server.api.enums.v1.ChecksumFlavor - (*v13.Link)(nil), // 76: temporal.api.common.v1.Link - (*v19.HistoryEvent)(nil), // 77: temporal.api.history.v1.HistoryEvent - (v1.CallbackState)(0), // 78: temporal.server.api.enums.v1.CallbackState - (v1.NexusOperationState)(0), // 79: temporal.server.api.enums.v1.NexusOperationState - (v11.NexusOperationCancellationState)(0), // 80: temporal.api.enums.v1.NexusOperationCancellationState - (*QueueState)(nil), // 81: temporal.server.api.persistence.v1.QueueState - (*v13.Payload)(nil), // 82: temporal.api.common.v1.Payload - (*UpdateInfo)(nil), // 83: temporal.server.api.persistence.v1.UpdateInfo - (*StateMachineMap)(nil), // 84: temporal.server.api.persistence.v1.StateMachineMap - (*StateMachineRef)(nil), // 85: temporal.server.api.persistence.v1.StateMachineRef + (*ActivityCommandTaskInfo)(nil), // 11: temporal.server.api.persistence.v1.ActivityCommandTaskInfo + (*NexusInvocationTaskInfo)(nil), // 12: temporal.server.api.persistence.v1.NexusInvocationTaskInfo + (*NexusCancelationTaskInfo)(nil), // 13: temporal.server.api.persistence.v1.NexusCancelationTaskInfo + (*ActivityInfo)(nil), // 14: temporal.server.api.persistence.v1.ActivityInfo + (*TimerInfo)(nil), // 15: temporal.server.api.persistence.v1.TimerInfo + (*ChildExecutionInfo)(nil), // 16: temporal.server.api.persistence.v1.ChildExecutionInfo + (*RequestCancelInfo)(nil), // 17: temporal.server.api.persistence.v1.RequestCancelInfo + (*SignalInfo)(nil), // 18: temporal.server.api.persistence.v1.SignalInfo + (*Checksum)(nil), // 19: temporal.server.api.persistence.v1.Checksum + (*Callback)(nil), // 20: temporal.server.api.persistence.v1.Callback + (*HSMCompletionCallbackArg)(nil), // 21: temporal.server.api.persistence.v1.HSMCompletionCallbackArg + (*CallbackInfo)(nil), // 22: temporal.server.api.persistence.v1.CallbackInfo + (*NexusOperationInfo)(nil), // 23: temporal.server.api.persistence.v1.NexusOperationInfo + (*NexusOperationCancellationInfo)(nil), // 24: temporal.server.api.persistence.v1.NexusOperationCancellationInfo + (*ResetChildInfo)(nil), // 25: temporal.server.api.persistence.v1.ResetChildInfo + (*WorkflowPauseInfo)(nil), // 26: temporal.server.api.persistence.v1.WorkflowPauseInfo + nil, // 27: temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry + nil, // 28: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry + nil, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry + nil, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry + nil, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry + nil, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry + nil, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry + nil, // 34: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry + (*TransferTaskInfo_CloseExecutionTaskDetails)(nil), // 35: temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails + (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 36: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + (*ActivityInfo_PauseInfo)(nil), // 37: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + (*ActivityInfo_PauseInfo_Manual)(nil), // 38: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + (*Callback_Nexus)(nil), // 39: temporal.server.api.persistence.v1.Callback.Nexus + (*Callback_HSM)(nil), // 40: temporal.server.api.persistence.v1.Callback.HSM + nil, // 41: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + (*CallbackInfo_WorkflowClosed)(nil), // 42: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + (*CallbackInfo_Trigger)(nil), // 43: temporal.server.api.persistence.v1.CallbackInfo.Trigger + (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 45: google.protobuf.Duration + (v1.WorkflowTaskType)(0), // 46: temporal.server.api.enums.v1.WorkflowTaskType + (v11.SuggestContinueAsNewReason)(0), // 47: temporal.api.enums.v1.SuggestContinueAsNewReason + (*v12.ResetPoints)(nil), // 48: temporal.api.workflow.v1.ResetPoints + (*v14.VersionHistories)(nil), // 49: temporal.server.api.history.v1.VersionHistories + (*v15.VectorClock)(nil), // 50: temporal.server.api.clock.v1.VectorClock + (*v16.BaseExecutionInfo)(nil), // 51: temporal.server.api.workflow.v1.BaseExecutionInfo + (*v13.WorkerVersionStamp)(nil), // 52: temporal.api.common.v1.WorkerVersionStamp + (*VersionedTransition)(nil), // 53: temporal.server.api.persistence.v1.VersionedTransition + (*StateMachineTimerGroup)(nil), // 54: temporal.server.api.persistence.v1.StateMachineTimerGroup + (*StateMachineTombstoneBatch)(nil), // 55: temporal.server.api.persistence.v1.StateMachineTombstoneBatch + (*v12.WorkflowExecutionVersioningInfo)(nil), // 56: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + (*v13.Priority)(nil), // 57: temporal.api.common.v1.Priority + (v11.WorkflowTaskFailedCause)(0), // 58: temporal.api.enums.v1.WorkflowTaskFailedCause + (v11.TimeoutType)(0), // 59: temporal.api.enums.v1.TimeoutType + (v1.WorkflowExecutionState)(0), // 60: temporal.server.api.enums.v1.WorkflowExecutionState + (v11.WorkflowExecutionStatus)(0), // 61: temporal.api.enums.v1.WorkflowExecutionStatus + (v11.EventType)(0), // 62: temporal.api.enums.v1.EventType + (v1.TaskType)(0), // 63: temporal.server.api.enums.v1.TaskType + (*ChasmTaskInfo)(nil), // 64: temporal.server.api.persistence.v1.ChasmTaskInfo + (v1.TaskPriority)(0), // 65: temporal.server.api.enums.v1.TaskPriority + (*v14.VersionHistoryItem)(nil), // 66: temporal.server.api.history.v1.VersionHistoryItem + (v1.WorkflowBackoffType)(0), // 67: temporal.server.api.enums.v1.WorkflowBackoffType + (*StateMachineTaskInfo)(nil), // 68: temporal.server.api.persistence.v1.StateMachineTaskInfo + (v1.ActivityCommandType)(0), // 69: temporal.server.api.enums.v1.ActivityCommandType + (*v17.Failure)(nil), // 70: temporal.api.failure.v1.Failure + (*v13.Payloads)(nil), // 71: temporal.api.common.v1.Payloads + (*v13.ActivityType)(nil), // 72: temporal.api.common.v1.ActivityType + (*v18.Deployment)(nil), // 73: temporal.api.deployment.v1.Deployment + (*v18.WorkerDeploymentVersion)(nil), // 74: temporal.api.deployment.v1.WorkerDeploymentVersion + (v11.ParentClosePolicy)(0), // 75: temporal.api.enums.v1.ParentClosePolicy + (v1.ChecksumFlavor)(0), // 76: temporal.server.api.enums.v1.ChecksumFlavor + (*v13.Link)(nil), // 77: temporal.api.common.v1.Link + (*v19.HistoryEvent)(nil), // 78: temporal.api.history.v1.HistoryEvent + (v1.CallbackState)(0), // 79: temporal.server.api.enums.v1.CallbackState + (v1.NexusOperationState)(0), // 80: temporal.server.api.enums.v1.NexusOperationState + (v11.NexusOperationCancellationState)(0), // 81: temporal.api.enums.v1.NexusOperationCancellationState + (*QueueState)(nil), // 82: temporal.server.api.persistence.v1.QueueState + (*v13.Payload)(nil), // 83: temporal.api.common.v1.Payload + (*UpdateInfo)(nil), // 84: temporal.server.api.persistence.v1.UpdateInfo + (*StateMachineMap)(nil), // 85: temporal.server.api.persistence.v1.StateMachineMap + (*StateMachineRef)(nil), // 86: temporal.server.api.persistence.v1.StateMachineRef } var file_temporal_server_api_persistence_v1_executions_proto_depIdxs = []int32{ 44, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp - 26, // 1: temporal.server.api.persistence.v1.ShardInfo.replication_dlq_ack_level:type_name -> temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry - 27, // 2: temporal.server.api.persistence.v1.ShardInfo.queue_states:type_name -> temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry + 27, // 1: temporal.server.api.persistence.v1.ShardInfo.replication_dlq_ack_level:type_name -> temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry + 28, // 2: temporal.server.api.persistence.v1.ShardInfo.queue_states:type_name -> temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry 45, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration 45, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration 45, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration @@ -5236,8 +5238,8 @@ var file_temporal_server_api_persistence_v1_executions_proto_depIdxs = []int32{ 45, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration 44, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp 48, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints - 28, // 19: temporal.server.api.persistence.v1.WorkflowExecutionInfo.search_attributes:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry - 29, // 20: temporal.server.api.persistence.v1.WorkflowExecutionInfo.memo:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry + 29, // 19: temporal.server.api.persistence.v1.WorkflowExecutionInfo.search_attributes:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry + 30, // 20: temporal.server.api.persistence.v1.WorkflowExecutionInfo.memo:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry 49, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories 2, // 22: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_stats:type_name -> temporal.server.api.persistence.v1.ExecutionStats 44, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp @@ -5246,9 +5248,9 @@ var file_temporal_server_api_persistence_v1_executions_proto_depIdxs = []int32{ 44, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp 51, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo 52, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 30, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.update_infos:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry + 31, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.update_infos:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry 53, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 31, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machines_by_type:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry + 32, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machines_by_type:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry 54, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup 53, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition 53, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition @@ -5257,117 +5259,118 @@ var file_temporal_server_api_persistence_v1_executions_proto_depIdxs = []int32{ 56, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo 53, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition 53, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 32, // 40: temporal.server.api.persistence.v1.WorkflowExecutionInfo.children_initialized_post_reset_point:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry + 33, // 40: temporal.server.api.persistence.v1.WorkflowExecutionInfo.children_initialized_post_reset_point:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry 57, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority - 25, // 42: temporal.server.api.persistence.v1.WorkflowExecutionInfo.pause_info:type_name -> temporal.server.api.persistence.v1.WorkflowPauseInfo + 26, // 42: temporal.server.api.persistence.v1.WorkflowExecutionInfo.pause_info:type_name -> temporal.server.api.persistence.v1.WorkflowPauseInfo 58, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause 59, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType 60, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState 61, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus 53, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition 44, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp - 33, // 49: temporal.server.api.persistence.v1.WorkflowExecutionState.request_ids:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry + 34, // 49: temporal.server.api.persistence.v1.WorkflowExecutionState.request_ids:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry 62, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType 63, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType 44, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 34, // 53: temporal.server.api.persistence.v1.TransferTaskInfo.close_execution_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails + 35, // 53: temporal.server.api.persistence.v1.TransferTaskInfo.close_execution_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails 64, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 35, // 55: temporal.server.api.persistence.v1.TransferTaskInfo.cancel_activity_nexus_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CancelActivityNexusTaskDetails - 63, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 65, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority - 53, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 6, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo - 66, // 61: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 63, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 44, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp - 64, // 65: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 59, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType - 67, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType - 44, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 64, // 70: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 72: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 63, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 68, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo - 64, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 44, // 77: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 78: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp - 45, // 79: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 45, // 81: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 45, // 82: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration - 45, // 83: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 45, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 44, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp - 69, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure - 70, // 87: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads - 44, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp - 71, // 89: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType - 36, // 90: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - 52, // 91: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 53, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 44, // 93: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 94: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 72, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment - 73, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion - 57, // 97: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority - 37, // 98: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - 44, // 99: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp - 53, // 100: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 74, // 101: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy - 50, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 53, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 57, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority - 53, // 105: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 106: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 75, // 107: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor - 39, // 108: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus - 40, // 109: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM - 76, // 110: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link - 77, // 111: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent - 19, // 112: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback - 43, // 113: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger - 44, // 114: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp - 78, // 115: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState - 44, // 116: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 69, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 118: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 45, // 119: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 79, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState - 44, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 69, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 45, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp - 44, // 128: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp - 80, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState - 44, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 69, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 44, // 133: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 81, // 134: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState - 82, // 135: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload - 82, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload - 83, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo - 84, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap - 24, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo - 4, // 140: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo - 44, // 141: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 38, // 142: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - 41, // 143: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - 85, // 144: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef - 42, // 145: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - 146, // [146:146] is the sub-list for method output_type - 146, // [146:146] is the sub-list for method input_type - 146, // [146:146] is the sub-list for extension type_name - 146, // [146:146] is the sub-list for extension extendee - 0, // [0:146] is the sub-list for field type_name + 63, // 55: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 65, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority + 53, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 6, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo + 66, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 63, // 61: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 44, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp + 64, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 63, // 65: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 59, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType + 67, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType + 44, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 64, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 63, // 70: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 63, // 72: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 68, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo + 64, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 11, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.activity_command_info:type_name -> temporal.server.api.persistence.v1.ActivityCommandTaskInfo + 69, // 77: temporal.server.api.persistence.v1.ActivityCommandTaskInfo.command_type:type_name -> temporal.server.api.enums.v1.ActivityCommandType + 44, // 78: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 79: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp + 45, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 81: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 82: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 83: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration + 45, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 45, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 44, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp + 70, // 87: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure + 71, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads + 44, // 89: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp + 72, // 90: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType + 36, // 91: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + 52, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 53, // 93: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 44, // 94: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 73, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment + 74, // 97: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 57, // 98: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority + 37, // 99: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + 44, // 100: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp + 53, // 101: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 75, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy + 50, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 53, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 57, // 105: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 53, // 106: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 107: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 76, // 108: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor + 39, // 109: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus + 40, // 110: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM + 77, // 111: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link + 78, // 112: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent + 20, // 113: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback + 43, // 114: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger + 44, // 115: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp + 79, // 116: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState + 44, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 70, // 118: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 119: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 45, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 44, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 80, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState + 44, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 70, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 45, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 44, // 128: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp + 44, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp + 81, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState + 44, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 70, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 133: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 44, // 134: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 82, // 135: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState + 83, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload + 83, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload + 84, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo + 85, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap + 25, // 140: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo + 4, // 141: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo + 44, // 142: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 38, // 143: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + 41, // 144: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + 86, // 145: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef + 42, // 146: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + 147, // [147:147] is the sub-list for method output_type + 147, // [147:147] is the sub-list for method input_type + 147, // [147:147] is the sub-list for extension type_name + 147, // [147:147] is the sub-list for extension extendee + 0, // [0:147] is the sub-list for field type_name } func init() { file_temporal_server_api_persistence_v1_executions_proto_init() } @@ -5386,7 +5389,6 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { file_temporal_server_api_persistence_v1_executions_proto_msgTypes[5].OneofWrappers = []any{ (*TransferTaskInfo_CloseExecutionTaskDetails_)(nil), (*TransferTaskInfo_ChasmTaskInfo)(nil), - (*TransferTaskInfo_CancelActivityNexusTaskDetails_)(nil), } file_temporal_server_api_persistence_v1_executions_proto_msgTypes[7].OneofWrappers = []any{ (*VisibilityTaskInfo_ChasmTaskInfo)(nil), @@ -5397,12 +5399,13 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { file_temporal_server_api_persistence_v1_executions_proto_msgTypes[10].OneofWrappers = []any{ (*OutboundTaskInfo_StateMachineInfo)(nil), (*OutboundTaskInfo_ChasmTaskInfo)(nil), + (*OutboundTaskInfo_ActivityCommandInfo)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[13].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[14].OneofWrappers = []any{ (*ActivityInfo_UseWorkflowBuildIdInfo_)(nil), (*ActivityInfo_LastIndependentlyAssignedBuildId)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[19].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[20].OneofWrappers = []any{ (*Callback_Nexus_)(nil), (*Callback_Hsm)(nil), } diff --git a/common/metrics/metric_defs.go b/common/metrics/metric_defs.go index f77fb50d785..08c99e689dc 100644 --- a/common/metrics/metric_defs.go +++ b/common/metrics/metric_defs.go @@ -567,7 +567,6 @@ const ( TaskTypeTransferActiveTaskStartChildExecution = "TransferActiveTaskStartChildExecution" TaskTypeTransferActiveTaskResetWorkflow = "TransferActiveTaskResetWorkflow" TaskTypeTransferActiveTaskDeleteExecution = "TransferActiveTaskDeleteExecution" - TaskTypeTransferActiveTaskCancelActivityNexus = "TransferActiveTaskCancelActivityNexus" TaskTypeTransferStandbyTaskActivity = "TransferStandbyTaskActivity" TaskTypeTransferStandbyTaskWorkflowTask = "TransferStandbyTaskWorkflowTask" TaskTypeTransferStandbyTaskCloseExecution = "TransferStandbyTaskCloseExecution" diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index 62b2fc343a2..2e7b3a19ec6 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -38,8 +38,6 @@ func serializeTransferTask( transferTask = transferDeleteExecutionTaskToProto(task) case *tasks.ChasmTask: transferTask = transferChasmTaskToProto(task) - case *tasks.CancelActivityNexusTask: - transferTask = transferCancelActivityNexusTaskToProto(task) default: return nil, serviceerror.NewInternalf("Unknown transfer task type: %v", task) } @@ -88,8 +86,6 @@ func deserializeTransferTask( task = transferDeleteExecutionTaskFromProto(transferTask) case enumsspb.TASK_TYPE_CHASM: task = transferChasmTaskFromProto(transferTask) - case enumsspb.TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS: - task = transferCancelActivityNexusTaskFromProto(transferTask) default: return nil, serviceerror.NewInternalf("Unknown transfer task type: %v", transferTask.TaskType) } @@ -110,40 +106,6 @@ func transferChasmTaskFromProto(task *persistencespb.TransferTaskInfo) tasks.Tas } } -func transferCancelActivityNexusTaskToProto(task *tasks.CancelActivityNexusTask) *persistencespb.TransferTaskInfo { - return &persistencespb.TransferTaskInfo{ - NamespaceId: task.NamespaceID, - WorkflowId: task.WorkflowID, - RunId: task.RunID, - TaskId: task.TaskID, - TaskType: task.GetType(), - Version: task.Version, - VisibilityTime: timestamppb.New(task.VisibilityTimestamp), - TaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails_{ - CancelActivityNexusTaskDetails: &persistencespb.TransferTaskInfo_CancelActivityNexusTaskDetails{ - ScheduledEventIds: task.ScheduledEventIDs, - WorkerControlTaskQueue: task.WorkerControlTaskQueue, - }, - }, - } -} - -func transferCancelActivityNexusTaskFromProto(task *persistencespb.TransferTaskInfo) tasks.Task { - details := task.GetCancelActivityNexusTaskDetails() - return &tasks.CancelActivityNexusTask{ - WorkflowKey: definition.NewWorkflowKey( - task.NamespaceId, - task.WorkflowId, - task.RunId, - ), - VisibilityTimestamp: task.VisibilityTime.AsTime(), - TaskID: task.TaskId, - Version: task.Version, - ScheduledEventIDs: details.GetScheduledEventIds(), - WorkerControlTaskQueue: details.GetWorkerControlTaskQueue(), - } -} - func serializeTimerTask( encoder Encoder, task tasks.Task, @@ -1484,6 +1446,22 @@ func serializeOutboundTask( ChasmTaskInfo: task.Info, }, } + case *tasks.ActivityCommandTask: + outboundTaskInfo = &persistencespb.OutboundTaskInfo{ + NamespaceId: task.NamespaceID, + WorkflowId: task.WorkflowID, + RunId: task.RunID, + TaskId: task.TaskID, + TaskType: task.GetType(), + Destination: task.Destination, + VisibilityTime: timestamppb.New(task.VisibilityTimestamp), + TaskDetails: &persistencespb.OutboundTaskInfo_ActivityCommandInfo{ + ActivityCommandInfo: &persistencespb.ActivityCommandTaskInfo{ + CommandType: task.CommandType, + ScheduledEventIds: task.ScheduledEventIDs, + }, + }, + } default: return nil, serviceerror.NewInternalf("unknown outbound task type while serializing: %v", task) } @@ -1526,6 +1504,20 @@ func deserializeOutboundTask( Info: info.GetChasmTaskInfo(), Destination: info.Destination, }, nil + case enumsspb.TASK_TYPE_ACTIVITY_COMMAND: + activityCommandInfo := info.GetActivityCommandInfo() + return &tasks.ActivityCommandTask{ + WorkflowKey: definition.NewWorkflowKey( + info.NamespaceId, + info.WorkflowId, + info.RunId, + ), + VisibilityTimestamp: info.VisibilityTime.AsTime(), + TaskID: info.TaskId, + CommandType: activityCommandInfo.GetCommandType(), + ScheduledEventIDs: activityCommandInfo.GetScheduledEventIds(), + Destination: info.Destination, + }, nil default: return nil, serviceerror.NewInternalf("unknown outbound task type while deserializing: %v", info) } diff --git a/common/persistence/serialization/task_serializers_test.go b/common/persistence/serialization/task_serializers_test.go index 6e7ad603d9c..3d635274635 100644 --- a/common/persistence/serialization/task_serializers_test.go +++ b/common/persistence/serialization/task_serializers_test.go @@ -169,17 +169,17 @@ func (s *taskSerializerSuite) TestTransferResetTask() { s.assertEqualTasks(resetTask) } -func (s *taskSerializerSuite) TestTransferCancelActivityNexusTask() { - cancelActivityNexusTask := &tasks.CancelActivityNexusTask{ - WorkflowKey: s.workflowKey, - VisibilityTimestamp: time.Unix(0, rand.Int63()).UTC(), - TaskID: rand.Int63(), - Version: rand.Int63(), - ScheduledEventIDs: []int64{rand.Int63(), rand.Int63(), rand.Int63()}, - WorkerControlTaskQueue: "test-control-queue", +func (s *taskSerializerSuite) TestOutboundActivityCommandTask() { + activityCommandTask := &tasks.ActivityCommandTask{ + WorkflowKey: s.workflowKey, + VisibilityTimestamp: time.Unix(0, rand.Int63()).UTC(), + TaskID: rand.Int63(), + CommandType: enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, + ScheduledEventIDs: []int64{rand.Int63(), rand.Int63(), rand.Int63()}, + Destination: "test-control-queue", } - s.assertEqualTasks(cancelActivityNexusTask) + s.assertEqualTasks(activityCommandTask) } func (s *taskSerializerSuite) TestTimerWorkflowTask() { diff --git a/common/testing/testvars/test_vars.go b/common/testing/testvars/test_vars.go index 23e823412c9..1e1ab4ec6c5 100644 --- a/common/testing/testvars/test_vars.go +++ b/common/testing/testvars/test_vars.go @@ -396,6 +396,15 @@ func (tv *TestVars) WorkerIdentity() string { return getOrCreate(tv, "worker_identity", tv.uniqueString, tv.stringNSetter) } +func (tv *TestVars) WorkerInstanceKey() string { + return getOrCreate(tv, "worker_instance_key", tv.uniqueString, tv.stringNSetter) +} + +// ControlQueueName returns the Nexus task queue name used to deliver control tasks to this worker. +func (tv *TestVars) ControlQueueName(ns string) string { + return fmt.Sprintf("/temporal-sys/worker-commands/%s/%s", ns, tv.WorkerInstanceKey()) +} + func (tv *TestVars) TimerID() string { return getOrCreate(tv, "timer_id", tv.uniqueString, tv.stringNSetter) } diff --git a/proto/internal/temporal/server/api/enums/v1/task.proto b/proto/internal/temporal/server/api/enums/v1/task.proto index c0150d2abd3..4eb75a50fee 100644 --- a/proto/internal/temporal/server/api/enums/v1/task.proto +++ b/proto/internal/temporal/server/api/enums/v1/task.proto @@ -60,8 +60,14 @@ enum TaskType { // A task with side effects generated by a CHASM component. TASK_TYPE_CHASM = 33; - // A task to cancel a running activity via Nexus control queue. - TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS = 34; + // A task to send commands to activities via Nexus. Also see ActivityCommandType. + TASK_TYPE_ACTIVITY_COMMAND = 34; +} + +// ActivityCommandType specifies the type of command to send to activities. +enum ActivityCommandType { + ACTIVITY_COMMAND_TYPE_UNSPECIFIED = 0; + ACTIVITY_COMMAND_TYPE_CANCEL = 1; } // TaskPriority is only used for replication task as of May 2024 diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 00fb34de5ff..3a1d68f47d1 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -351,21 +351,11 @@ message TransferTaskInfo { bool can_skip_visibility_archival = 1; } - // Details for a Nexus task that cancels activities. - message CancelActivityNexusTaskDetails { - // Scheduled event IDs of activities to cancel. - repeated int64 scheduled_event_ids = 1; - // The Nexus queue to dispatch the cancel request to. - string worker_control_task_queue = 2; - } - oneof task_details { CloseExecutionTaskDetails close_execution_task_details = 16; // If the task addresses a CHASM component, this field will be set. ChasmTaskInfo chasm_task_info = 18; - - CancelActivityNexusTaskDetails cancel_activity_nexus_task_details = 19; } // Stamp represents the "version" of the entity's internal state for which the transfer task was created. // It increases monotonically when the entity's options are modified. @@ -493,9 +483,20 @@ message OutboundTaskInfo { // If the task addresses a CHASM component, this field will be set. ChasmTaskInfo chasm_task_info = 9; + + // If the task is an activity command task. + ActivityCommandTaskInfo activity_command_info = 10; } } +// ActivityCommandTaskInfo contains details for activity command operations. +message ActivityCommandTaskInfo { + // Type of command to send. + temporal.server.api.enums.v1.ActivityCommandType command_type = 1; + // Scheduled event IDs of activities to send command to. + repeated int64 scheduled_event_ids = 2; +} + message NexusInvocationTaskInfo { int32 attempt = 1; } diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 31c03d70e9e..46218b4f448 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -18,6 +18,7 @@ import ( protocolpb "go.temporal.io/api/protocol/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/api/matchingservice/v1" "go.temporal.io/server/common" @@ -57,15 +58,16 @@ type ( workflowTaskCompletedID int64 // internal state - hasBufferedEventsOrMessages bool - workflowTaskFailedCause *workflowTaskFailedCause - activityNotStartedCancelled bool - newMutableState historyi.MutableState - stopProcessing bool // should stop processing any more commands - mutableState historyi.MutableState - effects effect.Controller - initiatedChildExecutionsInBatch map[string]struct{} // Set of initiated child executions in the workflow task - updateRegistry update.Registry + hasBufferedEventsOrMessages bool + workflowTaskFailedCause *workflowTaskFailedCause + activityNotStartedCancelled bool + newMutableState historyi.MutableState + stopProcessing bool // should stop processing any more commands + mutableState historyi.MutableState + effects effect.Controller + initiatedChildExecutionsInBatch map[string]struct{} // Set of initiated child executions in the workflow task + updateRegistry update.Registry + pendingActivityCancelsByControlQueue map[string][]int64 // Batched activity cancels by control queue // validation attrValidator *api.CommandAttrValidator @@ -210,6 +212,10 @@ func (handler *workflowTaskCompletedHandler) handleCommands( } } + if err := handler.flushBatchedActivityCommandTasks(); err != nil { + return nil, err + } + return mutations, nil } @@ -664,17 +670,35 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( return nil, err } handler.activityNotStartedCancelled = true - } else if ai.StartedEventId != common.EmptyEventID { - // Activity has started - create cancel task and send to worker via Nexus. - // TODO: Batch tasks for the same control queue. - if err := handler.mutableState.AddCancelActivityNexusTasks(ai.ScheduledEventId); err != nil { - return nil, err + } else if ai.StartedEventId != common.EmptyEventID && ai.WorkerControlTaskQueue != "" { + // Activity has started and worker supports Nexus control tasks - collect for batched dispatch. + if handler.pendingActivityCancelsByControlQueue == nil { + handler.pendingActivityCancelsByControlQueue = make(map[string][]int64) } + handler.pendingActivityCancelsByControlQueue[ai.WorkerControlTaskQueue] = append( + handler.pendingActivityCancelsByControlQueue[ai.WorkerControlTaskQueue], + ai.ScheduledEventId, + ) } } return actCancelReqEvent, nil } +// flushBatchedActivityCommandTasks creates ActivityCommandTasks for all collected activity cancellations, +// batched by control queue. +func (handler *workflowTaskCompletedHandler) flushBatchedActivityCommandTasks() error { + for controlQueue, scheduledEventIDs := range handler.pendingActivityCancelsByControlQueue { + if err := handler.mutableState.AddActivityCommandTasks( + scheduledEventIDs, + controlQueue, + enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, + ); err != nil { + return err + } + } + return nil +} + func (handler *workflowTaskCompletedHandler) handleCommandStartTimer( _ context.Context, attr *commandpb.StartTimerCommandAttributes, diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go index c68b8d1478f..ec87e5f2651 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go @@ -16,6 +16,7 @@ import ( sdkpb "go.temporal.io/api/sdk/v1" "go.temporal.io/api/serviceerror" updatepb "go.temporal.io/api/update/v1" + enumsspb "go.temporal.io/server/api/enums/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/backoff" "go.temporal.io/server/common/collection" @@ -384,3 +385,71 @@ func mustMarshalAny(t *testing.T, pb proto.Message) *anypb.Any { require.NoError(t, a.MarshalFrom(pb)) return &a } + +func TestFlushBatchedActivityCommandTasks(t *testing.T) { + t.Parallel() + + t.Run("batches activities by control queue", func(t *testing.T) { + ctrl := gomock.NewController(t) + ms := historyi.NewMockMutableState(ctrl) + + ms.EXPECT().AddActivityCommandTasks( + []int64{5, 6, 7}, + "control-queue-1", + gomock.Any(), + ).Return(nil).Times(1) + + handler := &workflowTaskCompletedHandler{ + mutableState: ms, + pendingActivityCancelsByControlQueue: map[string][]int64{ + "control-queue-1": {5, 6, 7}, + }, + } + + err := handler.flushBatchedActivityCommandTasks() + require.NoError(t, err) + }) + + t.Run("creates separate tasks for different control queues", func(t *testing.T) { + ctrl := gomock.NewController(t) + ms := historyi.NewMockMutableState(ctrl) + + // Capture calls to verify both queues are processed + calls := make(map[string][]int64) + ms.EXPECT().AddActivityCommandTasks( + gomock.Any(), + gomock.Any(), + enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, + ).DoAndReturn(func(ids []int64, queue string, _ enumsspb.ActivityCommandType) error { + calls[queue] = ids + return nil + }).Times(2) + + handler := &workflowTaskCompletedHandler{ + mutableState: ms, + pendingActivityCancelsByControlQueue: map[string][]int64{ + "control-queue-1": {5, 6}, + "control-queue-2": {7, 8}, + }, + } + + err := handler.flushBatchedActivityCommandTasks() + require.NoError(t, err) + + require.Equal(t, []int64{5, 6}, calls["control-queue-1"]) + require.Equal(t, []int64{7, 8}, calls["control-queue-2"]) + }) + + t.Run("does nothing when no pending cancels", func(t *testing.T) { + ctrl := gomock.NewController(t) + ms := historyi.NewMockMutableState(ctrl) + + handler := &workflowTaskCompletedHandler{ + mutableState: ms, + pendingActivityCancelsByControlQueue: nil, + } + + err := handler.flushBatchedActivityCommandTasks() + require.NoError(t, err) + }) +} diff --git a/service/history/interfaces/mutable_state.go b/service/history/interfaces/mutable_state.go index ebf16ab351c..631d64bc43a 100644 --- a/service/history/interfaces/mutable_state.go +++ b/service/history/interfaces/mutable_state.go @@ -46,7 +46,7 @@ type ( AddActivityTaskCancelRequestedEvent(int64, int64, string) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) AddActivityTaskCanceledEvent(int64, int64, int64, *commonpb.Payloads, string) (*historypb.HistoryEvent, error) - AddCancelActivityNexusTasks(int64) error + AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error AddActivityTaskCompletedEvent(int64, int64, *workflowservice.RespondActivityTaskCompletedRequest) (*historypb.HistoryEvent, error) AddActivityTaskFailedEvent(int64, int64, *failurepb.Failure, enumspb.RetryState, string, *commonpb.WorkerVersionStamp) (*historypb.HistoryEvent, error) AddActivityTaskScheduledEvent(int64, *commandpb.ScheduleActivityTaskCommandAttributes, bool) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) diff --git a/service/history/interfaces/mutable_state_mock.go b/service/history/interfaces/mutable_state_mock.go index 9b89e383594..c0a0f7f2d55 100644 --- a/service/history/interfaces/mutable_state_mock.go +++ b/service/history/interfaces/mutable_state_mock.go @@ -408,6 +408,20 @@ func (mr *MockMutableStateMockRecorder) AddHistorySize(size any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHistorySize", reflect.TypeOf((*MockMutableState)(nil).AddHistorySize), size) } +// AddActivityCommandTasks mocks base method. +func (m *MockMutableState) AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, notificationType enums0.ActivityCommandType) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddActivityCommandTasks", scheduledEventIDs, controlQueue, notificationType) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddActivityCommandTasks indicates an expected call of AddActivityCommandTasks. +func (mr *MockMutableStateMockRecorder) AddActivityCommandTasks(scheduledEventIDs, controlQueue, notificationType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityCommandTasks", reflect.TypeOf((*MockMutableState)(nil).AddActivityCommandTasks), scheduledEventIDs, controlQueue, notificationType) +} + // AddReapplyCandidateEvent mocks base method. func (m *MockMutableState) AddReapplyCandidateEvent(event *history.HistoryEvent) { m.ctrl.T.Helper() diff --git a/service/history/queues/metrics.go b/service/history/queues/metrics.go index c2b581eb617..65148834cb0 100644 --- a/service/history/queues/metrics.go +++ b/service/history/queues/metrics.go @@ -48,8 +48,6 @@ func GetActiveTransferTaskTypeTagValue( return metrics.TaskTypeTransferActiveTaskResetWorkflow case *tasks.DeleteExecutionTask: return metrics.TaskTypeTransferActiveTaskDeleteExecution - case *tasks.CancelActivityNexusTask: - return metrics.TaskTypeTransferActiveTaskCancelActivityNexus case *tasks.ChasmTask: return prefix + "." + getCHASMTaskTypeTagValue(t, chasmRegistry) default: @@ -198,6 +196,8 @@ func GetOutboundTaskTypeTagValue( return prefix + "." + task.StateMachineTaskType() case *tasks.ChasmTask: return prefix + "." + getCHASMTaskTypeTagValue(task, chasmRegistry) + case *tasks.ActivityCommandTask: + return prefix + ".ActivityCommand." + task.CommandType.String() default: return prefix + "Unknown" } diff --git a/service/history/queues/priority_assigner.go b/service/history/queues/priority_assigner.go index e8ee618f3e4..cbaf7d4b951 100644 --- a/service/history/queues/priority_assigner.go +++ b/service/history/queues/priority_assigner.go @@ -49,7 +49,6 @@ func (a *priorityAssignerImpl) Assign(executable Executable) tasks.Priority { enumsspb.TASK_TYPE_TRANSFER_DELETE_EXECUTION, enumsspb.TASK_TYPE_VISIBILITY_DELETE_EXECUTION, enumsspb.TASK_TYPE_ARCHIVAL_ARCHIVE_EXECUTION, - enumsspb.TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS, enumsspb.TASK_TYPE_UNSPECIFIED: // add more task types here if we believe it's ok to delay those tasks // and assign them the same priority as throttled tasks diff --git a/service/history/tasks/activity_command_task.go b/service/history/tasks/activity_command_task.go new file mode 100644 index 00000000000..805c9d6eb06 --- /dev/null +++ b/service/history/tasks/activity_command_task.go @@ -0,0 +1,72 @@ +package tasks + +import ( + "fmt" + "time" + + enumsspb "go.temporal.io/server/api/enums/v1" + "go.temporal.io/server/common/definition" +) + +var _ Task = (*ActivityCommandTask)(nil) +var _ HasDestination = (*ActivityCommandTask)(nil) + +type ( + // ActivityCommandTask sends commands to activities via Nexus. + ActivityCommandTask struct { + definition.WorkflowKey + VisibilityTimestamp time.Time + TaskID int64 + + // CommandType specifies the type of command. + CommandType enumsspb.ActivityCommandType + // ScheduledEventIDs of activities to send command to (batched by worker). + ScheduledEventIDs []int64 + // Destination is the worker control task queue for outbound queue grouping. + Destination string + } +) + +func (t *ActivityCommandTask) GetKey() Key { + return NewImmediateKey(t.TaskID) +} + +func (t *ActivityCommandTask) GetTaskID() int64 { + return t.TaskID +} + +func (t *ActivityCommandTask) SetTaskID(id int64) { + t.TaskID = id +} + +func (t *ActivityCommandTask) GetVisibilityTime() time.Time { + return t.VisibilityTimestamp +} + +func (t *ActivityCommandTask) SetVisibilityTime(timestamp time.Time) { + t.VisibilityTimestamp = timestamp +} + +func (t *ActivityCommandTask) GetCategory() Category { + return CategoryOutbound +} + +func (t *ActivityCommandTask) GetType() enumsspb.TaskType { + return enumsspb.TASK_TYPE_ACTIVITY_COMMAND +} + +// GetDestination implements HasDestination for outbound queue grouping. +func (t *ActivityCommandTask) GetDestination() string { + return t.Destination +} + +func (t *ActivityCommandTask) String() string { + return fmt.Sprintf("ActivityCommandTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, CommandType: %v, ScheduledEventIDs: %v, Destination: %v}", + t.WorkflowKey.String(), + t.VisibilityTimestamp, + t.TaskID, + t.CommandType, + t.ScheduledEventIDs, + t.Destination, + ) +} diff --git a/service/history/tasks/cancel_activity_nexus_task.go b/service/history/tasks/cancel_activity_nexus_task.go deleted file mode 100644 index 0ed3112daa0..00000000000 --- a/service/history/tasks/cancel_activity_nexus_task.go +++ /dev/null @@ -1,71 +0,0 @@ -package tasks - -import ( - "fmt" - "time" - - enumsspb "go.temporal.io/server/api/enums/v1" - "go.temporal.io/server/common/definition" -) - -var _ Task = (*CancelActivityNexusTask)(nil) - -type ( - CancelActivityNexusTask struct { - definition.WorkflowKey - VisibilityTimestamp time.Time - TaskID int64 - Version int64 - - // ScheduledEventIDs of activities to cancel (batched by worker). - ScheduledEventIDs []int64 - // WorkerControlTaskQueue is the Nexus queue to dispatch the cancel request to. - WorkerControlTaskQueue string - } -) - -func (t *CancelActivityNexusTask) GetKey() Key { - return NewImmediateKey(t.TaskID) -} - -func (t *CancelActivityNexusTask) GetVersion() int64 { - return t.Version -} - -func (t *CancelActivityNexusTask) SetVersion(version int64) { - t.Version = version -} - -func (t *CancelActivityNexusTask) GetTaskID() int64 { - return t.TaskID -} - -func (t *CancelActivityNexusTask) SetTaskID(id int64) { - t.TaskID = id -} - -func (t *CancelActivityNexusTask) GetVisibilityTime() time.Time { - return t.VisibilityTimestamp -} - -func (t *CancelActivityNexusTask) SetVisibilityTime(timestamp time.Time) { - t.VisibilityTimestamp = timestamp -} - -func (t *CancelActivityNexusTask) GetCategory() Category { - return CategoryTransfer -} - -func (t *CancelActivityNexusTask) GetType() enumsspb.TaskType { - return enumsspb.TASK_TYPE_TRANSFER_CANCEL_ACTIVITY_NEXUS -} - -func (t *CancelActivityNexusTask) String() string { - return fmt.Sprintf("CancelActivityNexusTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, ScheduledEventIDs: %v, Version: %v}", - t.WorkflowKey.String(), - t.VisibilityTimestamp, - t.TaskID, - t.ScheduledEventIDs, - t.Version, - ) -} diff --git a/service/history/tasks/utils.go b/service/history/tasks/utils.go index 1784b608bdd..3c446342b6d 100644 --- a/service/history/tasks/utils.go +++ b/service/history/tasks/utils.go @@ -78,13 +78,6 @@ func GetTransferTaskEventID( eventID = common.FirstEventID case *ChasmTask: return getChasmTaskEventID() - case *CancelActivityNexusTask: - if len(task.ScheduledEventIDs) > 0 { - eventID = task.ScheduledEventIDs[0] - } else { - // Should never happen. - eventID = common.FirstEventID - } case *FakeTask: // no-op default: diff --git a/service/history/transfer_queue_active_task_executor.go b/service/history/transfer_queue_active_task_executor.go index 4fda52b25ae..3fba82a3e86 100644 --- a/service/history/transfer_queue_active_task_executor.go +++ b/service/history/transfer_queue_active_task_executor.go @@ -145,9 +145,6 @@ func (t *transferQueueActiveTaskExecutor) Execute( err = t.processDeleteExecutionTask(ctx, task) case *tasks.ChasmTask: err = t.executeChasmSideEffectTransferTask(ctx, task) - case *tasks.CancelActivityNexusTask: - // TODO: Implement dispatch to worker control queue - err = nil default: err = errUnknownTransferTask } diff --git a/service/history/transfer_queue_standby_task_executor.go b/service/history/transfer_queue_standby_task_executor.go index 03839817966..4e61c43aafd 100644 --- a/service/history/transfer_queue_standby_task_executor.go +++ b/service/history/transfer_queue_standby_task_executor.go @@ -108,10 +108,6 @@ func (t *transferQueueStandbyTaskExecutor) Execute( err = t.processDeleteExecutionTask(ctx, task, false) case *tasks.ChasmTask: err = t.executeChasmSideEffectTransferTask(ctx, task) - case *tasks.CancelActivityNexusTask: - // Nexus operation is synchronous. So if the failover happens waiting for the Nexus response, - // the task will be retried in standby. - err = nil default: err = errUnknownTransferTask } diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index 759102c90e5..29b714685d0 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -4369,8 +4369,8 @@ func (ms *MutableStateImpl) AddActivityTaskCancelRequestedEvent( return actCancelReqEvent, ai, nil } -func (ms *MutableStateImpl) AddCancelActivityNexusTasks(scheduledEventID int64) error { - return ms.taskGenerator.GenerateCancelActivityNexusTasks(scheduledEventID) +func (ms *MutableStateImpl) AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error { + return ms.taskGenerator.GenerateActivityCommandTasks(scheduledEventIDs, controlQueue, commandType) } func (ms *MutableStateImpl) ApplyActivityTaskCancelRequestedEvent( diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index ef1073d819d..7c8a2db0bc0 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -63,7 +63,7 @@ type ( activityScheduledEventID int64, ) error GenerateActivityRetryTasks(activityInfo *persistencespb.ActivityInfo) error - GenerateCancelActivityNexusTasks(scheduledEventID int64) error + GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error GenerateChildWorkflowTasks( childInitiatedEventId int64, ) error @@ -584,22 +584,20 @@ func (r *TaskGeneratorImpl) GenerateActivityRetryTasks(activityInfo *persistence return nil } -func (r *TaskGeneratorImpl) GenerateCancelActivityNexusTasks(scheduledEventID int64) error { +func (r *TaskGeneratorImpl) GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error { if !r.config.EnableActivityCancellationNexusTask() { return nil } - ai, ok := r.mutableState.GetActivityInfo(scheduledEventID) - // If control queue is not set, it means the worker that this activity belongs to does not support Nexus tasks. - if !ok || ai.WorkerControlTaskQueue == "" { + if len(scheduledEventIDs) == 0 || controlQueue == "" { return nil } - r.mutableState.AddTasks(&tasks.CancelActivityNexusTask{ - WorkflowKey: r.mutableState.GetWorkflowKey(), - ScheduledEventIDs: []int64{scheduledEventID}, - WorkerControlTaskQueue: ai.WorkerControlTaskQueue, - Version: ai.Version, + r.mutableState.AddTasks(&tasks.ActivityCommandTask{ + WorkflowKey: r.mutableState.GetWorkflowKey(), + CommandType: commandType, + ScheduledEventIDs: scheduledEventIDs, + Destination: controlQueue, }) return nil } diff --git a/service/history/workflow/task_generator_mock.go b/service/history/workflow/task_generator_mock.go index 63f740f755a..2e38a9545eb 100644 --- a/service/history/workflow/task_generator_mock.go +++ b/service/history/workflow/task_generator_mock.go @@ -14,6 +14,7 @@ import ( time "time" history "go.temporal.io/api/history/v1" + enums "go.temporal.io/server/api/enums/v1" persistence "go.temporal.io/server/api/persistence/v1" hsm "go.temporal.io/server/service/history/hsm" interfaces "go.temporal.io/server/service/history/interfaces" @@ -189,6 +190,20 @@ func (mr *MockTaskGeneratorMockRecorder) GenerateMigrationTasks(targetClusters a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateMigrationTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateMigrationTasks), targetClusters) } +// GenerateActivityCommandTasks mocks base method. +func (m *MockTaskGenerator) GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, notificationType enums.ActivityCommandType) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateActivityCommandTasks", scheduledEventIDs, controlQueue, notificationType) + ret0, _ := ret[0].(error) + return ret0 +} + +// GenerateActivityCommandTasks indicates an expected call of GenerateActivityCommandTasks. +func (mr *MockTaskGeneratorMockRecorder) GenerateActivityCommandTasks(scheduledEventIDs, controlQueue, notificationType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateActivityCommandTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateActivityCommandTasks), scheduledEventIDs, controlQueue, notificationType) +} + // GenerateRecordWorkflowStartedTasks mocks base method. func (m *MockTaskGenerator) GenerateRecordWorkflowStartedTasks(startEvent *history.HistoryEvent) error { m.ctrl.T.Helper() diff --git a/service/history/workflow/task_generator_test.go b/service/history/workflow/task_generator_test.go index 385762cdec0..545d2e6b38b 100644 --- a/service/history/workflow/task_generator_test.go +++ b/service/history/workflow/task_generator_test.go @@ -1066,3 +1066,80 @@ func TestTaskGeneratorImpl_GenerateDeleteHistoryEventTask_ActivityRetention(t *t }) } } + +func TestGenerateActivityCommandTasks(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + featureEnabled bool + scheduledEventIDs []int64 + controlQueue string + expectTask bool + }{ + { + name: "creates task when enabled with valid inputs", + featureEnabled: true, + scheduledEventIDs: []int64{5, 6, 7}, + controlQueue: "test-control-queue", + expectTask: true, + }, + { + name: "no task when feature disabled", + featureEnabled: false, + scheduledEventIDs: []int64{5, 6, 7}, + controlQueue: "test-control-queue", + expectTask: false, + }, + { + name: "no task when scheduledEventIDs empty", + featureEnabled: true, + scheduledEventIDs: []int64{}, + controlQueue: "test-control-queue", + expectTask: false, + }, + { + name: "no task when controlQueue empty", + featureEnabled: true, + scheduledEventIDs: []int64{5, 6, 7}, + controlQueue: "", + expectTask: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + mutableState := historyi.NewMockMutableState(ctrl) + mutableState.EXPECT().GetWorkflowKey().Return(definition.NewWorkflowKey( + tests.NamespaceID.String(), tests.WorkflowID, tests.RunID, + )).AnyTimes() + + var capturedTasks []tasks.Task + if tc.expectTask { + mutableState.EXPECT().AddTasks(gomock.Any()).Do(func(ts ...tasks.Task) { + capturedTasks = append(capturedTasks, ts...) + }).Times(1) + } + + cfg := &configs.Config{ + EnableActivityCancellationNexusTask: func() bool { return tc.featureEnabled }, + } + + taskGenerator := NewTaskGenerator(nil, mutableState, cfg, nil, log.NewTestLogger()) + err := taskGenerator.GenerateActivityCommandTasks(tc.scheduledEventIDs, tc.controlQueue, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL) + require.NoError(t, err) + + if tc.expectTask { + require.Len(t, capturedTasks, 1) + notifyTask, ok := capturedTasks[0].(*tasks.ActivityCommandTask) + require.True(t, ok) + assert.Equal(t, tc.scheduledEventIDs, notifyTask.ScheduledEventIDs) + assert.Equal(t, tc.controlQueue, notifyTask.Destination) + assert.Equal(t, tests.NamespaceID.String(), notifyTask.NamespaceID) + assert.Equal(t, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, notifyTask.CommandType) + } + }) + } +} From 7f5e0fca5a4ac9433d43a74218a0a5b74189e22d Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 17 Feb 2026 21:51:29 -0800 Subject: [PATCH 21/40] Regenerate mocks with go-generate for correct ordering and parameter names --- .../history/interfaces/mutable_state_mock.go | 28 +++++++++---------- .../history/workflow/task_generator_mock.go | 28 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/service/history/interfaces/mutable_state_mock.go b/service/history/interfaces/mutable_state_mock.go index c0a0f7f2d55..065f3cec4cf 100644 --- a/service/history/interfaces/mutable_state_mock.go +++ b/service/history/interfaces/mutable_state_mock.go @@ -70,6 +70,20 @@ func (m *MockMutableState) EXPECT() *MockMutableStateMockRecorder { return m.recorder } +// AddActivityCommandTasks mocks base method. +func (m *MockMutableState) AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enums0.ActivityCommandType) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddActivityCommandTasks", scheduledEventIDs, controlQueue, commandType) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddActivityCommandTasks indicates an expected call of AddActivityCommandTasks. +func (mr *MockMutableStateMockRecorder) AddActivityCommandTasks(scheduledEventIDs, controlQueue, commandType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityCommandTasks", reflect.TypeOf((*MockMutableState)(nil).AddActivityCommandTasks), scheduledEventIDs, controlQueue, commandType) +} + // AddActivityTaskCancelRequestedEvent mocks base method. func (m *MockMutableState) AddActivityTaskCancelRequestedEvent(arg0, arg1 int64, arg2 string) (*history.HistoryEvent, *persistence.ActivityInfo, error) { m.ctrl.T.Helper() @@ -408,20 +422,6 @@ func (mr *MockMutableStateMockRecorder) AddHistorySize(size any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHistorySize", reflect.TypeOf((*MockMutableState)(nil).AddHistorySize), size) } -// AddActivityCommandTasks mocks base method. -func (m *MockMutableState) AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, notificationType enums0.ActivityCommandType) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddActivityCommandTasks", scheduledEventIDs, controlQueue, notificationType) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddActivityCommandTasks indicates an expected call of AddActivityCommandTasks. -func (mr *MockMutableStateMockRecorder) AddActivityCommandTasks(scheduledEventIDs, controlQueue, notificationType any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityCommandTasks", reflect.TypeOf((*MockMutableState)(nil).AddActivityCommandTasks), scheduledEventIDs, controlQueue, notificationType) -} - // AddReapplyCandidateEvent mocks base method. func (m *MockMutableState) AddReapplyCandidateEvent(event *history.HistoryEvent) { m.ctrl.T.Helper() diff --git a/service/history/workflow/task_generator_mock.go b/service/history/workflow/task_generator_mock.go index 2e38a9545eb..e4bfa61da07 100644 --- a/service/history/workflow/task_generator_mock.go +++ b/service/history/workflow/task_generator_mock.go @@ -46,6 +46,20 @@ func (m *MockTaskGenerator) EXPECT() *MockTaskGeneratorMockRecorder { return m.recorder } +// GenerateActivityCommandTasks mocks base method. +func (m *MockTaskGenerator) GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enums.ActivityCommandType) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateActivityCommandTasks", scheduledEventIDs, controlQueue, commandType) + ret0, _ := ret[0].(error) + return ret0 +} + +// GenerateActivityCommandTasks indicates an expected call of GenerateActivityCommandTasks. +func (mr *MockTaskGeneratorMockRecorder) GenerateActivityCommandTasks(scheduledEventIDs, controlQueue, commandType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateActivityCommandTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateActivityCommandTasks), scheduledEventIDs, controlQueue, commandType) +} + // GenerateActivityRetryTasks mocks base method. func (m *MockTaskGenerator) GenerateActivityRetryTasks(activityInfo *persistence.ActivityInfo) error { m.ctrl.T.Helper() @@ -190,20 +204,6 @@ func (mr *MockTaskGeneratorMockRecorder) GenerateMigrationTasks(targetClusters a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateMigrationTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateMigrationTasks), targetClusters) } -// GenerateActivityCommandTasks mocks base method. -func (m *MockTaskGenerator) GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, notificationType enums.ActivityCommandType) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateActivityCommandTasks", scheduledEventIDs, controlQueue, notificationType) - ret0, _ := ret[0].(error) - return ret0 -} - -// GenerateActivityCommandTasks indicates an expected call of GenerateActivityCommandTasks. -func (mr *MockTaskGeneratorMockRecorder) GenerateActivityCommandTasks(scheduledEventIDs, controlQueue, notificationType any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateActivityCommandTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateActivityCommandTasks), scheduledEventIDs, controlQueue, notificationType) -} - // GenerateRecordWorkflowStartedTasks mocks base method. func (m *MockTaskGenerator) GenerateRecordWorkflowStartedTasks(startEvent *history.HistoryEvent) error { m.ctrl.T.Helper() From cd17334871c8b744e067b1bff9017e483ee11d1f Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Wed, 18 Feb 2026 13:41:42 -0800 Subject: [PATCH 22/40] Change ActivityCommandTask to use task_tokens instead of scheduled_event_ids --- api/persistence/v1/executions.pb.go | 21 +++--- .../serialization/task_serializers.go | 6 +- .../serialization/task_serializers_test.go | 2 +- .../api/persistence/v1/executions.proto | 7 +- .../workflow_task_completed_handler.go | 26 ++++++-- .../workflow_task_completed_handler_test.go | 27 ++++---- service/history/interfaces/mutable_state.go | 2 +- .../history/interfaces/mutable_state_mock.go | 8 +-- .../history/tasks/activity_command_task.go | 8 +-- .../history/workflow/mutable_state_impl.go | 4 +- service/history/workflow/task_generator.go | 14 ++-- .../history/workflow/task_generator_mock.go | 8 +-- .../history/workflow/task_generator_test.go | 66 ++++++++++--------- 13 files changed, 113 insertions(+), 86 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index fea1986e34a..7a606d6faa3 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -2358,15 +2358,15 @@ func (*OutboundTaskInfo_ChasmTaskInfo) isOutboundTaskInfo_TaskDetails() {} func (*OutboundTaskInfo_ActivityCommandInfo) isOutboundTaskInfo_TaskDetails() {} -// ActivityCommandTaskInfo contains details for activity command operations. +// ActivityCommandTaskInfo contains details for activity command operation. type ActivityCommandTaskInfo struct { state protoimpl.MessageState `protogen:"open.v1"` // Type of command to send. CommandType v1.ActivityCommandType `protobuf:"varint,1,opt,name=command_type,json=commandType,proto3,enum=temporal.server.api.enums.v1.ActivityCommandType" json:"command_type,omitempty"` - // Scheduled event IDs of activities to send command to. - ScheduledEventIds []int64 `protobuf:"varint,2,rep,packed,name=scheduled_event_ids,json=scheduledEventIds,proto3" json:"scheduled_event_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Task tokens of activities. + TaskTokens [][]byte `protobuf:"bytes,2,rep,name=task_tokens,json=taskTokens,proto3" json:"task_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ActivityCommandTaskInfo) Reset() { @@ -2406,9 +2406,9 @@ func (x *ActivityCommandTaskInfo) GetCommandType() v1.ActivityCommandType { return v1.ActivityCommandType(0) } -func (x *ActivityCommandTaskInfo) GetScheduledEventIds() []int64 { +func (x *ActivityCommandTaskInfo) GetTaskTokens() [][]byte { if x != nil { - return x.ScheduledEventIds + return x.TaskTokens } return nil } @@ -4919,10 +4919,11 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x0fchasm_task_info\x18\t \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12q\n" + "\x15activity_command_info\x18\n" + " \x01(\v2;.temporal.server.api.persistence.v1.ActivityCommandTaskInfoH\x00R\x13activityCommandInfoB\x0e\n" + - "\ftask_details\"\x9f\x01\n" + + "\ftask_details\"\x90\x01\n" + "\x17ActivityCommandTaskInfo\x12T\n" + - "\fcommand_type\x18\x01 \x01(\x0e21.temporal.server.api.enums.v1.ActivityCommandTypeR\vcommandType\x12.\n" + - "\x13scheduled_event_ids\x18\x02 \x03(\x03R\x11scheduledEventIds\"3\n" + + "\fcommand_type\x18\x01 \x01(\x0e21.temporal.server.api.enums.v1.ActivityCommandTypeR\vcommandType\x12\x1f\n" + + "\vtask_tokens\x18\x02 \x03(\fR\n" + + "taskTokens\"3\n" + "\x17NexusInvocationTaskInfo\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"4\n" + "\x18NexusCancelationTaskInfo\x12\x18\n" + diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index 2e7b3a19ec6..a9397e4bf5c 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -1457,8 +1457,8 @@ func serializeOutboundTask( VisibilityTime: timestamppb.New(task.VisibilityTimestamp), TaskDetails: &persistencespb.OutboundTaskInfo_ActivityCommandInfo{ ActivityCommandInfo: &persistencespb.ActivityCommandTaskInfo{ - CommandType: task.CommandType, - ScheduledEventIds: task.ScheduledEventIDs, + CommandType: task.CommandType, + TaskTokens: task.TaskTokens, }, }, } @@ -1515,7 +1515,7 @@ func deserializeOutboundTask( VisibilityTimestamp: info.VisibilityTime.AsTime(), TaskID: info.TaskId, CommandType: activityCommandInfo.GetCommandType(), - ScheduledEventIDs: activityCommandInfo.GetScheduledEventIds(), + TaskTokens: activityCommandInfo.GetTaskTokens(), Destination: info.Destination, }, nil default: diff --git a/common/persistence/serialization/task_serializers_test.go b/common/persistence/serialization/task_serializers_test.go index 3d635274635..9afca6ae738 100644 --- a/common/persistence/serialization/task_serializers_test.go +++ b/common/persistence/serialization/task_serializers_test.go @@ -175,7 +175,7 @@ func (s *taskSerializerSuite) TestOutboundActivityCommandTask() { VisibilityTimestamp: time.Unix(0, rand.Int63()).UTC(), TaskID: rand.Int63(), CommandType: enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, - ScheduledEventIDs: []int64{rand.Int63(), rand.Int63(), rand.Int63()}, + TaskTokens: [][]byte{[]byte("token1"), []byte("token2"), []byte("token3")}, Destination: "test-control-queue", } diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 3a1d68f47d1..75e2641f5d0 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -489,12 +489,13 @@ message OutboundTaskInfo { } } -// ActivityCommandTaskInfo contains details for activity command operations. +// ActivityCommandTaskInfo contains details for activity command operation. message ActivityCommandTaskInfo { // Type of command to send. temporal.server.api.enums.v1.ActivityCommandType command_type = 1; - // Scheduled event IDs of activities to send command to. - repeated int64 scheduled_event_ids = 2; + + // Task tokens of activities. + repeated bytes task_tokens = 2; } message NexusInvocationTaskInfo { diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 46218b4f448..1e52af2c6da 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -67,7 +67,7 @@ type ( effects effect.Controller initiatedChildExecutionsInBatch map[string]struct{} // Set of initiated child executions in the workflow task updateRegistry update.Registry - pendingActivityCancelsByControlQueue map[string][]int64 // Batched activity cancels by control queue + pendingActivityCancelsByControlQueue map[string][][]byte // Batched activity cancel task tokens by control queue // validation attrValidator *api.CommandAttrValidator @@ -672,12 +672,28 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( handler.activityNotStartedCancelled = true } else if ai.StartedEventId != common.EmptyEventID && ai.WorkerControlTaskQueue != "" { // Activity has started and worker supports Nexus control tasks - collect for batched dispatch. + taskToken, err := handler.tokenSerializer.Serialize(tasktoken.NewActivityTaskToken( + handler.mutableState.GetNamespaceEntry().ID().String(), + handler.mutableState.GetWorkflowKey().WorkflowID, + handler.mutableState.GetWorkflowKey().RunID, + ai.ScheduledEventId, + ai.ActivityId, + ai.ActivityType.GetName(), + ai.Attempt, + nil, // Clock not needed for cancel + ai.Version, + ai.StartVersion, + nil, + )) + if err != nil { + return nil, err + } if handler.pendingActivityCancelsByControlQueue == nil { - handler.pendingActivityCancelsByControlQueue = make(map[string][]int64) + handler.pendingActivityCancelsByControlQueue = make(map[string][][]byte) } handler.pendingActivityCancelsByControlQueue[ai.WorkerControlTaskQueue] = append( handler.pendingActivityCancelsByControlQueue[ai.WorkerControlTaskQueue], - ai.ScheduledEventId, + taskToken, ) } } @@ -687,9 +703,9 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( // flushBatchedActivityCommandTasks creates ActivityCommandTasks for all collected activity cancellations, // batched by control queue. func (handler *workflowTaskCompletedHandler) flushBatchedActivityCommandTasks() error { - for controlQueue, scheduledEventIDs := range handler.pendingActivityCancelsByControlQueue { + for controlQueue, taskTokens := range handler.pendingActivityCancelsByControlQueue { if err := handler.mutableState.AddActivityCommandTasks( - scheduledEventIDs, + taskTokens, controlQueue, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, ); err != nil { diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go index ec87e5f2651..270f6f5863d 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go @@ -389,20 +389,25 @@ func mustMarshalAny(t *testing.T, pb proto.Message) *anypb.Any { func TestFlushBatchedActivityCommandTasks(t *testing.T) { t.Parallel() + token1 := []byte("token1") + token2 := []byte("token2") + token3 := []byte("token3") + token4 := []byte("token4") + t.Run("batches activities by control queue", func(t *testing.T) { ctrl := gomock.NewController(t) ms := historyi.NewMockMutableState(ctrl) ms.EXPECT().AddActivityCommandTasks( - []int64{5, 6, 7}, + [][]byte{token1, token2, token3}, "control-queue-1", gomock.Any(), ).Return(nil).Times(1) handler := &workflowTaskCompletedHandler{ mutableState: ms, - pendingActivityCancelsByControlQueue: map[string][]int64{ - "control-queue-1": {5, 6, 7}, + pendingActivityCancelsByControlQueue: map[string][][]byte{ + "control-queue-1": {token1, token2, token3}, }, } @@ -415,29 +420,29 @@ func TestFlushBatchedActivityCommandTasks(t *testing.T) { ms := historyi.NewMockMutableState(ctrl) // Capture calls to verify both queues are processed - calls := make(map[string][]int64) + calls := make(map[string][][]byte) ms.EXPECT().AddActivityCommandTasks( gomock.Any(), gomock.Any(), enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, - ).DoAndReturn(func(ids []int64, queue string, _ enumsspb.ActivityCommandType) error { - calls[queue] = ids + ).DoAndReturn(func(tokens [][]byte, queue string, _ enumsspb.ActivityCommandType) error { + calls[queue] = tokens return nil }).Times(2) handler := &workflowTaskCompletedHandler{ mutableState: ms, - pendingActivityCancelsByControlQueue: map[string][]int64{ - "control-queue-1": {5, 6}, - "control-queue-2": {7, 8}, + pendingActivityCancelsByControlQueue: map[string][][]byte{ + "control-queue-1": {token1, token2}, + "control-queue-2": {token3, token4}, }, } err := handler.flushBatchedActivityCommandTasks() require.NoError(t, err) - require.Equal(t, []int64{5, 6}, calls["control-queue-1"]) - require.Equal(t, []int64{7, 8}, calls["control-queue-2"]) + require.Equal(t, [][]byte{token1, token2}, calls["control-queue-1"]) + require.Equal(t, [][]byte{token3, token4}, calls["control-queue-2"]) }) t.Run("does nothing when no pending cancels", func(t *testing.T) { diff --git a/service/history/interfaces/mutable_state.go b/service/history/interfaces/mutable_state.go index 631d64bc43a..3e742c11f6a 100644 --- a/service/history/interfaces/mutable_state.go +++ b/service/history/interfaces/mutable_state.go @@ -46,7 +46,7 @@ type ( AddActivityTaskCancelRequestedEvent(int64, int64, string) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) AddActivityTaskCanceledEvent(int64, int64, int64, *commonpb.Payloads, string) (*historypb.HistoryEvent, error) - AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error + AddActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error AddActivityTaskCompletedEvent(int64, int64, *workflowservice.RespondActivityTaskCompletedRequest) (*historypb.HistoryEvent, error) AddActivityTaskFailedEvent(int64, int64, *failurepb.Failure, enumspb.RetryState, string, *commonpb.WorkerVersionStamp) (*historypb.HistoryEvent, error) AddActivityTaskScheduledEvent(int64, *commandpb.ScheduleActivityTaskCommandAttributes, bool) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) diff --git a/service/history/interfaces/mutable_state_mock.go b/service/history/interfaces/mutable_state_mock.go index 065f3cec4cf..145487d13bb 100644 --- a/service/history/interfaces/mutable_state_mock.go +++ b/service/history/interfaces/mutable_state_mock.go @@ -71,17 +71,17 @@ func (m *MockMutableState) EXPECT() *MockMutableStateMockRecorder { } // AddActivityCommandTasks mocks base method. -func (m *MockMutableState) AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enums0.ActivityCommandType) error { +func (m *MockMutableState) AddActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enums0.ActivityCommandType) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddActivityCommandTasks", scheduledEventIDs, controlQueue, commandType) + ret := m.ctrl.Call(m, "AddActivityCommandTasks", taskTokens, controlQueue, commandType) ret0, _ := ret[0].(error) return ret0 } // AddActivityCommandTasks indicates an expected call of AddActivityCommandTasks. -func (mr *MockMutableStateMockRecorder) AddActivityCommandTasks(scheduledEventIDs, controlQueue, commandType any) *gomock.Call { +func (mr *MockMutableStateMockRecorder) AddActivityCommandTasks(taskTokens, controlQueue, commandType any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityCommandTasks", reflect.TypeOf((*MockMutableState)(nil).AddActivityCommandTasks), scheduledEventIDs, controlQueue, commandType) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityCommandTasks", reflect.TypeOf((*MockMutableState)(nil).AddActivityCommandTasks), taskTokens, controlQueue, commandType) } // AddActivityTaskCancelRequestedEvent mocks base method. diff --git a/service/history/tasks/activity_command_task.go b/service/history/tasks/activity_command_task.go index 805c9d6eb06..c4a82433646 100644 --- a/service/history/tasks/activity_command_task.go +++ b/service/history/tasks/activity_command_task.go @@ -20,8 +20,8 @@ type ( // CommandType specifies the type of command. CommandType enumsspb.ActivityCommandType - // ScheduledEventIDs of activities to send command to (batched by worker). - ScheduledEventIDs []int64 + // TaskTokens of activities to send command to (batched by worker). + TaskTokens [][]byte // Destination is the worker control task queue for outbound queue grouping. Destination string } @@ -61,12 +61,12 @@ func (t *ActivityCommandTask) GetDestination() string { } func (t *ActivityCommandTask) String() string { - return fmt.Sprintf("ActivityCommandTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, CommandType: %v, ScheduledEventIDs: %v, Destination: %v}", + return fmt.Sprintf("ActivityCommandTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, CommandType: %v, TaskTokens: %d, Destination: %v}", t.WorkflowKey.String(), t.VisibilityTimestamp, t.TaskID, t.CommandType, - t.ScheduledEventIDs, + len(t.TaskTokens), t.Destination, ) } diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index 29b714685d0..aa948f4db50 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -4369,8 +4369,8 @@ func (ms *MutableStateImpl) AddActivityTaskCancelRequestedEvent( return actCancelReqEvent, ai, nil } -func (ms *MutableStateImpl) AddActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error { - return ms.taskGenerator.GenerateActivityCommandTasks(scheduledEventIDs, controlQueue, commandType) +func (ms *MutableStateImpl) AddActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error { + return ms.taskGenerator.GenerateActivityCommandTasks(taskTokens, controlQueue, commandType) } func (ms *MutableStateImpl) ApplyActivityTaskCancelRequestedEvent( diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index 7c8a2db0bc0..2f61b17a691 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -63,7 +63,7 @@ type ( activityScheduledEventID int64, ) error GenerateActivityRetryTasks(activityInfo *persistencespb.ActivityInfo) error - GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error + GenerateActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error GenerateChildWorkflowTasks( childInitiatedEventId int64, ) error @@ -584,20 +584,20 @@ func (r *TaskGeneratorImpl) GenerateActivityRetryTasks(activityInfo *persistence return nil } -func (r *TaskGeneratorImpl) GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enumsspb.ActivityCommandType) error { +func (r *TaskGeneratorImpl) GenerateActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error { if !r.config.EnableActivityCancellationNexusTask() { return nil } - if len(scheduledEventIDs) == 0 || controlQueue == "" { + if len(taskTokens) == 0 || controlQueue == "" { return nil } r.mutableState.AddTasks(&tasks.ActivityCommandTask{ - WorkflowKey: r.mutableState.GetWorkflowKey(), - CommandType: commandType, - ScheduledEventIDs: scheduledEventIDs, - Destination: controlQueue, + WorkflowKey: r.mutableState.GetWorkflowKey(), + CommandType: commandType, + TaskTokens: taskTokens, + Destination: controlQueue, }) return nil } diff --git a/service/history/workflow/task_generator_mock.go b/service/history/workflow/task_generator_mock.go index e4bfa61da07..a91df24c428 100644 --- a/service/history/workflow/task_generator_mock.go +++ b/service/history/workflow/task_generator_mock.go @@ -47,17 +47,17 @@ func (m *MockTaskGenerator) EXPECT() *MockTaskGeneratorMockRecorder { } // GenerateActivityCommandTasks mocks base method. -func (m *MockTaskGenerator) GenerateActivityCommandTasks(scheduledEventIDs []int64, controlQueue string, commandType enums.ActivityCommandType) error { +func (m *MockTaskGenerator) GenerateActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enums.ActivityCommandType) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateActivityCommandTasks", scheduledEventIDs, controlQueue, commandType) + ret := m.ctrl.Call(m, "GenerateActivityCommandTasks", taskTokens, controlQueue, commandType) ret0, _ := ret[0].(error) return ret0 } // GenerateActivityCommandTasks indicates an expected call of GenerateActivityCommandTasks. -func (mr *MockTaskGeneratorMockRecorder) GenerateActivityCommandTasks(scheduledEventIDs, controlQueue, commandType any) *gomock.Call { +func (mr *MockTaskGeneratorMockRecorder) GenerateActivityCommandTasks(taskTokens, controlQueue, commandType any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateActivityCommandTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateActivityCommandTasks), scheduledEventIDs, controlQueue, commandType) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateActivityCommandTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateActivityCommandTasks), taskTokens, controlQueue, commandType) } // GenerateActivityRetryTasks mocks base method. diff --git a/service/history/workflow/task_generator_test.go b/service/history/workflow/task_generator_test.go index 545d2e6b38b..3f35d2d7b0f 100644 --- a/service/history/workflow/task_generator_test.go +++ b/service/history/workflow/task_generator_test.go @@ -1070,40 +1070,44 @@ func TestTaskGeneratorImpl_GenerateDeleteHistoryEventTask_ActivityRetention(t *t func TestGenerateActivityCommandTasks(t *testing.T) { t.Parallel() + token1 := []byte("token1") + token2 := []byte("token2") + token3 := []byte("token3") + testCases := []struct { - name string - featureEnabled bool - scheduledEventIDs []int64 - controlQueue string - expectTask bool + name string + featureEnabled bool + taskTokens [][]byte + controlQueue string + expectTask bool }{ { - name: "creates task when enabled with valid inputs", - featureEnabled: true, - scheduledEventIDs: []int64{5, 6, 7}, - controlQueue: "test-control-queue", - expectTask: true, + name: "creates task when enabled with valid inputs", + featureEnabled: true, + taskTokens: [][]byte{token1, token2, token3}, + controlQueue: "test-control-queue", + expectTask: true, }, { - name: "no task when feature disabled", - featureEnabled: false, - scheduledEventIDs: []int64{5, 6, 7}, - controlQueue: "test-control-queue", - expectTask: false, + name: "no task when feature disabled", + featureEnabled: false, + taskTokens: [][]byte{token1, token2, token3}, + controlQueue: "test-control-queue", + expectTask: false, }, { - name: "no task when scheduledEventIDs empty", - featureEnabled: true, - scheduledEventIDs: []int64{}, - controlQueue: "test-control-queue", - expectTask: false, + name: "no task when taskTokens empty", + featureEnabled: true, + taskTokens: [][]byte{}, + controlQueue: "test-control-queue", + expectTask: false, }, { - name: "no task when controlQueue empty", - featureEnabled: true, - scheduledEventIDs: []int64{5, 6, 7}, - controlQueue: "", - expectTask: false, + name: "no task when controlQueue empty", + featureEnabled: true, + taskTokens: [][]byte{token1, token2, token3}, + controlQueue: "", + expectTask: false, }, } @@ -1128,17 +1132,17 @@ func TestGenerateActivityCommandTasks(t *testing.T) { } taskGenerator := NewTaskGenerator(nil, mutableState, cfg, nil, log.NewTestLogger()) - err := taskGenerator.GenerateActivityCommandTasks(tc.scheduledEventIDs, tc.controlQueue, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL) + err := taskGenerator.GenerateActivityCommandTasks(tc.taskTokens, tc.controlQueue, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL) require.NoError(t, err) if tc.expectTask { require.Len(t, capturedTasks, 1) - notifyTask, ok := capturedTasks[0].(*tasks.ActivityCommandTask) + commandTask, ok := capturedTasks[0].(*tasks.ActivityCommandTask) require.True(t, ok) - assert.Equal(t, tc.scheduledEventIDs, notifyTask.ScheduledEventIDs) - assert.Equal(t, tc.controlQueue, notifyTask.Destination) - assert.Equal(t, tests.NamespaceID.String(), notifyTask.NamespaceID) - assert.Equal(t, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, notifyTask.CommandType) + assert.Equal(t, tc.taskTokens, commandTask.TaskTokens) + assert.Equal(t, tc.controlQueue, commandTask.Destination) + assert.Equal(t, tests.NamespaceID.String(), commandTask.NamespaceID) + assert.Equal(t, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, commandTask.CommandType) } }) } From 6a1a66bc5495caa580f7f2953a5f437612344cc8 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 24 Feb 2026 19:40:06 -0800 Subject: [PATCH 23/40] Change ActivityCommandTaskInfo to WorkerCommandsTask Renames and restructures the persistence proto to use WorkerCommandsTask with repeated WorkerCommand messages. Each command uses oneof for extensibility (currently supports CancelActivity). Updates serializers to convert between the Go task struct and new proto format. Co-authored-by: Cursor --- .../v1/executions.go-helpers.pb.go | 20 +- api/persistence/v1/executions.pb.go | 606 +++++++++++------- .../serialization/task_serializers.go | 36 +- .../api/persistence/v1/executions.proto | 23 +- 4 files changed, 412 insertions(+), 273 deletions(-) diff --git a/api/persistence/v1/executions.go-helpers.pb.go b/api/persistence/v1/executions.go-helpers.pb.go index 965f8080bb0..c4d89e58d41 100644 --- a/api/persistence/v1/executions.go-helpers.pb.go +++ b/api/persistence/v1/executions.go-helpers.pb.go @@ -412,35 +412,35 @@ func (this *OutboundTaskInfo) Equal(that interface{}) bool { return proto.Equal(this, that1) } -// Marshal an object of type ActivityCommandTaskInfo to the protobuf v3 wire format -func (val *ActivityCommandTaskInfo) Marshal() ([]byte, error) { +// Marshal an object of type WorkerCommandsTask to the protobuf v3 wire format +func (val *WorkerCommandsTask) Marshal() ([]byte, error) { return proto.Marshal(val) } -// Unmarshal an object of type ActivityCommandTaskInfo from the protobuf v3 wire format -func (val *ActivityCommandTaskInfo) Unmarshal(buf []byte) error { +// Unmarshal an object of type WorkerCommandsTask from the protobuf v3 wire format +func (val *WorkerCommandsTask) Unmarshal(buf []byte) error { return proto.Unmarshal(buf, val) } // Size returns the size of the object, in bytes, once serialized -func (val *ActivityCommandTaskInfo) Size() int { +func (val *WorkerCommandsTask) Size() int { return proto.Size(val) } -// Equal returns whether two ActivityCommandTaskInfo values are equivalent by recursively +// Equal returns whether two WorkerCommandsTask values are equivalent by recursively // comparing the message's fields. // For more information see the documentation for // https://pkg.go.dev/google.golang.org/protobuf/proto#Equal -func (this *ActivityCommandTaskInfo) Equal(that interface{}) bool { +func (this *WorkerCommandsTask) Equal(that interface{}) bool { if that == nil { return this == nil } - var that1 *ActivityCommandTaskInfo + var that1 *WorkerCommandsTask switch t := that.(type) { - case *ActivityCommandTaskInfo: + case *WorkerCommandsTask: that1 = t - case ActivityCommandTaskInfo: + case WorkerCommandsTask: that1 = &t default: return false diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index 7a606d6faa3..be01d9a0ac5 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -2214,7 +2214,7 @@ type OutboundTaskInfo struct { // // *OutboundTaskInfo_StateMachineInfo // *OutboundTaskInfo_ChasmTaskInfo - // *OutboundTaskInfo_ActivityCommandInfo + // *OutboundTaskInfo_WorkerCommandsTask TaskDetails isOutboundTaskInfo_TaskDetails `protobuf_oneof:"task_details"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2324,10 +2324,10 @@ func (x *OutboundTaskInfo) GetChasmTaskInfo() *ChasmTaskInfo { return nil } -func (x *OutboundTaskInfo) GetActivityCommandInfo() *ActivityCommandTaskInfo { +func (x *OutboundTaskInfo) GetWorkerCommandsTask() *WorkerCommandsTask { if x != nil { - if x, ok := x.TaskDetails.(*OutboundTaskInfo_ActivityCommandInfo); ok { - return x.ActivityCommandInfo + if x, ok := x.TaskDetails.(*OutboundTaskInfo_WorkerCommandsTask); ok { + return x.WorkerCommandsTask } } return nil @@ -2347,42 +2347,39 @@ type OutboundTaskInfo_ChasmTaskInfo struct { ChasmTaskInfo *ChasmTaskInfo `protobuf:"bytes,9,opt,name=chasm_task_info,json=chasmTaskInfo,proto3,oneof"` } -type OutboundTaskInfo_ActivityCommandInfo struct { - // If the task is an activity command task. - ActivityCommandInfo *ActivityCommandTaskInfo `protobuf:"bytes,10,opt,name=activity_command_info,json=activityCommandInfo,proto3,oneof"` +type OutboundTaskInfo_WorkerCommandsTask struct { + // If the task is a worker commands task. + WorkerCommandsTask *WorkerCommandsTask `protobuf:"bytes,10,opt,name=worker_commands_task,json=workerCommandsTask,proto3,oneof"` } func (*OutboundTaskInfo_StateMachineInfo) isOutboundTaskInfo_TaskDetails() {} func (*OutboundTaskInfo_ChasmTaskInfo) isOutboundTaskInfo_TaskDetails() {} -func (*OutboundTaskInfo_ActivityCommandInfo) isOutboundTaskInfo_TaskDetails() {} +func (*OutboundTaskInfo_WorkerCommandsTask) isOutboundTaskInfo_TaskDetails() {} -// ActivityCommandTaskInfo contains details for activity command operation. -type ActivityCommandTaskInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Type of command to send. - CommandType v1.ActivityCommandType `protobuf:"varint,1,opt,name=command_type,json=commandType,proto3,enum=temporal.server.api.enums.v1.ActivityCommandType" json:"command_type,omitempty"` - // Task tokens of activities. - TaskTokens [][]byte `protobuf:"bytes,2,rep,name=task_tokens,json=taskTokens,proto3" json:"task_tokens,omitempty"` +// WorkerCommandsTask contains worker commands to dispatch via Nexus. +type WorkerCommandsTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + Commands []*WorkerCommandsTask_WorkerCommand `protobuf:"bytes,1,rep,name=commands,proto3" json:"commands,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ActivityCommandTaskInfo) Reset() { - *x = ActivityCommandTaskInfo{} +func (x *WorkerCommandsTask) Reset() { + *x = WorkerCommandsTask{} mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ActivityCommandTaskInfo) String() string { +func (x *WorkerCommandsTask) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ActivityCommandTaskInfo) ProtoMessage() {} +func (*WorkerCommandsTask) ProtoMessage() {} -func (x *ActivityCommandTaskInfo) ProtoReflect() protoreflect.Message { +func (x *WorkerCommandsTask) ProtoReflect() protoreflect.Message { mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2394,21 +2391,14 @@ func (x *ActivityCommandTaskInfo) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ActivityCommandTaskInfo.ProtoReflect.Descriptor instead. -func (*ActivityCommandTaskInfo) Descriptor() ([]byte, []int) { +// Deprecated: Use WorkerCommandsTask.ProtoReflect.Descriptor instead. +func (*WorkerCommandsTask) Descriptor() ([]byte, []int) { return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11} } -func (x *ActivityCommandTaskInfo) GetCommandType() v1.ActivityCommandType { +func (x *WorkerCommandsTask) GetCommands() []*WorkerCommandsTask_WorkerCommand { if x != nil { - return x.CommandType - } - return v1.ActivityCommandType(0) -} - -func (x *ActivityCommandTaskInfo) GetTaskTokens() [][]byte { - if x != nil { - return x.TaskTokens + return x.Commands } return nil } @@ -4194,6 +4184,117 @@ func (x *TransferTaskInfo_CloseExecutionTaskDetails) GetCanSkipVisibilityArchiva return false } +type WorkerCommandsTask_WorkerCommand struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Type: + // + // *WorkerCommandsTask_WorkerCommand_CancelActivity + Type isWorkerCommandsTask_WorkerCommand_Type `protobuf_oneof:"type"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkerCommandsTask_WorkerCommand) Reset() { + *x = WorkerCommandsTask_WorkerCommand{} + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkerCommandsTask_WorkerCommand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkerCommandsTask_WorkerCommand) ProtoMessage() {} + +func (x *WorkerCommandsTask_WorkerCommand) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkerCommandsTask_WorkerCommand.ProtoReflect.Descriptor instead. +func (*WorkerCommandsTask_WorkerCommand) Descriptor() ([]byte, []int) { + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11, 0} +} + +func (x *WorkerCommandsTask_WorkerCommand) GetType() isWorkerCommandsTask_WorkerCommand_Type { + if x != nil { + return x.Type + } + return nil +} + +func (x *WorkerCommandsTask_WorkerCommand) GetCancelActivity() *WorkerCommandsTask_CancelActivity { + if x != nil { + if x, ok := x.Type.(*WorkerCommandsTask_WorkerCommand_CancelActivity); ok { + return x.CancelActivity + } + } + return nil +} + +type isWorkerCommandsTask_WorkerCommand_Type interface { + isWorkerCommandsTask_WorkerCommand_Type() +} + +type WorkerCommandsTask_WorkerCommand_CancelActivity struct { + CancelActivity *WorkerCommandsTask_CancelActivity `protobuf:"bytes,1,opt,name=cancel_activity,json=cancelActivity,proto3,oneof"` +} + +func (*WorkerCommandsTask_WorkerCommand_CancelActivity) isWorkerCommandsTask_WorkerCommand_Type() {} + +type WorkerCommandsTask_CancelActivity struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Task token identifying the activity to cancel. + TaskToken []byte `protobuf:"bytes,1,opt,name=task_token,json=taskToken,proto3" json:"task_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkerCommandsTask_CancelActivity) Reset() { + *x = WorkerCommandsTask_CancelActivity{} + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkerCommandsTask_CancelActivity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkerCommandsTask_CancelActivity) ProtoMessage() {} + +func (x *WorkerCommandsTask_CancelActivity) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkerCommandsTask_CancelActivity.ProtoReflect.Descriptor instead. +func (*WorkerCommandsTask_CancelActivity) Descriptor() ([]byte, []int) { + return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11, 1} +} + +func (x *WorkerCommandsTask_CancelActivity) GetTaskToken() []byte { + if x != nil { + return x.TaskToken + } + return nil +} + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] type ActivityInfo_UseWorkflowBuildIdInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4208,7 +4309,7 @@ type ActivityInfo_UseWorkflowBuildIdInfo struct { func (x *ActivityInfo_UseWorkflowBuildIdInfo) Reset() { *x = ActivityInfo_UseWorkflowBuildIdInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4220,7 +4321,7 @@ func (x *ActivityInfo_UseWorkflowBuildIdInfo) String() string { func (*ActivityInfo_UseWorkflowBuildIdInfo) ProtoMessage() {} func (x *ActivityInfo_UseWorkflowBuildIdInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4265,7 +4366,7 @@ type ActivityInfo_PauseInfo struct { func (x *ActivityInfo_PauseInfo) Reset() { *x = ActivityInfo_PauseInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4277,7 +4378,7 @@ func (x *ActivityInfo_PauseInfo) String() string { func (*ActivityInfo_PauseInfo) ProtoMessage() {} func (x *ActivityInfo_PauseInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4355,7 +4456,7 @@ type ActivityInfo_PauseInfo_Manual struct { func (x *ActivityInfo_PauseInfo_Manual) Reset() { *x = ActivityInfo_PauseInfo_Manual{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4367,7 +4468,7 @@ func (x *ActivityInfo_PauseInfo_Manual) String() string { func (*ActivityInfo_PauseInfo_Manual) ProtoMessage() {} func (x *ActivityInfo_PauseInfo_Manual) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4412,7 +4513,7 @@ type Callback_Nexus struct { func (x *Callback_Nexus) Reset() { *x = Callback_Nexus{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4424,7 +4525,7 @@ func (x *Callback_Nexus) String() string { func (*Callback_Nexus) ProtoMessage() {} func (x *Callback_Nexus) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4474,7 +4575,7 @@ type Callback_HSM struct { func (x *Callback_HSM) Reset() { *x = Callback_HSM{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4486,7 +4587,7 @@ func (x *Callback_HSM) String() string { func (*Callback_HSM) ProtoMessage() {} func (x *Callback_HSM) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4546,7 +4647,7 @@ type CallbackInfo_WorkflowClosed struct { func (x *CallbackInfo_WorkflowClosed) Reset() { *x = CallbackInfo_WorkflowClosed{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4558,7 +4659,7 @@ func (x *CallbackInfo_WorkflowClosed) String() string { func (*CallbackInfo_WorkflowClosed) ProtoMessage() {} func (x *CallbackInfo_WorkflowClosed) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4586,7 +4687,7 @@ type CallbackInfo_Trigger struct { func (x *CallbackInfo_Trigger) Reset() { *x = CallbackInfo_Trigger{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4598,7 +4699,7 @@ func (x *CallbackInfo_Trigger) String() string { func (*CallbackInfo_Trigger) ProtoMessage() {} func (x *CallbackInfo_Trigger) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4905,7 +5006,7 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x06run_id\x18\x04 \x01(\tR\x05runId\x12C\n" + "\ttask_type\x18\x05 \x01(\x0e2&.temporal.server.api.enums.v1.TaskTypeR\btaskType\x12\x18\n" + "\aversion\x18\x06 \x01(\x03R\aversion\x12C\n" + - "\x0fvisibility_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\"\xfc\x04\n" + + "\x0fvisibility_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\"\xf5\x04\n" + "\x10OutboundTaskInfo\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + @@ -4916,14 +5017,18 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x0fvisibility_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\x0evisibilityTime\x12 \n" + "\vdestination\x18\a \x01(\tR\vdestination\x12h\n" + "\x12state_machine_info\x18\b \x01(\v28.temporal.server.api.persistence.v1.StateMachineTaskInfoH\x00R\x10stateMachineInfo\x12[\n" + - "\x0fchasm_task_info\x18\t \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12q\n" + - "\x15activity_command_info\x18\n" + - " \x01(\v2;.temporal.server.api.persistence.v1.ActivityCommandTaskInfoH\x00R\x13activityCommandInfoB\x0e\n" + - "\ftask_details\"\x90\x01\n" + - "\x17ActivityCommandTaskInfo\x12T\n" + - "\fcommand_type\x18\x01 \x01(\x0e21.temporal.server.api.enums.v1.ActivityCommandTypeR\vcommandType\x12\x1f\n" + - "\vtask_tokens\x18\x02 \x03(\fR\n" + - "taskTokens\"3\n" + + "\x0fchasm_task_info\x18\t \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12j\n" + + "\x14worker_commands_task\x18\n" + + " \x01(\v26.temporal.server.api.persistence.v1.WorkerCommandsTaskH\x00R\x12workerCommandsTaskB\x0e\n" + + "\ftask_details\"\xb3\x02\n" + + "\x12WorkerCommandsTask\x12`\n" + + "\bcommands\x18\x01 \x03(\v2D.temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommandR\bcommands\x1a\x89\x01\n" + + "\rWorkerCommand\x12p\n" + + "\x0fcancel_activity\x18\x01 \x01(\v2E.temporal.server.api.persistence.v1.WorkerCommandsTask.CancelActivityH\x00R\x0ecancelActivityB\x06\n" + + "\x04type\x1a/\n" + + "\x0eCancelActivity\x12\x1d\n" + + "\n" + + "task_token\x18\x01 \x01(\fR\ttaskToken\"3\n" + "\x17NexusInvocationTaskInfo\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"4\n" + "\x18NexusCancelationTaskInfo\x12\x18\n" + @@ -5129,7 +5234,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP() []by return file_temporal_server_api_persistence_v1_executions_proto_rawDescData } -var file_temporal_server_api_persistence_v1_executions_proto_msgTypes = make([]protoimpl.MessageInfo, 44) +var file_temporal_server_api_persistence_v1_executions_proto_msgTypes = make([]protoimpl.MessageInfo, 46) var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ (*ShardInfo)(nil), // 0: temporal.server.api.persistence.v1.ShardInfo (*WorkflowExecutionInfo)(nil), // 1: temporal.server.api.persistence.v1.WorkflowExecutionInfo @@ -5142,7 +5247,7 @@ var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ (*TimerTaskInfo)(nil), // 8: temporal.server.api.persistence.v1.TimerTaskInfo (*ArchivalTaskInfo)(nil), // 9: temporal.server.api.persistence.v1.ArchivalTaskInfo (*OutboundTaskInfo)(nil), // 10: temporal.server.api.persistence.v1.OutboundTaskInfo - (*ActivityCommandTaskInfo)(nil), // 11: temporal.server.api.persistence.v1.ActivityCommandTaskInfo + (*WorkerCommandsTask)(nil), // 11: temporal.server.api.persistence.v1.WorkerCommandsTask (*NexusInvocationTaskInfo)(nil), // 12: temporal.server.api.persistence.v1.NexusInvocationTaskInfo (*NexusCancelationTaskInfo)(nil), // 13: temporal.server.api.persistence.v1.NexusCancelationTaskInfo (*ActivityInfo)(nil), // 14: temporal.server.api.persistence.v1.ActivityInfo @@ -5167,211 +5272,213 @@ var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ nil, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry nil, // 34: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry (*TransferTaskInfo_CloseExecutionTaskDetails)(nil), // 35: temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 36: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - (*ActivityInfo_PauseInfo)(nil), // 37: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - (*ActivityInfo_PauseInfo_Manual)(nil), // 38: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - (*Callback_Nexus)(nil), // 39: temporal.server.api.persistence.v1.Callback.Nexus - (*Callback_HSM)(nil), // 40: temporal.server.api.persistence.v1.Callback.HSM - nil, // 41: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - (*CallbackInfo_WorkflowClosed)(nil), // 42: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - (*CallbackInfo_Trigger)(nil), // 43: temporal.server.api.persistence.v1.CallbackInfo.Trigger - (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 45: google.protobuf.Duration - (v1.WorkflowTaskType)(0), // 46: temporal.server.api.enums.v1.WorkflowTaskType - (v11.SuggestContinueAsNewReason)(0), // 47: temporal.api.enums.v1.SuggestContinueAsNewReason - (*v12.ResetPoints)(nil), // 48: temporal.api.workflow.v1.ResetPoints - (*v14.VersionHistories)(nil), // 49: temporal.server.api.history.v1.VersionHistories - (*v15.VectorClock)(nil), // 50: temporal.server.api.clock.v1.VectorClock - (*v16.BaseExecutionInfo)(nil), // 51: temporal.server.api.workflow.v1.BaseExecutionInfo - (*v13.WorkerVersionStamp)(nil), // 52: temporal.api.common.v1.WorkerVersionStamp - (*VersionedTransition)(nil), // 53: temporal.server.api.persistence.v1.VersionedTransition - (*StateMachineTimerGroup)(nil), // 54: temporal.server.api.persistence.v1.StateMachineTimerGroup - (*StateMachineTombstoneBatch)(nil), // 55: temporal.server.api.persistence.v1.StateMachineTombstoneBatch - (*v12.WorkflowExecutionVersioningInfo)(nil), // 56: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - (*v13.Priority)(nil), // 57: temporal.api.common.v1.Priority - (v11.WorkflowTaskFailedCause)(0), // 58: temporal.api.enums.v1.WorkflowTaskFailedCause - (v11.TimeoutType)(0), // 59: temporal.api.enums.v1.TimeoutType - (v1.WorkflowExecutionState)(0), // 60: temporal.server.api.enums.v1.WorkflowExecutionState - (v11.WorkflowExecutionStatus)(0), // 61: temporal.api.enums.v1.WorkflowExecutionStatus - (v11.EventType)(0), // 62: temporal.api.enums.v1.EventType - (v1.TaskType)(0), // 63: temporal.server.api.enums.v1.TaskType - (*ChasmTaskInfo)(nil), // 64: temporal.server.api.persistence.v1.ChasmTaskInfo - (v1.TaskPriority)(0), // 65: temporal.server.api.enums.v1.TaskPriority - (*v14.VersionHistoryItem)(nil), // 66: temporal.server.api.history.v1.VersionHistoryItem - (v1.WorkflowBackoffType)(0), // 67: temporal.server.api.enums.v1.WorkflowBackoffType - (*StateMachineTaskInfo)(nil), // 68: temporal.server.api.persistence.v1.StateMachineTaskInfo - (v1.ActivityCommandType)(0), // 69: temporal.server.api.enums.v1.ActivityCommandType - (*v17.Failure)(nil), // 70: temporal.api.failure.v1.Failure - (*v13.Payloads)(nil), // 71: temporal.api.common.v1.Payloads - (*v13.ActivityType)(nil), // 72: temporal.api.common.v1.ActivityType - (*v18.Deployment)(nil), // 73: temporal.api.deployment.v1.Deployment - (*v18.WorkerDeploymentVersion)(nil), // 74: temporal.api.deployment.v1.WorkerDeploymentVersion - (v11.ParentClosePolicy)(0), // 75: temporal.api.enums.v1.ParentClosePolicy - (v1.ChecksumFlavor)(0), // 76: temporal.server.api.enums.v1.ChecksumFlavor - (*v13.Link)(nil), // 77: temporal.api.common.v1.Link - (*v19.HistoryEvent)(nil), // 78: temporal.api.history.v1.HistoryEvent - (v1.CallbackState)(0), // 79: temporal.server.api.enums.v1.CallbackState - (v1.NexusOperationState)(0), // 80: temporal.server.api.enums.v1.NexusOperationState - (v11.NexusOperationCancellationState)(0), // 81: temporal.api.enums.v1.NexusOperationCancellationState - (*QueueState)(nil), // 82: temporal.server.api.persistence.v1.QueueState - (*v13.Payload)(nil), // 83: temporal.api.common.v1.Payload - (*UpdateInfo)(nil), // 84: temporal.server.api.persistence.v1.UpdateInfo - (*StateMachineMap)(nil), // 85: temporal.server.api.persistence.v1.StateMachineMap - (*StateMachineRef)(nil), // 86: temporal.server.api.persistence.v1.StateMachineRef + (*WorkerCommandsTask_WorkerCommand)(nil), // 36: temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommand + (*WorkerCommandsTask_CancelActivity)(nil), // 37: temporal.server.api.persistence.v1.WorkerCommandsTask.CancelActivity + (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 38: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + (*ActivityInfo_PauseInfo)(nil), // 39: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + (*ActivityInfo_PauseInfo_Manual)(nil), // 40: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + (*Callback_Nexus)(nil), // 41: temporal.server.api.persistence.v1.Callback.Nexus + (*Callback_HSM)(nil), // 42: temporal.server.api.persistence.v1.Callback.HSM + nil, // 43: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + (*CallbackInfo_WorkflowClosed)(nil), // 44: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + (*CallbackInfo_Trigger)(nil), // 45: temporal.server.api.persistence.v1.CallbackInfo.Trigger + (*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 47: google.protobuf.Duration + (v1.WorkflowTaskType)(0), // 48: temporal.server.api.enums.v1.WorkflowTaskType + (v11.SuggestContinueAsNewReason)(0), // 49: temporal.api.enums.v1.SuggestContinueAsNewReason + (*v12.ResetPoints)(nil), // 50: temporal.api.workflow.v1.ResetPoints + (*v14.VersionHistories)(nil), // 51: temporal.server.api.history.v1.VersionHistories + (*v15.VectorClock)(nil), // 52: temporal.server.api.clock.v1.VectorClock + (*v16.BaseExecutionInfo)(nil), // 53: temporal.server.api.workflow.v1.BaseExecutionInfo + (*v13.WorkerVersionStamp)(nil), // 54: temporal.api.common.v1.WorkerVersionStamp + (*VersionedTransition)(nil), // 55: temporal.server.api.persistence.v1.VersionedTransition + (*StateMachineTimerGroup)(nil), // 56: temporal.server.api.persistence.v1.StateMachineTimerGroup + (*StateMachineTombstoneBatch)(nil), // 57: temporal.server.api.persistence.v1.StateMachineTombstoneBatch + (*v12.WorkflowExecutionVersioningInfo)(nil), // 58: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + (*v13.Priority)(nil), // 59: temporal.api.common.v1.Priority + (v11.WorkflowTaskFailedCause)(0), // 60: temporal.api.enums.v1.WorkflowTaskFailedCause + (v11.TimeoutType)(0), // 61: temporal.api.enums.v1.TimeoutType + (v1.WorkflowExecutionState)(0), // 62: temporal.server.api.enums.v1.WorkflowExecutionState + (v11.WorkflowExecutionStatus)(0), // 63: temporal.api.enums.v1.WorkflowExecutionStatus + (v11.EventType)(0), // 64: temporal.api.enums.v1.EventType + (v1.TaskType)(0), // 65: temporal.server.api.enums.v1.TaskType + (*ChasmTaskInfo)(nil), // 66: temporal.server.api.persistence.v1.ChasmTaskInfo + (v1.TaskPriority)(0), // 67: temporal.server.api.enums.v1.TaskPriority + (*v14.VersionHistoryItem)(nil), // 68: temporal.server.api.history.v1.VersionHistoryItem + (v1.WorkflowBackoffType)(0), // 69: temporal.server.api.enums.v1.WorkflowBackoffType + (*StateMachineTaskInfo)(nil), // 70: temporal.server.api.persistence.v1.StateMachineTaskInfo + (*v17.Failure)(nil), // 71: temporal.api.failure.v1.Failure + (*v13.Payloads)(nil), // 72: temporal.api.common.v1.Payloads + (*v13.ActivityType)(nil), // 73: temporal.api.common.v1.ActivityType + (*v18.Deployment)(nil), // 74: temporal.api.deployment.v1.Deployment + (*v18.WorkerDeploymentVersion)(nil), // 75: temporal.api.deployment.v1.WorkerDeploymentVersion + (v11.ParentClosePolicy)(0), // 76: temporal.api.enums.v1.ParentClosePolicy + (v1.ChecksumFlavor)(0), // 77: temporal.server.api.enums.v1.ChecksumFlavor + (*v13.Link)(nil), // 78: temporal.api.common.v1.Link + (*v19.HistoryEvent)(nil), // 79: temporal.api.history.v1.HistoryEvent + (v1.CallbackState)(0), // 80: temporal.server.api.enums.v1.CallbackState + (v1.NexusOperationState)(0), // 81: temporal.server.api.enums.v1.NexusOperationState + (v11.NexusOperationCancellationState)(0), // 82: temporal.api.enums.v1.NexusOperationCancellationState + (*QueueState)(nil), // 83: temporal.server.api.persistence.v1.QueueState + (*v13.Payload)(nil), // 84: temporal.api.common.v1.Payload + (*UpdateInfo)(nil), // 85: temporal.server.api.persistence.v1.UpdateInfo + (*StateMachineMap)(nil), // 86: temporal.server.api.persistence.v1.StateMachineMap + (*StateMachineRef)(nil), // 87: temporal.server.api.persistence.v1.StateMachineRef } var file_temporal_server_api_persistence_v1_executions_proto_depIdxs = []int32{ - 44, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp + 46, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp 27, // 1: temporal.server.api.persistence.v1.ShardInfo.replication_dlq_ack_level:type_name -> temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry 28, // 2: temporal.server.api.persistence.v1.ShardInfo.queue_states:type_name -> temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry - 45, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration - 45, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration - 45, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration - 44, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp - 44, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp - 45, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration - 44, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp - 44, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp - 46, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType - 47, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason - 45, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 45, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 44, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp - 48, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints + 47, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration + 47, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration + 47, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration + 46, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp + 46, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp + 47, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration + 46, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp + 46, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp + 46, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp + 48, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType + 49, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason + 47, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration + 47, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 47, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 46, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp + 50, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints 29, // 19: temporal.server.api.persistence.v1.WorkflowExecutionInfo.search_attributes:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry 30, // 20: temporal.server.api.persistence.v1.WorkflowExecutionInfo.memo:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry - 49, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 51, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories 2, // 22: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_stats:type_name -> temporal.server.api.persistence.v1.ExecutionStats - 44, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp - 44, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp - 50, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock - 44, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp - 51, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo - 52, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 46, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp + 46, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp + 52, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock + 46, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp + 53, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo + 54, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp 31, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.update_infos:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry - 53, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 55, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition 32, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machines_by_type:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry - 54, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup - 53, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 55, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch - 56, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - 53, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 56, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup + 55, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 55, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 55, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 57, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch + 58, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + 55, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 55, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition 33, // 40: temporal.server.api.persistence.v1.WorkflowExecutionInfo.children_initialized_post_reset_point:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry - 57, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 59, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority 26, // 42: temporal.server.api.persistence.v1.WorkflowExecutionInfo.pause_info:type_name -> temporal.server.api.persistence.v1.WorkflowPauseInfo - 58, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause - 59, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType - 60, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState - 61, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus - 53, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 44, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp + 60, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause + 61, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType + 62, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState + 63, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus + 55, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 46, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp 34, // 49: temporal.server.api.persistence.v1.WorkflowExecutionState.request_ids:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry - 62, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType - 63, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 64, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType + 65, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 46, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp 35, // 53: temporal.server.api.persistence.v1.TransferTaskInfo.close_execution_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - 64, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 55: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 65, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority - 53, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 66, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 65, // 55: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 46, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 67, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority + 55, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition 6, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo - 66, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 63, // 61: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 44, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp - 64, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 65: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 59, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType - 67, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType - 44, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 64, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 70: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 63, // 72: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 68, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo - 64, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 11, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.activity_command_info:type_name -> temporal.server.api.persistence.v1.ActivityCommandTaskInfo - 69, // 77: temporal.server.api.persistence.v1.ActivityCommandTaskInfo.command_type:type_name -> temporal.server.api.enums.v1.ActivityCommandType - 44, // 78: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 79: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp - 45, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 81: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 45, // 82: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 45, // 83: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration - 45, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 45, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 44, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp - 70, // 87: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure - 71, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads - 44, // 89: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp - 72, // 90: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType - 36, // 91: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - 52, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 53, // 93: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 44, // 94: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 73, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment - 74, // 97: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion - 57, // 98: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority - 37, // 99: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - 44, // 100: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp - 53, // 101: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 75, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy - 50, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 53, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 57, // 105: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority - 53, // 106: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 107: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 76, // 108: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor - 39, // 109: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus - 40, // 110: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM - 77, // 111: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link - 78, // 112: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent + 68, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 65, // 61: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 46, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 46, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp + 66, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 65, // 65: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 61, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType + 69, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType + 46, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 66, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 65, // 70: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 46, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 65, // 72: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 46, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 70, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo + 66, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 11, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.worker_commands_task:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask + 36, // 77: temporal.server.api.persistence.v1.WorkerCommandsTask.commands:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommand + 46, // 78: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 46, // 79: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp + 47, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 47, // 81: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 47, // 82: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 47, // 83: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration + 47, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 47, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 46, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp + 71, // 87: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure + 72, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads + 46, // 89: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp + 73, // 90: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType + 38, // 91: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + 54, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 55, // 93: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 46, // 94: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp + 46, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 74, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment + 75, // 97: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 59, // 98: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority + 39, // 99: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + 46, // 100: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp + 55, // 101: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 76, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy + 52, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 55, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 59, // 105: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 55, // 106: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 55, // 107: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 77, // 108: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor + 41, // 109: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus + 42, // 110: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM + 78, // 111: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link + 79, // 112: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent 20, // 113: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback - 43, // 114: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger - 44, // 115: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp - 79, // 116: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState - 44, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 70, // 118: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 119: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 45, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 80, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState - 44, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 70, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 45, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 128: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp - 44, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp - 81, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState - 44, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 70, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 133: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 44, // 134: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 82, // 135: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState - 83, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload - 83, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload - 84, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo - 85, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap + 45, // 114: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger + 46, // 115: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp + 80, // 116: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState + 46, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 71, // 118: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 46, // 119: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 47, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 46, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 81, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState + 46, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 71, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 46, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 47, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 47, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 46, // 128: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp + 46, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp + 82, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState + 46, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 71, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 46, // 133: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 46, // 134: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 83, // 135: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState + 84, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload + 84, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload + 85, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo + 86, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap 25, // 140: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo 4, // 141: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo - 44, // 142: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 38, // 143: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - 41, // 144: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - 86, // 145: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef - 42, // 146: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - 147, // [147:147] is the sub-list for method output_type - 147, // [147:147] is the sub-list for method input_type - 147, // [147:147] is the sub-list for extension type_name - 147, // [147:147] is the sub-list for extension extendee - 0, // [0:147] is the sub-list for field type_name + 37, // 142: temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommand.cancel_activity:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask.CancelActivity + 46, // 143: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 40, // 144: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + 43, // 145: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + 87, // 146: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef + 44, // 147: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + 148, // [148:148] is the sub-list for method output_type + 148, // [148:148] is the sub-list for method input_type + 148, // [148:148] is the sub-list for extension type_name + 148, // [148:148] is the sub-list for extension extendee + 0, // [0:148] is the sub-list for field type_name } func init() { file_temporal_server_api_persistence_v1_executions_proto_init() } @@ -5400,7 +5507,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { file_temporal_server_api_persistence_v1_executions_proto_msgTypes[10].OneofWrappers = []any{ (*OutboundTaskInfo_StateMachineInfo)(nil), (*OutboundTaskInfo_ChasmTaskInfo)(nil), - (*OutboundTaskInfo_ActivityCommandInfo)(nil), + (*OutboundTaskInfo_WorkerCommandsTask)(nil), } file_temporal_server_api_persistence_v1_executions_proto_msgTypes[14].OneofWrappers = []any{ (*ActivityInfo_UseWorkflowBuildIdInfo_)(nil), @@ -5410,11 +5517,14 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { (*Callback_Nexus_)(nil), (*Callback_Hsm)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36].OneofWrappers = []any{ + (*WorkerCommandsTask_WorkerCommand_CancelActivity)(nil), + } + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39].OneofWrappers = []any{ (*ActivityInfo_PauseInfo_Manual_)(nil), (*ActivityInfo_PauseInfo_RuleId)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[45].OneofWrappers = []any{ (*CallbackInfo_Trigger_WorkflowClosed)(nil), } type x struct{} @@ -5423,7 +5533,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_persistence_v1_executions_proto_rawDesc), len(file_temporal_server_api_persistence_v1_executions_proto_rawDesc)), NumEnums: 0, - NumMessages: 44, + NumMessages: 46, NumExtensions: 0, NumServices: 0, }, diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index a9397e4bf5c..fcaf83d62d0 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -1447,6 +1447,21 @@ func serializeOutboundTask( }, } case *tasks.ActivityCommandTask: + commands := make([]*persistencespb.WorkerCommandsTask_WorkerCommand, 0, len(task.TaskTokens)) + for _, token := range task.TaskTokens { + switch task.CommandType { + case enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL: + commands = append(commands, &persistencespb.WorkerCommandsTask_WorkerCommand{ + Type: &persistencespb.WorkerCommandsTask_WorkerCommand_CancelActivity{ + CancelActivity: &persistencespb.WorkerCommandsTask_CancelActivity{ + TaskToken: token, + }, + }, + }) + default: + return nil, serviceerror.NewInternalf("unknown activity command type: %v", task.CommandType) + } + } outboundTaskInfo = &persistencespb.OutboundTaskInfo{ NamespaceId: task.NamespaceID, WorkflowId: task.WorkflowID, @@ -1455,10 +1470,9 @@ func serializeOutboundTask( TaskType: task.GetType(), Destination: task.Destination, VisibilityTime: timestamppb.New(task.VisibilityTimestamp), - TaskDetails: &persistencespb.OutboundTaskInfo_ActivityCommandInfo{ - ActivityCommandInfo: &persistencespb.ActivityCommandTaskInfo{ - CommandType: task.CommandType, - TaskTokens: task.TaskTokens, + TaskDetails: &persistencespb.OutboundTaskInfo_WorkerCommandsTask{ + WorkerCommandsTask: &persistencespb.WorkerCommandsTask{ + Commands: commands, }, }, } @@ -1505,7 +1519,15 @@ func deserializeOutboundTask( Destination: info.Destination, }, nil case enumsspb.TASK_TYPE_ACTIVITY_COMMAND: - activityCommandInfo := info.GetActivityCommandInfo() + workerCommandsTask := info.GetWorkerCommandsTask() + var commandType enumsspb.ActivityCommandType + var taskTokens [][]byte + for _, cmd := range workerCommandsTask.GetCommands() { + if cancelActivity := cmd.GetCancelActivity(); cancelActivity != nil { + commandType = enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL + taskTokens = append(taskTokens, cancelActivity.GetTaskToken()) + } + } return &tasks.ActivityCommandTask{ WorkflowKey: definition.NewWorkflowKey( info.NamespaceId, @@ -1514,8 +1536,8 @@ func deserializeOutboundTask( ), VisibilityTimestamp: info.VisibilityTime.AsTime(), TaskID: info.TaskId, - CommandType: activityCommandInfo.GetCommandType(), - TaskTokens: activityCommandInfo.GetTaskTokens(), + CommandType: commandType, + TaskTokens: taskTokens, Destination: info.Destination, }, nil default: diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 75e2641f5d0..c61481b90bf 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -484,18 +484,25 @@ message OutboundTaskInfo { // If the task addresses a CHASM component, this field will be set. ChasmTaskInfo chasm_task_info = 9; - // If the task is an activity command task. - ActivityCommandTaskInfo activity_command_info = 10; + // If the task is a worker commands task. + WorkerCommandsTask worker_commands_task = 10; } } -// ActivityCommandTaskInfo contains details for activity command operation. -message ActivityCommandTaskInfo { - // Type of command to send. - temporal.server.api.enums.v1.ActivityCommandType command_type = 1; +// WorkerCommandsTask contains worker commands to dispatch via Nexus. +message WorkerCommandsTask { + repeated WorkerCommand commands = 1; - // Task tokens of activities. - repeated bytes task_tokens = 2; + message WorkerCommand { + oneof type { + CancelActivity cancel_activity = 1; + } + } + + message CancelActivity { + // Task token identifying the activity to cancel. + bytes task_token = 1; + } } message NexusInvocationTaskInfo { From d5591a4d13b3d46dd2e857ddd0603d7a125d6e6d Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 13 Mar 2026 13:06:45 -0700 Subject: [PATCH 24/40] Update api-go dependency to merged activity-cancel branch Made-with: Cursor --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 92b96de1dd7..9cd443c040b 100644 --- a/go.mod +++ b/go.mod @@ -174,4 +174,4 @@ require ( modernc.org/memory v1.11.0 // indirect ) -replace go.temporal.io/api => github.com/temporalio/api-go v1.62.2-0.20260217225453-e6a9241288b9 +replace go.temporal.io/api => github.com/temporalio/api-go v1.62.2-0.20260313200323-1c4f982100b9 diff --git a/go.sum b/go.sum index 204f76e6e49..35a5f560975 100644 --- a/go.sum +++ b/go.sum @@ -310,8 +310,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/temporalio/api-go v1.62.2-0.20260217225453-e6a9241288b9 h1:nv1DjGfsfM/ITt5ehoHodVzCtTxv+mM+sMqM9Zgx0Tc= -github.com/temporalio/api-go v1.62.2-0.20260217225453-e6a9241288b9/go.mod h1:oewVgOWEx67DlpbXkEJl5PlcpDPXjR8h9+raDfl0fpo= +github.com/temporalio/api-go v1.62.2-0.20260313200323-1c4f982100b9 h1:YssGqIj8Lkg7fRrbkMWylvs1cMKoLyAZIGYSWb/60QA= +github.com/temporalio/api-go v1.62.2-0.20260313200323-1c4f982100b9/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4= github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04= From 7f842cf6dd2ce46cda158e4a46fff4329feffcea Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 13 Mar 2026 15:36:13 -0700 Subject: [PATCH 25/40] Generalize task to WorkerCommandsTask using API WorkerCommand type - Rename ActivityCommandTask to WorkerCommandsTask, store []*workerpb.WorkerCommand directly - Use API worker.v1.WorkerCommand in persistence proto instead of duplicate nested types - Simplify serializer: commands pass through without conversion - Rename EnableActivityCancellationNexusTask to EnableCancelActivityWorkerCommand - Rename GenerateActivityCommandTasks to GenerateWorkerCommandsTasks - Rename AddActivityCommandTasks to AddWorkerCommandsTasks - Build WorkerCommand objects at the handler level (respondworkflowtaskcompleted) - Update all tests, mocks, and metrics Made-with: Cursor --- api/persistence/v1/executions.pb.go | 581 +++++++----------- common/dynamicconfig/constants.go | 6 +- .../serialization/task_serializers.go | 33 +- .../serialization/task_serializers_test.go | 32 +- go.mod | 2 +- go.sum | 4 +- .../api/persistence/v1/executions.proto | 14 +- .../workflow_task_completed_handler.go | 33 +- .../workflow_task_completed_handler_test.go | 62 +- service/history/configs/config.go | 4 +- service/history/interfaces/mutable_state.go | 3 +- .../history/interfaces/mutable_state_mock.go | 29 +- service/history/queues/metrics.go | 4 +- .../history/tasks/activity_command_task.go | 38 +- .../history/workflow/mutable_state_impl.go | 5 +- service/history/workflow/task_generator.go | 14 +- .../history/workflow/task_generator_mock.go | 30 +- .../history/workflow/task_generator_test.go | 38 +- 18 files changed, 414 insertions(+), 518 deletions(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index be01d9a0ac5..ef702471b1b 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -12,10 +12,11 @@ import ( unsafe "unsafe" v13 "go.temporal.io/api/common/v1" - v18 "go.temporal.io/api/deployment/v1" + v19 "go.temporal.io/api/deployment/v1" v11 "go.temporal.io/api/enums/v1" - v17 "go.temporal.io/api/failure/v1" - v19 "go.temporal.io/api/history/v1" + v18 "go.temporal.io/api/failure/v1" + v110 "go.temporal.io/api/history/v1" + v17 "go.temporal.io/api/worker/v1" v12 "go.temporal.io/api/workflow/v1" v15 "go.temporal.io/server/api/clock/v1" v1 "go.temporal.io/server/api/enums/v1" @@ -2360,8 +2361,8 @@ func (*OutboundTaskInfo_WorkerCommandsTask) isOutboundTaskInfo_TaskDetails() {} // WorkerCommandsTask contains worker commands to dispatch via Nexus. type WorkerCommandsTask struct { - state protoimpl.MessageState `protogen:"open.v1"` - Commands []*WorkerCommandsTask_WorkerCommand `protobuf:"bytes,1,rep,name=commands,proto3" json:"commands,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Commands []*v17.WorkerCommand `protobuf:"bytes,1,rep,name=commands,proto3" json:"commands,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2396,7 +2397,7 @@ func (*WorkerCommandsTask) Descriptor() ([]byte, []int) { return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11} } -func (x *WorkerCommandsTask) GetCommands() []*WorkerCommandsTask_WorkerCommand { +func (x *WorkerCommandsTask) GetCommands() []*v17.WorkerCommand { if x != nil { return x.Commands } @@ -2527,7 +2528,7 @@ type ActivityInfo struct { RetryExpirationTime *timestamppb.Timestamp `protobuf:"bytes,24,opt,name=retry_expiration_time,json=retryExpirationTime,proto3" json:"retry_expiration_time,omitempty"` RetryBackoffCoefficient float64 `protobuf:"fixed64,25,opt,name=retry_backoff_coefficient,json=retryBackoffCoefficient,proto3" json:"retry_backoff_coefficient,omitempty"` RetryNonRetryableErrorTypes []string `protobuf:"bytes,26,rep,name=retry_non_retryable_error_types,json=retryNonRetryableErrorTypes,proto3" json:"retry_non_retryable_error_types,omitempty"` - RetryLastFailure *v17.Failure `protobuf:"bytes,27,opt,name=retry_last_failure,json=retryLastFailure,proto3" json:"retry_last_failure,omitempty"` + RetryLastFailure *v18.Failure `protobuf:"bytes,27,opt,name=retry_last_failure,json=retryLastFailure,proto3" json:"retry_last_failure,omitempty"` RetryLastWorkerIdentity string `protobuf:"bytes,28,opt,name=retry_last_worker_identity,json=retryLastWorkerIdentity,proto3" json:"retry_last_worker_identity,omitempty"` ScheduledEventId int64 `protobuf:"varint,30,opt,name=scheduled_event_id,json=scheduledEventId,proto3" json:"scheduled_event_id,omitempty"` LastHeartbeatDetails *v13.Payloads `protobuf:"bytes,31,opt,name=last_heartbeat_details,json=lastHeartbeatDetails,proto3" json:"last_heartbeat_details,omitempty"` @@ -2566,14 +2567,14 @@ type ActivityInfo struct { // The deployment this activity was dispatched to most recently. Present only if the activity // was dispatched to a versioned worker. // Deprecated. Replaced by last_worker_deployment_version. - LastStartedDeployment *v18.Deployment `protobuf:"bytes,43,opt,name=last_started_deployment,json=lastStartedDeployment,proto3" json:"last_started_deployment,omitempty"` + LastStartedDeployment *v19.Deployment `protobuf:"bytes,43,opt,name=last_started_deployment,json=lastStartedDeployment,proto3" json:"last_started_deployment,omitempty"` // The deployment this activity was dispatched to most recently. Present only if the activity // was dispatched to a versioned worker. // Deprecated. Clean up with versioning-3.1. [cleanup-old-wv] LastWorkerDeploymentVersion string `protobuf:"bytes,44,opt,name=last_worker_deployment_version,json=lastWorkerDeploymentVersion,proto3" json:"last_worker_deployment_version,omitempty"` // The deployment version this activity was dispatched to most recently. Present only if the activity // was dispatched to a versioned worker. - LastDeploymentVersion *v18.WorkerDeploymentVersion `protobuf:"bytes,49,opt,name=last_deployment_version,json=lastDeploymentVersion,proto3" json:"last_deployment_version,omitempty"` + LastDeploymentVersion *v19.WorkerDeploymentVersion `protobuf:"bytes,49,opt,name=last_deployment_version,json=lastDeploymentVersion,proto3" json:"last_deployment_version,omitempty"` // Priority metadata. If this message is not present, or any fields are not // present, they inherit the values from the workflow. Priority *v13.Priority `protobuf:"bytes,45,opt,name=priority,proto3" json:"priority,omitempty"` @@ -2787,7 +2788,7 @@ func (x *ActivityInfo) GetRetryNonRetryableErrorTypes() []string { return nil } -func (x *ActivityInfo) GetRetryLastFailure() *v17.Failure { +func (x *ActivityInfo) GetRetryLastFailure() *v18.Failure { if x != nil { return x.RetryLastFailure } @@ -2903,7 +2904,7 @@ func (x *ActivityInfo) GetPaused() bool { return false } -func (x *ActivityInfo) GetLastStartedDeployment() *v18.Deployment { +func (x *ActivityInfo) GetLastStartedDeployment() *v19.Deployment { if x != nil { return x.LastStartedDeployment } @@ -2917,7 +2918,7 @@ func (x *ActivityInfo) GetLastWorkerDeploymentVersion() string { return "" } -func (x *ActivityInfo) GetLastDeploymentVersion() *v18.WorkerDeploymentVersion { +func (x *ActivityInfo) GetLastDeploymentVersion() *v19.WorkerDeploymentVersion { if x != nil { return x.LastDeploymentVersion } @@ -3538,7 +3539,7 @@ type HSMCompletionCallbackArg struct { // run ID of the workflow that just completed. RunId string `protobuf:"bytes,3,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` // Last event of the completed workflow. - LastEvent *v19.HistoryEvent `protobuf:"bytes,4,opt,name=last_event,json=lastEvent,proto3" json:"last_event,omitempty"` + LastEvent *v110.HistoryEvent `protobuf:"bytes,4,opt,name=last_event,json=lastEvent,proto3" json:"last_event,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3594,7 +3595,7 @@ func (x *HSMCompletionCallbackArg) GetRunId() string { return "" } -func (x *HSMCompletionCallbackArg) GetLastEvent() *v19.HistoryEvent { +func (x *HSMCompletionCallbackArg) GetLastEvent() *v110.HistoryEvent { if x != nil { return x.LastEvent } @@ -3616,7 +3617,7 @@ type CallbackInfo struct { // The time when the last attempt completed. LastAttemptCompleteTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_attempt_complete_time,json=lastAttemptCompleteTime,proto3" json:"last_attempt_complete_time,omitempty"` // The last attempt's failure, if any. - LastAttemptFailure *v17.Failure `protobuf:"bytes,7,opt,name=last_attempt_failure,json=lastAttemptFailure,proto3" json:"last_attempt_failure,omitempty"` + LastAttemptFailure *v18.Failure `protobuf:"bytes,7,opt,name=last_attempt_failure,json=lastAttemptFailure,proto3" json:"last_attempt_failure,omitempty"` // The time when the next attempt is scheduled. NextAttemptScheduleTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=next_attempt_schedule_time,json=nextAttemptScheduleTime,proto3" json:"next_attempt_schedule_time,omitempty"` // Request ID that added the callback. @@ -3697,7 +3698,7 @@ func (x *CallbackInfo) GetLastAttemptCompleteTime() *timestamppb.Timestamp { return nil } -func (x *CallbackInfo) GetLastAttemptFailure() *v17.Failure { +func (x *CallbackInfo) GetLastAttemptFailure() *v18.Failure { if x != nil { return x.LastAttemptFailure } @@ -3749,7 +3750,7 @@ type NexusOperationInfo struct { // The time when the last attempt completed. LastAttemptCompleteTime *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=last_attempt_complete_time,json=lastAttemptCompleteTime,proto3" json:"last_attempt_complete_time,omitempty"` // The last attempt's failure, if any. - LastAttemptFailure *v17.Failure `protobuf:"bytes,13,opt,name=last_attempt_failure,json=lastAttemptFailure,proto3" json:"last_attempt_failure,omitempty"` + LastAttemptFailure *v18.Failure `protobuf:"bytes,13,opt,name=last_attempt_failure,json=lastAttemptFailure,proto3" json:"last_attempt_failure,omitempty"` // The time when the next attempt is scheduled. NextAttemptScheduleTime *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=next_attempt_schedule_time,json=nextAttemptScheduleTime,proto3" json:"next_attempt_schedule_time,omitempty"` // Endpoint ID, the name is also stored here (field 1) but we use the ID internally to avoid failing operation @@ -3878,7 +3879,7 @@ func (x *NexusOperationInfo) GetLastAttemptCompleteTime() *timestamppb.Timestamp return nil } -func (x *NexusOperationInfo) GetLastAttemptFailure() *v17.Failure { +func (x *NexusOperationInfo) GetLastAttemptFailure() *v18.Failure { if x != nil { return x.LastAttemptFailure } @@ -3932,7 +3933,7 @@ type NexusOperationCancellationInfo struct { // The time when the last attempt completed. LastAttemptCompleteTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=last_attempt_complete_time,json=lastAttemptCompleteTime,proto3" json:"last_attempt_complete_time,omitempty"` // The last attempt's failure, if any. - LastAttemptFailure *v17.Failure `protobuf:"bytes,5,opt,name=last_attempt_failure,json=lastAttemptFailure,proto3" json:"last_attempt_failure,omitempty"` + LastAttemptFailure *v18.Failure `protobuf:"bytes,5,opt,name=last_attempt_failure,json=lastAttemptFailure,proto3" json:"last_attempt_failure,omitempty"` // The time when the next attempt is scheduled. NextAttemptScheduleTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=next_attempt_schedule_time,json=nextAttemptScheduleTime,proto3" json:"next_attempt_schedule_time,omitempty"` // The event ID of the NEXUS_OPERATION_CANCEL_REQUESTED event for this cancelation. @@ -3999,7 +4000,7 @@ func (x *NexusOperationCancellationInfo) GetLastAttemptCompleteTime() *timestamp return nil } -func (x *NexusOperationCancellationInfo) GetLastAttemptFailure() *v17.Failure { +func (x *NexusOperationCancellationInfo) GetLastAttemptFailure() *v18.Failure { if x != nil { return x.LastAttemptFailure } @@ -4184,117 +4185,6 @@ func (x *TransferTaskInfo_CloseExecutionTaskDetails) GetCanSkipVisibilityArchiva return false } -type WorkerCommandsTask_WorkerCommand struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Type: - // - // *WorkerCommandsTask_WorkerCommand_CancelActivity - Type isWorkerCommandsTask_WorkerCommand_Type `protobuf_oneof:"type"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WorkerCommandsTask_WorkerCommand) Reset() { - *x = WorkerCommandsTask_WorkerCommand{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WorkerCommandsTask_WorkerCommand) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WorkerCommandsTask_WorkerCommand) ProtoMessage() {} - -func (x *WorkerCommandsTask_WorkerCommand) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WorkerCommandsTask_WorkerCommand.ProtoReflect.Descriptor instead. -func (*WorkerCommandsTask_WorkerCommand) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11, 0} -} - -func (x *WorkerCommandsTask_WorkerCommand) GetType() isWorkerCommandsTask_WorkerCommand_Type { - if x != nil { - return x.Type - } - return nil -} - -func (x *WorkerCommandsTask_WorkerCommand) GetCancelActivity() *WorkerCommandsTask_CancelActivity { - if x != nil { - if x, ok := x.Type.(*WorkerCommandsTask_WorkerCommand_CancelActivity); ok { - return x.CancelActivity - } - } - return nil -} - -type isWorkerCommandsTask_WorkerCommand_Type interface { - isWorkerCommandsTask_WorkerCommand_Type() -} - -type WorkerCommandsTask_WorkerCommand_CancelActivity struct { - CancelActivity *WorkerCommandsTask_CancelActivity `protobuf:"bytes,1,opt,name=cancel_activity,json=cancelActivity,proto3,oneof"` -} - -func (*WorkerCommandsTask_WorkerCommand_CancelActivity) isWorkerCommandsTask_WorkerCommand_Type() {} - -type WorkerCommandsTask_CancelActivity struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Task token identifying the activity to cancel. - TaskToken []byte `protobuf:"bytes,1,opt,name=task_token,json=taskToken,proto3" json:"task_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WorkerCommandsTask_CancelActivity) Reset() { - *x = WorkerCommandsTask_CancelActivity{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WorkerCommandsTask_CancelActivity) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WorkerCommandsTask_CancelActivity) ProtoMessage() {} - -func (x *WorkerCommandsTask_CancelActivity) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WorkerCommandsTask_CancelActivity.ProtoReflect.Descriptor instead. -func (*WorkerCommandsTask_CancelActivity) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP(), []int{11, 1} -} - -func (x *WorkerCommandsTask_CancelActivity) GetTaskToken() []byte { - if x != nil { - return x.TaskToken - } - return nil -} - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] type ActivityInfo_UseWorkflowBuildIdInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4309,7 +4199,7 @@ type ActivityInfo_UseWorkflowBuildIdInfo struct { func (x *ActivityInfo_UseWorkflowBuildIdInfo) Reset() { *x = ActivityInfo_UseWorkflowBuildIdInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4321,7 +4211,7 @@ func (x *ActivityInfo_UseWorkflowBuildIdInfo) String() string { func (*ActivityInfo_UseWorkflowBuildIdInfo) ProtoMessage() {} func (x *ActivityInfo_UseWorkflowBuildIdInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4366,7 +4256,7 @@ type ActivityInfo_PauseInfo struct { func (x *ActivityInfo_PauseInfo) Reset() { *x = ActivityInfo_PauseInfo{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4378,7 +4268,7 @@ func (x *ActivityInfo_PauseInfo) String() string { func (*ActivityInfo_PauseInfo) ProtoMessage() {} func (x *ActivityInfo_PauseInfo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4456,7 +4346,7 @@ type ActivityInfo_PauseInfo_Manual struct { func (x *ActivityInfo_PauseInfo_Manual) Reset() { *x = ActivityInfo_PauseInfo_Manual{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4468,7 +4358,7 @@ func (x *ActivityInfo_PauseInfo_Manual) String() string { func (*ActivityInfo_PauseInfo_Manual) ProtoMessage() {} func (x *ActivityInfo_PauseInfo_Manual) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4513,7 +4403,7 @@ type Callback_Nexus struct { func (x *Callback_Nexus) Reset() { *x = Callback_Nexus{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[41] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4525,7 +4415,7 @@ func (x *Callback_Nexus) String() string { func (*Callback_Nexus) ProtoMessage() {} func (x *Callback_Nexus) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[41] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4575,7 +4465,7 @@ type Callback_HSM struct { func (x *Callback_HSM) Reset() { *x = Callback_HSM{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4587,7 +4477,7 @@ func (x *Callback_HSM) String() string { func (*Callback_HSM) ProtoMessage() {} func (x *Callback_HSM) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4647,7 +4537,7 @@ type CallbackInfo_WorkflowClosed struct { func (x *CallbackInfo_WorkflowClosed) Reset() { *x = CallbackInfo_WorkflowClosed{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[44] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4659,7 +4549,7 @@ func (x *CallbackInfo_WorkflowClosed) String() string { func (*CallbackInfo_WorkflowClosed) ProtoMessage() {} func (x *CallbackInfo_WorkflowClosed) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[44] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4687,7 +4577,7 @@ type CallbackInfo_Trigger struct { func (x *CallbackInfo_Trigger) Reset() { *x = CallbackInfo_Trigger{} - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[45] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4699,7 +4589,7 @@ func (x *CallbackInfo_Trigger) String() string { func (*CallbackInfo_Trigger) ProtoMessage() {} func (x *CallbackInfo_Trigger) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[45] + mi := &file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4745,7 +4635,7 @@ var File_temporal_server_api_persistence_v1_executions_proto protoreflect.FileDe const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\n" + - "3temporal/server/api/persistence/v1/executions.proto\x12\"temporal.server.api.persistence.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a\"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a%temporal/api/history/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a*temporal/server/api/clock/v1/message.proto\x1a)temporal/server/api/enums/v1/common.proto\x1a(temporal/server/api/enums/v1/nexus.proto\x1a+temporal/server/api/enums/v1/workflow.proto\x1a'temporal/server/api/enums/v1/task.proto\x1a5temporal/server/api/enums/v1/workflow_task_type.proto\x1a,temporal/server/api/history/v1/message.proto\x1a.temporal/server/api/persistence/v1/chasm.proto\x1a/temporal/server/api/persistence/v1/queues.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a/temporal/server/api/persistence/v1/update.proto\x1a-temporal/server/api/workflow/v1/message.proto\"\xa3\x05\n" + + "3temporal/server/api/persistence/v1/executions.proto\x12\"temporal.server.api.persistence.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a\"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/event_type.proto\x1a(temporal/api/enums/v1/failed_cause.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a%temporal/api/history/v1/message.proto\x1a(temporal/api/deployment/v1/message.proto\x1a$temporal/api/worker/v1/message.proto\x1a*temporal/server/api/clock/v1/message.proto\x1a)temporal/server/api/enums/v1/common.proto\x1a(temporal/server/api/enums/v1/nexus.proto\x1a+temporal/server/api/enums/v1/workflow.proto\x1a'temporal/server/api/enums/v1/task.proto\x1a5temporal/server/api/enums/v1/workflow_task_type.proto\x1a,temporal/server/api/history/v1/message.proto\x1a.temporal/server/api/persistence/v1/chasm.proto\x1a/temporal/server/api/persistence/v1/queues.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a/temporal/server/api/persistence/v1/update.proto\x1a-temporal/server/api/workflow/v1/message.proto\"\xa3\x05\n" + "\tShardInfo\x12\x19\n" + "\bshard_id\x18\x01 \x01(\x05R\ashardId\x12\x19\n" + "\brange_id\x18\x02 \x01(\x03R\arangeId\x12\x14\n" + @@ -5020,15 +4910,9 @@ const file_temporal_server_api_persistence_v1_executions_proto_rawDesc = "" + "\x0fchasm_task_info\x18\t \x01(\v21.temporal.server.api.persistence.v1.ChasmTaskInfoH\x00R\rchasmTaskInfo\x12j\n" + "\x14worker_commands_task\x18\n" + " \x01(\v26.temporal.server.api.persistence.v1.WorkerCommandsTaskH\x00R\x12workerCommandsTaskB\x0e\n" + - "\ftask_details\"\xb3\x02\n" + - "\x12WorkerCommandsTask\x12`\n" + - "\bcommands\x18\x01 \x03(\v2D.temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommandR\bcommands\x1a\x89\x01\n" + - "\rWorkerCommand\x12p\n" + - "\x0fcancel_activity\x18\x01 \x01(\v2E.temporal.server.api.persistence.v1.WorkerCommandsTask.CancelActivityH\x00R\x0ecancelActivityB\x06\n" + - "\x04type\x1a/\n" + - "\x0eCancelActivity\x12\x1d\n" + - "\n" + - "task_token\x18\x01 \x01(\fR\ttaskToken\"3\n" + + "\ftask_details\"W\n" + + "\x12WorkerCommandsTask\x12A\n" + + "\bcommands\x18\x01 \x03(\v2%.temporal.api.worker.v1.WorkerCommandR\bcommands\"3\n" + "\x17NexusInvocationTaskInfo\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"4\n" + "\x18NexusCancelationTaskInfo\x12\x18\n" + @@ -5234,7 +5118,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_rawDescGZIP() []by return file_temporal_server_api_persistence_v1_executions_proto_rawDescData } -var file_temporal_server_api_persistence_v1_executions_proto_msgTypes = make([]protoimpl.MessageInfo, 46) +var file_temporal_server_api_persistence_v1_executions_proto_msgTypes = make([]protoimpl.MessageInfo, 44) var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ (*ShardInfo)(nil), // 0: temporal.server.api.persistence.v1.ShardInfo (*WorkflowExecutionInfo)(nil), // 1: temporal.server.api.persistence.v1.WorkflowExecutionInfo @@ -5272,213 +5156,211 @@ var file_temporal_server_api_persistence_v1_executions_proto_goTypes = []any{ nil, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry nil, // 34: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry (*TransferTaskInfo_CloseExecutionTaskDetails)(nil), // 35: temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - (*WorkerCommandsTask_WorkerCommand)(nil), // 36: temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommand - (*WorkerCommandsTask_CancelActivity)(nil), // 37: temporal.server.api.persistence.v1.WorkerCommandsTask.CancelActivity - (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 38: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - (*ActivityInfo_PauseInfo)(nil), // 39: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - (*ActivityInfo_PauseInfo_Manual)(nil), // 40: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - (*Callback_Nexus)(nil), // 41: temporal.server.api.persistence.v1.Callback.Nexus - (*Callback_HSM)(nil), // 42: temporal.server.api.persistence.v1.Callback.HSM - nil, // 43: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - (*CallbackInfo_WorkflowClosed)(nil), // 44: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - (*CallbackInfo_Trigger)(nil), // 45: temporal.server.api.persistence.v1.CallbackInfo.Trigger - (*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 47: google.protobuf.Duration - (v1.WorkflowTaskType)(0), // 48: temporal.server.api.enums.v1.WorkflowTaskType - (v11.SuggestContinueAsNewReason)(0), // 49: temporal.api.enums.v1.SuggestContinueAsNewReason - (*v12.ResetPoints)(nil), // 50: temporal.api.workflow.v1.ResetPoints - (*v14.VersionHistories)(nil), // 51: temporal.server.api.history.v1.VersionHistories - (*v15.VectorClock)(nil), // 52: temporal.server.api.clock.v1.VectorClock - (*v16.BaseExecutionInfo)(nil), // 53: temporal.server.api.workflow.v1.BaseExecutionInfo - (*v13.WorkerVersionStamp)(nil), // 54: temporal.api.common.v1.WorkerVersionStamp - (*VersionedTransition)(nil), // 55: temporal.server.api.persistence.v1.VersionedTransition - (*StateMachineTimerGroup)(nil), // 56: temporal.server.api.persistence.v1.StateMachineTimerGroup - (*StateMachineTombstoneBatch)(nil), // 57: temporal.server.api.persistence.v1.StateMachineTombstoneBatch - (*v12.WorkflowExecutionVersioningInfo)(nil), // 58: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - (*v13.Priority)(nil), // 59: temporal.api.common.v1.Priority - (v11.WorkflowTaskFailedCause)(0), // 60: temporal.api.enums.v1.WorkflowTaskFailedCause - (v11.TimeoutType)(0), // 61: temporal.api.enums.v1.TimeoutType - (v1.WorkflowExecutionState)(0), // 62: temporal.server.api.enums.v1.WorkflowExecutionState - (v11.WorkflowExecutionStatus)(0), // 63: temporal.api.enums.v1.WorkflowExecutionStatus - (v11.EventType)(0), // 64: temporal.api.enums.v1.EventType - (v1.TaskType)(0), // 65: temporal.server.api.enums.v1.TaskType - (*ChasmTaskInfo)(nil), // 66: temporal.server.api.persistence.v1.ChasmTaskInfo - (v1.TaskPriority)(0), // 67: temporal.server.api.enums.v1.TaskPriority - (*v14.VersionHistoryItem)(nil), // 68: temporal.server.api.history.v1.VersionHistoryItem - (v1.WorkflowBackoffType)(0), // 69: temporal.server.api.enums.v1.WorkflowBackoffType - (*StateMachineTaskInfo)(nil), // 70: temporal.server.api.persistence.v1.StateMachineTaskInfo - (*v17.Failure)(nil), // 71: temporal.api.failure.v1.Failure - (*v13.Payloads)(nil), // 72: temporal.api.common.v1.Payloads - (*v13.ActivityType)(nil), // 73: temporal.api.common.v1.ActivityType - (*v18.Deployment)(nil), // 74: temporal.api.deployment.v1.Deployment - (*v18.WorkerDeploymentVersion)(nil), // 75: temporal.api.deployment.v1.WorkerDeploymentVersion - (v11.ParentClosePolicy)(0), // 76: temporal.api.enums.v1.ParentClosePolicy - (v1.ChecksumFlavor)(0), // 77: temporal.server.api.enums.v1.ChecksumFlavor - (*v13.Link)(nil), // 78: temporal.api.common.v1.Link - (*v19.HistoryEvent)(nil), // 79: temporal.api.history.v1.HistoryEvent - (v1.CallbackState)(0), // 80: temporal.server.api.enums.v1.CallbackState - (v1.NexusOperationState)(0), // 81: temporal.server.api.enums.v1.NexusOperationState - (v11.NexusOperationCancellationState)(0), // 82: temporal.api.enums.v1.NexusOperationCancellationState - (*QueueState)(nil), // 83: temporal.server.api.persistence.v1.QueueState - (*v13.Payload)(nil), // 84: temporal.api.common.v1.Payload - (*UpdateInfo)(nil), // 85: temporal.server.api.persistence.v1.UpdateInfo - (*StateMachineMap)(nil), // 86: temporal.server.api.persistence.v1.StateMachineMap - (*StateMachineRef)(nil), // 87: temporal.server.api.persistence.v1.StateMachineRef + (*ActivityInfo_UseWorkflowBuildIdInfo)(nil), // 36: temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + (*ActivityInfo_PauseInfo)(nil), // 37: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + (*ActivityInfo_PauseInfo_Manual)(nil), // 38: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + (*Callback_Nexus)(nil), // 39: temporal.server.api.persistence.v1.Callback.Nexus + (*Callback_HSM)(nil), // 40: temporal.server.api.persistence.v1.Callback.HSM + nil, // 41: temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + (*CallbackInfo_WorkflowClosed)(nil), // 42: temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + (*CallbackInfo_Trigger)(nil), // 43: temporal.server.api.persistence.v1.CallbackInfo.Trigger + (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 45: google.protobuf.Duration + (v1.WorkflowTaskType)(0), // 46: temporal.server.api.enums.v1.WorkflowTaskType + (v11.SuggestContinueAsNewReason)(0), // 47: temporal.api.enums.v1.SuggestContinueAsNewReason + (*v12.ResetPoints)(nil), // 48: temporal.api.workflow.v1.ResetPoints + (*v14.VersionHistories)(nil), // 49: temporal.server.api.history.v1.VersionHistories + (*v15.VectorClock)(nil), // 50: temporal.server.api.clock.v1.VectorClock + (*v16.BaseExecutionInfo)(nil), // 51: temporal.server.api.workflow.v1.BaseExecutionInfo + (*v13.WorkerVersionStamp)(nil), // 52: temporal.api.common.v1.WorkerVersionStamp + (*VersionedTransition)(nil), // 53: temporal.server.api.persistence.v1.VersionedTransition + (*StateMachineTimerGroup)(nil), // 54: temporal.server.api.persistence.v1.StateMachineTimerGroup + (*StateMachineTombstoneBatch)(nil), // 55: temporal.server.api.persistence.v1.StateMachineTombstoneBatch + (*v12.WorkflowExecutionVersioningInfo)(nil), // 56: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + (*v13.Priority)(nil), // 57: temporal.api.common.v1.Priority + (v11.WorkflowTaskFailedCause)(0), // 58: temporal.api.enums.v1.WorkflowTaskFailedCause + (v11.TimeoutType)(0), // 59: temporal.api.enums.v1.TimeoutType + (v1.WorkflowExecutionState)(0), // 60: temporal.server.api.enums.v1.WorkflowExecutionState + (v11.WorkflowExecutionStatus)(0), // 61: temporal.api.enums.v1.WorkflowExecutionStatus + (v11.EventType)(0), // 62: temporal.api.enums.v1.EventType + (v1.TaskType)(0), // 63: temporal.server.api.enums.v1.TaskType + (*ChasmTaskInfo)(nil), // 64: temporal.server.api.persistence.v1.ChasmTaskInfo + (v1.TaskPriority)(0), // 65: temporal.server.api.enums.v1.TaskPriority + (*v14.VersionHistoryItem)(nil), // 66: temporal.server.api.history.v1.VersionHistoryItem + (v1.WorkflowBackoffType)(0), // 67: temporal.server.api.enums.v1.WorkflowBackoffType + (*StateMachineTaskInfo)(nil), // 68: temporal.server.api.persistence.v1.StateMachineTaskInfo + (*v17.WorkerCommand)(nil), // 69: temporal.api.worker.v1.WorkerCommand + (*v18.Failure)(nil), // 70: temporal.api.failure.v1.Failure + (*v13.Payloads)(nil), // 71: temporal.api.common.v1.Payloads + (*v13.ActivityType)(nil), // 72: temporal.api.common.v1.ActivityType + (*v19.Deployment)(nil), // 73: temporal.api.deployment.v1.Deployment + (*v19.WorkerDeploymentVersion)(nil), // 74: temporal.api.deployment.v1.WorkerDeploymentVersion + (v11.ParentClosePolicy)(0), // 75: temporal.api.enums.v1.ParentClosePolicy + (v1.ChecksumFlavor)(0), // 76: temporal.server.api.enums.v1.ChecksumFlavor + (*v13.Link)(nil), // 77: temporal.api.common.v1.Link + (*v110.HistoryEvent)(nil), // 78: temporal.api.history.v1.HistoryEvent + (v1.CallbackState)(0), // 79: temporal.server.api.enums.v1.CallbackState + (v1.NexusOperationState)(0), // 80: temporal.server.api.enums.v1.NexusOperationState + (v11.NexusOperationCancellationState)(0), // 81: temporal.api.enums.v1.NexusOperationCancellationState + (*QueueState)(nil), // 82: temporal.server.api.persistence.v1.QueueState + (*v13.Payload)(nil), // 83: temporal.api.common.v1.Payload + (*UpdateInfo)(nil), // 84: temporal.server.api.persistence.v1.UpdateInfo + (*StateMachineMap)(nil), // 85: temporal.server.api.persistence.v1.StateMachineMap + (*StateMachineRef)(nil), // 86: temporal.server.api.persistence.v1.StateMachineRef } var file_temporal_server_api_persistence_v1_executions_proto_depIdxs = []int32{ - 46, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp + 44, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp 27, // 1: temporal.server.api.persistence.v1.ShardInfo.replication_dlq_ack_level:type_name -> temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry 28, // 2: temporal.server.api.persistence.v1.ShardInfo.queue_states:type_name -> temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry - 47, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration - 47, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration - 47, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration - 46, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp - 46, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp - 47, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration - 46, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp - 46, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp - 46, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp - 48, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType - 49, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason - 47, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration - 47, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 47, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 46, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp - 50, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints + 45, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration + 45, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration + 45, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration + 44, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp + 44, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp + 45, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration + 44, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp + 44, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp + 46, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType + 47, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason + 45, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 45, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 44, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp + 48, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints 29, // 19: temporal.server.api.persistence.v1.WorkflowExecutionInfo.search_attributes:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry 30, // 20: temporal.server.api.persistence.v1.WorkflowExecutionInfo.memo:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry - 51, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 49, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories 2, // 22: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_stats:type_name -> temporal.server.api.persistence.v1.ExecutionStats - 46, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp - 46, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp - 52, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock - 46, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp - 53, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo - 54, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 44, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp + 44, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp + 50, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock + 44, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp + 51, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo + 52, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp 31, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.update_infos:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry - 55, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition 32, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machines_by_type:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry - 56, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup - 55, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 55, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 55, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 57, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch - 58, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - 55, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 55, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 54, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup + 53, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 55, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch + 56, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + 53, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition 33, // 40: temporal.server.api.persistence.v1.WorkflowExecutionInfo.children_initialized_post_reset_point:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry - 59, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 57, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority 26, // 42: temporal.server.api.persistence.v1.WorkflowExecutionInfo.pause_info:type_name -> temporal.server.api.persistence.v1.WorkflowPauseInfo - 60, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause - 61, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType - 62, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState - 63, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus - 55, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 46, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp + 58, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause + 59, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType + 60, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState + 61, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus + 53, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 44, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp 34, // 49: temporal.server.api.persistence.v1.WorkflowExecutionState.request_ids:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry - 64, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType - 65, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 46, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 62, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType + 63, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp 35, // 53: temporal.server.api.persistence.v1.TransferTaskInfo.close_execution_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - 66, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 65, // 55: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 46, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 67, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority - 55, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 64, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 63, // 55: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 65, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority + 53, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition 6, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo - 68, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 65, // 61: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 46, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 46, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp - 66, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 65, // 65: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 61, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType - 69, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType - 46, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 66, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 65, // 70: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 46, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 65, // 72: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 46, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 70, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo - 66, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 66, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 63, // 61: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 44, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp + 64, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 63, // 65: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 59, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType + 67, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType + 44, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 64, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 63, // 70: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 63, // 72: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 44, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 68, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo + 64, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo 11, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.worker_commands_task:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask - 36, // 77: temporal.server.api.persistence.v1.WorkerCommandsTask.commands:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommand - 46, // 78: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 46, // 79: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp - 47, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 47, // 81: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 47, // 82: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 47, // 83: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration - 47, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 47, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 46, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp - 71, // 87: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure - 72, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads - 46, // 89: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp - 73, // 90: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType - 38, // 91: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - 54, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 55, // 93: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 46, // 94: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp - 46, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 74, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment - 75, // 97: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion - 59, // 98: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority - 39, // 99: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - 46, // 100: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp - 55, // 101: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 76, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy - 52, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 55, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 59, // 105: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority - 55, // 106: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 55, // 107: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 77, // 108: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor - 41, // 109: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus - 42, // 110: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM - 78, // 111: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link - 79, // 112: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent + 69, // 77: temporal.server.api.persistence.v1.WorkerCommandsTask.commands:type_name -> temporal.api.worker.v1.WorkerCommand + 44, // 78: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 79: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp + 45, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 81: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 82: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 83: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration + 45, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 45, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 44, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp + 70, // 87: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure + 71, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads + 44, // 89: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp + 72, // 90: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType + 36, // 91: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + 52, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 53, // 93: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 44, // 94: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp + 44, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 73, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment + 74, // 97: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 57, // 98: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority + 37, // 99: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + 44, // 100: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp + 53, // 101: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 75, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy + 50, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 53, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 57, // 105: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 53, // 106: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 53, // 107: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 76, // 108: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor + 39, // 109: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus + 40, // 110: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM + 77, // 111: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link + 78, // 112: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent 20, // 113: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback - 45, // 114: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger - 46, // 115: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp - 80, // 116: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState - 46, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 71, // 118: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 46, // 119: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 47, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 46, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 81, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState - 46, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 71, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 46, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 47, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 47, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 46, // 128: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp - 46, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp - 82, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState - 46, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 71, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 46, // 133: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 46, // 134: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 83, // 135: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState - 84, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload - 84, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload - 85, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo - 86, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap + 43, // 114: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger + 44, // 115: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp + 79, // 116: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState + 44, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 70, // 118: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 119: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 45, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 44, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 80, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState + 44, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 70, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 45, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 45, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 44, // 128: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp + 44, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp + 81, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState + 44, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 70, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 44, // 133: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 44, // 134: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 82, // 135: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState + 83, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload + 83, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload + 84, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo + 85, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap 25, // 140: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo 4, // 141: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo - 37, // 142: temporal.server.api.persistence.v1.WorkerCommandsTask.WorkerCommand.cancel_activity:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask.CancelActivity - 46, // 143: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 40, // 144: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - 43, // 145: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - 87, // 146: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef - 44, // 147: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - 148, // [148:148] is the sub-list for method output_type - 148, // [148:148] is the sub-list for method input_type - 148, // [148:148] is the sub-list for extension type_name - 148, // [148:148] is the sub-list for extension extendee - 0, // [0:148] is the sub-list for field type_name + 44, // 142: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 38, // 143: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + 41, // 144: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + 86, // 145: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef + 42, // 146: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + 147, // [147:147] is the sub-list for method output_type + 147, // [147:147] is the sub-list for method input_type + 147, // [147:147] is the sub-list for extension type_name + 147, // [147:147] is the sub-list for extension extendee + 0, // [0:147] is the sub-list for field type_name } func init() { file_temporal_server_api_persistence_v1_executions_proto_init() } @@ -5517,14 +5399,11 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { (*Callback_Nexus_)(nil), (*Callback_Hsm)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[36].OneofWrappers = []any{ - (*WorkerCommandsTask_WorkerCommand_CancelActivity)(nil), - } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[39].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37].OneofWrappers = []any{ (*ActivityInfo_PauseInfo_Manual_)(nil), (*ActivityInfo_PauseInfo_RuleId)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[45].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43].OneofWrappers = []any{ (*CallbackInfo_Trigger_WorkflowClosed)(nil), } type x struct{} @@ -5533,7 +5412,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_persistence_v1_executions_proto_rawDesc), len(file_temporal_server_api_persistence_v1_executions_proto_rawDesc)), NumEnums: 0, - NumMessages: 46, + NumMessages: 44, NumExtensions: 0, NumServices: 0, }, diff --git a/common/dynamicconfig/constants.go b/common/dynamicconfig/constants.go index b85f052b861..2fd1b474e33 100644 --- a/common/dynamicconfig/constants.go +++ b/common/dynamicconfig/constants.go @@ -189,10 +189,10 @@ config as the other services.`, false, `EnableActivityEagerExecution indicates if activity eager execution is enabled per namespace`, ) - EnableActivityCancellationNexusTask = NewGlobalBoolSetting( - "system.enableActivityCancellationNexusTask", + EnableCancelActivityWorkerCommand = NewGlobalBoolSetting( + "system.enableCancelActivityWorkerCommand", false, - `EnableActivityCancellationNexusTask enables pushing activity cancellation to workers via Nexus task`, + `EnableCancelActivityWorkerCommand enables pushing activity cancellation to workers via Nexus worker commands`, ) NamespaceMinRetentionGlobal = NewGlobalDurationSetting( "system.namespaceMinRetentionGlobal", diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index fcaf83d62d0..442d6ba5a0b 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -1446,22 +1446,7 @@ func serializeOutboundTask( ChasmTaskInfo: task.Info, }, } - case *tasks.ActivityCommandTask: - commands := make([]*persistencespb.WorkerCommandsTask_WorkerCommand, 0, len(task.TaskTokens)) - for _, token := range task.TaskTokens { - switch task.CommandType { - case enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL: - commands = append(commands, &persistencespb.WorkerCommandsTask_WorkerCommand{ - Type: &persistencespb.WorkerCommandsTask_WorkerCommand_CancelActivity{ - CancelActivity: &persistencespb.WorkerCommandsTask_CancelActivity{ - TaskToken: token, - }, - }, - }) - default: - return nil, serviceerror.NewInternalf("unknown activity command type: %v", task.CommandType) - } - } + case *tasks.WorkerCommandsTask: outboundTaskInfo = &persistencespb.OutboundTaskInfo{ NamespaceId: task.NamespaceID, WorkflowId: task.WorkflowID, @@ -1472,7 +1457,7 @@ func serializeOutboundTask( VisibilityTime: timestamppb.New(task.VisibilityTimestamp), TaskDetails: &persistencespb.OutboundTaskInfo_WorkerCommandsTask{ WorkerCommandsTask: &persistencespb.WorkerCommandsTask{ - Commands: commands, + Commands: task.Commands, }, }, } @@ -1519,16 +1504,7 @@ func deserializeOutboundTask( Destination: info.Destination, }, nil case enumsspb.TASK_TYPE_ACTIVITY_COMMAND: - workerCommandsTask := info.GetWorkerCommandsTask() - var commandType enumsspb.ActivityCommandType - var taskTokens [][]byte - for _, cmd := range workerCommandsTask.GetCommands() { - if cancelActivity := cmd.GetCancelActivity(); cancelActivity != nil { - commandType = enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL - taskTokens = append(taskTokens, cancelActivity.GetTaskToken()) - } - } - return &tasks.ActivityCommandTask{ + return &tasks.WorkerCommandsTask{ WorkflowKey: definition.NewWorkflowKey( info.NamespaceId, info.WorkflowId, @@ -1536,8 +1512,7 @@ func deserializeOutboundTask( ), VisibilityTimestamp: info.VisibilityTime.AsTime(), TaskID: info.TaskId, - CommandType: commandType, - TaskTokens: taskTokens, + Commands: info.GetWorkerCommandsTask().GetCommands(), Destination: info.Destination, }, nil default: diff --git a/common/persistence/serialization/task_serializers_test.go b/common/persistence/serialization/task_serializers_test.go index 9afca6ae738..febb0300be8 100644 --- a/common/persistence/serialization/task_serializers_test.go +++ b/common/persistence/serialization/task_serializers_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" enumsspb "go.temporal.io/server/api/enums/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/definition" @@ -169,17 +170,36 @@ func (s *taskSerializerSuite) TestTransferResetTask() { s.assertEqualTasks(resetTask) } -func (s *taskSerializerSuite) TestOutboundActivityCommandTask() { - activityCommandTask := &tasks.ActivityCommandTask{ +func (s *taskSerializerSuite) TestOutboundWorkerCommandsTask() { + workerCommandsTask := &tasks.WorkerCommandsTask{ WorkflowKey: s.workflowKey, VisibilityTimestamp: time.Unix(0, rand.Int63()).UTC(), TaskID: rand.Int63(), - CommandType: enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, - TaskTokens: [][]byte{[]byte("token1"), []byte("token2"), []byte("token3")}, - Destination: "test-control-queue", + Commands: []*workerpb.WorkerCommand{ + {Type: &workerpb.WorkerCommand_CancelActivity{ + CancelActivity: &workerpb.CancelActivityCommand{TaskToken: []byte("token1")}, + }}, + {Type: &workerpb.WorkerCommand_CancelActivity{ + CancelActivity: &workerpb.CancelActivityCommand{TaskToken: []byte("token2")}, + }}, + {Type: &workerpb.WorkerCommand_CancelActivity{ + CancelActivity: &workerpb.CancelActivityCommand{TaskToken: []byte("token3")}, + }}, + }, + Destination: "test-control-queue", } - s.assertEqualTasks(activityCommandTask) + s.assertEqualTasksWithOpts(workerCommandsTask, + func(task, deserializedTask tasks.Task) { + orig := task.(*tasks.WorkerCommandsTask).Commands + deser := deserializedTask.(*tasks.WorkerCommandsTask).Commands + s.Require().Len(deser, len(orig)) + for i := range orig { + protorequire.ProtoEqual(s.T(), orig[i], deser[i]) + } + }, + cmpopts.IgnoreFields(tasks.WorkerCommandsTask{}, "Commands"), + ) } func (s *taskSerializerSuite) TestTimerWorkflowTask() { diff --git a/go.mod b/go.mod index 9cd443c040b..b2171713416 100644 --- a/go.mod +++ b/go.mod @@ -174,4 +174,4 @@ require ( modernc.org/memory v1.11.0 // indirect ) -replace go.temporal.io/api => github.com/temporalio/api-go v1.62.2-0.20260313200323-1c4f982100b9 +replace go.temporal.io/api => github.com/temporalio/api-go v1.62.2-0.20260313212811-d44912090759 diff --git a/go.sum b/go.sum index 35a5f560975..e9d2b357fac 100644 --- a/go.sum +++ b/go.sum @@ -310,8 +310,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/temporalio/api-go v1.62.2-0.20260313200323-1c4f982100b9 h1:YssGqIj8Lkg7fRrbkMWylvs1cMKoLyAZIGYSWb/60QA= -github.com/temporalio/api-go v1.62.2-0.20260313200323-1c4f982100b9/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +github.com/temporalio/api-go v1.62.2-0.20260313212811-d44912090759 h1:CSlBGjKIgi770YWTYB1dt2AJuLKU6yArSZL636UStdo= +github.com/temporalio/api-go v1.62.2-0.20260313212811-d44912090759/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4= github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04= diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index c61481b90bf..d60626da7fb 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -15,6 +15,7 @@ import "temporal/api/failure/v1/message.proto"; import "temporal/api/workflow/v1/message.proto"; import "temporal/api/history/v1/message.proto"; import "temporal/api/deployment/v1/message.proto"; +import "temporal/api/worker/v1/message.proto"; import "temporal/server/api/clock/v1/message.proto"; import "temporal/server/api/enums/v1/common.proto"; @@ -491,18 +492,7 @@ message OutboundTaskInfo { // WorkerCommandsTask contains worker commands to dispatch via Nexus. message WorkerCommandsTask { - repeated WorkerCommand commands = 1; - - message WorkerCommand { - oneof type { - CancelActivity cancel_activity = 1; - } - } - - message CancelActivity { - // Task token identifying the activity to cancel. - bytes task_token = 1; - } + repeated temporal.api.worker.v1.WorkerCommand commands = 1; } message NexusInvocationTaskInfo { diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 1e52af2c6da..c3d81b12dff 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -17,8 +17,8 @@ import ( historypb "go.temporal.io/api/history/v1" protocolpb "go.temporal.io/api/protocol/v1" "go.temporal.io/api/serviceerror" + workerpb "go.temporal.io/api/worker/v1" "go.temporal.io/api/workflowservice/v1" - enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/api/matchingservice/v1" "go.temporal.io/server/common" @@ -67,7 +67,7 @@ type ( effects effect.Controller initiatedChildExecutionsInBatch map[string]struct{} // Set of initiated child executions in the workflow task updateRegistry update.Registry - pendingActivityCancelsByControlQueue map[string][][]byte // Batched activity cancel task tokens by control queue + pendingWorkerCommandsByControlQueue map[string][]*workerpb.WorkerCommand // Batched worker commands by control queue // validation attrValidator *api.CommandAttrValidator @@ -212,7 +212,7 @@ func (handler *workflowTaskCompletedHandler) handleCommands( } } - if err := handler.flushBatchedActivityCommandTasks(); err != nil { + if err := handler.flushWorkerCommandsTasks(); err != nil { return nil, err } @@ -688,26 +688,31 @@ func (handler *workflowTaskCompletedHandler) handleCommandRequestCancelActivity( if err != nil { return nil, err } - if handler.pendingActivityCancelsByControlQueue == nil { - handler.pendingActivityCancelsByControlQueue = make(map[string][][]byte) + if handler.pendingWorkerCommandsByControlQueue == nil { + handler.pendingWorkerCommandsByControlQueue = make(map[string][]*workerpb.WorkerCommand) } - handler.pendingActivityCancelsByControlQueue[ai.WorkerControlTaskQueue] = append( - handler.pendingActivityCancelsByControlQueue[ai.WorkerControlTaskQueue], - taskToken, + handler.pendingWorkerCommandsByControlQueue[ai.WorkerControlTaskQueue] = append( + handler.pendingWorkerCommandsByControlQueue[ai.WorkerControlTaskQueue], + &workerpb.WorkerCommand{ + Type: &workerpb.WorkerCommand_CancelActivity{ + CancelActivity: &workerpb.CancelActivityCommand{ + TaskToken: taskToken, + }, + }, + }, ) } } return actCancelReqEvent, nil } -// flushBatchedActivityCommandTasks creates ActivityCommandTasks for all collected activity cancellations, +// flushWorkerCommandsTasks creates WorkerCommandsTasks for all collected worker commands, // batched by control queue. -func (handler *workflowTaskCompletedHandler) flushBatchedActivityCommandTasks() error { - for controlQueue, taskTokens := range handler.pendingActivityCancelsByControlQueue { - if err := handler.mutableState.AddActivityCommandTasks( - taskTokens, +func (handler *workflowTaskCompletedHandler) flushWorkerCommandsTasks() error { + for controlQueue, commands := range handler.pendingWorkerCommandsByControlQueue { + if err := handler.mutableState.AddWorkerCommandsTasks( + commands, controlQueue, - enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, ); err != nil { return err } diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go index 270f6f5863d..7e575fdcde7 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go @@ -16,7 +16,7 @@ import ( sdkpb "go.temporal.io/api/sdk/v1" "go.temporal.io/api/serviceerror" updatepb "go.temporal.io/api/update/v1" - enumsspb "go.temporal.io/server/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/backoff" "go.temporal.io/server/common/collection" @@ -386,7 +386,7 @@ func mustMarshalAny(t *testing.T, pb proto.Message) *anypb.Any { return &a } -func TestFlushBatchedActivityCommandTasks(t *testing.T) { +func TestFlushWorkerCommandsTasks(t *testing.T) { t.Parallel() token1 := []byte("token1") @@ -394,24 +394,38 @@ func TestFlushBatchedActivityCommandTasks(t *testing.T) { token3 := []byte("token3") token4 := []byte("token4") - t.Run("batches activities by control queue", func(t *testing.T) { + makeCommands := func(tokens ...[]byte) []*workerpb.WorkerCommand { + commands := make([]*workerpb.WorkerCommand, 0, len(tokens)) + for _, token := range tokens { + commands = append(commands, &workerpb.WorkerCommand{ + Type: &workerpb.WorkerCommand_CancelActivity{ + CancelActivity: &workerpb.CancelActivityCommand{ + TaskToken: token, + }, + }, + }) + } + return commands + } + + t.Run("batches commands by control queue", func(t *testing.T) { ctrl := gomock.NewController(t) ms := historyi.NewMockMutableState(ctrl) - ms.EXPECT().AddActivityCommandTasks( - [][]byte{token1, token2, token3}, + expectedCommands := makeCommands(token1, token2, token3) + ms.EXPECT().AddWorkerCommandsTasks( + expectedCommands, "control-queue-1", - gomock.Any(), ).Return(nil).Times(1) handler := &workflowTaskCompletedHandler{ mutableState: ms, - pendingActivityCancelsByControlQueue: map[string][][]byte{ - "control-queue-1": {token1, token2, token3}, + pendingWorkerCommandsByControlQueue: map[string][]*workerpb.WorkerCommand{ + "control-queue-1": expectedCommands, }, } - err := handler.flushBatchedActivityCommandTasks() + err := handler.flushWorkerCommandsTasks() require.NoError(t, err) }) @@ -419,42 +433,40 @@ func TestFlushBatchedActivityCommandTasks(t *testing.T) { ctrl := gomock.NewController(t) ms := historyi.NewMockMutableState(ctrl) - // Capture calls to verify both queues are processed - calls := make(map[string][][]byte) - ms.EXPECT().AddActivityCommandTasks( + calls := make(map[string][]*workerpb.WorkerCommand) + ms.EXPECT().AddWorkerCommandsTasks( gomock.Any(), gomock.Any(), - enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, - ).DoAndReturn(func(tokens [][]byte, queue string, _ enumsspb.ActivityCommandType) error { - calls[queue] = tokens + ).DoAndReturn(func(commands []*workerpb.WorkerCommand, queue string) error { + calls[queue] = commands return nil }).Times(2) handler := &workflowTaskCompletedHandler{ mutableState: ms, - pendingActivityCancelsByControlQueue: map[string][][]byte{ - "control-queue-1": {token1, token2}, - "control-queue-2": {token3, token4}, + pendingWorkerCommandsByControlQueue: map[string][]*workerpb.WorkerCommand{ + "control-queue-1": makeCommands(token1, token2), + "control-queue-2": makeCommands(token3, token4), }, } - err := handler.flushBatchedActivityCommandTasks() + err := handler.flushWorkerCommandsTasks() require.NoError(t, err) - require.Equal(t, [][]byte{token1, token2}, calls["control-queue-1"]) - require.Equal(t, [][]byte{token3, token4}, calls["control-queue-2"]) + require.Len(t, calls["control-queue-1"], 2) + require.Len(t, calls["control-queue-2"], 2) }) - t.Run("does nothing when no pending cancels", func(t *testing.T) { + t.Run("does nothing when no pending commands", func(t *testing.T) { ctrl := gomock.NewController(t) ms := historyi.NewMockMutableState(ctrl) handler := &workflowTaskCompletedHandler{ - mutableState: ms, - pendingActivityCancelsByControlQueue: nil, + mutableState: ms, + pendingWorkerCommandsByControlQueue: nil, } - err := handler.flushBatchedActivityCommandTasks() + err := handler.flushWorkerCommandsTasks() require.NoError(t, err) }) } diff --git a/service/history/configs/config.go b/service/history/configs/config.go index 5e7a8d84032..5568e383965 100644 --- a/service/history/configs/config.go +++ b/service/history/configs/config.go @@ -357,7 +357,7 @@ type Config struct { EnableCrossNamespaceCommands dynamicconfig.BoolPropertyFn EnableActivityEagerExecution dynamicconfig.BoolPropertyFnWithNamespaceFilter EnableActivityRetryStampIncrement dynamicconfig.BoolPropertyFn - EnableActivityCancellationNexusTask dynamicconfig.BoolPropertyFn + EnableCancelActivityWorkerCommand dynamicconfig.BoolPropertyFn EnableEagerWorkflowStart dynamicconfig.BoolPropertyFnWithNamespaceFilter NamespaceCacheRefreshInterval dynamicconfig.DurationPropertyFn @@ -730,7 +730,7 @@ func NewConfig( EnableCrossNamespaceCommands: dynamicconfig.EnableCrossNamespaceCommands.Get(dc), EnableActivityEagerExecution: dynamicconfig.EnableActivityEagerExecution.Get(dc), EnableActivityRetryStampIncrement: dynamicconfig.EnableActivityRetryStampIncrement.Get(dc), - EnableActivityCancellationNexusTask: dynamicconfig.EnableActivityCancellationNexusTask.Get(dc), + EnableCancelActivityWorkerCommand: dynamicconfig.EnableCancelActivityWorkerCommand.Get(dc), EnableEagerWorkflowStart: dynamicconfig.EnableEagerWorkflowStart.Get(dc), NamespaceCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc), diff --git a/service/history/interfaces/mutable_state.go b/service/history/interfaces/mutable_state.go index 3e742c11f6a..83205b65570 100644 --- a/service/history/interfaces/mutable_state.go +++ b/service/history/interfaces/mutable_state.go @@ -14,6 +14,7 @@ import ( historypb "go.temporal.io/api/history/v1" taskqueuepb "go.temporal.io/api/taskqueue/v1" updatepb "go.temporal.io/api/update/v1" + workerpb "go.temporal.io/api/worker/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" clockspb "go.temporal.io/server/api/clock/v1" @@ -46,7 +47,7 @@ type ( AddActivityTaskCancelRequestedEvent(int64, int64, string) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) AddActivityTaskCanceledEvent(int64, int64, int64, *commonpb.Payloads, string) (*historypb.HistoryEvent, error) - AddActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error + AddWorkerCommandsTasks(commands []*workerpb.WorkerCommand, controlQueue string) error AddActivityTaskCompletedEvent(int64, int64, *workflowservice.RespondActivityTaskCompletedRequest) (*historypb.HistoryEvent, error) AddActivityTaskFailedEvent(int64, int64, *failurepb.Failure, enumspb.RetryState, string, *commonpb.WorkerVersionStamp) (*historypb.HistoryEvent, error) AddActivityTaskScheduledEvent(int64, *commandpb.ScheduleActivityTaskCommandAttributes, bool) (*historypb.HistoryEvent, *persistencespb.ActivityInfo, error) diff --git a/service/history/interfaces/mutable_state_mock.go b/service/history/interfaces/mutable_state_mock.go index 145487d13bb..7b62533cec7 100644 --- a/service/history/interfaces/mutable_state_mock.go +++ b/service/history/interfaces/mutable_state_mock.go @@ -22,6 +22,7 @@ import ( history "go.temporal.io/api/history/v1" taskqueue "go.temporal.io/api/taskqueue/v1" update "go.temporal.io/api/update/v1" + worker "go.temporal.io/api/worker/v1" workflow "go.temporal.io/api/workflow/v1" workflowservice "go.temporal.io/api/workflowservice/v1" clock "go.temporal.io/server/api/clock/v1" @@ -70,20 +71,6 @@ func (m *MockMutableState) EXPECT() *MockMutableStateMockRecorder { return m.recorder } -// AddActivityCommandTasks mocks base method. -func (m *MockMutableState) AddActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enums0.ActivityCommandType) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddActivityCommandTasks", taskTokens, controlQueue, commandType) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddActivityCommandTasks indicates an expected call of AddActivityCommandTasks. -func (mr *MockMutableStateMockRecorder) AddActivityCommandTasks(taskTokens, controlQueue, commandType any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddActivityCommandTasks", reflect.TypeOf((*MockMutableState)(nil).AddActivityCommandTasks), taskTokens, controlQueue, commandType) -} - // AddActivityTaskCancelRequestedEvent mocks base method. func (m *MockMutableState) AddActivityTaskCancelRequestedEvent(arg0, arg1 int64, arg2 string) (*history.HistoryEvent, *persistence.ActivityInfo, error) { m.ctrl.T.Helper() @@ -646,6 +633,20 @@ func (mr *MockMutableStateMockRecorder) AddUpsertWorkflowSearchAttributesEvent(a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpsertWorkflowSearchAttributesEvent", reflect.TypeOf((*MockMutableState)(nil).AddUpsertWorkflowSearchAttributesEvent), arg0, arg1) } +// AddWorkerCommandsTasks mocks base method. +func (m *MockMutableState) AddWorkerCommandsTasks(commands []*worker.WorkerCommand, controlQueue string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddWorkerCommandsTasks", commands, controlQueue) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddWorkerCommandsTasks indicates an expected call of AddWorkerCommandsTasks. +func (mr *MockMutableStateMockRecorder) AddWorkerCommandsTasks(commands, controlQueue any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddWorkerCommandsTasks", reflect.TypeOf((*MockMutableState)(nil).AddWorkerCommandsTasks), commands, controlQueue) +} + // AddWorkflowExecutionCancelRequestedEvent mocks base method. func (m *MockMutableState) AddWorkflowExecutionCancelRequestedEvent(arg0 *historyservice.RequestCancelWorkflowExecutionRequest) (*history.HistoryEvent, error) { m.ctrl.T.Helper() diff --git a/service/history/queues/metrics.go b/service/history/queues/metrics.go index 65148834cb0..7b52439a024 100644 --- a/service/history/queues/metrics.go +++ b/service/history/queues/metrics.go @@ -196,8 +196,8 @@ func GetOutboundTaskTypeTagValue( return prefix + "." + task.StateMachineTaskType() case *tasks.ChasmTask: return prefix + "." + getCHASMTaskTypeTagValue(task, chasmRegistry) - case *tasks.ActivityCommandTask: - return prefix + ".ActivityCommand." + task.CommandType.String() + case *tasks.WorkerCommandsTask: + return prefix + ".WorkerCommands" default: return prefix + "Unknown" } diff --git a/service/history/tasks/activity_command_task.go b/service/history/tasks/activity_command_task.go index c4a82433646..f0a43390f88 100644 --- a/service/history/tasks/activity_command_task.go +++ b/service/history/tasks/activity_command_task.go @@ -4,69 +4,67 @@ import ( "fmt" "time" + workerpb "go.temporal.io/api/worker/v1" enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/common/definition" ) -var _ Task = (*ActivityCommandTask)(nil) -var _ HasDestination = (*ActivityCommandTask)(nil) +var _ Task = (*WorkerCommandsTask)(nil) +var _ HasDestination = (*WorkerCommandsTask)(nil) type ( - // ActivityCommandTask sends commands to activities via Nexus. - ActivityCommandTask struct { + // WorkerCommandsTask sends commands to workers via Nexus. + WorkerCommandsTask struct { definition.WorkflowKey VisibilityTimestamp time.Time TaskID int64 - // CommandType specifies the type of command. - CommandType enumsspb.ActivityCommandType - // TaskTokens of activities to send command to (batched by worker). - TaskTokens [][]byte + // Commands to send to the worker. + Commands []*workerpb.WorkerCommand // Destination is the worker control task queue for outbound queue grouping. Destination string } ) -func (t *ActivityCommandTask) GetKey() Key { +func (t *WorkerCommandsTask) GetKey() Key { return NewImmediateKey(t.TaskID) } -func (t *ActivityCommandTask) GetTaskID() int64 { +func (t *WorkerCommandsTask) GetTaskID() int64 { return t.TaskID } -func (t *ActivityCommandTask) SetTaskID(id int64) { +func (t *WorkerCommandsTask) SetTaskID(id int64) { t.TaskID = id } -func (t *ActivityCommandTask) GetVisibilityTime() time.Time { +func (t *WorkerCommandsTask) GetVisibilityTime() time.Time { return t.VisibilityTimestamp } -func (t *ActivityCommandTask) SetVisibilityTime(timestamp time.Time) { +func (t *WorkerCommandsTask) SetVisibilityTime(timestamp time.Time) { t.VisibilityTimestamp = timestamp } -func (t *ActivityCommandTask) GetCategory() Category { +func (t *WorkerCommandsTask) GetCategory() Category { return CategoryOutbound } -func (t *ActivityCommandTask) GetType() enumsspb.TaskType { +func (t *WorkerCommandsTask) GetType() enumsspb.TaskType { return enumsspb.TASK_TYPE_ACTIVITY_COMMAND } // GetDestination implements HasDestination for outbound queue grouping. -func (t *ActivityCommandTask) GetDestination() string { +func (t *WorkerCommandsTask) GetDestination() string { return t.Destination } -func (t *ActivityCommandTask) String() string { - return fmt.Sprintf("ActivityCommandTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, CommandType: %v, TaskTokens: %d, Destination: %v}", +func (t *WorkerCommandsTask) String() string { + return fmt.Sprintf("WorkerCommandsTask{WorkflowKey: %s, VisibilityTimestamp: %v, TaskID: %v, Commands: %d, Destination: %v}", t.WorkflowKey.String(), t.VisibilityTimestamp, t.TaskID, - t.CommandType, - len(t.TaskTokens), + len(t.Commands), t.Destination, ) } diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index aa948f4db50..ebadcd4c0ff 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -23,6 +23,7 @@ import ( "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" updatepb "go.temporal.io/api/update/v1" + workerpb "go.temporal.io/api/worker/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" clockspb "go.temporal.io/server/api/clock/v1" @@ -4369,8 +4370,8 @@ func (ms *MutableStateImpl) AddActivityTaskCancelRequestedEvent( return actCancelReqEvent, ai, nil } -func (ms *MutableStateImpl) AddActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error { - return ms.taskGenerator.GenerateActivityCommandTasks(taskTokens, controlQueue, commandType) +func (ms *MutableStateImpl) AddWorkerCommandsTasks(commands []*workerpb.WorkerCommand, controlQueue string) error { + return ms.taskGenerator.GenerateWorkerCommandsTasks(commands, controlQueue) } func (ms *MutableStateImpl) ApplyActivityTaskCancelRequestedEvent( diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index 2f61b17a691..47288f6ecec 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -9,6 +9,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" historypb "go.temporal.io/api/history/v1" "go.temporal.io/api/serviceerror" + workerpb "go.temporal.io/api/worker/v1" enumsspb "go.temporal.io/server/api/enums/v1" historyspb "go.temporal.io/server/api/history/v1" persistencespb "go.temporal.io/server/api/persistence/v1" @@ -63,7 +64,7 @@ type ( activityScheduledEventID int64, ) error GenerateActivityRetryTasks(activityInfo *persistencespb.ActivityInfo) error - GenerateActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error + GenerateWorkerCommandsTasks(commands []*workerpb.WorkerCommand, controlQueue string) error GenerateChildWorkflowTasks( childInitiatedEventId int64, ) error @@ -584,19 +585,18 @@ func (r *TaskGeneratorImpl) GenerateActivityRetryTasks(activityInfo *persistence return nil } -func (r *TaskGeneratorImpl) GenerateActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enumsspb.ActivityCommandType) error { - if !r.config.EnableActivityCancellationNexusTask() { +func (r *TaskGeneratorImpl) GenerateWorkerCommandsTasks(commands []*workerpb.WorkerCommand, controlQueue string) error { + if !r.config.EnableCancelActivityWorkerCommand() { return nil } - if len(taskTokens) == 0 || controlQueue == "" { + if len(commands) == 0 || controlQueue == "" { return nil } - r.mutableState.AddTasks(&tasks.ActivityCommandTask{ + r.mutableState.AddTasks(&tasks.WorkerCommandsTask{ WorkflowKey: r.mutableState.GetWorkflowKey(), - CommandType: commandType, - TaskTokens: taskTokens, + Commands: commands, Destination: controlQueue, }) return nil diff --git a/service/history/workflow/task_generator_mock.go b/service/history/workflow/task_generator_mock.go index a91df24c428..1505bd46a5a 100644 --- a/service/history/workflow/task_generator_mock.go +++ b/service/history/workflow/task_generator_mock.go @@ -14,7 +14,7 @@ import ( time "time" history "go.temporal.io/api/history/v1" - enums "go.temporal.io/server/api/enums/v1" + worker "go.temporal.io/api/worker/v1" persistence "go.temporal.io/server/api/persistence/v1" hsm "go.temporal.io/server/service/history/hsm" interfaces "go.temporal.io/server/service/history/interfaces" @@ -46,20 +46,6 @@ func (m *MockTaskGenerator) EXPECT() *MockTaskGeneratorMockRecorder { return m.recorder } -// GenerateActivityCommandTasks mocks base method. -func (m *MockTaskGenerator) GenerateActivityCommandTasks(taskTokens [][]byte, controlQueue string, commandType enums.ActivityCommandType) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateActivityCommandTasks", taskTokens, controlQueue, commandType) - ret0, _ := ret[0].(error) - return ret0 -} - -// GenerateActivityCommandTasks indicates an expected call of GenerateActivityCommandTasks. -func (mr *MockTaskGeneratorMockRecorder) GenerateActivityCommandTasks(taskTokens, controlQueue, commandType any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateActivityCommandTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateActivityCommandTasks), taskTokens, controlQueue, commandType) -} - // GenerateActivityRetryTasks mocks base method. func (m *MockTaskGenerator) GenerateActivityRetryTasks(activityInfo *persistence.ActivityInfo) error { m.ctrl.T.Helper() @@ -316,6 +302,20 @@ func (mr *MockTaskGeneratorMockRecorder) GenerateUserTimerTasks() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateUserTimerTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateUserTimerTasks)) } +// GenerateWorkerCommandsTasks mocks base method. +func (m *MockTaskGenerator) GenerateWorkerCommandsTasks(commands []*worker.WorkerCommand, controlQueue string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateWorkerCommandsTasks", commands, controlQueue) + ret0, _ := ret[0].(error) + return ret0 +} + +// GenerateWorkerCommandsTasks indicates an expected call of GenerateWorkerCommandsTasks. +func (mr *MockTaskGeneratorMockRecorder) GenerateWorkerCommandsTasks(commands, controlQueue any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateWorkerCommandsTasks", reflect.TypeOf((*MockTaskGenerator)(nil).GenerateWorkerCommandsTasks), commands, controlQueue) +} + // GenerateWorkflowCloseTasks mocks base method. func (m *MockTaskGenerator) GenerateWorkflowCloseTasks(closedTime time.Time, deleteAfterClose, skipCloseTransferTask bool) error { m.ctrl.T.Helper() diff --git a/service/history/workflow/task_generator_test.go b/service/history/workflow/task_generator_test.go index 3f35d2d7b0f..90cb03f4d56 100644 --- a/service/history/workflow/task_generator_test.go +++ b/service/history/workflow/task_generator_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" enumspb "go.temporal.io/api/enums/v1" historypb "go.temporal.io/api/history/v1" + workerpb "go.temporal.io/api/worker/v1" enumsspb "go.temporal.io/server/api/enums/v1" historyspb "go.temporal.io/server/api/history/v1" persistencespb "go.temporal.io/server/api/persistence/v1" @@ -1067,45 +1068,59 @@ func TestTaskGeneratorImpl_GenerateDeleteHistoryEventTask_ActivityRetention(t *t } } -func TestGenerateActivityCommandTasks(t *testing.T) { +func TestGenerateWorkerCommandsTasks(t *testing.T) { t.Parallel() token1 := []byte("token1") token2 := []byte("token2") token3 := []byte("token3") + makeCommands := func(tokens ...[]byte) []*workerpb.WorkerCommand { + commands := make([]*workerpb.WorkerCommand, 0, len(tokens)) + for _, token := range tokens { + commands = append(commands, &workerpb.WorkerCommand{ + Type: &workerpb.WorkerCommand_CancelActivity{ + CancelActivity: &workerpb.CancelActivityCommand{ + TaskToken: token, + }, + }, + }) + } + return commands + } + testCases := []struct { name string featureEnabled bool - taskTokens [][]byte + commands []*workerpb.WorkerCommand controlQueue string expectTask bool }{ { name: "creates task when enabled with valid inputs", featureEnabled: true, - taskTokens: [][]byte{token1, token2, token3}, + commands: makeCommands(token1, token2, token3), controlQueue: "test-control-queue", expectTask: true, }, { name: "no task when feature disabled", featureEnabled: false, - taskTokens: [][]byte{token1, token2, token3}, + commands: makeCommands(token1, token2, token3), controlQueue: "test-control-queue", expectTask: false, }, { - name: "no task when taskTokens empty", + name: "no task when commands empty", featureEnabled: true, - taskTokens: [][]byte{}, + commands: []*workerpb.WorkerCommand{}, controlQueue: "test-control-queue", expectTask: false, }, { name: "no task when controlQueue empty", featureEnabled: true, - taskTokens: [][]byte{token1, token2, token3}, + commands: makeCommands(token1, token2, token3), controlQueue: "", expectTask: false, }, @@ -1128,21 +1143,20 @@ func TestGenerateActivityCommandTasks(t *testing.T) { } cfg := &configs.Config{ - EnableActivityCancellationNexusTask: func() bool { return tc.featureEnabled }, + EnableCancelActivityWorkerCommand: func() bool { return tc.featureEnabled }, } taskGenerator := NewTaskGenerator(nil, mutableState, cfg, nil, log.NewTestLogger()) - err := taskGenerator.GenerateActivityCommandTasks(tc.taskTokens, tc.controlQueue, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL) + err := taskGenerator.GenerateWorkerCommandsTasks(tc.commands, tc.controlQueue) require.NoError(t, err) if tc.expectTask { require.Len(t, capturedTasks, 1) - commandTask, ok := capturedTasks[0].(*tasks.ActivityCommandTask) + commandTask, ok := capturedTasks[0].(*tasks.WorkerCommandsTask) require.True(t, ok) - assert.Equal(t, tc.taskTokens, commandTask.TaskTokens) + assert.Equal(t, tc.commands, commandTask.Commands) assert.Equal(t, tc.controlQueue, commandTask.Destination) assert.Equal(t, tests.NamespaceID.String(), commandTask.NamespaceID) - assert.Equal(t, enumsspb.ACTIVITY_COMMAND_TYPE_CANCEL, commandTask.CommandType) } }) } From 404ce2a5bde8eed170ec758b42c35dd428eff5fa Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 13 Mar 2026 16:35:41 -0700 Subject: [PATCH 26/40] Rename TASK_TYPE_ACTIVITY_COMMAND to TASK_TYPE_WORKER_COMMANDS and remove ActivityCommandType enum The command type is now encoded in the WorkerCommand oneof, making the separate ActivityCommandType enum unnecessary. Made-with: Cursor --- api/enums/v1/task.go-helpers.pb.go | 20 +--- api/enums/v1/task.pb.go | 108 ++++-------------- .../serialization/task_serializers.go | 2 +- .../temporal/server/api/enums/v1/task.proto | 10 +- .../history/tasks/activity_command_task.go | 2 +- 5 files changed, 30 insertions(+), 112 deletions(-) diff --git a/api/enums/v1/task.go-helpers.pb.go b/api/enums/v1/task.go-helpers.pb.go index 6953cba3fcc..30573a5d7af 100644 --- a/api/enums/v1/task.go-helpers.pb.go +++ b/api/enums/v1/task.go-helpers.pb.go @@ -57,7 +57,7 @@ var ( "ReplicationSyncVersionedTransition": 31, "ChasmPure": 32, "Chasm": 33, - "ActivityCommand": 34, + "WorkerCommands": 34, } ) @@ -72,24 +72,6 @@ func TaskTypeFromString(s string) (TaskType, error) { return TaskType(0), fmt.Errorf("%s is not a valid TaskType", s) } -var ( - ActivityCommandType_shorthandValue = map[string]int32{ - "Unspecified": 0, - "Cancel": 1, - } -) - -// ActivityCommandTypeFromString parses a ActivityCommandType value from either the protojson -// canonical SCREAMING_CASE enum or the traditional temporal PascalCase enum to ActivityCommandType -func ActivityCommandTypeFromString(s string) (ActivityCommandType, error) { - if v, ok := ActivityCommandType_value[s]; ok { - return ActivityCommandType(v), nil - } else if v, ok := ActivityCommandType_shorthandValue[s]; ok { - return ActivityCommandType(v), nil - } - return ActivityCommandType(0), fmt.Errorf("%s is not a valid ActivityCommandType", s) -} - var ( TaskPriority_shorthandValue = map[string]int32{ "Unspecified": 0, diff --git a/api/enums/v1/task.pb.go b/api/enums/v1/task.pb.go index bac5034329f..cbec1ef4b44 100644 --- a/api/enums/v1/task.pb.go +++ b/api/enums/v1/task.pb.go @@ -126,8 +126,8 @@ const ( TASK_TYPE_CHASM_PURE TaskType = 32 // A task with side effects generated by a CHASM component. TASK_TYPE_CHASM TaskType = 33 - // A task to send commands to activities via Nexus. Also see ActivityCommandType. - TASK_TYPE_ACTIVITY_COMMAND TaskType = 34 + // A task to send worker commands via Nexus. + TASK_TYPE_WORKER_COMMANDS TaskType = 34 ) // Enum value maps for TaskType. @@ -164,7 +164,7 @@ var ( 31: "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION", 32: "TASK_TYPE_CHASM_PURE", 33: "TASK_TYPE_CHASM", - 34: "TASK_TYPE_ACTIVITY_COMMAND", + 34: "TASK_TYPE_WORKER_COMMANDS", } TaskType_value = map[string]int32{ "TASK_TYPE_UNSPECIFIED": 0, @@ -198,7 +198,7 @@ var ( "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION": 31, "TASK_TYPE_CHASM_PURE": 32, "TASK_TYPE_CHASM": 33, - "TASK_TYPE_ACTIVITY_COMMAND": 34, + "TASK_TYPE_WORKER_COMMANDS": 34, } ) @@ -232,7 +232,7 @@ func (x TaskType) String() string { return "TransferSignalExecution" case TASK_TYPE_TRANSFER_RESET_WORKFLOW: - // ActivityCommandType specifies the type of command to send to activities. + // TaskPriority is only used for replication task as of May 2024 return "TransferResetWorkflow" case TASK_TYPE_WORKFLOW_TASK_TIMEOUT: return "WorkflowTaskTimeout" @@ -240,12 +240,14 @@ func (x TaskType) String() string { return "ActivityTimeout" case TASK_TYPE_USER_TIMER: return "UserTimer" + + // gap between index can be used for future priority levels if needed case TASK_TYPE_WORKFLOW_RUN_TIMEOUT: return "WorkflowRunTimeout" - - // Enum value maps for ActivityCommandType. case TASK_TYPE_DELETE_HISTORY_EVENT: return "DeleteHistoryEvent" + + // Enum value maps for TaskPriority. case TASK_TYPE_ACTIVITY_RETRY_TIMER: return "ActivityRetryTimer" case TASK_TYPE_WORKFLOW_BACKOFF_TIMER: @@ -271,19 +273,17 @@ func (x TaskType) String() string { case TASK_TYPE_WORKFLOW_EXECUTION_TIMEOUT: return "WorkflowExecutionTimeout" case TASK_TYPE_REPLICATION_SYNC_HSM: - - // Deprecated: Use ActivityCommandType.Descriptor instead. return "ReplicationSyncHsm" + + // Deprecated: Use TaskPriority.Descriptor instead. case TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION: return "ReplicationSyncVersionedTransition" case TASK_TYPE_CHASM_PURE: return "ChasmPure" case TASK_TYPE_CHASM: return "Chasm" - - // TaskPriority is only used for replication task as of May 2024 - case TASK_TYPE_ACTIVITY_COMMAND: - return "ActivityCommand" + case TASK_TYPE_WORKER_COMMANDS: + return "WorkerCommands" default: return strconv.Itoa(int(x)) } @@ -306,68 +306,15 @@ func (TaskType) EnumDescriptor() ([]byte, []int) { return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{1} } -type ActivityCommandType int32 - -const ( - ACTIVITY_COMMAND_TYPE_UNSPECIFIED ActivityCommandType = 0 - ACTIVITY_COMMAND_TYPE_CANCEL ActivityCommandType = 1 -) - -var ( - ActivityCommandType_name = map[int32]string{ - 0: "ACTIVITY_COMMAND_TYPE_UNSPECIFIED", - 1: "ACTIVITY_COMMAND_TYPE_CANCEL", - } - ActivityCommandType_value = map[string]int32{ - "ACTIVITY_COMMAND_TYPE_UNSPECIFIED": 0, - "ACTIVITY_COMMAND_TYPE_CANCEL": 1, - } -) - -func (x ActivityCommandType) Enum() *ActivityCommandType { - p := new(ActivityCommandType) - *p = x - return p -} - -func (x ActivityCommandType) String() string { - switch x { - case ACTIVITY_COMMAND_TYPE_UNSPECIFIED: - return "Unspecified" - case ACTIVITY_COMMAND_TYPE_CANCEL: - return "Cancel" - default: - return strconv.Itoa(int(x)) - } - -} - -func (ActivityCommandType) Descriptor() protoreflect.EnumDescriptor { - return file_temporal_server_api_enums_v1_task_proto_enumTypes[2].Descriptor() -} - -func (ActivityCommandType) Type() protoreflect.EnumType { - return &file_temporal_server_api_enums_v1_task_proto_enumTypes[2] -} - -func (x ActivityCommandType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -func (ActivityCommandType) EnumDescriptor() ([]byte, []int) { - return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{2} -} - type TaskPriority int32 const ( TASK_PRIORITY_UNSPECIFIED TaskPriority = 0 TASK_PRIORITY_HIGH TaskPriority = 1 - // gap between index can be used for future priority levels if needed + TASK_PRIORITY_LOW TaskPriority = 10 ) -// Enum value maps for TaskPriority. var ( TaskPriority_name = map[int32]string{ 0: "TASK_PRIORITY_UNSPECIFIED", @@ -402,20 +349,19 @@ func (x TaskPriority) String() string { } func (TaskPriority) Descriptor() protoreflect.EnumDescriptor { - return file_temporal_server_api_enums_v1_task_proto_enumTypes[3].Descriptor() + return file_temporal_server_api_enums_v1_task_proto_enumTypes[2].Descriptor() } func (TaskPriority) Type() protoreflect.EnumType { - return &file_temporal_server_api_enums_v1_task_proto_enumTypes[3] + return &file_temporal_server_api_enums_v1_task_proto_enumTypes[2] } func (x TaskPriority) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Use TaskPriority.Descriptor instead. func (TaskPriority) EnumDescriptor() ([]byte, []int) { - return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{3} + return file_temporal_server_api_enums_v1_task_proto_rawDescGZIP(), []int{2} } var File_temporal_server_api_enums_v1_task_proto protoreflect.FileDescriptor @@ -427,7 +373,7 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "TaskSource\x12\x1b\n" + "\x17TASK_SOURCE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13TASK_SOURCE_HISTORY\x10\x01\x12\x1a\n" + - "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\xd6\t\n" + + "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\xd5\t\n" + "\bTaskType\x12\x19\n" + "\x15TASK_TYPE_UNSPECIFIED\x10\x00\x12!\n" + "\x1dTASK_TYPE_REPLICATION_HISTORY\x10\x01\x12'\n" + @@ -460,11 +406,8 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "\x1eTASK_TYPE_REPLICATION_SYNC_HSM\x10\x1e\x123\n" + "/TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION\x10\x1f\x12\x18\n" + "\x14TASK_TYPE_CHASM_PURE\x10 \x12\x13\n" + - "\x0fTASK_TYPE_CHASM\x10!\x12\x1e\n" + - "\x1aTASK_TYPE_ACTIVITY_COMMAND\x10\"\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*^\n" + - "\x13ActivityCommandType\x12%\n" + - "!ACTIVITY_COMMAND_TYPE_UNSPECIFIED\x10\x00\x12 \n" + - "\x1cACTIVITY_COMMAND_TYPE_CANCEL\x10\x01*\\\n" + + "\x0fTASK_TYPE_CHASM\x10!\x12\x1d\n" + + "\x19TASK_TYPE_WORKER_COMMANDS\x10\"\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*\\\n" + "\fTaskPriority\x12\x1d\n" + "\x19TASK_PRIORITY_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12TASK_PRIORITY_HIGH\x10\x01\x12\x15\n" + @@ -483,12 +426,11 @@ func file_temporal_server_api_enums_v1_task_proto_rawDescGZIP() []byte { return file_temporal_server_api_enums_v1_task_proto_rawDescData } -var file_temporal_server_api_enums_v1_task_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_temporal_server_api_enums_v1_task_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_temporal_server_api_enums_v1_task_proto_goTypes = []any{ - (TaskSource)(0), // 0: temporal.server.api.enums.v1.TaskSource - (TaskType)(0), // 1: temporal.server.api.enums.v1.TaskType - (ActivityCommandType)(0), // 2: temporal.server.api.enums.v1.ActivityCommandType - (TaskPriority)(0), // 3: temporal.server.api.enums.v1.TaskPriority + (TaskSource)(0), // 0: temporal.server.api.enums.v1.TaskSource + (TaskType)(0), // 1: temporal.server.api.enums.v1.TaskType + (TaskPriority)(0), // 2: temporal.server.api.enums.v1.TaskPriority } var file_temporal_server_api_enums_v1_task_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -508,7 +450,7 @@ func file_temporal_server_api_enums_v1_task_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_enums_v1_task_proto_rawDesc), len(file_temporal_server_api_enums_v1_task_proto_rawDesc)), - NumEnums: 4, + NumEnums: 3, NumMessages: 0, NumExtensions: 0, NumServices: 0, diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index 442d6ba5a0b..800a53a4ee7 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -1503,7 +1503,7 @@ func deserializeOutboundTask( Info: info.GetChasmTaskInfo(), Destination: info.Destination, }, nil - case enumsspb.TASK_TYPE_ACTIVITY_COMMAND: + case enumsspb.TASK_TYPE_WORKER_COMMANDS: return &tasks.WorkerCommandsTask{ WorkflowKey: definition.NewWorkflowKey( info.NamespaceId, diff --git a/proto/internal/temporal/server/api/enums/v1/task.proto b/proto/internal/temporal/server/api/enums/v1/task.proto index 4eb75a50fee..e50593bd76c 100644 --- a/proto/internal/temporal/server/api/enums/v1/task.proto +++ b/proto/internal/temporal/server/api/enums/v1/task.proto @@ -60,14 +60,8 @@ enum TaskType { // A task with side effects generated by a CHASM component. TASK_TYPE_CHASM = 33; - // A task to send commands to activities via Nexus. Also see ActivityCommandType. - TASK_TYPE_ACTIVITY_COMMAND = 34; -} - -// ActivityCommandType specifies the type of command to send to activities. -enum ActivityCommandType { - ACTIVITY_COMMAND_TYPE_UNSPECIFIED = 0; - ACTIVITY_COMMAND_TYPE_CANCEL = 1; + // A task to send worker commands via Nexus. + TASK_TYPE_WORKER_COMMANDS = 34; } // TaskPriority is only used for replication task as of May 2024 diff --git a/service/history/tasks/activity_command_task.go b/service/history/tasks/activity_command_task.go index f0a43390f88..3a12a94e9e5 100644 --- a/service/history/tasks/activity_command_task.go +++ b/service/history/tasks/activity_command_task.go @@ -51,7 +51,7 @@ func (t *WorkerCommandsTask) GetCategory() Category { } func (t *WorkerCommandsTask) GetType() enumsspb.TaskType { - return enumsspb.TASK_TYPE_ACTIVITY_COMMAND + return enumsspb.TASK_TYPE_WORKER_COMMANDS } // GetDestination implements HasDestination for outbound queue grouping. From 3e0a3a0cee0533a9d3499cf356413854f0628b6f Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 13 Mar 2026 16:40:11 -0700 Subject: [PATCH 27/40] Rename activity_command_task.go to worker_commands_task.go Made-with: Cursor --- .../tasks/{activity_command_task.go => worker_commands_task.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename service/history/tasks/{activity_command_task.go => worker_commands_task.go} (100%) diff --git a/service/history/tasks/activity_command_task.go b/service/history/tasks/worker_commands_task.go similarity index 100% rename from service/history/tasks/activity_command_task.go rename to service/history/tasks/worker_commands_task.go From 0dd164a409887b5995df93cba3f295cb6f35cb3d Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Mon, 30 Mar 2026 10:03:36 -0700 Subject: [PATCH 28/40] Fix gofmt struct field alignment Made-with: Cursor --- .../workflow_task_completed_handler.go | 18 ++++++++--------- service/history/configs/config.go | 20 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index c3d81b12dff..980eff9e9ac 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -58,15 +58,15 @@ type ( workflowTaskCompletedID int64 // internal state - hasBufferedEventsOrMessages bool - workflowTaskFailedCause *workflowTaskFailedCause - activityNotStartedCancelled bool - newMutableState historyi.MutableState - stopProcessing bool // should stop processing any more commands - mutableState historyi.MutableState - effects effect.Controller - initiatedChildExecutionsInBatch map[string]struct{} // Set of initiated child executions in the workflow task - updateRegistry update.Registry + hasBufferedEventsOrMessages bool + workflowTaskFailedCause *workflowTaskFailedCause + activityNotStartedCancelled bool + newMutableState historyi.MutableState + stopProcessing bool // should stop processing any more commands + mutableState historyi.MutableState + effects effect.Controller + initiatedChildExecutionsInBatch map[string]struct{} // Set of initiated child executions in the workflow task + updateRegistry update.Registry pendingWorkerCommandsByControlQueue map[string][]*workerpb.WorkerCommand // Batched worker commands by control queue // validation diff --git a/service/history/configs/config.go b/service/history/configs/config.go index 5568e383965..e5cbb50d6b9 100644 --- a/service/history/configs/config.go +++ b/service/history/configs/config.go @@ -354,12 +354,12 @@ type Config struct { ESProcessorFlushInterval dynamicconfig.DurationPropertyFn ESProcessorAckTimeout dynamicconfig.DurationPropertyFn - EnableCrossNamespaceCommands dynamicconfig.BoolPropertyFn - EnableActivityEagerExecution dynamicconfig.BoolPropertyFnWithNamespaceFilter - EnableActivityRetryStampIncrement dynamicconfig.BoolPropertyFn + EnableCrossNamespaceCommands dynamicconfig.BoolPropertyFn + EnableActivityEagerExecution dynamicconfig.BoolPropertyFnWithNamespaceFilter + EnableActivityRetryStampIncrement dynamicconfig.BoolPropertyFn EnableCancelActivityWorkerCommand dynamicconfig.BoolPropertyFn - EnableEagerWorkflowStart dynamicconfig.BoolPropertyFnWithNamespaceFilter - NamespaceCacheRefreshInterval dynamicconfig.DurationPropertyFn + EnableEagerWorkflowStart dynamicconfig.BoolPropertyFnWithNamespaceFilter + NamespaceCacheRefreshInterval dynamicconfig.DurationPropertyFn // ArchivalQueueProcessor settings ArchivalProcessorSchedulerWorkerCount dynamicconfig.TypedSubscribable[int] @@ -727,12 +727,12 @@ func NewConfig( ESProcessorFlushInterval: dynamicconfig.WorkerESProcessorFlushInterval.Get(dc), ESProcessorAckTimeout: dynamicconfig.WorkerESProcessorAckTimeout.Get(dc), - EnableCrossNamespaceCommands: dynamicconfig.EnableCrossNamespaceCommands.Get(dc), - EnableActivityEagerExecution: dynamicconfig.EnableActivityEagerExecution.Get(dc), - EnableActivityRetryStampIncrement: dynamicconfig.EnableActivityRetryStampIncrement.Get(dc), + EnableCrossNamespaceCommands: dynamicconfig.EnableCrossNamespaceCommands.Get(dc), + EnableActivityEagerExecution: dynamicconfig.EnableActivityEagerExecution.Get(dc), + EnableActivityRetryStampIncrement: dynamicconfig.EnableActivityRetryStampIncrement.Get(dc), EnableCancelActivityWorkerCommand: dynamicconfig.EnableCancelActivityWorkerCommand.Get(dc), - EnableEagerWorkflowStart: dynamicconfig.EnableEagerWorkflowStart.Get(dc), - NamespaceCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc), + EnableEagerWorkflowStart: dynamicconfig.EnableEagerWorkflowStart.Get(dc), + NamespaceCacheRefreshInterval: dynamicconfig.NamespaceCacheRefreshInterval.Get(dc), // Archival related ArchivalTaskBatchSize: dynamicconfig.ArchivalTaskBatchSize.Get(dc), From f7297b0cf4483f7f9a2fea95cabe6d98a01382a4 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Mon, 6 Apr 2026 17:40:41 -0700 Subject: [PATCH 29/40] Align worker_control_task_queue comment with public API proto Match the comment style from temporalio/api#711. Co-Authored-By: Claude Opus 4.6 --- .../temporal/server/api/persistence/v1/executions.proto | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index c6145a76c25..753670d7789 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -656,7 +656,8 @@ message ActivityInfo { int64 start_version = 50; - // The task queue on which the server will send control tasks to the worker running this activity. + // A dedicated per-worker Nexus task queue on which the server sends control + // tasks (e.g. activity cancellation) to this specific worker instance. string worker_control_task_queue = 51; } From b8b13529228c9f379a1fc4ce9e30b3ee3d061ae2 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 7 Apr 2026 12:09:23 -0700 Subject: [PATCH 30/40] Update go.mod to latest api-go (includes API PR #708) Co-Authored-By: Claude Opus 4.6 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b2171713416..339588fc0b9 100644 --- a/go.mod +++ b/go.mod @@ -174,4 +174,4 @@ require ( modernc.org/memory v1.11.0 // indirect ) -replace go.temporal.io/api => github.com/temporalio/api-go v1.62.2-0.20260313212811-d44912090759 +replace go.temporal.io/api => github.com/temporalio/api-go v1.62.8-0.20260407190616-8574d6aa8b01 diff --git a/go.sum b/go.sum index e9d2b357fac..ca2528a51e9 100644 --- a/go.sum +++ b/go.sum @@ -310,8 +310,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/temporalio/api-go v1.62.2-0.20260313212811-d44912090759 h1:CSlBGjKIgi770YWTYB1dt2AJuLKU6yArSZL636UStdo= -github.com/temporalio/api-go v1.62.2-0.20260313212811-d44912090759/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +github.com/temporalio/api-go v1.62.8-0.20260407190616-8574d6aa8b01 h1:Rtf7bXBmXzNuUgJS+PGIVgvp/qRhIbNKqz9nrBynDQM= +github.com/temporalio/api-go v1.62.8-0.20260407190616-8574d6aa8b01/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4= github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04= From 1e276567f0a46f489506ccf582c0de0420628356 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Tue, 7 Apr 2026 12:10:07 -0700 Subject: [PATCH 31/40] Add clarifying comment for eager dispatch workerControlTaskQueue param Co-Authored-By: Claude Opus 4.6 --- .../workflow_task_completed_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 16e5a1c8e27..80318745ac0 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -563,7 +563,7 @@ func (handler *workflowTaskCompletedHandler) handlePostCommandEagerExecuteActivi stamp, nil, nil, - handler.workerControlTaskQueue, + handler.workerControlTaskQueue, // Eager: activity runs on the same worker that completed the WFT. ); err != nil { return nil, err } From 0b36762c11232b8ce73b1cc2698cec5df9cc7029 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Thu, 9 Apr 2026 14:59:31 -0700 Subject: [PATCH 32/40] Mark WorkerCommandsTask as low priority Co-Authored-By: Claude Opus 4.6 --- service/history/queues/priority_assigner.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/history/queues/priority_assigner.go b/service/history/queues/priority_assigner.go index cbaf7d4b951..063f3dca4f2 100644 --- a/service/history/queues/priority_assigner.go +++ b/service/history/queues/priority_assigner.go @@ -43,7 +43,8 @@ func (a *priorityAssignerImpl) Assign(executable Executable) tasks.Priority { case enumsspb.TASK_TYPE_ACTIVITY_TIMEOUT, enumsspb.TASK_TYPE_WORKFLOW_TASK_TIMEOUT, enumsspb.TASK_TYPE_WORKFLOW_RUN_TIMEOUT, - enumsspb.TASK_TYPE_WORKFLOW_EXECUTION_TIMEOUT: + enumsspb.TASK_TYPE_WORKFLOW_EXECUTION_TIMEOUT, + enumsspb.TASK_TYPE_WORKER_COMMANDS: return tasks.PriorityLow case enumsspb.TASK_TYPE_DELETE_HISTORY_EVENT, enumsspb.TASK_TYPE_TRANSFER_DELETE_EXECUTION, From 7f0514c2448f4a7b07ab1206300311bbddd62afa Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Thu, 9 Apr 2026 15:09:39 -0700 Subject: [PATCH 33/40] Regenerate proto after comment update Co-Authored-By: Claude Opus 4.6 --- api/persistence/v1/executions.pb.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/persistence/v1/executions.pb.go b/api/persistence/v1/executions.pb.go index 053f9a78485..6bd49c586bd 100644 --- a/api/persistence/v1/executions.pb.go +++ b/api/persistence/v1/executions.pb.go @@ -2601,7 +2601,8 @@ type ActivityInfo struct { // set to true if reset heartbeat flag was set with an activity reset ResetHeartbeats bool `protobuf:"varint,48,opt,name=reset_heartbeats,json=resetHeartbeats,proto3" json:"reset_heartbeats,omitempty"` StartVersion int64 `protobuf:"varint,50,opt,name=start_version,json=startVersion,proto3" json:"start_version,omitempty"` - // The task queue on which the server will send control tasks to the worker running this activity. + // A dedicated per-worker Nexus task queue on which the server sends control + // tasks (e.g. activity cancellation) to this specific worker instance. WorkerControlTaskQueue string `protobuf:"bytes,51,opt,name=worker_control_task_queue,json=workerControlTaskQueue,proto3" json:"worker_control_task_queue,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache From 9500ec68b5ce7605b9ab6f4e112ad5c1baf58cf0 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Thu, 9 Apr 2026 15:12:09 -0700 Subject: [PATCH 34/40] Remove stale replace directive for go.temporal.io/api Main already has the direct dependency at the same version. Co-Authored-By: Claude Opus 4.6 --- go.mod | 2 -- go.sum | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 8148722f1c1..20e025264c9 100644 --- a/go.mod +++ b/go.mod @@ -216,5 +216,3 @@ require ( modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) - -replace go.temporal.io/api => github.com/temporalio/api-go v1.62.8-0.20260406230818-5423d0dd678a diff --git a/go.sum b/go.sum index a0853a3ed75..6e4a8419509 100644 --- a/go.sum +++ b/go.sum @@ -375,8 +375,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/temporalio/api-go v1.62.8-0.20260406230818-5423d0dd678a h1:yCv2cj5CoITEXvCwG/cARzT4mMAz6pRmHhSPSsRFbhU= -github.com/temporalio/api-go v1.62.8-0.20260406230818-5423d0dd678a/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4= github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04= @@ -442,6 +440,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.temporal.io/api v1.62.8-0.20260406230818-5423d0dd678a h1:2nCxSSKutK1VP2eA7/lw5/DfHk+UxNtr1GN5KsZTSNo= +go.temporal.io/api v1.62.8-0.20260406230818-5423d0dd678a/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2/go.mod h1:T8dnzVPeO+gaUTj9eDgm/lT2lZH4+JXNvrGaQGyVi50= go.temporal.io/sdk v1.41.1 h1:yOpvsHyDD1lNuwlGBv/SUodCPhjv9nDeC9lLHW/fJUA= From 8c3ca5ea1de9482b2273c2ae78279154ce418676 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Thu, 9 Apr 2026 16:05:25 -0700 Subject: [PATCH 35/40] Merge main into kannan/activity-cancel/task-definition Resolve conflicts: - task.proto: bump TASK_TYPE_WORKER_COMMANDS from 34 to 35 (34 taken by REPLICATION_DELETE_EXECUTION) - executions.proto: merge imports, keep both outbound task details and worker_control_task_queue field - mutable_state_impl_test.go: keep both new test functions - go.mod: update go.temporal.io/api to v1.62.8-0.20260407190616-8574d6aa8b01 - Regenerated .pb.go files Co-Authored-By: Claude Opus 4.6 --- .github/.golangci.yml | 16 +- .github/CODEOWNERS | 94 +- .../actions/build-docker-images/action.yml | 56 +- .../build-docker-images/scripts/main.go | 111 +- .../build-docker-images/scripts/main_test.go | 78 + .github/actions/trivy-scan/action.yml | 108 - .github/copilot-instructions.md | 14 +- .github/workflows/ci-success-report.yml | 4 +- .github/workflows/create-tag.yml | 216 - .github/workflows/docker-build-manual.yml | 76 +- .github/workflows/features-integration.yml | 33 +- .github/workflows/flaky-tests-report.yml | 80 +- .github/workflows/linters.yml | 26 +- .github/workflows/optimize-test-sharding.yml | 6 + .../workflows/promote-admin-tools-image.yml | 6 - .github/workflows/promote-docker-image.yml | 119 +- .github/workflows/promote-server-image.yml | 6 - .github/workflows/run-tests.yml | 144 +- .github/workflows/trigger-publish.yml | 49 - .../trigger-version-info-service.yml | 2 +- .gitignore | 6 +- AGENTS.md | 2 + CONTRIBUTING.md | 8 +- Makefile | 66 +- api/adminservice/v1/request_response.pb.go | 183 +- api/adminservice/v1/service.pb.go | 95 +- api/archiver/v1/message.pb.go | 2 +- api/batch/v1/request_response.pb.go | 2 +- api/chasm/v1/message.go-helpers.pb.go | 43 + api/chasm/v1/message.pb.go | 230 + api/checksum/v1/message.pb.go | 2 +- api/common/v1/api_category.go-helpers.pb.go | 65 + api/common/v1/api_category.pb.go | 230 + api/deployment/v1/message.go-helpers.pb.go | 370 + api/deployment/v1/message.pb.go | 1319 +- api/enums/v1/cluster.go-helpers.pb.go | 1 + api/enums/v1/cluster.pb.go | 11 +- api/enums/v1/replication.go-helpers.pb.go | 1 + api/enums/v1/replication.pb.go | 17 +- api/enums/v1/task.go-helpers.pb.go | 3 +- api/enums/v1/task.pb.go | 20 +- api/health/v1/message.go-helpers.pb.go | 117 + api/health/v1/message.pb.go | 324 + api/historyservice/v1/request_response.pb.go | 1071 +- api/historyservice/v1/service.pb.go | 157 +- api/matchingservice/v1/request_response.pb.go | 96 +- api/matchingservice/v1/service.pb.go | 85 +- .../v1/executions.go-helpers.pb.go | 37 + api/persistence/v1/executions.pb.go | 840 +- api/persistence/v1/tasks.go-helpers.pb.go | 37 + api/persistence/v1/tasks.pb.go | 146 +- .../v1/workflow_mutable_state.pb.go | 4 +- api/replication/v1/message.pb.go | 2 +- api/schedule/v1/message.pb.go | 39 +- api/taskqueue/v1/message.pb.go | 81 +- api/token/v1/message.pb.go | 87 +- .../v1/request_response.go-helpers.pb.go | 154 + .../v1/request_response.pb.go | 426 + chasm/component.go | 36 +- chasm/component_mock.go | 141 +- chasm/context.go | 94 +- chasm/context_mock.go | 90 +- chasm/engine.go | 92 +- chasm/engine_mock.go | 18 +- chasm/field_test.go | 6 + chasm/interceptors.go | 4 +- chasm/lib/activity/activity.go | 228 +- chasm/lib/activity/activity_tasks.go | 143 +- chasm/lib/activity/activity_test.go | 95 + chasm/lib/activity/frontend.go | 125 +- chasm/lib/activity/frontend_test.go | 101 + chasm/lib/activity/fx.go | 17 +- .../v1/request_response.go-helpers.pb.go | 74 + .../gen/activitypb/v1/request_response.pb.go | 144 +- .../activity/gen/activitypb/v1/service.pb.go | 61 +- .../gen/activitypb/v1/service_client.pb.go | 43 + .../gen/activitypb/v1/service_grpc.pb.go | 37 + chasm/lib/activity/handler.go | 71 +- chasm/lib/activity/library.go | 92 +- .../activity/proto/v1/activity_state.proto | 300 +- .../activity/proto/v1/request_response.proto | 44 +- chasm/lib/activity/proto/v1/service.proto | 53 +- chasm/lib/activity/proto/v1/tasks.proto | 19 +- chasm/lib/activity/statemachine.go | 37 +- chasm/lib/activity/statemachine_test.go | 50 +- chasm/lib/activity/validator.go | 178 +- chasm/lib/activity/validator_test.go | 307 +- chasm/lib/callback/component.go | 18 +- chasm/lib/callback/fx.go | 10 +- ...sm_invocation.go => invocable_internal.go} | 46 +- ...us_invocation.go => invocable_outbound.go} | 36 +- chasm/lib/callback/library.go | 18 +- chasm/lib/callback/proto/v1/message.proto | 95 +- chasm/lib/callback/proto/v1/tasks.proto | 10 +- chasm/lib/callback/request.go | 78 +- chasm/lib/callback/request_test.go | 324 + chasm/lib/callback/{executors.go => tasks.go} | 133 +- .../{executors_test.go => tasks_test.go} | 52 +- ...ion_executors.go => cancellation_tasks.go} | 24 +- chasm/lib/nexusoperation/config.go | 2 - chasm/lib/nexusoperation/fx.go | 10 +- chasm/lib/nexusoperation/library.go | 21 +- ...ration_executors.go => operation_tasks.go} | 37 +- .../nexusoperation/proto/v1/operation.proto | 30 +- chasm/lib/nexusoperation/proto/v1/tasks.proto | 2 +- chasm/lib/scheduler/backfiller.go | 17 +- chasm/lib/scheduler/backfiller_tasks.go | 23 +- chasm/lib/scheduler/backfiller_tasks_test.go | 309 +- chasm/lib/scheduler/config.go | 5 + chasm/lib/scheduler/export_test.go | 16 + chasm/lib/scheduler/fx.go | 12 +- .../schedulerpb/v1/message.go-helpers.pb.go | 37 + .../gen/schedulerpb/v1/message.pb.go | 208 +- .../v1/request_response.go-helpers.pb.go | 222 + .../gen/schedulerpb/v1/request_response.pb.go | 387 +- .../gen/schedulerpb/v1/service.pb.go | 60 +- .../gen/schedulerpb/v1/service_client.pb.go | 129 + .../gen/schedulerpb/v1/service_grpc.pb.go | 111 + .../gen/schedulerpb/v1/tasks.go-helpers.pb.go | 74 + .../scheduler/gen/schedulerpb/v1/tasks.pb.go | 126 +- chasm/lib/scheduler/generator.go | 14 +- chasm/lib/scheduler/generator_tasks.go | 19 +- chasm/lib/scheduler/generator_tasks_test.go | 143 +- chasm/lib/scheduler/handler.go | 117 + chasm/lib/scheduler/handler_test.go | 63 +- chasm/lib/scheduler/helper_test.go | 234 +- chasm/lib/scheduler/invoker.go | 16 +- .../scheduler/invoker_execute_task_test.go | 239 +- .../invoker_process_buffer_task_test.go | 206 +- chasm/lib/scheduler/invoker_tasks.go | 107 +- chasm/lib/scheduler/library.go | 67 +- chasm/lib/scheduler/migration/migration.go | 100 +- .../lib/scheduler/migration/migration_test.go | 121 +- chasm/lib/scheduler/proto/v1/message.proto | 141 +- .../scheduler/proto/v1/request_response.proto | 86 +- chasm/lib/scheduler/proto/v1/service.proto | 67 +- chasm/lib/scheduler/proto/v1/tasks.proto | 12 +- chasm/lib/scheduler/scheduler.go | 160 +- .../scheduler/scheduler_idle_tasks_test.go | 141 +- chasm/lib/scheduler/scheduler_migrate_task.go | 201 + chasm/lib/scheduler/scheduler_migrate_test.go | 169 + chasm/lib/scheduler/scheduler_suite_test.go | 150 - chasm/lib/scheduler/scheduler_tasks.go | 235 +- chasm/lib/scheduler/scheduler_test.go | 285 + chasm/lib/scheduler/sentinel_test.go | 96 + chasm/lib/scheduler/spec_processor_test.go | 173 +- chasm/lib/tests/gen/testspb/v1/message.pb.go | 13 +- chasm/lib/tests/gen/testspb/v1/service.pb.go | 7 +- chasm/lib/tests/handler.go | 62 +- chasm/lib/tests/library.go | 24 +- chasm/lib/tests/payload.go | 123 +- chasm/lib/tests/proto/v1/message.proto | 20 +- .../lib/tests/proto/v1/request_response.proto | 6 +- chasm/lib/tests/proto/v1/service.proto | 12 +- chasm/lib/tests/tasks.go | 37 +- chasm/lib/workflow/workflow.go | 12 + chasm/library_core.go | 1 - chasm/ms_pointer.go | 2 +- chasm/node_backend_mock.go | 26 +- chasm/ref.go | 15 +- chasm/ref_test.go | 16 + chasm/registrable_component.go | 25 + chasm/registrable_task.go | 108 +- chasm/registry.go | 29 + chasm/registry_test.go | 34 +- chasm/search_attribute.go | 25 +- chasm/search_attribute_test.go | 128 +- chasm/task.go | 38 +- chasm/task_handler_base.go | 18 + chasm/task_mock.go | 124 +- chasm/test_component_test.go | 6 + chasm/test_library_test.go | 33 +- chasm/test_task_test.go | 2 + chasm/test_var_test.go | 21 +- chasm/tree.go | 310 +- chasm/tree_test.go | 249 +- chasm/visibility.go | 22 +- chasm/visibility_manager.go | 54 +- chasm/visibility_manager_mock.go | 10 +- chasm/visibility_test.go | 53 + chasm/visibility_value.go | 124 +- chasm/visibility_value_test.go | 71 +- chasm/workflow.go | 4 +- client/clientfactory.go | 2 +- client/frontend/client_gen.go | 40 + client/frontend/metric_client_gen.go | 56 + client/frontend/retryable_client_gen.go | 60 + client/history/caching_redirector_test.go | 16 +- client/history/client_test.go | 2 +- client/history/historytest/clienttest.go | 4 +- client/matching/loadbalancer.go | 4 +- client/matching/loadbalancer_test.go | 2 +- cmd/server/main.go | 12 - cmd/tools/fairsim/main.go | 15 + cmd/tools/flakereport/main.go | 15 + cmd/tools/gendynamicconfig/main.go | 5 + cmd/tools/genrpcserverinterceptors/main.go | 75 +- .../genrpcserverinterceptors/main_test.go | 45 +- .../server_interceptors.tmpl | 12 +- cmd/tools/getproto/files.go | 5 + cmd/tools/getproto/main.go | 2 +- cmd/tools/parallelize/main.go | 15 + cmd/tools/protogen/main.go | 2 +- common/api/metadata.go | 214 +- common/archiver/README.md | 130 +- common/archiver/filestore/util.go | 2 +- common/archiver/gcloud/connector/client.go | 9 +- .../archiver/gcloud/connector/client_test.go | 2 +- common/archiver/gcloud/util.go | 8 +- common/archiver/gcloud/visibility_archiver.go | 2 +- .../gcloud/visibility_archiver_test.go | 6 +- common/archiver/history_iterator.go | 4 +- common/archiver/history_iterator_test.go | 10 +- common/archiver/options.go | 8 +- common/archiver/provider/provider.go | 171 +- common/archiver/provider/provider_mock.go | 78 + common/archiver/provider/provider_test.go | 618 + common/archiver/s3store/util.go | 2 +- common/authorization/audience_mapper.go | 2 +- common/authorization/authorizer.go | 5 +- common/authorization/claim_mapper.go | 19 + common/authorization/default_authorizer.go | 5 +- .../authorization/default_jwt_claim_mapper.go | 10 +- common/authorization/interceptor.go | 39 +- common/authorization/interceptor_test.go | 81 +- common/authorization/roles.go | 4 +- common/authorization/token_key_provider.go | 2 +- common/backoff/jitter_test.go | 6 +- common/backoff/retrypolicy_test.go | 8 +- common/cache/cache.go | 16 +- common/cache/lru.go | 24 +- common/cache/lru_test.go | 8 +- common/cache/simple.go | 24 +- common/cache/simple_test.go | 8 +- common/cache/size_getter.go | 2 +- common/checksum/crc_test.go | 4 +- common/client_cache.go | 20 +- common/codec/jsonpb.go | 2 +- common/collection/concurrent_tx_map.go | 28 +- common/collection/concurrent_tx_map_test.go | 22 +- common/collection/interface.go | 26 +- common/collection/paging_iterator_test.go | 6 +- common/collection/priority_queue.go | 4 +- common/collection/priority_queue_test.go | 4 +- common/collection/sync_map_test.go | 10 +- common/collection/util.go | 2 +- common/config/config.go | 6 + common/dynamicconfig/client.go | 1 + common/dynamicconfig/collection_test.go | 27 +- common/dynamicconfig/config/testConfig.yaml | 6 + common/dynamicconfig/constants.go | 129 +- common/dynamicconfig/file_based_client.go | 97 +- .../dynamicconfig/file_based_client_test.go | 429 +- common/dynamicconfig/gradual_change_test.go | 4 +- common/dynamicconfig/memory_client.go | 40 +- common/dynamicconfig/memory_client_test.go | 79 + common/dynamicconfig/setting_gen.go | 239 + common/dynamicconfig/yaml_loader.go | 21 +- common/finalizer/finalizer.go | 2 +- common/future/future_test.go | 12 +- common/goro/adaptive_pool.go | 2 +- common/headers/caller_info.go | 20 - common/headers/headers.go | 50 + common/health/check_types.go | 18 + common/locks/condition_variable_test.go | 6 +- common/locks/id_mutex.go | 18 +- common/locks/id_mutex_test.go | 12 +- common/locks/priority_mutex_test.go | 2 +- common/locks/priority_semaphore_test.go | 2 +- common/log/panic_test.go | 4 +- common/log/sdk_logger.go | 14 +- common/log/tag/interface.go | 2 +- common/log/tag/tags.go | 45 +- common/log/throttle_logger.go | 21 +- common/log/zap_logger.go | 11 +- common/masker/masker.go | 10 +- common/masker/masker_test.go | 2 +- common/membership/ringpop/factory.go | 2 + common/membership/ringpop/monitor.go | 52 +- common/membership/ringpop/monitor_test.go | 2 +- common/membership/ringpop/service_resolver.go | 26 +- common/membership/ringpop/test_cluster.go | 5 +- common/metrics/baggage_bench_test.go | 6 +- common/metrics/config_test.go | 2 +- common/metrics/grpc.go | 14 +- common/metrics/grpc_test.go | 12 +- common/metrics/metric_defs.go | 234 +- common/metrics/metricstest/capture_handler.go | 18 +- common/metrics/tags.go | 96 +- common/namespace/namespace.go | 4 +- common/namespace/nsregistry/registry.go | 28 +- common/namespace/nsregistry/registry_test.go | 7 +- .../nsregistry/registry_watch_test.go | 68 +- common/namespace/nsreplication/data_merger.go | 21 + .../replication_task_executor.go | 17 +- .../replication_task_executor_test.go | 4 + .../transmission_task_handler.go | 20 +- .../transmission_task_handler_test.go | 112 +- common/namespace/replication_resolver.go | 4 +- common/namespace/replication_resolver_test.go | 10 +- common/nexus/endpoint_registry.go | 9 +- common/nexus/endpoint_registry_test.go | 89 +- common/nexus/failure.go | 1 + common/nexus/failure_test.go | 53 + common/number/number.go | 6 +- common/number/number_test.go | 6 +- common/payload/payload.go | 36 +- common/payload/payload_test.go | 124 + common/payloads/payloads.go | 4 +- .../cassandra/cluster_metadata_store.go | 4 +- common/persistence/cassandra/common.go | 10 +- common/persistence/cassandra/errors.go | 18 +- common/persistence/cassandra/errors_test.go | 38 +- common/persistence/cassandra/history_store.go | 10 +- .../cassandra/matching_task_store_queue.go | 6 +- .../matching_task_store_user_data.go | 15 +- .../cassandra/matching_task_store_v1.go | 8 +- .../cassandra/matching_task_store_v2.go | 37 +- .../persistence/cassandra/metadata_store.go | 30 +- .../cassandra/mutable_state_store.go | 16 +- .../cassandra/mutable_state_task_store.go | 4 +- .../cassandra/nexus_endpoint_store.go | 48 +- common/persistence/cassandra/queue_store.go | 33 +- .../persistence/cassandra/queue_v2_store.go | 6 +- common/persistence/cassandra/shard_store.go | 4 +- common/persistence/cassandra/util.go | 6 +- common/persistence/client/quotas_test.go | 6 +- common/persistence/history_node_util_test.go | 4 +- .../nosqlplugin/cassandra/gocql/batch.go | 2 +- .../nosqlplugin/cassandra/gocql/errors.go | 4 + .../cassandra/gocql/errors_test.go | 145 + .../nosqlplugin/cassandra/gocql/interfaces.go | 18 +- .../nosql/nosqlplugin/cassandra/gocql/iter.go | 4 +- .../nosqlplugin/cassandra/gocql/query.go | 10 +- .../nosqlplugin/cassandra/gocql/session.go | 4 +- .../nosql/nosqlplugin/cassandra/gocql/uuid.go | 4 +- .../cluster_metadata_manager.go | 2 +- .../history_v2_persistence.go | 18 +- .../persistence_test_base.go | 2 +- .../persistence-tests/queue_persistence.go | 8 +- common/persistence/persistencetest/queues.go | 2 +- common/persistence/serialization/codec.go | 29 + .../persistence/serialization/serializer.go | 42 +- .../serialization/serializer_test.go | 2 +- .../serialization/task_serializers.go | 37 + .../serialization/task_serializers_test.go | 11 + common/persistence/serializer_test.go | 2 +- common/persistence/sql/common.go | 4 +- common/persistence/sql/sqlplugin/db_handle.go | 8 +- .../persistence/sql/sqlplugin/interfaces.go | 10 +- .../persistence/sql/sqlplugin/mysql/admin.go | 2 +- .../sql/sqlplugin/mysql/cluster_metadata.go | 2 +- .../persistence/sql/sqlplugin/mysql/events.go | 6 +- .../sql/sqlplugin/mysql/visibility.go | 2 +- .../sqlplugin/postgresql/cluster_metadata.go | 2 +- .../sql/sqlplugin/postgresql/events.go | 6 +- .../sql/sqlplugin/postgresql/visibility.go | 2 +- .../persistence/sql/sqlplugin/sqlite/admin.go | 2 +- .../sql/sqlplugin/sqlite/cluster_metadata.go | 2 +- .../sql/sqlplugin/sqlite/events.go | 6 +- .../tests/history_execution_activity.go | 6 +- .../tests/history_execution_buffer.go | 4 +- .../tests/history_execution_child_workflow.go | 6 +- .../tests/history_execution_request_cancel.go | 6 +- .../tests/history_execution_signal.go | 6 +- .../history_execution_signal_requested.go | 6 +- .../tests/history_execution_timer.go | 6 +- .../sql/sqlplugin/tests/history_node.go | 8 +- .../tests/history_replication_task.go | 4 +- .../tests/history_replication_task_dlq.go | 4 +- .../sql/sqlplugin/tests/history_timer_task.go | 4 +- .../sqlplugin/tests/history_transfer_task.go | 4 +- .../tests/history_visibility_task.go | 4 +- .../sql/sqlplugin/tests/namespace.go | 2 +- .../sql/sqlplugin/tests/queue_message.go | 4 +- .../sql/sqlplugin/tests/queue_v2.go | 2 +- .../sql/sqlplugin/tests/visibility.go | 28 +- .../persistence/sql/sqlplugin/visibility.go | 8 +- common/persistence/tests/cassandra_test.go | 52 +- .../history_task_queue_manager_test_suite.go | 6 +- common/persistence/tests/mysql_test.go | 44 + common/persistence/tests/postgresql_test.go | 2 + .../persistence/tests/queue_v2_test_suite.go | 8 +- common/persistence/tests/sqlite_test.go | 81 + common/persistence/tests/task_queue_task.go | 8 +- .../tests/visibility_persistence_suite.go | 16 +- .../versionhistory/version_history_item.go | 2 +- .../visibility/chasm_visibility_manager.go | 40 +- .../chasm_visibility_manager_test.go | 147 +- .../visibility/manager/visibility_manager.go | 31 +- .../manager/visibility_manager_mock.go | 11 +- .../elasticsearch/client/bulk_processor.go | 2 +- .../store/elasticsearch/client/client.go | 2 +- .../store/elasticsearch/client/client_v7.go | 34 +- .../store/elasticsearch/client/logger.go | 4 +- .../store/elasticsearch/converter_test.go | 6 +- .../store/elasticsearch/processor.go | 12 +- .../store/elasticsearch/processor_test.go | 28 +- .../store/elasticsearch/query_interceptors.go | 4 +- .../elasticsearch/query_interceptors_test.go | 8 +- .../store/elasticsearch/visibility_store.go | 90 +- .../visibility_store_read_test.go | 33 +- .../store/query/converter_legacy.go | 8 +- .../visibility/store/query/converter_test.go | 12 +- .../visibility/store/query/errors.go | 2 +- .../store/query/interceptors_legacy.go | 4 +- .../store/query/interceptors_legacy_test.go | 6 +- .../visibility/store/query/resolve.go | 8 +- .../visibility/store/query/resolve_test.go | 25 + .../visibility/store/sql/visibility_store.go | 58 +- common/persistence/visibility/store/util.go | 16 +- .../visibility/store/visibility_store.go | 6 +- .../visibility/store/visibility_store_mock.go | 5 +- .../visibility/visibility_manager_dual.go | 15 +- .../visibility/visibility_manager_impl.go | 133 +- .../visibility_manager_rate_limited.go | 11 +- .../visibility/visibility_manager_test.go | 49 + .../visibility/visiblity_manager_metrics.go | 47 +- common/primitives/task_queues.go | 12 +- common/primitives/uuid.go | 2 +- common/resource/fx.go | 8 +- common/rpc/grpc.go | 4 +- .../rpc/interceptor/business_id_extractor.go | 70 + .../interceptor/business_id_interceptor.go | 48 + .../business_id_interceptor_test.go | 118 + common/rpc/interceptor/caller_info.go | 4 +- common/rpc/interceptor/caller_info_test.go | 12 +- .../interceptor/concurrent_request_limit.go | 4 +- .../concurrent_request_limit_test.go | 4 +- .../rpc/interceptor/frontend_service_error.go | 4 +- common/rpc/interceptor/health.go | 4 +- common/rpc/interceptor/health_check.go | 119 +- common/rpc/interceptor/health_check_test.go | 50 + .../logtags/history_service_server_gen.go | 4 +- .../logtags/workflow_service_server_gen.go | 39 +- .../interceptor/logtags/workflow_tags_test.go | 2 +- common/rpc/interceptor/mask_internal_error.go | 6 +- .../interceptor/mask_internal_error_test.go | 2 +- common/rpc/interceptor/metadata_context.go | 4 +- common/rpc/interceptor/namespace.go | 4 +- common/rpc/interceptor/namespace_handover.go | 5 +- common/rpc/interceptor/namespace_logger.go | 4 +- .../rpc/interceptor/namespace_rate_limit.go | 70 +- .../interceptor/namespace_rate_limit_test.go | 239 + common/rpc/interceptor/namespace_test.go | 2 +- common/rpc/interceptor/namespace_validator.go | 28 +- .../interceptor/namespace_validator_test.go | 32 +- common/rpc/interceptor/rate_limit.go | 4 +- common/rpc/interceptor/redirection.go | 32 +- common/rpc/interceptor/redirection_test.go | 92 +- common/rpc/interceptor/retry.go | 6 +- common/rpc/interceptor/sdk_version.go | 4 +- common/rpc/interceptor/sdk_version_test.go | 10 +- .../interceptor/service_error_interceptor.go | 4 +- common/rpc/interceptor/slow_request_logger.go | 6 +- common/rpc/interceptor/stream_error.go | 6 +- common/rpc/interceptor/telemetry_test.go | 2 +- common/rpc/request_issues.go | 2 +- common/rpc/test/rpc_common_test.go | 2 +- common/sdk/factory.go | 2 +- common/searchattribute/encode.go | 32 +- common/searchattribute/encode_test.go | 55 +- common/searchattribute/manager.go | 2 +- common/searchattribute/manager_test.go | 2 +- common/searchattribute/mapper.go | 95 +- common/searchattribute/mapper_test.go | 116 + common/searchattribute/name_type_map.go | 138 +- common/searchattribute/name_type_map_test.go | 675 +- .../{ => sadefs}/encode_value.go | 30 +- .../{ => sadefs}/encode_value_test.go | 2 +- common/searchattribute/sadefs/errors.go | 9 + common/searchattribute/sadefs/util.go | 10 +- common/searchattribute/search_attirbute.go | 6 - common/searchattribute/stringify.go | 30 +- common/searchattribute/stringify_test.go | 50 +- common/searchattribute/system_provider.go | 2 +- common/searchattribute/test_provider.go | 28 +- common/searchattribute/validator.go | 6 +- common/serviceerror/convert.go | 2 +- common/sqlquery/query.go | 2 +- common/taskqueue/stats.go | 43 + common/taskqueue/stats_test.go | 111 + common/tasks/execution_aware_scheduler.go | 129 + .../tasks/execution_aware_scheduler_test.go | 440 + common/tasks/execution_queue_scheduler.go | 285 + .../tasks/execution_queue_scheduler_test.go | 595 + common/tasks/fifo_scheduler.go | 7 +- common/tasks/fifo_scheduler_test.go | 4 +- .../interleaved_weighted_round_robin_test.go | 23 +- common/tasks/sequential_scheduler.go | 10 +- common/tasks/sequential_scheduler_test.go | 14 +- common/tasks/sequential_task_queue.go | 2 +- common/tasks/sequential_task_queue_test.go | 2 +- common/telemetry/config.go | 4 +- common/testing/event_generator.go | 22 +- common/testing/fakedata/fakedata.go | 2 +- common/testing/generator_interface.go | 12 +- common/testing/grpcinject/grpcinject.go | 2 +- common/testing/history_event_util.go | 82 +- .../v1/service_grpc.pb.mock.go | 80 + common/testing/mocksdk/client_mock.go | 78 + common/testing/nettest/pipe_benchmark_test.go | 2 +- common/testing/parallelsuite/guard.go | 58 + common/testing/parallelsuite/suite.go | 194 + common/testing/parallelsuite/suite_test.go | 119 + common/testing/parallelsuite/testify.go | 37 + common/testing/protoassert/assert.go | 2 +- common/testing/protoassert/pretty_print.go | 2 +- common/testing/protoassert/testify_assert.go | 12 +- common/testing/taskpoller/taskpoller.go | 12 + common/testing/testhooks/hooks.go | 43 + common/testing/testhooks/key.go | 14 - common/testing/testhooks/noop_impl.go | 24 +- common/testing/testhooks/test_impl.go | 67 +- common/testing/testhooks/test_impl_test.go | 72 +- common/testing/testlogger/testlogger.go | 64 +- common/testing/testvars/test_vars.go | 20 +- common/tqid/task_queue_validator.go | 6 +- common/tqid/task_queue_validator_test.go | 18 +- common/util.go | 4 +- common/util/util.go | 2 +- common/util/wildcard.go | 9 + common/util_test.go | 8 +- .../routing_info_cache_test.go | 8 +- common/worker_versioning/worker_versioning.go | 18 +- .../worker_versioning_test.go | 2 +- components/callbacks/fx.go | 7 +- components/callbacks/request.go | 76 +- components/callbacks/request_test.go | 324 + components/nexusoperations/config.go | 4 +- components/nexusoperations/events_test.go | 2 +- components/nexusoperations/executors_test.go | 7 +- components/nexusoperations/frontend/fx.go | 1 - .../nexusoperations/frontend/handler.go | 9 +- .../nexusoperations/workflow/commands.go | 16 +- .../nexusoperations/workflow/commands_test.go | 54 +- config/development-cluster-a.yaml | 4 +- config/development-cluster-b.yaml | 11 +- config/development-cluster-c.yaml | 11 +- config/dynamicconfig/development-cass.yaml | 2 - config/dynamicconfig/development-sql.yaml | 2 - config/dynamicconfig/development-xdc.yaml | 2 - develop/docker-compose/docker-compose.yml | 2 +- develop/github/docker-compose.yml | 4 +- docker/docker-bake.hcl | 16 +- docker/targets/admin-tools.Dockerfile | 11 +- docker/targets/server.Dockerfile | 11 +- docs/Makefile | 15 +- docs/_assets/chasm-asm.d2 | 27 + docs/_assets/chasm-asm.svg | 185 + docs/_assets/chasm-component-state.d2 | 19 + docs/_assets/chasm-component-state.svg | 171 + docs/_assets/chasm-componentref.d2 | 18 + docs/_assets/chasm-componentref.svg | 171 + docs/_assets/chasm-context.d2 | 13 + docs/_assets/chasm-context.svg | 178 + docs/_assets/chasm-engine-read.d2 | 24 + docs/_assets/chasm-engine-read.svg | 185 + docs/_assets/chasm-engine.d2 | 24 + docs/_assets/chasm-engine.svg | 184 + docs/_assets/chasm-execution.d2 | 19 + docs/_assets/chasm-execution.svg | 172 + docs/_assets/chasm-executionkey.d2 | 17 + docs/_assets/chasm-executionkey.svg | 170 + docs/_assets/chasm-lifecycle.d2 | 21 + docs/_assets/chasm-lifecycle.svg | 177 + docs/_assets/chasm-search-attributes.d2 | 13 + docs/_assets/chasm-search-attributes.svg | 178 + docs/_assets/chasm-task-handler.d2 | 20 + docs/_assets/chasm-task-handler.svg | 182 + docs/_assets/chasm-tasks.d2 | 54 + docs/_assets/chasm-tasks.svg | 192 + docs/_assets/chasm-transition.d2 | 16 + docs/_assets/chasm-transition.svg | 179 + docs/_assets/chasm-transitions.d2 | 13 + docs/_assets/chasm-transitions.svg | 177 + docs/_assets/chasm-visibility.d2 | 16 + docs/_assets/chasm-visibility.svg | 169 + docs/_assets/chasm-vt.d2 | 16 + docs/_assets/chasm-vt.svg | 169 + docs/_assets/matching-context.d2 | 6 + docs/_assets/retries.d2 | 6 + docs/architecture/chasm.md | 374 +- docs/architecture/nexus.md | 16 - .../architecture/speculative-workflow-task.md | 8 +- docs/development/macos/cassandra.md | 11 +- docs/development/testing.md | 57 +- go.mod | 163 +- go.sum | 353 +- proto/internal/buf.yaml | 10 +- .../adminservice/v1/request_response.proto | 120 +- .../server/api/adminservice/v1/service.proto | 383 +- .../server/api/archiver/v1/message.proto | 61 +- .../api/batch/v1/request_response.proto | 15 +- .../server/api/chasm/v1/message.proto | 22 + .../server/api/checksum/v1/message.proto | 70 +- .../temporal/server/api/cli/v1/message.proto | 91 +- .../server/api/clock/v1/message.proto | 24 +- .../server/api/cluster/v1/message.proto | 33 +- .../server/api/common/v1/api_category.proto | 35 + .../temporal/server/api/common/v1/dlq.proto | 4 +- .../server/api/deployment/v1/message.proto | 658 +- .../server/api/enums/v1/cluster.proto | 26 +- .../temporal/server/api/enums/v1/common.proto | 34 +- .../server/api/enums/v1/fairness_state.proto | 10 +- .../temporal/server/api/enums/v1/nexus.proto | 38 +- .../server/api/enums/v1/predicate.proto | 26 +- .../server/api/enums/v1/replication.proto | 39 +- .../temporal/server/api/enums/v1/task.proto | 107 +- .../server/api/enums/v1/workflow.proto | 28 +- .../api/enums/v1/workflow_task_type.proto | 10 +- .../server/api/errordetails/v1/message.proto | 59 +- .../server/api/health/v1/message.proto | 44 + .../server/api/history/v1/message.proto | 39 +- .../historyservice/v1/request_response.proto | 1710 ++- .../api/historyservice/v1/service.proto | 936 +- .../matchingservice/v1/request_response.proto | 885 +- .../api/matchingservice/v1/service.proto | 468 +- .../server/api/metrics/v1/message.proto | 2 +- .../server/api/namespace/v1/message.proto | 12 +- .../server/api/persistence/v1/chasm.proto | 170 +- .../api/persistence/v1/chasm_visibility.proto | 5 +- .../api/persistence/v1/cluster_metadata.proto | 37 +- .../api/persistence/v1/executions.proto | 1479 +-- .../api/persistence/v1/history_tree.proto | 35 +- .../server/api/persistence/v1/hsm.proto | 204 +- .../api/persistence/v1/namespaces.proto | 60 +- .../server/api/persistence/v1/nexus.proto | 85 +- .../api/persistence/v1/predicates.proto | 61 +- .../api/persistence/v1/queue_metadata.proto | 3 +- .../server/api/persistence/v1/queues.proto | 40 +- .../api/persistence/v1/task_queues.proto | 197 +- .../server/api/persistence/v1/tasks.proto | 148 +- .../server/api/persistence/v1/update.proto | 55 +- .../v1/workflow_mutable_state.proto | 100 +- .../server/api/replication/v1/message.proto | 368 +- .../server/api/routing/v1/extension.proto | 20 +- .../server/api/schedule/v1/message.proto | 212 +- .../server/api/taskqueue/v1/message.proto | 203 +- .../api/testservice/v1/request_response.proto | 3 +- .../server/api/testservice/v1/service.proto | 7 +- .../server/api/token/v1/message.proto | 124 +- .../v1/request_response.proto | 49 + .../server/api/workflow/v1/message.proto | 41 +- protogen | Bin 0 -> 3392386 bytes service/frontend/admin_handler.go | 170 +- service/frontend/admin_handler_test.go | 99 +- service/frontend/configs/quotas.go | 113 +- service/frontend/configs/quotas_test.go | 14 +- service/frontend/errors.go | 3 + service/frontend/fx.go | 39 +- service/frontend/fx_test.go | 2 + service/frontend/health_check.go | 151 +- service/frontend/health_check_test.go | 129 +- service/frontend/http_api_server.go | 6 +- service/frontend/namespace_handler.go | 3 + service/frontend/namespace_handler_test.go | 1 + service/frontend/nexus_handler.go | 4 +- service/frontend/nexus_handler_test.go | 5 + service/frontend/nexus_http_handler.go | 35 +- service/frontend/nexus_http_handler_test.go | 151 + service/frontend/operator_handler.go | 17 - service/frontend/operator_handler_test.go | 4 +- service/frontend/protojson_marshaler.go | 4 +- service/frontend/service.go | 28 +- service/frontend/workflow_handler.go | 424 +- .../frontend/workflow_handler_second_test.go | 65 + service/frontend/workflow_handler_test.go | 21 +- service/history/api/addtasks/api_test.go | 2 +- service/history/api/command_attr_validator.go | 6 +- .../api/command_attr_validator_test.go | 63 +- .../deletedlqtaskstest/apitest.go | 2 +- service/history/api/deleteworkflow/api.go | 2 +- .../api/forcedeleteworkflowexecution/api.go | 26 +- service/history/api/get_history_util.go | 113 +- service/history/api/get_history_util_test.go | 316 + service/history/api/get_workflow_util.go | 12 +- .../api/getworkflowexecutionhistory/api.go | 202 +- .../api/getworkflowexecutionrawhistory/api.go | 10 +- .../getworkflowexecutionrawhistoryv2/api.go | 10 +- .../api/listqueues/listqueuestest/apitest.go | 2 +- service/history/api/listtasks/api_test.go | 2 +- service/history/api/multioperation/api.go | 4 +- service/history/api/pauseactivity/api.go | 14 + service/history/api/reapplyevents/api.go | 1 - .../api/recordactivitytaskstarted/api.go | 2 +- .../api/recordworkflowtaskstarted/api.go | 13 +- service/history/api/resetactivity/api.go | 14 + service/history/api/resetworkflow/api.go | 1 - .../api/respondactivitytaskcanceled/api.go | 5 +- .../api/respondactivitytaskcompleted/api.go | 5 +- .../api/respondactivitytaskfailed/api.go | 9 +- .../api/respondactivitytaskfailed/api_test.go | 90 +- .../api/respondworkflowtaskcompleted/api.go | 11 +- .../respondworkflowtaskcompleted/api_test.go | 4 +- .../workflow_size_checker_test.go | 2 +- .../workflow_task_completed_handler.go | 2 + .../workflow_task_completed_handler_test.go | 1 + .../api/respondworkflowtaskfailed/api.go | 21 +- service/history/api/unpauseactivity/api.go | 24 + .../history/api/updateactivityoptions/api.go | 40 +- .../api/updateactivityoptions/api_test.go | 68 + service/history/archival_queue_factory.go | 8 + .../history/archival_queue_factory_test.go | 1 - service/history/chasm_engine.go | 381 +- service/history/chasm_engine_test.go | 864 +- service/history/chasm_task_util.go | 81 +- service/history/chasm_task_util_test.go | 73 + service/history/configs/config.go | 40 +- service/history/configs/quotas_test.go | 2 +- service/history/configs/task.go | 6 +- .../history/deletemanager/delete_manager.go | 32 +- .../deletemanager/delete_manager_mock.go | 12 +- .../deletemanager/delete_manager_test.go | 55 + service/history/events/cache.go | 2 +- service/history/events/notifier.go | 8 +- service/history/events/notifier_test.go | 2 +- service/history/fx.go | 4 +- service/history/handler.go | 175 +- service/history/history_engine2_test.go | 4 +- service/history/history_engine_test.go | 69 +- .../history/historybuilder/event_factory.go | 50 +- service/history/historybuilder/event_store.go | 6 +- .../historybuilder/history_builder_test.go | 156 +- service/history/hsm/tree_test.go | 4 +- service/history/interfaces/chasm_tree.go | 8 +- service/history/interfaces/chasm_tree_mock.go | 36 +- service/history/ndc/conflict_resolver.go | 6 +- service/history/ndc/conflict_resolver_test.go | 6 +- service/history/ndc/state_rebuilder.go | 17 +- service/history/ndc/state_rebuilder_mock.go | 8 +- service/history/ndc/state_rebuilder_test.go | 9 +- service/history/ndc/transaction_manager.go | 7 +- .../transaction_manager_existing_workflow.go | 8 +- ...nsaction_manager_existing_workflow_test.go | 16 +- .../ndc/transaction_manager_new_workflow.go | 6 +- .../transaction_manager_new_workflow_test.go | 7 +- .../history/ndc/transaction_manager_test.go | 2 - service/history/ndc/workflow.go | 22 +- service/history/ndc/workflow_mock.go | 10 +- service/history/ndc/workflow_resetter.go | 16 +- service/history/ndc/workflow_resetter_mock.go | 8 +- .../ndc/workflow_state_replicator_test.go | 4 +- service/history/ndc_standby_task_util.go | 10 +- service/history/ndc_task_util.go | 13 +- .../outbound_queue_active_task_executor.go | 2 +- ...utbound_queue_active_task_executor_test.go | 3 +- service/history/outbound_queue_factory.go | 3 + .../outbound_queue_standby_task_executor.go | 32 +- ...tbound_queue_standby_task_executor_test.go | 90 + service/history/queue_factory_base.go | 4 +- service/history/queues/dlq_writer_test.go | 6 +- service/history/queues/executable.go | 153 +- service/history/queues/executable_factory.go | 2 +- service/history/queues/executable_test.go | 188 +- .../queues/memory_scheduled_queue_test.go | 6 +- service/history/queues/metrics_test.go | 19 + service/history/queues/queue_base.go | 13 +- service/history/queues/queue_base_test.go | 2 +- .../history/queues/queue_scheduled_test.go | 8 + service/history/queues/reader_group_test.go | 2 +- service/history/queues/scheduler.go | 78 +- service/history/queues/scheduler_mock.go | 38 + ...speculative_workflow_task_timeout_queue.go | 2 +- service/history/replication/ack_manager.go | 2 + service/history/replication/batchable_task.go | 2 +- .../eventhandler/event_importer_test.go | 2 +- .../eventhandler/resend_handler_test.go | 2 +- .../executable_activity_state_task.go | 2 +- ...executable_backfill_history_events_task.go | 2 +- .../executable_delete_execution_task.go | 180 + .../replication/executable_history_task.go | 2 +- .../replication/executable_noop_task.go | 2 +- .../replication/executable_sync_hsm_task.go | 2 +- ...ecutable_sync_versioned_transition_task.go | 2 +- .../history/replication/executable_task.go | 10 +- .../replication/executable_task_converter.go | 9 + .../replication/executable_task_test.go | 1 + .../replication/executable_task_tool_box.go | 3 + .../replication/executable_task_tracker.go | 2 +- .../replication/executable_unknown_task.go | 2 +- ...utable_verify_versioned_transition_task.go | 2 +- .../executable_workflow_state_task.go | 2 +- service/history/replication/fx.go | 5 +- service/history/replication/metrics.go | 4 + .../history/replication/raw_task_converter.go | 10 + .../replication/raw_task_converter_test.go | 20 + .../replication/sequential_batch_queue.go | 4 +- .../history/replication/sequential_queue.go | 8 +- service/history/replication/stream_sender.go | 21 +- service/history/replication/task_fetcher.go | 2 +- .../history/replication/task_fetcher_test.go | 6 +- service/history/replication/task_processor.go | 4 +- service/history/shard/context_impl.go | 68 +- service/history/shard/context_test.go | 18 +- service/history/shard/controller_test.go | 20 +- .../history/shard/task_key_generator_test.go | 4 +- .../history/shard/task_key_manager_test.go | 2 +- .../delete_execution_replication_task.go | 57 + .../tasks/delete_workflow_execution_stage.go | 1 + service/history/tasks/key_test.go | 4 +- service/history/tasks/utils.go | 2 +- .../timer_queue_active_task_executor.go | 2 +- .../timer_queue_active_task_executor_test.go | 7 +- service/history/timer_queue_factory.go | 9 + .../timer_queue_standby_task_executor.go | 64 +- .../timer_queue_standby_task_executor_test.go | 94 + .../transfer_queue_active_task_executor.go | 3 +- ...ransfer_queue_active_task_executor_test.go | 14 +- service/history/transfer_queue_factory.go | 9 + .../transfer_queue_standby_task_executor.go | 66 +- ...ansfer_queue_standby_task_executor_test.go | 94 + service/history/visibility_queue_factory.go | 9 + .../history/visibility_queue_task_executor.go | 3 +- service/history/workflow/cache/cache_test.go | 32 +- service/history/workflow/context.go | 11 +- service/history/workflow/metrics.go | 9 +- service/history/workflow/metrics_test.go | 153 + .../history/workflow/mutable_state_impl.go | 103 +- .../workflow/mutable_state_impl_test.go | 153 +- service/history/workflow/noop_chasm_tree.go | 14 +- .../history/workflow/query_registry_test.go | 8 +- service/history/workflow/retry.go | 4 +- service/history/workflow/task_generator.go | 18 +- .../history/workflow/task_generator_test.go | 28 +- service/history/workflow/task_refresher.go | 6 +- service/history/workflow/update/registry.go | 7 + service/history/workflow/update/util.go | 14 +- .../workflow/workflow_task_state_machine.go | 61 +- service/history/workflow_rebuilder.go | 5 - service/matching/ack_manager_test.go | 8 +- service/matching/backlog_manager_test.go | 243 +- service/matching/config.go | 24 +- service/matching/configs/quotas_test.go | 2 +- service/matching/counter/cmsketch.go | 142 +- service/matching/counter/cmsketch_test.go | 175 + service/matching/counter/hybrid.go | 3 + service/matching/counter/map.go | 41 +- service/matching/counter/map_test.go | 41 + service/matching/db.go | 29 + service/matching/fair_backlog_manager.go | 23 +- service/matching/fair_task_reader.go | 72 +- service/matching/fair_task_writer.go | 19 +- service/matching/forwarder.go | 4 +- service/matching/forwarder_test.go | 31 +- service/matching/fx.go | 19 +- service/matching/handler.go | 63 +- service/matching/handler_test.go | 249 + .../matching/hooks/task_lifecycle_hooks.go | 46 + service/matching/matcher.go | 3 + service/matching/matcher_test.go | 30 +- service/matching/matching_engine.go | 251 +- service/matching/matching_engine_test.go | 222 +- .../matching/physical_task_queue_manager.go | 115 +- .../physical_task_queue_manager_interface.go | 4 +- .../physical_task_queue_manager_mock.go | 16 +- .../physical_task_queue_manager_test.go | 43 +- service/matching/pri_backlog_manager.go | 1 + service/matching/pri_forwarder.go | 20 + service/matching/pri_matcher.go | 3 + service/matching/pri_task_reader.go | 19 +- service/matching/reachability.go | 2 +- service/matching/task.go | 34 +- .../matching/task_queue_partition_manager.go | 136 +- .../task_queue_partition_manager_test.go | 204 +- service/matching/task_tracker.go | 77 +- service/matching/task_tracker_test.go | 25 +- service/matching/user_data_manager.go | 61 +- service/matching/user_data_manager_test.go | 88 +- service/matching/version_rule_helper_test.go | 6 +- service/matching/version_rule_test.go | 4 +- service/matching/version_sets_test.go | 2 +- service/matching/workers/registry_impl.go | 94 +- .../matching/workers/registry_impl_test.go | 264 +- service/matching/workers/registry_test.go | 156 +- .../workers/worker_metrics_emitter.go | 96 + .../workers/worker_metrics_emitter_test.go | 115 + .../matching/workers/worker_query_engine.go | 96 +- .../worker_query_engine_helpers_test.go | 2 +- .../workers/worker_query_engine_test.go | 194 + service/worker/batcher/activities.go | 35 +- .../batcher/activities_namespace_test.go | 156 + service/worker/batcher/activities_test.go | 72 +- service/worker/batcher/workflow.go | 2 +- service/worker/batcher/workflow_test.go | 8 +- service/worker/dummy/workflow.go | 4 +- service/worker/fx.go | 34 +- service/worker/migration/activities.go | 362 +- service/worker/migration/activities_test.go | 13 +- .../force_replication_workflow_test.go | 6 +- service/worker/pernamespaceworker.go | 74 +- service/worker/pernamespaceworker_test.go | 14 +- .../replication_message_processor.go | 9 + service/worker/replicator/replicator.go | 9 + .../scanner/build_ids/scavenger_test.go | 2 +- .../worker/scanner/executor/executor_test.go | 4 +- service/worker/scanner/executor/runq.go | 2 +- service/worker/scanner/history/scavenger.go | 2 +- service/worker/scanner/scanner.go | 4 +- service/worker/scanner/scanner_test.go | 4 +- .../worker/scanner/taskqueue/mocks_test.go | 2 +- .../scanner/taskqueue/scavenger_test.go | 10 +- service/worker/scheduler/activities.go | 32 +- service/worker/scheduler/activities_test.go | 102 + service/worker/scheduler/calendar_test.go | 2 +- service/worker/scheduler/fx.go | 15 +- service/worker/scheduler/workflow.go | 138 +- service/worker/scheduler/workflow_test.go | 311 +- service/worker/service.go | 16 +- service/worker/workerdeployment/activities.go | 28 +- service/worker/workerdeployment/client.go | 549 +- .../worker/workerdeployment/compute_util.go | 105 + .../workerdeployment/compute_util_test.go | 51 + service/worker/workerdeployment/fx.go | 23 +- .../v2/run_1775685309/expected_counts.txt | 6 + ...9d6f17-068c-7a06-8ed1-fcacd6f8afea.json.gz | Bin 0 -> 3842 bytes ...4a7ffd-ae5c-4733-9685-2ffb63fdd662.json.gz | Bin 0 -> 3667 bytes ...754e89-0f02-45a7-b2d0-51f5d7f28d86.json.gz | Bin 0 -> 4839 bytes ...99a2b9-2bf9-4a0a-8147-08839f4493ea.json.gz | Bin 0 -> 3669 bytes ...ef5374-249a-47bd-85b1-4a51a910a079.json.gz | Bin 0 -> 3868 bytes ...bc2150-36c6-41bb-8ed9-320d10890207.json.gz | Bin 0 -> 2487 bytes ...e2bfdc-cd4a-4f53-aa41-c7de8f79a81f.json.gz | Bin 0 -> 3687 bytes ...28076e-569d-4d27-baac-7ecabc6a5bdf.json.gz | Bin 0 -> 3875 bytes ...92fc1c-8a4c-4a39-a7f4-494f0fd876be.json.gz | Bin 0 -> 3885 bytes ...432420-68c8-4089-ae02-18a8fbc73e1d.json.gz | Bin 0 -> 3972 bytes ...09a05b-ff03-486f-a132-4f8260d7156d.json.gz | Bin 0 -> 3942 bytes ...9d6f17-0239-7795-8331-4eaa45970f83.json.gz | Bin 0 -> 4036 bytes ...309e89-6521-4882-b4c8-1f33f04010ff.json.gz | Bin 0 -> 4060 bytes ...2ca45d-68d4-466a-ac66-995790f0142c.json.gz | Bin 0 -> 2969 bytes ...62db09-ce1a-4488-877c-12c5c2c1094a.json.gz | Bin 0 -> 2831 bytes ...084e10-0859-489e-bd7f-64124a754a4f.json.gz | Bin 0 -> 2367 bytes ...17bc4a-0f39-4b94-9f81-175ca8138012.json.gz | Bin 0 -> 3713 bytes ...1d483f-d549-4756-8bde-7bb3f7a5ba2d.json.gz | Bin 0 -> 4120 bytes ...8f65d4-dee2-4833-b854-77e99c3275b0.json.gz | Bin 0 -> 3364 bytes ...7446a9-2ede-49fb-a984-b032f73d7211.json.gz | Bin 0 -> 4083 bytes ...15ed0c-e054-469b-921c-3d46f0ef7d8c.json.gz | Bin 0 -> 4079 bytes ...927d1c-0c7f-4811-bc65-6cad0abef0cc.json.gz | Bin 0 -> 3724 bytes ...1dce9d-e117-4615-9483-27a962f44fd9.json.gz | Bin 0 -> 3338 bytes ...54f7d5-2026-4803-b4ee-9fd823b9b028.json.gz | Bin 0 -> 2891 bytes ...0bd42f-c6fd-4e13-bbf6-bb4cb33731a3.json.gz | Bin 0 -> 3841 bytes ...ec8a92-81b8-4f1d-a34a-5c4ab8def082.json.gz | Bin 0 -> 2665 bytes ...01d2a8-3692-4dc6-a64b-df567415ffee.json.gz | Bin 0 -> 4110 bytes ...51970a-4601-49d2-bdfe-e807da355279.json.gz | Bin 0 -> 2736 bytes ...3e9705-5c62-41df-8177-54404a236e6b.json.gz | Bin 0 -> 2775 bytes ...1fc082-38b7-414b-a0c7-28f7a44c1e70.json.gz | Bin 0 -> 2976 bytes .../replaytester/worker/worker.go | 2 +- service/worker/workerdeployment/util.go | 46 +- .../workerdeployment/version_activities.go | 18 + .../workerdeployment/version_workflow.go | 169 +- .../workerdeployment/version_workflow_test.go | 507 +- service/worker/workerdeployment/workflow.go | 230 +- .../worker/workerdeployment/workflow_test.go | 744 +- temporal/fx.go | 85 +- temporal/interrupt.go | 4 +- temporal/server_impl.go | 4 +- temporal/server_option.go | 19 +- temporal/server_options.go | 35 +- temporal/server_test.go | 150 +- temporaltest/logger.go | 12 +- temporaltest/server_test.go | 10 +- tests/activity_api_batch_reset_test.go | 221 +- tests/activity_api_batch_security_test.go | 95 + tests/activity_api_batch_unpause_test.go | 143 +- .../activity_api_batch_update_options_test.go | 140 +- tests/activity_api_pause_test.go | 320 +- tests/activity_api_reset_test.go | 192 +- tests/activity_api_rules_test.go | 28 +- tests/activity_api_update_test.go | 150 +- tests/activity_test.go | 73 +- ...admin_batch_refresh_workflow_tasks_test.go | 8 +- tests/admin_test.go | 63 +- tests/advanced_visibility_test.go | 146 +- tests/archival_test.go | 134 +- tests/callbacks_migration_test.go | 2 - tests/callbacks_test.go | 20 +- tests/cancel_workflow_test.go | 3 +- tests/chasm_test.go | 204 +- tests/child_workflow_test.go | 10 +- tests/client_data_converter_test.go | 18 +- tests/client_misc_test.go | 100 +- tests/continue_as_new_test.go | 30 +- tests/cron_test.go | 46 +- tests/describe_test.go | 67 +- tests/dlq_test.go | 20 +- tests/eager_workflow_start_test.go | 2 +- tests/gethistory_test.go | 44 +- tests/http_api_test.go | 8 +- tests/links_test.go | 42 +- tests/matching_utils.go | 77 + tests/max_buffered_event_test.go | 61 +- tests/mixedbrain/build_util.go | 130 + tests/mixedbrain/build_util_test.go | 63 + tests/mixedbrain/config_util.go | 212 + tests/mixedbrain/mixed_brain_test.go | 187 + tests/mixedbrain/proxy_util.go | 75 + tests/mixedbrain/server_util.go | 170 + tests/namespace_delete_test.go | 4 +- tests/ndc/ndc_test.go | 18 +- tests/ndc/replication_migration_back_test.go | 6 +- tests/ndc/replication_test.go | 2 +- tests/ndc/test_data.go | 6 +- tests/nexus_api_test.go | 514 +- tests/nexus_api_validation_test.go | 258 +- tests/nexus_endpoint_test.go | 73 +- tests/nexus_test_base.go | 75 +- tests/nexus_workflow_test.go | 1437 ++- tests/nil_search_attribute_test.go | 375 + tests/pause_workflow_execution_test.go | 128 +- tests/poller_scaling_test.go | 24 +- tests/premature_eos_test.go | 173 + tests/priority_fairness_test.go | 21 +- tests/purge_dlq_tasks_api_test.go | 4 +- tests/query_workflow_test.go | 46 +- tests/relay_task_test.go | 7 +- tests/reset_workflow_test.go | 8 +- tests/schedule_migration_test.go | 1305 ++ tests/schedule_test.go | 2621 ++-- tests/signal_workflow_test.go | 57 +- tests/sizelimit_test.go | 322 +- tests/standalone_activity_test.go | 655 +- tests/stickytq_test.go | 10 +- tests/task_queue_stats_test.go | 859 +- tests/task_queue_test.go | 523 +- tests/testcore/context.go | 69 +- tests/testcore/functional_test_base.go | 206 +- tests/testcore/matching_behavior.go | 89 + tests/testcore/metric_capture.go | 132 + tests/testcore/metric_capture_test.go | 93 + tests/testcore/onebox.go | 32 +- tests/testcore/replication_stream_recorder.go | 16 +- tests/testcore/shard_salt.txt | 2 +- tests/testcore/test_cluster.go | 112 +- tests/testcore/test_data_converter.go | 12 +- tests/testcore/test_env.go | 286 +- tests/testcore/utils.go | 11 + tests/testutils/tls.go | 2 +- tests/transient_task_test.go | 179 +- tests/update_workflow_sdk_test.go | 19 +- tests/update_workflow_test.go | 10103 ++++++++-------- tests/user_timers_test.go | 4 +- tests/versioning_3_test.go | 1845 ++- tests/versioning_test.go | 137 +- tests/worker_deployment_test.go | 508 +- tests/worker_deployment_version_test.go | 2826 +++-- tests/worker_registry_test.go | 67 +- tests/workflow_alias_search_attribute_test.go | 4 +- tests/workflow_buffered_events_test.go | 6 +- tests/workflow_delete_execution_test.go | 6 +- tests/workflow_failures_test.go | 29 +- tests/workflow_reset_test.go | 6 +- tests/workflow_reset_with_child_test.go | 12 +- tests/workflow_task_reported_problems_test.go | 10 +- tests/workflow_task_test.go | 37 +- tests/workflow_test.go | 30 +- tests/workflow_timer_test.go | 59 +- tests/workflow_visibility_test.go | 2 +- tests/xdc/activity_api_test.go | 10 +- tests/xdc/base.go | 96 +- tests/xdc/chasm_test.go | 299 +- .../xdc/delete_execution_replication_test.go | 401 + tests/xdc/failover_test.go | 107 +- tests/xdc/history_replication_dlq_test.go | 2 +- ...ry_replication_signals_and_updates_test.go | 30 +- tests/xdc/nexus_request_forwarding_test.go | 34 +- tests/xdc/nexus_state_replication_test.go | 25 +- tests/xdc/replication_enable_test.go | 2 +- tests/xdc/stream_based_replication_test.go | 2 +- tests/xdc/user_data_replication_test.go | 18 +- tests/xdc/visibility_test.go | 19 +- .../workflow_task_reported_problems_test.go | 10 +- tools/cassandra/cqlclient.go | 2 +- tools/cassandra/setup_task_tests.go | 3 +- tools/cassandra/update_task_tests.go | 3 +- tools/ci-notify/app.go | 2 +- tools/common/schema/mock_db.go | 2 +- tools/common/schema/test/dbtest.go | 28 + tools/common/schema/test/setuptest.go | 12 +- tools/common/schema/test/updatetest.go | 10 +- tools/common/schema/types.go | 2 +- tools/elasticsearch/tasks.go | 2 +- tools/fairsim/README.md | 153 + tools/fairsim/sim.go | 611 + tools/fairsim/sim_test.go | 178 + tools/flakereport/bisect.go | 506 + tools/flakereport/bisect_test.go | 568 + tools/flakereport/doc.go | 76 + tools/flakereport/flakereport.go | 398 + tools/flakereport/git.go | 40 + tools/flakereport/github.go | 292 + tools/flakereport/parallel.go | 151 + tools/flakereport/parser.go | 444 + tools/flakereport/parser_test.go | 313 + tools/flakereport/report.go | 102 + tools/flakereport/slack.go | 318 + tools/flakereport/types.go | 145 + tools/flakereport/writer.go | 233 + tools/flakes/.python-version | 1 - tools/flakes/README.md | 86 - tools/flakes/main.py | 604 - tools/flakes/pyproject.toml | 9 - tools/flakes/uv.lock | 87 - tools/parallelize/parallelize.go | 198 + tools/parallelize/parallelize_test.go | 175 + tools/sql/clitest/setup_task_tests.go | 7 +- tools/sql/clitest/update_task_tests.go | 7 +- tools/sql/conn.go | 2 +- tools/tdbg/chasm_registry.go | 2 +- tools/tdbg/commands.go | 56 +- tools/tdbg/dlq_v1_service.go | 4 +- tools/tdbg/dlq_v2_service.go | 2 +- tools/tdbg/flags.go | 4 + tools/tdbg/task_queue_commands.go | 4 +- tools/tdbg/tdbg_commands.go | 47 + tools/tdbg/util.go | 19 +- tools/testrunner/junit.go | 3 + tools/testrunner/junit_test.go | 4 +- tools/testrunner/log.go | 18 + tools/testrunner/log_test.go | 44 + tools/testrunner/testrunner.go | 78 +- tools/testrunner/testrunner_test.go | 67 + tools/tests/cql_cli_test.go | 4 + tools/tests/mysql_cli_test.go | 15 +- tools/tests/postgresql_cli_test.go | 12 +- tools/tests/test_data.go | 3 - 1124 files changed, 69649 insertions(+), 26369 deletions(-) create mode 100644 .github/actions/build-docker-images/scripts/main_test.go delete mode 100644 .github/actions/trivy-scan/action.yml delete mode 100644 .github/workflows/create-tag.yml delete mode 100644 .github/workflows/trigger-publish.yml create mode 100644 api/chasm/v1/message.go-helpers.pb.go create mode 100644 api/chasm/v1/message.pb.go create mode 100644 api/common/v1/api_category.go-helpers.pb.go create mode 100644 api/common/v1/api_category.pb.go create mode 100644 api/health/v1/message.go-helpers.pb.go create mode 100644 api/health/v1/message.pb.go create mode 100644 api/visibilityservice/v1/request_response.go-helpers.pb.go create mode 100644 api/visibilityservice/v1/request_response.pb.go create mode 100644 chasm/lib/activity/frontend_test.go rename chasm/lib/callback/{chasm_invocation.go => invocable_internal.go} (85%) rename chasm/lib/callback/{nexus_invocation.go => invocable_outbound.go} (74%) create mode 100644 chasm/lib/callback/request_test.go rename chasm/lib/callback/{executors.go => tasks.go} (68%) rename chasm/lib/callback/{executors_test.go => tasks_test.go} (94%) rename chasm/lib/nexusoperation/{cancellation_executors.go => cancellation_tasks.go} (69%) rename chasm/lib/nexusoperation/{operation_executors.go => operation_tasks.go} (64%) create mode 100644 chasm/lib/scheduler/scheduler_migrate_task.go create mode 100644 chasm/lib/scheduler/scheduler_migrate_test.go delete mode 100644 chasm/lib/scheduler/scheduler_suite_test.go create mode 100644 chasm/lib/scheduler/sentinel_test.go create mode 100644 chasm/task_handler_base.go create mode 100644 cmd/tools/fairsim/main.go create mode 100644 cmd/tools/flakereport/main.go create mode 100644 cmd/tools/parallelize/main.go create mode 100644 common/archiver/provider/provider_test.go create mode 100644 common/health/check_types.go create mode 100644 common/namespace/nsreplication/data_merger.go create mode 100644 common/persistence/nosql/nosqlplugin/cassandra/gocql/errors_test.go create mode 100644 common/rpc/interceptor/health_check_test.go create mode 100644 common/rpc/interceptor/namespace_rate_limit_test.go rename common/searchattribute/{ => sadefs}/encode_value.go (86%) rename common/searchattribute/{ => sadefs}/encode_value_test.go (99%) create mode 100644 common/searchattribute/sadefs/errors.go create mode 100644 common/taskqueue/stats.go create mode 100644 common/taskqueue/stats_test.go create mode 100644 common/tasks/execution_aware_scheduler.go create mode 100644 common/tasks/execution_aware_scheduler_test.go create mode 100644 common/tasks/execution_queue_scheduler.go create mode 100644 common/tasks/execution_queue_scheduler_test.go create mode 100644 common/testing/parallelsuite/guard.go create mode 100644 common/testing/parallelsuite/suite.go create mode 100644 common/testing/parallelsuite/suite_test.go create mode 100644 common/testing/parallelsuite/testify.go create mode 100644 common/testing/testhooks/hooks.go delete mode 100644 common/testing/testhooks/key.go create mode 100644 components/callbacks/request_test.go create mode 100644 docs/_assets/chasm-asm.d2 create mode 100644 docs/_assets/chasm-asm.svg create mode 100644 docs/_assets/chasm-component-state.d2 create mode 100644 docs/_assets/chasm-component-state.svg create mode 100644 docs/_assets/chasm-componentref.d2 create mode 100644 docs/_assets/chasm-componentref.svg create mode 100644 docs/_assets/chasm-context.d2 create mode 100644 docs/_assets/chasm-context.svg create mode 100644 docs/_assets/chasm-engine-read.d2 create mode 100644 docs/_assets/chasm-engine-read.svg create mode 100644 docs/_assets/chasm-engine.d2 create mode 100644 docs/_assets/chasm-engine.svg create mode 100644 docs/_assets/chasm-execution.d2 create mode 100644 docs/_assets/chasm-execution.svg create mode 100644 docs/_assets/chasm-executionkey.d2 create mode 100644 docs/_assets/chasm-executionkey.svg create mode 100644 docs/_assets/chasm-lifecycle.d2 create mode 100644 docs/_assets/chasm-lifecycle.svg create mode 100644 docs/_assets/chasm-search-attributes.d2 create mode 100644 docs/_assets/chasm-search-attributes.svg create mode 100644 docs/_assets/chasm-task-handler.d2 create mode 100644 docs/_assets/chasm-task-handler.svg create mode 100644 docs/_assets/chasm-tasks.d2 create mode 100644 docs/_assets/chasm-tasks.svg create mode 100644 docs/_assets/chasm-transition.d2 create mode 100644 docs/_assets/chasm-transition.svg create mode 100644 docs/_assets/chasm-transitions.d2 create mode 100644 docs/_assets/chasm-transitions.svg create mode 100644 docs/_assets/chasm-visibility.d2 create mode 100644 docs/_assets/chasm-visibility.svg create mode 100644 docs/_assets/chasm-vt.d2 create mode 100644 docs/_assets/chasm-vt.svg create mode 100644 proto/internal/temporal/server/api/chasm/v1/message.proto create mode 100644 proto/internal/temporal/server/api/common/v1/api_category.proto create mode 100644 proto/internal/temporal/server/api/health/v1/message.proto create mode 100644 proto/internal/temporal/server/api/visibilityservice/v1/request_response.proto create mode 100755 protogen create mode 100644 service/frontend/nexus_http_handler_test.go create mode 100644 service/history/api/get_history_util_test.go create mode 100644 service/history/chasm_task_util_test.go create mode 100644 service/history/replication/executable_delete_execution_task.go create mode 100644 service/history/tasks/delete_execution_replication_task.go create mode 100644 service/matching/handler_test.go create mode 100644 service/matching/hooks/task_lifecycle_hooks.go create mode 100644 service/matching/workers/worker_metrics_emitter.go create mode 100644 service/matching/workers/worker_metrics_emitter_test.go create mode 100644 service/worker/batcher/activities_namespace_test.go create mode 100644 service/worker/scheduler/activities_test.go create mode 100644 service/worker/workerdeployment/compute_util.go create mode 100644 service/worker/workerdeployment/compute_util_test.go create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/expected_counts.txt create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_019d6f17-068c-7a06-8ed1-fcacd6f8afea.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_224a7ffd-ae5c-4733-9685-2ffb63fdd662.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_46754e89-0f02-45a7-b2d0-51f5d7f28d86.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_4e99a2b9-2bf9-4a0a-8147-08839f4493ea.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_51ef5374-249a-47bd-85b1-4a51a910a079.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_5abc2150-36c6-41bb-8ed9-320d10890207.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_73e2bfdc-cd4a-4f53-aa41-c7de8f79a81f.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_9a28076e-569d-4d27-baac-7ecabc6a5bdf.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_9a92fc1c-8a4c-4a39-a7f4-494f0fd876be.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_ec432420-68c8-4089-ae02-18a8fbc73e1d.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_fb09a05b-ff03-486f-a132-4f8260d7156d.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_019d6f17-0239-7795-8331-4eaa45970f83.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_20309e89-6521-4882-b4c8-1f33f04010ff.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_272ca45d-68d4-466a-ac66-995790f0142c.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_2962db09-ce1a-4488-877c-12c5c2c1094a.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_3f084e10-0859-489e-bd7f-64124a754a4f.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_4117bc4a-0f39-4b94-9f81-175ca8138012.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_411d483f-d549-4756-8bde-7bb3f7a5ba2d.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_6c8f65d4-dee2-4833-b854-77e99c3275b0.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_8c7446a9-2ede-49fb-a984-b032f73d7211.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_a115ed0c-e054-469b-921c-3d46f0ef7d8c.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_a1927d1c-0c7f-4811-bc65-6cad0abef0cc.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_ab1dce9d-e117-4615-9483-27a962f44fd9.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_be54f7d5-2026-4803-b4ee-9fd823b9b028.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_ce0bd42f-c6fd-4e13-bbf6-bb4cb33731a3.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_d5ec8a92-81b8-4f1d-a34a-5c4ab8def082.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_dc01d2a8-3692-4dc6-a64b-df567415ffee.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_e151970a-4601-49d2-bdfe-e807da355279.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_e63e9705-5c62-41df-8177-54404a236e6b.json.gz create mode 100644 service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_e81fc082-38b7-414b-a0c7-28f7a44c1e70.json.gz create mode 100644 tests/activity_api_batch_security_test.go create mode 100644 tests/matching_utils.go create mode 100644 tests/mixedbrain/build_util.go create mode 100644 tests/mixedbrain/build_util_test.go create mode 100644 tests/mixedbrain/config_util.go create mode 100644 tests/mixedbrain/mixed_brain_test.go create mode 100644 tests/mixedbrain/proxy_util.go create mode 100644 tests/mixedbrain/server_util.go create mode 100644 tests/nil_search_attribute_test.go create mode 100644 tests/premature_eos_test.go create mode 100644 tests/schedule_migration_test.go create mode 100644 tests/testcore/matching_behavior.go create mode 100644 tests/testcore/metric_capture.go create mode 100644 tests/testcore/metric_capture_test.go create mode 100644 tests/xdc/delete_execution_replication_test.go create mode 100644 tools/fairsim/README.md create mode 100644 tools/fairsim/sim.go create mode 100644 tools/fairsim/sim_test.go create mode 100644 tools/flakereport/bisect.go create mode 100644 tools/flakereport/bisect_test.go create mode 100644 tools/flakereport/doc.go create mode 100644 tools/flakereport/flakereport.go create mode 100644 tools/flakereport/git.go create mode 100644 tools/flakereport/github.go create mode 100644 tools/flakereport/parallel.go create mode 100644 tools/flakereport/parser.go create mode 100644 tools/flakereport/parser_test.go create mode 100644 tools/flakereport/report.go create mode 100644 tools/flakereport/slack.go create mode 100644 tools/flakereport/types.go create mode 100644 tools/flakereport/writer.go delete mode 100644 tools/flakes/.python-version delete mode 100644 tools/flakes/README.md delete mode 100644 tools/flakes/main.py delete mode 100644 tools/flakes/pyproject.toml delete mode 100644 tools/flakes/uv.lock create mode 100644 tools/parallelize/parallelize.go create mode 100644 tools/parallelize/parallelize_test.go diff --git a/.github/.golangci.yml b/.github/.golangci.yml index 10cbe436d17..826d07f6ccc 100644 --- a/.github/.golangci.yml +++ b/.github/.golangci.yml @@ -34,7 +34,7 @@ linters: forbid: - pattern: time.Sleep msg: "Please use require.Eventually or assert.Eventually instead unless you've no other option" - - pattern: panic + - pattern: "^panic$" msg: "Please avoid using panic in application code" - pattern: time\.Now msg: "Using time.Now is not allowed in chasm/lib package (non-test files), use ctx.Now(component) instead" @@ -46,6 +46,10 @@ linters: msg: "Do not use .UnixNano() for Cassandra timestamps. Use p.UnixMilliseconds() which returns milliseconds." - pattern: FunctionalTestBase msg: "FunctionalTestBase is deprecated. Use testcore.NewEnv(t) instead. See docs/development/testing.md for details." + - pattern: context\.Background\(\) + msg: "Avoid context.Background() in tests; use t.Context() to respect test timeouts and cancellation" + - pattern: 'assert\.\w+' + msg: "Use require.X / protorequire.X instead of assert.X / protoassert.X — assert doesn't stop the test on failure. assert.CollectT is still allowed for EventuallyWithT callbacks." depguard: rules: main: @@ -67,6 +71,9 @@ linters: # internal server pbs have their own suffix to avoid naming conflicts - pkg: go.temporal.io/server/api/(\w+)/v1 alias: ${1}spb + testifylint: + disable: + - suite-method-signature # parallelsuite.Run supports extra args passed to Test* methods exhaustive: # Presence of "default" case in switch statements satisfies exhaustiveness, # even if all enum members are not listed. @@ -191,6 +198,13 @@ linters: text: "FunctionalTestBase" linters: - forbidigo + - path-except: tests/.+_test\.go # only enforce in test files + text: "context.Background" + linters: + - forbidigo + - text: "use of `assert\\.CollectT`" # allowed for EventuallyWithT callbacks + linters: + - forbidigo - path: _test\.go|tests/.+\.go|common/testing/ text: "(cyclomatic|cognitive)" # false positives when using subtests linters: diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5ef54e44bb2..1ad8bd67244 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,96 @@ # Syntax is here: # https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax -* @temporalio/server @temporalio/cgs +* @temporalio/server @temporalio/cgs @temporalio/nexus + +# CHASM + +# This only matching files directly in /chasm/ folder, but not any nested files +/chasm/* @temporalio/oss-foundations + +/chasm/lib/activity/ @temporalio/act +/chasm/lib/scheduler/ @temporalio/act + +/chasm/lib/nexusoperation/ @temporalio/act @temporalio/nexus +/chasm/lib/callback/ @temporalio/act @temporalio/nexus + +# Service Clients + +/client/matching/ @temporalio/oss-matching + +# Common + +/common/taskqueue/ @temporalio/oss-matching +/common/worker_versioning/ @temporalio/oss-matching +/common/tqid/ @temporalio/oss-matching + +/common/archiver/ @temporalio/oss-foundations +/common/searchattribute/ @temporalio/oss-foundations + +/common/nexus/ @temporalio/act @temporalio/nexus + +/common/tasks/ @temporalio/oss-foundations @temporalio/cgs +/common/persistence/ @temporalio/oss-foundations @temporalio/oss-matching @temporalio/cgs + +# Components + +/components/ @temporalio/act @temporalio/nexus + +# Proto Definitions + +/proto/internal/temporal/server/api/schedule/ @temporalio/act + +/proto/internal/temporal/server/api/deployment/ @temporalio/oss-matching +/proto/internal/temporal/server/api/matchingservice/ @temporalio/oss-matching +/proto/internal/temporal/server/api/taskqueue/ @temporalio/oss-matching + +/proto/internal/temporal/server/api/archiver/ @temporalio/oss-foundations + +/proto/internal/temporal/server/api/replication/ @temporalio/cgs + +# DB Schema +/schema/**/visibility/ @temporalio/oss-foundations + +# History Service + +/service/history/archival/ @temporalio/oss-foundations +/service/history/*chasm* @temporalio/oss-foundations +/service/history/deletemanager/ @temporalio/oss-foundations +/service/history/queues/ @temporalio/oss-foundations +/service/history/shard/ @temporalio/oss-foundations +/service/history/*queue* @temporalio/oss-foundations +/service/history/*statemachine* @temporalio/oss-foundations + +/service/history/ndc/ @temporalio/cgs +/service/history/replication/ @temporalio/cgs + +/service/history/tasks/ @temporalio/oss-foundations @temporalio/cgs +/service/history/*task* @temporalio/oss-foundations @temporalio/cgs + +# Matching Service + +/service/matching/ @temporalio/oss-matching + +# Worker Service + +/service/worker/batcher/ @temporalio/act +/service/worker/scheduler/ @temporalio/act + +/service/worker/workerdeployment/ @temporalio/oss-matching + +/service/worker/addsearchattributes/ @temporalio/oss-foundations +/service/worker/deletenamespace/ @temporalio/oss-foundations +/service/worker/dlq/ @temporalio/oss-foundations + +/service/worker/scanner/ @temporalio/oss-foundations @temporalio/oss-matching +/service/worker/scanner/executions/ @temporalio/oss-foundations +/service/worker/scanner/history/ @temporalio/oss-foundations +/service/worker/scanner/build_ids/ @temporalio/oss-matching +/service/worker/scanner/taskqueue/ @temporalio/oss-matching + +/service/worker/migration/ @temporalio/cgs +/service/worker/replicator/ @temporalio/cgs + +/service/worker/parentclosepolicy/ @temporalio/oss-foundations @temporalio/act + +/tools/ @temporalio/server diff --git a/.github/actions/build-docker-images/action.yml b/.github/actions/build-docker-images/action.yml index cf2afe13ea9..4798db2579a 100644 --- a/.github/actions/build-docker-images/action.yml +++ b/.github/actions/build-docker-images/action.yml @@ -23,13 +23,13 @@ inputs: required: false default: "false" cli-version: - description: "Temporal CLI version to download" + description: "Temporal CLI version to download (uses default from build helper if empty)" required: false - default: "1.5.1" + default: "" alpine-tag: description: "Alpine base image tag" required: false - default: "3.23.3" + default: "" dockerhub-username: description: "Docker Hub username" required: false @@ -37,6 +37,17 @@ inputs: description: "Docker Hub token" required: false +outputs: + branch-tag: + description: "Docker-safe branch tag (e.g., branch-main, branch-release-v1.30.0)" + value: ${{ steps.image-tags.outputs.tag }} + sha-tag: + description: "Docker SHA tag (e.g., sha-abc1234)" + value: ${{ steps.image-tags.outputs.sha }} + sha-full-tag: + description: "Docker full SHA tag (e.g., sha-abcdef123456...)" + value: ${{ steps.image-tags.outputs.sha-full }} + runs: using: composite steps: @@ -70,12 +81,19 @@ runs: run: | .github/actions/build-docker-images/scripts/docker-build-helper download-cli + - name: Extract CLI version from binary + id: extract-cli-version + shell: bash + working-directory: ${{ github.workspace }} + run: | + .github/actions/build-docker-images/scripts/docker-build-helper extract-binary-version temporal cli-version + - name: Extract server version from binary id: extract-version shell: bash working-directory: ${{ github.workspace }} run: | - .github/actions/build-docker-images/scripts/docker-build-helper extract-version + .github/actions/build-docker-images/scripts/docker-build-helper extract-binary-version temporal-server server-version - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -96,22 +114,30 @@ runs: working-directory: ${{ github.workspace }} env: IMAGE_REPO: temporaliotest - IMAGE_SHA_TAG: ${{ steps.image-tags.outputs.sha }} + IMAGE_SHA_SHORT_TAG: ${{ steps.image-tags.outputs.sha }} + IMAGE_SHA_FULL_TAG: ${{ steps.image-tags.outputs.sha-full }} IMAGE_BRANCH_TAG: ${{ steps.image-tags.outputs.tag }} - TEMPORAL_SHA: ${{ github.sha }} + TEMPORAL_SHA: ${{ steps.image-tags.outputs.git-sha }} TAG_LATEST: ${{ inputs.tag-latest }} ALPINE_TAG: ${{ inputs.alpine-tag }} + PLATFORM: ${{ inputs.platform }} + LOAD: ${{ inputs.load }} SERVER_VERSION: ${{ steps.extract-version.outputs.server-version }} + CLI_VERSION: ${{ steps.extract-cli-version.outputs.cli-version }} run: | - if [ -n "${{ inputs.platform }}" ]; then + if [ -z "${ALPINE_TAG}" ]; then + unset ALPINE_TAG + fi + + if [ -n "${PLATFORM}" ]; then docker buildx bake \ - --set "*.platform=${{ inputs.platform }}" \ - ${{ inputs.load == 'true' && '--load' || '' }} \ + --set "*.platform=${PLATFORM}" \ + $( [ "${LOAD}" = "true" ] && echo --load ) \ -f docker/docker-bake.hcl \ server admin-tools else docker buildx bake \ - ${{ inputs.load == 'true' && '--load' || '' }} \ + $( [ "${LOAD}" = "true" ] && echo --load ) \ -f docker/docker-bake.hcl \ server admin-tools fi @@ -122,13 +148,19 @@ runs: working-directory: ${{ github.workspace }} env: IMAGE_REPO: temporaliotest - IMAGE_SHA_TAG: ${{ steps.image-tags.outputs.sha }} + IMAGE_SHA_SHORT_TAG: ${{ steps.image-tags.outputs.sha }} + IMAGE_SHA_FULL_TAG: ${{ steps.image-tags.outputs.sha-full }} IMAGE_BRANCH_TAG: ${{ steps.image-tags.outputs.tag }} - TEMPORAL_SHA: ${{ github.sha }} + TEMPORAL_SHA: ${{ steps.image-tags.outputs.git-sha }} TAG_LATEST: ${{ inputs.tag-latest }} ALPINE_TAG: ${{ inputs.alpine-tag }} SERVER_VERSION: ${{ steps.extract-version.outputs.server-version }} + CLI_VERSION: ${{ steps.extract-cli-version.outputs.cli-version }} run: | + if [ -z "${ALPINE_TAG}" ]; then + unset ALPINE_TAG + fi + docker buildx bake \ --push \ -f docker/docker-bake.hcl \ diff --git a/.github/actions/build-docker-images/scripts/main.go b/.github/actions/build-docker-images/scripts/main.go index b3f04ad37aa..b609d8c6179 100644 --- a/.github/actions/build-docker-images/scripts/main.go +++ b/.github/actions/build-docker-images/scripts/main.go @@ -14,7 +14,7 @@ import ( var validArchs = []string{"amd64", "arm64"} // defaultCliVersion should be updated to the latest cli version -const defaultCliVersion = "1.5.1" +const defaultCliVersion = "1.6.1" func main() { if len(os.Args) < 2 { @@ -23,7 +23,7 @@ func main() { fmt.Fprintf(os.Stderr, " set-image-tags - Generate Docker image tags from branch and SHA\n") fmt.Fprintf(os.Stderr, " organize-binaries - Organize binaries for Docker\n") fmt.Fprintf(os.Stderr, " download-cli - Download Temporal CLI\n") - fmt.Fprintf(os.Stderr, " extract-version - Extract version from temporal-server binary\n") + fmt.Fprintf(os.Stderr, " extract-binary-version - Extract version from a binary\n") os.Exit(1) } @@ -45,8 +45,12 @@ func main() { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } - case "extract-version": - if err := extractVersion(); err != nil { + case "extract-binary-version": + if len(os.Args) != 4 { + fmt.Fprintf(os.Stderr, "Usage: %s extract-binary-version \n", os.Args[0]) + os.Exit(1) + } + if err := extractBinaryVersion(os.Args[2], os.Args[3]); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } @@ -56,18 +60,37 @@ func main() { } } -// setImageTags generates Docker image tags from branch name and commit SHA -func setImageTags() error { - // Get GITHUB_REF from environment - ref := os.Getenv("GITHUB_REF") - if ref == "" { - return fmt.Errorf("GITHUB_REF environment variable not set") +// resolveGitInfo resolves the current git ref and SHA from the working tree. +func resolveGitInfo() (ref string, sha string, err error) { + shaCmd := exec.Command("git", "rev-parse", "HEAD") + shaOut, err := shaCmd.Output() + if err != nil { + return "", "", fmt.Errorf("failed to resolve git SHA: %w", err) } + sha = strings.TrimSpace(string(shaOut)) - // Get GITHUB_SHA from environment - sha := os.Getenv("GITHUB_SHA") - if sha == "" { - return fmt.Errorf("GITHUB_SHA environment variable not set") + // Use the symbolic ref (branch name) if available, otherwise fall back + // to a tag name or the raw SHA. + refCmd := exec.Command("git", "symbolic-ref", "HEAD") + if refOut, err := refCmd.Output(); err == nil { + ref = strings.TrimSpace(string(refOut)) + } else { + tagCmd := exec.Command("git", "describe", "--tags", "--exact-match", "HEAD") + if tagOut, tagErr := tagCmd.Output(); tagErr == nil { + ref = strings.TrimSpace(string(tagOut)) + } else { + ref = sha + } + } + + return ref, sha, nil +} + +// setImageTags generates Docker image tags from branch name and commit SHA +func setImageTags() error { + ref, sha, err := resolveGitInfo() + if err != nil { + return err } // Remove refs/heads/ or refs/tags/ prefix @@ -101,16 +124,19 @@ func setImageTags() error { return fmt.Errorf("failed to generate valid Docker tag from branch name") } - // Generate short SHA tag (first 7 characters with "sha-" prefix) + // Generate SHA tags. Keep the short tag for compatibility and add the full + // SHA tag to avoid collisions for automation. shortSha := sha if len(shortSha) > 7 { shortSha = shortSha[:7] } shaTag := fmt.Sprintf("sha-%s", shortSha) + fullShaTag := fmt.Sprintf("sha-%s", sha) fmt.Printf("Original: %s\n", ref) fmt.Printf("Sanitized: %s\n", safeTag) - fmt.Printf("SHA tag: %s\n", shaTag) + fmt.Printf("Short SHA tag: %s\n", shaTag) + fmt.Printf("Full SHA tag: %s\n", fullShaTag) // Set outputs for GitHub Actions if err := setOutput("tag", safeTag); err != nil { @@ -119,6 +145,12 @@ func setImageTags() error { if err := setOutput("sha", shaTag); err != nil { return fmt.Errorf("failed to set sha output: %w", err) } + if err := setOutput("sha-full", fullShaTag); err != nil { + return fmt.Errorf("failed to set sha-full output: %w", err) + } + if err := setOutput("git-sha", sha); err != nil { + return fmt.Errorf("failed to set git-sha output: %w", err) + } return nil } @@ -403,50 +435,57 @@ func downloadCLIForArch(arch string) error { return nil } -// extractVersion extracts the version from the temporal-server binary -func extractVersion() error { - // Try to find the temporal-server binary in any available architecture directory - var binaryPath string +// findBuildBinary finds a binary by name in the docker/build/{arch}/ directories. +func findBuildBinary(name string) (string, error) { for _, arch := range validArchs { - candidatePath := filepath.Join("docker", "build", arch, "temporal-server") + candidatePath := filepath.Join("docker", "build", arch, name) if _, err := os.Stat(candidatePath); err == nil { - binaryPath = candidatePath - break + return candidatePath, nil } } + return "", fmt.Errorf("%s binary not found in docker/build/{amd64,arm64}/", name) +} - if binaryPath == "" { - return fmt.Errorf("temporal-server binary not found in docker/build/{amd64,arm64}/") +// extractBinaryVersion finds a binary, runs --version, parses the output, and sets a GitHub Actions output. +func extractBinaryVersion(binaryName, outputName string) error { + binaryPath, err := findBuildBinary(binaryName) + if err != nil { + return err } fmt.Printf("Extracting version from %s\n", binaryPath) - // Run the binary with --version flag cmd := exec.Command(binaryPath, "--version") output, err := cmd.Output() if err != nil { return fmt.Errorf("failed to run %s --version: %w", binaryPath, err) } - // Parse the version from output like "temporal version 1.29.0" - outputStr := strings.TrimSpace(string(output)) - versionRegex := regexp.MustCompile(`^temporal version (\d+\.\d+\.\d+)`) - matches := versionRegex.FindStringSubmatch(outputStr) - if len(matches) < 2 { - return fmt.Errorf("failed to parse version from output: %s", outputStr) + version, err := parseTemporalVersion(string(output)) + if err != nil { + return err } - - version := matches[1] fmt.Printf("Extracted version: %s\n", version) - // Set output for GitHub Actions - if err := setOutput("server-version", version); err != nil { + if err := setOutput(outputName, version); err != nil { return fmt.Errorf("failed to set output: %w", err) } return nil } +// parseTemporalVersion extracts the version from output like +// "temporal version 1.29.0" or "temporal version 0.0.0-DEV (Server 1.30.1, UI 2.45.3)" +func parseTemporalVersion(output string) (string, error) { + s := strings.TrimSpace(output) + re := regexp.MustCompile(`^temporal version\s+(\d+\.\d+\.\d+\S*)`) + matches := re.FindStringSubmatch(s) + if len(matches) < 2 { + return "", fmt.Errorf("failed to parse version from output: %s", s) + } + return matches[1], nil +} + // Helper functions func setOutput(name, value string) error { diff --git a/.github/actions/build-docker-images/scripts/main_test.go b/.github/actions/build-docker-images/scripts/main_test.go new file mode 100644 index 00000000000..7e431e7759c --- /dev/null +++ b/.github/actions/build-docker-images/scripts/main_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "testing" +) + +func TestParseTemporalVersion(t *testing.T) { + tests := []struct { + name string + output string + want string + wantErr bool + }{ + { + name: "server version", + output: "temporal version 1.29.0", + want: "1.29.0", + }, + { + name: "cli release version", + output: "temporal version 1.6.0 (Server 1.30.0, UI 2.45.0)", + want: "1.6.0", + }, + { + name: "cli dev version", + output: "temporal version 0.0.0-DEV (Server 1.30.1, UI 2.45.3)", + want: "0.0.0-DEV", + }, + { + name: "with trailing newline", + output: "temporal version 1.6.0 (Server 1.30.0, UI 2.45.0)\n", + want: "1.6.0", + }, + { + name: "with leading and trailing whitespace", + output: " \n temporal version 1.6.0 (Server 1.30.0, UI 2.45.0) \n ", + want: "1.6.0", + }, + { + name: "pre-release version", + output: "temporal version 1.31.0-151.5", + want: "1.31.0-151.5", + }, + { + name: "extra spaces between version and number", + output: "temporal version 1.6.1", + want: "1.6.1", + }, + { + name: "empty output", + output: "", + wantErr: true, + }, + { + name: "unexpected format", + output: "something else entirely", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseTemporalVersion(tt.output) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got version %q", got) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + } + }) + } +} diff --git a/.github/actions/trivy-scan/action.yml b/.github/actions/trivy-scan/action.yml deleted file mode 100644 index 905379d190d..00000000000 --- a/.github/actions/trivy-scan/action.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: "Trivy Security Scan" -description: "Run Trivy vulnerability scanner on Docker images" - -inputs: - image-tags: - description: "Image tags (one per line or comma-separated). First tag will be used for scanning." - required: true - image-name: - description: "Image name without tag (e.g., temporaliotest/server)" - required: true - -outputs: - scan-result: - description: "Result of the scan (pass or fail)" - value: ${{ steps.evaluate.outputs.result }} - critical-count: - description: "Number of critical vulnerabilities found" - value: ${{ steps.evaluate.outputs.critical-count }} - high-count: - description: "Number of high vulnerabilities found" - value: ${{ steps.evaluate.outputs.high-count }} - -runs: - using: "composite" - steps: - - name: Set variables - id: vars - shell: bash - env: - IMAGE_NAME: ${{ inputs.image-name }} - IMAGE_TAGS: ${{ inputs.image-tags }} - run: | - # Get first tag - tag=$(echo "$IMAGE_TAGS" | head -1) - - # Construct image reference - image_ref="${IMAGE_NAME}:${tag}" - echo "image-ref=${image_ref}" >> $GITHUB_OUTPUT - - # Create safe name for artifacts - name=$(echo "$IMAGE_NAME" | sed 's/.*\///; s/[^a-zA-Z0-9._-]/-/g') - echo "safe-name=${name}" >> $GITHUB_OUTPUT - echo "artifact-name=trivy-${name}-results" >> $GITHUB_OUTPUT - - - name: Install Trivy - uses: aquasecurity/setup-trivy@v0.2.4 - with: - version: v0.65.0 - - - name: Scan Container Image - id: scan - shell: bash - env: - IMAGE_REF: ${{ steps.vars.outputs.image-ref }} - SAFE_NAME: ${{ steps.vars.outputs.safe-name }} - run: | - echo "Scanning $IMAGE_REF for CRITICAL,HIGH vulnerabilities..." - - # Scan and output table format for logs - trivy image \ - --severity CRITICAL,HIGH \ - --format table \ - --no-progress \ - "$IMAGE_REF" | tee trivy-scan-${SAFE_NAME}.txt - - echo "" - echo "Generating detailed JSON report..." - - # Scan and output JSON format for parsing - trivy image \ - --severity CRITICAL,HIGH \ - --format json \ - --no-progress \ - "$IMAGE_REF" > trivy-scan-${SAFE_NAME}.json - - - name: Evaluate scan results - id: evaluate - shell: bash - env: - SAFE_NAME: ${{ steps.vars.outputs.safe-name }} - FAIL_ON_VULNS: ${{ inputs.fail-on-vulnerabilities }} - run: | - # Count vulnerabilities by severity - CRITICAL_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-scan-${SAFE_NAME}.json) - HIGH_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-scan-${SAFE_NAME}.json) - MEDIUM_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="MEDIUM")] | length' trivy-scan-${SAFE_NAME}.json) - LOW_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="LOW")] | length' trivy-scan-${SAFE_NAME}.json) - - echo "critical-count=${CRITICAL_COUNT}" >> $GITHUB_OUTPUT - echo "high-count=${HIGH_COUNT}" >> $GITHUB_OUTPUT - - echo "" - echo "=== Vulnerability Summary ===" - echo "Critical: $CRITICAL_COUNT" - echo "High: $HIGH_COUNT" - echo "Medium: $MEDIUM_COUNT" - echo "Low: $LOW_COUNT" - echo "============================" - echo "" - - # Set result status without failing - if [ "$CRITICAL_COUNT" -gt 0 ] || [ "$HIGH_COUNT" -gt 0 ]; then - echo "result=fail" >> $GITHUB_OUTPUT - echo "⚠️ Security vulnerabilities found!" - else - echo "result=pass" >> $GITHUB_OUTPUT - echo "✓ No critical or high vulnerabilities found." - fi diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9013ec8f0ce..8bbb22cbc4c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -19,7 +19,7 @@ Apply these patterns when reviewing PRs or suggesting code changes. - Avoid stuttering: don't use `ActivityStatus` in package `activity`, just `Status` - Use `ok` boolean pattern instead of nil checks where idiomatic -## 3. Testify Suite Correctness +## 3. Testify Suite Correctness and Reliability - Never use `s.T()` in subtests - use the subtest's `t` parameter - Never use suite assertion methods (`s.NoError`, `s.Equal`) from goroutines - causes panics @@ -27,6 +27,15 @@ Apply these patterns when reviewing PRs or suggesting code changes. - Use `require.ErrorAs(t, err, &specificErr)` for specific error type checks - Prefer `require` over `assert` - it's rarely useful to continue a test after a failed assertion - Add comments explaining why `Eventually` is needed (e.g., eventual consistency) +- Do not use single-value type assertions on errors (`err.(*T)`); this panics instead of failing the test when the type doesn't match. Use `errors.As` with a guarded return. +- When launching a goroutine to maintain a precondition for later assertions (e.g., keeping pollers active so a deployment version gets registered), loop until context cancellation rather than running once. A single attempt that times out exits silently, leaving downstream Eventually/propagation waits to hang until their own deadline. +- Never call testify assertions (`s.NoError`, `s.Equal`, `require.NoError`, even `assert.NoError`) inside a `go func()` — if the goroutine outlives the test, the assertion panics the binary with `panic: Fail in goroutine after TestXxx has completed`. Move assertions to the test goroutine or use a buffered error channel. +- Any `<-ch` that isn't inside a `select` with `ctx.Done()` will hang indefinitely if the sender never sends. Always provide a context cancellation fallback. +- Never write to package-level or global variables in tests — parallel tests share the same process; thread values through function parameters instead. +- Never use `time.Sleep` or `time.Since(start) > threshold` to enforce ordering — use channels, `sync.WaitGroup`, or `EventuallyWithT` instead. +- When using `EventuallyWithT` (or similar) to wait for a condition driven by a background goroutine, ensure the goroutine's timeout is longer than the `EventuallyWithT` deadline — if the background op times out first, the condition will never be satisfied and the wait will hang until its own deadline. +- Do not silently discard errors from precondition operations with `_, _ = f()` — if `f()` failing invalidates the rest of the test, surface the error or loop until it succeeds. +- Be suspicious of `go s.someHelper(ctx, ...)` calls where the goroutine runs exactly once and the test then immediately waits for something that helper was supposed to cause. If the operation can fail transiently (network, tight deadline, busy CI), the single attempt may fail silently and the wait will never succeed. Either loop the goroutine until `ctx.Done()`, or check that the operation succeeded before proceeding. ## 4. Inline Code / Avoid Abstractions @@ -44,6 +53,8 @@ Apply these patterns when reviewing PRs or suggesting code changes. - Wrap errors with context when there's something interesting or informative to add, e.g. `fmt.Errorf("multi-operation part 2: %w", err)` - Don't panic in library code - return errors and let caller decide - Validate early in handlers, not deep in business logic +- Use `errors.AsType` instead of `errors.As` +- Use `require.ErrorContains` instead of two separate assertions (`require.Error` + `require.Contains`) ## 6. Consistency with Codebase @@ -68,3 +79,4 @@ Apply these patterns when reviewing PRs or suggesting code changes. - Prefer `sync.Mutex` over `sync.RWMutex` almost always, except when reads are much more common than writes (>1000×) or readers hold the lock for significant time - Don't do IO while holding locks - use side effect tasks - Clone data before releasing locks if it might be modified +- Proto message fields accessed outside the workflow lock must be cloned, not aliased: use `common.CloneProto(...)` rather than returning the pointer directly. diff --git a/.github/workflows/ci-success-report.yml b/.github/workflows/ci-success-report.yml index d6d4db394ff..1538c4eadb7 100644 --- a/.github/workflows/ci-success-report.yml +++ b/.github/workflows/ci-success-report.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} @@ -45,7 +45,7 @@ jobs: token: ${{ steps.generate_token.outputs.token }} - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: 'go.mod' diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml deleted file mode 100644 index d792e9362ba..00000000000 --- a/.github/workflows/create-tag.yml +++ /dev/null @@ -1,216 +0,0 @@ -name: "Create a tag" - -on: - workflow_dispatch: - inputs: - branch: - description: "Branch to be tagged" - required: true - tag: - description: "Tag for new version (1.23.4)" - required: true - skip_deps_check: - type: boolean - description: "Skip dependencies version check (you can skip only when creating non-release tags)" - default: false - release_notes: - type: boolean - description: "Create draft release notes" - default: false - base_tag: - description: "Base tag to generate commit list for release notes" - required: false - update_deps: - type: boolean - description: "Create PR updating dependencies post-release" - default: false -permissions: - contents: read - pull-requests: write - -jobs: - create-tag: - name: "Create a tag" - runs-on: ubuntu-latest - - defaults: - run: - shell: bash - - steps: - - name: Generate token - id: generate_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} - private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: true - token: ${{ steps.generate_token.outputs.token }} - ref: ${{ github.event.inputs.branch }} - fetch-depth: 0 - fetch-tags: true - - - name: Set up Github credentials - run: | - git config --local user.name 'Temporal Data' - git config --local user.email 'commander-data@temporal.io' - - - name: Get current version - id: get_current_version - run: | - CURRENT_VERSION=$(grep '^\s*ServerVersion = ".*"$' common/headers/version_checker.go | sed 's/^.*"\(.*\)"$/\1/') - [ -z "$CURRENT_VERSION" ] && exit 1 - echo "CURRENT_VERSION=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" - - - name: Prepare new version string - id: new_version - env: - TAG: '${{ github.event.inputs.tag }}' - run: | - if [[ "${TAG}" =~ ^v.* ]]; then - echo "tag_with_v=${TAG}" >> "$GITHUB_OUTPUT" - echo "tag_no_v=${TAG#v}" >> "$GITHUB_OUTPUT" - else - echo "tag_with_v=v${TAG}" >> "$GITHUB_OUTPUT" - echo "tag_no_v=${TAG}" >> "$GITHUB_OUTPUT" - fi - - - name: Validate API and SDK dependencies - env: - SKIP_CHECK: ${{ github.event.inputs.skip_deps_check }} - TAG: ${{ steps.new_version.outputs.tag_with_v }} - run: | - SEMVER_RE='^v[0-9]+\.[0-9]+\.[0-9]+$' - - if [[ "$SKIP_CHECK" == "true" ]]; then - if [[ ! "$TAG" =~ $SEMVER_RE ]]; then - echo "::notice::Skipping dependencies check" - exit 0 - fi - echo "::warning::Cannot skip dependencies check when creating a potential release tag $TAG" - fi - - MODULES=( "go.temporal.io/api" "go.temporal.io/sdk" ) - for module in "${MODULES[@]}"; do - version=$(go list -f '{{.Version}}' -m "$module") - if [[ ! "$version" =~ $SEMVER_RE ]]; then - echo "::error::Using non-tagged version of module $module with version $version" - exit 1 - fi - replace=$(go list -f '{{.Replace}}' -m "$module") - if [[ "$replace" != "" ]]; then - echo "::error::Module $module is replaced with $replace" - exit 1 - fi - done - - - name: Update Server version - if: ${{ steps.get_current_version.outputs.CURRENT_VERSION != github.event.inputs.tag }} - env: - TAG: ${{ steps.new_version.outputs.tag_no_v }} - BRANCH: ${{ github.event.inputs.branch }} - run: | - sed -i -e "s/ServerVersion = \".*\"$/ServerVersion = \"$TAG\"/g" common/headers/version_checker.go - git add . - git commit -m "Bump Server version to $TAG" - git push origin "$BRANCH" - - - name: Create and push tag - env: - TAG: ${{ steps.new_version.outputs.tag_with_v }} - BRANCH: ${{ github.event.inputs.branch }} - run: | - if [ -z "$(git tag -l "$TAG")" ]; then - git tag "$TAG" - git push origin "$TAG" - elif [ "$(git rev-list -n 1 "$TAG")" != "$(git rev-parse HEAD)" ]; then - echo "::error::Tag already exists and it doesn't reference current HEAD of branch $BRANCH" - exit 1 - fi - - - name: Create draft release notes - if: ${{ github.event.inputs.release_notes == 'true' }} - env: - GH_TOKEN: ${{ steps.generate_token.outputs.token }} - BASE_TAG: ${{ github.event.inputs.base_tag }} - TAG: ${{ steps.new_version.outputs.tag_with_v }} - run: | - if [ -z "$BASE_TAG" ] || [ -z "$(git tag -l "$BASE_TAG")" ]; then - echo "::error::Base tag not specified or does not exist" - exit 1 - fi - - TEMPFILE=$(mktemp) - cat > "$TEMPFILE" <<- EOF - ## Breaking Changes - Document them here, if any - - ## Deprecation Announcements - Document them here, if any. - - ## Release Highlights - Add highlights if any. - - ### Helpful links to get you started with Temporal - [Temporal Docs](https://docs.temporal.io/) - [Server](https://github.com/temporalio/temporal) - [Docker Compose](https://github.com/temporalio/docker-compose) - [Helm Chart](https://github.com/temporalio/helm-charts) - - ### Docker images for this release (use the tag \`${TAG#v}\`) - [Server](https://hub.docker.com/r/temporalio/server) - [Server With Auto Setup](https://hub.docker.com/r/temporalio/auto-setup) ([what is Auto-Setup?](https://docs.temporal.io/blog/auto-setup)) - [Admin-Tools](https://hub.docker.com/r/temporalio/admin-tools) - - **Full Changelog**: https://github.com/temporalio/temporal/compare/${BASE_TAG}...${TAG} - EOF - - gh repo set-default ${{ github.repository }} - gh release create "$TAG" --verify-tag --draft --title "$TAG" -F "$TEMPFILE" - - update-deps: - name: "Update dependencies in main branch" - runs-on: ubuntu-latest - needs: [create-tag] - if: ${{ github.event.inputs.update_deps == 'true' }} - - defaults: - run: - shell: bash - - steps: - - name: Generate token - id: generate_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} - private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: true - token: ${{ steps.generate_token.outputs.token }} - ref: main - - - name: Set up Github credentials - run: | - git config --local user.name 'Temporal Data' - git config --local user.email 'commander-data@temporal.io' - - - name: Create PR updating dependencies - run: | - make update-dependencies - make go-generate - BRANCH="temporal-data/update-dependencies-$(git rev-parse --short HEAD)" - git checkout -b "${BRANCH}" - git add . - git commit -m "Update dependencies" --author ${{ github.actor }} - git push origin "${BRANCH}" - gh pr create --fill --reviewer ${{ github.actor }},${{ github.triggering_actor }} diff --git a/.github/workflows/docker-build-manual.yml b/.github/workflows/docker-build-manual.yml index fc103eba73e..8d84da70503 100644 --- a/.github/workflows/docker-build-manual.yml +++ b/.github/workflows/docker-build-manual.yml @@ -1,22 +1,17 @@ name: Manual Docker Build +# Dispatch this workflow from the branch you want to build from. on: workflow_dispatch: inputs: - ref: - description: "Git ref (branch, tag, or SHA) to build from" - required: true - default: "main" cli-version: - description: "Temporal CLI version to include in images" - required: true - default: "1.5.1" + description: "Optional Temporal CLI version override (leave empty to use default)" + default: "" alpine-tag: - description: "Alpine base image tag (e.g., 3.23.3)" - required: true - default: "3.23.3" + description: "Optional Alpine base image tag override (leave empty to use default from docker-bake.hcl)" + default: "" push: - description: "Push images to Docker Hub" + description: "Push images to Docker Hub (temporaliotest/server and temporaliotest/admin-tools)" required: true type: boolean default: false @@ -25,15 +20,6 @@ on: required: true type: boolean default: false - platform: - description: "Platform to build (leave empty for multi-arch, or specify linux/amd64 or linux/arm64)" - required: false - default: "" - snapshot: - description: "Build in snapshot mode (for non-release builds)" - required: true - type: boolean - default: true permissions: contents: read @@ -45,61 +31,59 @@ jobs: - name: Checkout code uses: actions/checkout@v6 with: - ref: ${{ inputs.ref }} fetch-depth: 0 - - name: Determine single-arch parameter - id: arch-param - run: | - if [ -n "${{ inputs.platform }}" ]; then - # Extract arch from platform (e.g., linux/amd64 -> amd64) - ARCH=$(echo "${{ inputs.platform }}" | cut -d'/' -f2) - echo "single-arch=${ARCH}" >> "$GITHUB_OUTPUT" - else - echo "single-arch=" >> "$GITHUB_OUTPUT" - fi - - name: Build binaries uses: ./.github/actions/build-binaries with: - snapshot: ${{ inputs.snapshot }} - single-arch: ${{ steps.arch-param.outputs.single-arch }} + snapshot: true - name: Build Docker images + id: build-docker if: ${{ !inputs.push }} uses: ./.github/actions/build-docker-images with: push: false tag-latest: ${{ inputs.tag-latest }} - platform: ${{ inputs.platform }} - cli-version: ${{ inputs.cli-version }} alpine-tag: ${{ inputs.alpine-tag }} - load: ${{ inputs.platform == 'linux/amd64' || inputs.platform == '' }} + cli-version: ${{ inputs.cli-version }} - name: Build and push Docker images + id: push-docker if: ${{ inputs.push }} uses: ./.github/actions/build-docker-images with: push: true tag-latest: ${{ inputs.tag-latest }} - platform: ${{ inputs.platform }} - cli-version: ${{ inputs.cli-version }} alpine-tag: ${{ inputs.alpine-tag }} + cli-version: ${{ inputs.cli-version }} dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - name: Output image tags + env: + SHA_TAG: ${{ steps.build-docker.outputs.sha-tag || steps.push-docker.outputs.sha-tag }} + SHA_FULL_TAG: ${{ steps.build-docker.outputs.sha-full-tag || steps.push-docker.outputs.sha-full-tag }} + BRANCH_TAG: ${{ steps.build-docker.outputs.branch-tag || steps.push-docker.outputs.branch-tag }} + PUSHED: ${{ inputs.push }} + TAG_LATEST: ${{ inputs.tag-latest }} run: | { echo "### Docker Images Built" echo "" - echo "**Git Ref:** ${{ inputs.ref }}" - echo "**CLI Version:** ${{ inputs.cli-version }}" - echo "**Platform:** ${{ inputs.platform || 'linux/amd64,linux/arm64' }}" - echo "**Pushed to Docker Hub:** ${{ inputs.push }}" - echo "**Tagged as latest:** ${{ inputs.tag-latest }}" + echo "**Branch:** ${GITHUB_REF_NAME}" + echo "**Short SHA Tag:** ${SHA_TAG}" + echo "**Full SHA Tag:** ${SHA_FULL_TAG}" + echo "**Branch Tag:** ${BRANCH_TAG}" + echo "**Platform:** linux/amd64,linux/arm64" + echo "**Pushed to Docker Hub:** ${PUSHED}" + echo "**Tagged as latest:** ${TAG_LATEST}" echo "" echo "**Image Tags:**" - echo "- temporaliotest/server:sha-${GITHUB_SHA:0:7}" - echo "- temporaliotest/admin-tools:sha-${GITHUB_SHA:0:7}" + echo "- temporaliotest/server:${SHA_TAG}" + echo "- temporaliotest/admin-tools:${SHA_TAG}" + echo "- temporaliotest/server:${SHA_FULL_TAG}" + echo "- temporaliotest/admin-tools:${SHA_FULL_TAG}" + echo "- temporaliotest/server:${BRANCH_TAG}" + echo "- temporaliotest/admin-tools:${BRANCH_TAG}" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/features-integration.yml b/.github/workflows/features-integration.yml index a5287f51309..217d31c0d7f 100644 --- a/.github/workflows/features-integration.yml +++ b/.github/workflows/features-integration.yml @@ -32,6 +32,7 @@ jobs: single-arch: amd64 - name: Build Docker images + id: build-docker uses: ./.github/actions/build-docker-images with: push: false @@ -39,24 +40,13 @@ jobs: platform: linux/amd64 load: true - - name: Get Docker image tag - id: image-tag - uses: actions/github-script@v8 - with: - script: | - const ref = context.ref.replace('refs/heads/', '').replace('refs/tags/', ''); - const branchName = `branch-${ref}`; - let safeTag = branchName.replace(/[^a-zA-Z0-9._-]/g, '-'); - safeTag = safeTag.toLowerCase(); - safeTag = safeTag.replace(/^[^a-z0-9]+/, ''); - safeTag = safeTag.substring(0, 128); - core.setOutput('tag', safeTag); - - name: Save Docker image as artifact + env: + BRANCH_TAG: ${{ steps.build-docker.outputs.branch-tag }} run: | - docker save temporaliotest/server:${{ steps.image-tag.outputs.tag }} -o /tmp/temporal-server.tar - docker save temporaliotest/admin-tools:${{ steps.image-tag.outputs.tag }} -o /tmp/temporal-admin-tools.tar - echo ${{ steps.image-tag.outputs.tag }} > /tmp/image_tag + docker save "temporaliotest/server:${BRANCH_TAG}" -o /tmp/temporal-server.tar + docker save "temporaliotest/admin-tools:${BRANCH_TAG}" -o /tmp/temporal-admin-tools.tar + echo "${BRANCH_TAG}" > /tmp/image_tag - name: Prepare artifact working-directory: ${{ github.workspace }} @@ -66,7 +56,7 @@ jobs: cp ./develop/docker-compose/docker-compose.yml /tmp/docker-compose.yml - name: Upload Docker artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: temporal-server-docker path: | @@ -134,6 +124,14 @@ jobs: version-is-repo-ref: false docker-image-artifact-name: temporal-server-docker + feature-tests-ruby: + needs: build-docker-image + uses: temporalio/features/.github/workflows/ruby.yaml@main + with: + version: __latest_features_docker_image__ + version-is-repo-ref: false + docker-image-artifact-name: temporal-server-docker + feature-tests-status: name: Tests Status needs: @@ -143,6 +141,7 @@ jobs: - feature-tests-python - feature-tests-java - feature-tests-dotnet + - feature-tests-ruby runs-on: ubuntu-latest if: always() env: diff --git a/.github/workflows/flaky-tests-report.yml b/.github/workflows/flaky-tests-report.yml index ec29b567fe7..591fdc1b69d 100644 --- a/.github/workflows/flaky-tests-report.yml +++ b/.github/workflows/flaky-tests-report.yml @@ -3,19 +3,24 @@ name: Flaky Tests Report on: schedule: # Run on Wednesdays at noon Eastern time (5 PM UTC) - - cron: '0 17 * * 3' + - cron: "0 17 * * 3" workflow_dispatch: inputs: days: - description: 'Number of days to look back for flaky tests' + description: "Number of days to look back for flaky tests" required: false - default: '7' + default: "7" type: string max_links: - description: 'Maximum number of failure links to show per test' + description: "Maximum number of failure links to show per test" required: false - default: '3' + default: "3" type: string + notify_slack: + description: "Send Slack notification" + required: false + default: true + type: boolean permissions: contents: read @@ -27,7 +32,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} @@ -40,68 +45,37 @@ jobs: token: ${{ steps.generate_token.outputs.token }} fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install uv - uses: astral-sh/setup-uv@v4 + - name: Set up Go + uses: actions/setup-go@v6 with: - version: ">=0.8.0" - - - name: Install Python dependencies - run: | - cd tools/flakes - uv sync + go-version-file: "go.mod" - - name: Install tringa - run: | - uv tool install git+https://github.com/dandavison/tringa@main --force - - - name: Run tringa - id: generate-output-file + - name: Generate flaky test report + id: process-flaky-tests env: GH_TOKEN: ${{ steps.generate_token.outputs.token }} + SLACK_WEBHOOK: ${{ (github.event_name == 'schedule' || github.event.inputs.notify_slack == 'true') && secrets.SLACK_WEBHOOK || '' }} DAYS_PARAM: ${{ github.event.inputs.days || '7' }} - run: | - set -euo pipefail - - # Create output directory - mkdir -p tools/flakes/out - - tringa --json --since-days "$DAYS_PARAM" repo sql \ - 'select classname, name, artifact from test where passed = false and skipped = false order by classname, name, artifact desc' \ - --branch main \ - --workflow-id 80591745 \ - https://github.com/temporalio/temporal > tools/flakes/out/out.json - - echo "✅ Tringa command completed" - echo "📊 Output file size: $(wc -c < tools/flakes/out/out.json) bytes" - echo "📄 Full output file:" - cat tools/flakes/out/out.json - - - name: Run Python script to process flaky tests - id: process-flaky-tests - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + MAX_LINKS_PARAM: ${{ github.event.inputs.max_links || '3' }} RUN_ID: ${{ github.run_id }} REF_NAME: ${{ github.ref_name }} SHA: ${{ github.sha }} - MAX_LINKS_PARAM: ${{ github.event.inputs.max_links || '3' }} run: | - set -x - cd tools/flakes && uv run main.py \ - --file out/out.json \ - --github-summary \ + set -euo pipefail + + go run ./cmd/tools/flakereport generate \ + --days "$DAYS_PARAM" \ + --max-links "$MAX_LINKS_PARAM" \ + --output-dir tools/flakes/out \ --slack-webhook "$SLACK_WEBHOOK" \ --run-id "$RUN_ID" \ --ref-name "$REF_NAME" \ --sha "$SHA" \ - --max-links "$MAX_LINKS_PARAM" + --bisect \ + --bisect-days 28 - name: Upload generated reports - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: steps.process-flaky-tests.outcome == 'success' with: name: flaky-tests-reports-${{ github.run_number }} diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index de11c9e5771..a3110beb51a 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -86,7 +86,7 @@ jobs: - name: lint system workflows with workflowcheck run: make workflowcheck - fmt-imports: + fmt: runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v6 @@ -99,9 +99,9 @@ jobs: check-latest: true cache: true - - name: format golang import statements + - name: apply formatters run: | - make fmt-imports + make fmt - name: check-is-dirty run: | @@ -112,7 +112,7 @@ jobs: exit 1 fi - lint-yaml: + parallelize-tests: runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v6 @@ -125,8 +125,18 @@ jobs: check-latest: true cache: true - - name: check yaml formatting - run: make lint-yaml + - name: check test parallelization + run: make parallelize-tests + + - name: check-is-dirty + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "Detected uncommitted changes after running parallelize-tests." + echo "Run 'make parallelize-tests' locally and commit the changes." + git status + git diff + exit 1 + fi golangci: runs-on: ubuntu-24.04-arm @@ -157,12 +167,12 @@ jobs: linters-succeed: name: All Linters Succeed needs: + - fmt - lint-api - lint-protos - lint-actions - - fmt-imports - - lint-yaml - golangci + - parallelize-tests runs-on: ubuntu-24.04-arm if: always() env: diff --git a/.github/workflows/optimize-test-sharding.yml b/.github/workflows/optimize-test-sharding.yml index 0ef19e1621a..9220235d741 100644 --- a/.github/workflows/optimize-test-sharding.yml +++ b/.github/workflows/optimize-test-sharding.yml @@ -6,6 +6,9 @@ on: - cron: "0 7 * * *" workflow_dispatch: # Allow manual trigger +permissions: + contents: read + env: SALT_FILE: tests/testcore/shard_salt.txt BRANCH: auto/optimize-test-sharding @@ -58,6 +61,9 @@ jobs: # This will also close a previous, stuck PR if it exists. git push origin --delete ${{ env.BRANCH }} 2>/dev/null || true + git config --local user.name 'Temporal Data' + git config --local user.email 'commander-data@temporal.io' + git checkout -b ${{ env.BRANCH }} git add "${{ env.SALT_FILE }}" git commit -m "Update test shard salt" diff --git a/.github/workflows/promote-admin-tools-image.yml b/.github/workflows/promote-admin-tools-image.yml index d6521c7d68f..cbe98019e9f 100644 --- a/.github/workflows/promote-admin-tools-image.yml +++ b/.github/workflows/promote-admin-tools-image.yml @@ -9,11 +9,6 @@ on: target-tags: description: "Target tags for temporalio registry (comma or newline separated, e.g., 1.29.1, latest)" required: true - override-security-scan: - description: "Override security scan failures (use with caution)" - type: boolean - default: false - required: false permissions: contents: read @@ -25,7 +20,6 @@ jobs: image-name: admin-tools source-tag: ${{ inputs.source-tag }} target-tags: ${{ inputs.target-tags }} - override-security-scan: ${{ inputs.override-security-scan }} secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/promote-docker-image.yml b/.github/workflows/promote-docker-image.yml index 13d6c0ff071..e545aa75023 100644 --- a/.github/workflows/promote-docker-image.yml +++ b/.github/workflows/promote-docker-image.yml @@ -15,10 +15,6 @@ on: description: "Target tags for temporalio registry (comma or newline separated, e.g., 1.29.1, latest)" required: true type: string - override-security-scan: - description: "Override security scan failures (use with caution)" - type: boolean - default: false secrets: DOCKERHUB_USERNAME: required: true @@ -82,93 +78,8 @@ jobs: core.info(` Source: ${sourceTag}`); core.info(` Target tags: ${targetTags.join(', ')}`); - scan-image: - needs: validate-inputs - runs-on: ubuntu-latest - outputs: - scan-result: ${{ steps.scan.outputs.scan-result }} - critical-count: ${{ steps.scan.outputs.critical-count }} - high-count: ${{ steps.scan.outputs.high-count }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Pull source image - env: - SOURCE_TAG: ${{ needs.validate-inputs.outputs.source-tag-safe }} - IMAGE_NAME: ${{ inputs.image-name }} - run: | - echo "Pulling temporaliotest/${IMAGE_NAME}:${SOURCE_TAG}" - docker pull "temporaliotest/${IMAGE_NAME}:${SOURCE_TAG}" - - - name: Scan image with Trivy - id: scan - uses: ./.github/actions/trivy-scan - with: - image-name: temporaliotest/${{ inputs.image-name }} - image-tags: ${{ needs.validate-inputs.outputs.source-tag-safe }} - - - name: Upload Trivy scan results - if: always() - uses: actions/upload-artifact@v4 - with: - name: trivy-${{ inputs.image-name }}-scan-results - path: trivy-scan-${{ inputs.image-name }}.json - retention-days: 30 - - check-security-gate: - needs: [scan-image] - runs-on: ubuntu-latest - outputs: - can-promote: ${{ steps.check.outputs.can-promote }} - steps: - - name: Evaluate security scan results - id: check - uses: actions/github-script@v8 - env: - OVERRIDE: ${{ inputs.override-security-scan }} - SCAN_RESULT: ${{ needs.scan-image.outputs.scan-result }} - CRITICAL_COUNT: ${{ needs.scan-image.outputs.critical-count }} - HIGH_COUNT: ${{ needs.scan-image.outputs.high-count }} - IMAGE_NAME: ${{ inputs.image-name }} - with: - script: | - const override = process.env.OVERRIDE === 'true'; - const scanResult = process.env.SCAN_RESULT; - const criticalCount = process.env.CRITICAL_COUNT; - const highCount = process.env.HIGH_COUNT; - const imageName = process.env.IMAGE_NAME; - - core.info('=== Security Scan Results ==='); - core.info(`${imageName} image: ${scanResult}`); - core.info(` Critical: ${criticalCount}`); - core.info(` High: ${highCount}`); - core.info(''); - core.info(`Override enabled: ${override}`); - core.info('=============================='); - - if (scanResult === 'fail') { - if (override) { - core.info(''); - core.warning('Security vulnerabilities detected but OVERRIDE is enabled!'); - core.info('Proceeding with image promotion despite security issues.'); - core.setOutput('can-promote', 'true'); - } else { - core.info(''); - core.error('Security vulnerabilities detected. Promotion BLOCKED.'); - core.info("To proceed anyway, re-run with 'override-security-scan' enabled."); - core.setOutput('can-promote', 'false'); - core.setFailed('Security vulnerabilities detected'); - } - } else { - core.info(''); - core.info('✓ Security scan passed. Proceeding with promotion.'); - core.setOutput('can-promote', 'true'); - } - promote: - needs: [validate-inputs, check-security-gate] - if: needs.check-security-gate.outputs.can-promote == 'true' + needs: [validate-inputs] runs-on: ubuntu-latest steps: - name: Log in to Docker Hub @@ -177,7 +88,12 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Pull, tag, and push image + - name: Install crane + run: | + curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz crane + sudo mv crane /usr/local/bin/ + + - name: Promote image uses: actions/github-script@v8 env: SOURCE_TAG: ${{ needs.validate-inputs.outputs.source-tag-safe }} @@ -189,23 +105,16 @@ jobs: const sourceTag = process.env.SOURCE_TAG; const targetTags = JSON.parse(process.env.TARGET_TAGS); const imageName = process.env.IMAGE_NAME; + const source = `temporaliotest/${imageName}:${sourceTag}`; core.info(`Promoting ${imageName} image...`); - core.info(` From: temporaliotest/${imageName}:${sourceTag}`); + core.info(` From: ${source}`); core.info(` To: ${targetTags.map(t => `temporalio/${imageName}:${t}`).join(', ')}`); - // Pull from test registry - core.info('Pulling source image...'); - execSync(`docker pull temporaliotest/${imageName}:${sourceTag}`, { stdio: 'inherit' }); - - // Tag for each target tag - for (const targetTag of targetTags) { - core.info(`Tagging as temporalio/${imageName}:${targetTag}`); - execSync(`docker tag temporaliotest/${imageName}:${sourceTag} temporalio/${imageName}:${targetTag}`, { stdio: 'inherit' }); + for (const tag of targetTags) { + const target = `temporalio/${imageName}:${tag}`; + core.info(`Copying ${source} -> ${target}`); + execSync(`crane copy ${source} ${target}`, { stdio: 'inherit' }); } - // Push all tags at once - core.info('Pushing all tags to production registry...'); - execSync(`docker push --all-tags temporalio/${imageName}`, { stdio: 'inherit' }); - - core.info(`✓ ${imageName} image promoted successfully to all tags`); + core.info(`${imageName} image promoted successfully to all tags`); diff --git a/.github/workflows/promote-server-image.yml b/.github/workflows/promote-server-image.yml index a0dfbb6b391..5ea7c36124d 100644 --- a/.github/workflows/promote-server-image.yml +++ b/.github/workflows/promote-server-image.yml @@ -9,11 +9,6 @@ on: target-tags: description: "Target tags for temporalio registry (comma or newline separated, e.g., 1.29.1, latest)" required: true - override-security-scan: - description: "Override security scan failures (use with caution)" - type: boolean - default: false - required: false permissions: contents: read @@ -25,7 +20,6 @@ jobs: image-name: server source-tag: ${{ inputs.source-tag }} target-tags: ${{ inputs.target-tags }} - override-security-scan: ${{ inputs.override-security-scan }} secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index de329e088b5..6422c701f5b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -228,7 +228,7 @@ jobs: - name: Restore dependencies id: restore-deps - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/go/pkg/mod key: go-${{ runner.os }}${{ runner.arch }}-${{ hashFiles('go.mod') }}-deps-${{ hashFiles('go.sum') }} @@ -236,14 +236,14 @@ jobs: - run: make pre-build-functional-test-coverage - name: Save dependencies - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: ${{ steps.restore-deps.outputs.cache-hit != 'true' }} with: path: ~/go/pkg/mod key: ${{ steps.restore-deps.outputs.cache-primary-key }} - name: Save build outputs - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: ~/.cache/go-build key: go-${{ runner.os }}${{ runner.arch }}-build-${{ env.COMMIT }} @@ -266,13 +266,13 @@ jobs: cache: false # do our own caching - name: Restore dependencies - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/go/pkg/mod key: go-${{ runner.os }}${{ runner.arch }}-${{ hashFiles('go.mod') }}-deps-${{ hashFiles('go.sum') }} - name: Restore build outputs - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/.cache/go-build key: go-${{ runner.os }}${{ runner.arch }}-build-${{ env.COMMIT }} @@ -303,13 +303,13 @@ jobs: cache: false # do our own caching - name: Restore dependencies - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/go/pkg/mod key: go-${{ runner.os }}${{ runner.arch }}-${{ hashFiles('go.mod') }}-deps-${{ hashFiles('go.sum') }} - name: Restore build outputs - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/.cache/go-build key: go-${{ runner.os }}${{ runner.arch }}-build-${{ env.COMMIT }} @@ -320,7 +320,7 @@ jobs: - name: Print memory snapshot if: always() - run: if [ -f /tmp/memory_snapshot.txt ]; then cat /tmp/memory_snapshot.txt; fi + run: cat /tmp/memory_snapshot.txt || true - name: Generate crash report if: failure() # if the tests failed, we would expect one JUnit XML report per attempt; otherwise it must have crashed @@ -329,8 +329,8 @@ jobs: CRASH_REPORT_NAME="$GITHUB_JOB" make report-test-crash - name: Generate test summary - uses: mikepenz/action-junit-report@v5.0.0-rc01 - if: failure() + uses: mikepenz/action-junit-report@v6 + if: ${{ !cancelled() }} with: report_paths: ./.testoutput/junit.*.xml detailed_summary: true @@ -354,12 +354,19 @@ jobs: flags: unit-test report_type: test_results + - name: Get job ID + id: get_job_id + uses: ./.github/actions/get-job-id + with: + job_name: Unit test + run_id: ${{ github.run_id }} + - name: Upload test results to GitHub # Can't pin to major because the action linter doesn't recognize the include-hidden-files flag. - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v6 if: ${{ !cancelled() }} with: - name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--unit-test + name: junit-xml--${{ github.run_id }}--${{ steps.get_job_id.outputs.job_id }}--${{ github.run_attempt }}--unit-test path: ./.testoutput/junit.*.xml include-hidden-files: true retention-days: 28 @@ -390,13 +397,13 @@ jobs: cache: false # do our own caching - name: Restore dependencies - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/go/pkg/mod key: go-${{ runner.os }}${{ runner.arch }}-${{ hashFiles('go.mod') }}-deps-${{ hashFiles('go.sum') }} - name: Restore build outputs - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/.cache/go-build key: go-${{ runner.os }}${{ runner.arch }}-build-${{ env.COMMIT }} @@ -413,7 +420,7 @@ jobs: - name: Print memory snapshot if: always() - run: if [ -f /tmp/memory_snapshot.txt ]; then cat /tmp/memory_snapshot.txt; fi + run: cat /tmp/memory_snapshot.txt || true - name: Generate crash report if: failure() # if the tests failed, we would expect one JUnit XML report per attempt; otherwise it must have crashed @@ -422,8 +429,8 @@ jobs: CRASH_REPORT_NAME="$GITHUB_JOB" make report-test-crash - name: Generate test summary - uses: mikepenz/action-junit-report@v5.0.0-rc01 - if: failure() + uses: mikepenz/action-junit-report@v6 + if: ${{ !cancelled() }} with: report_paths: ./.testoutput/junit.*.xml detailed_summary: true @@ -447,12 +454,19 @@ jobs: flags: integration-test report_type: test_results + - name: Get job ID + id: get_job_id + uses: ./.github/actions/get-job-id + with: + job_name: Integration test + run_id: ${{ github.run_id }} + - name: Upload test results to GitHub # Can't pin to major because the action linter doesn't recognize the include-hidden-files flag. - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v6 if: ${{ !cancelled() }} with: - name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--integration-test + name: junit-xml--${{ github.run_id }}--${{ steps.get_job_id.outputs.job_id }}--${{ github.run_attempt }}--integration-test path: ./.testoutput/junit.*.xml include-hidden-files: true retention-days: 28 @@ -501,13 +515,13 @@ jobs: cache: false # do our own caching - name: Restore dependencies - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/go/pkg/mod key: go-${{ runner.os }}${{ runner.arch }}-${{ hashFiles('go.mod') }}-deps-${{ hashFiles('go.sum') }} - name: Restore build outputs - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/.cache/go-build key: go-${{ runner.os }}${{ runner.arch }}-build-${{ env.COMMIT }} @@ -536,10 +550,16 @@ jobs: TEST_TOTAL_SHARDS: ${{ matrix.total_shards }} TEST_SHARD_INDEX: ${{ matrix.total_shards && matrix.shard_index }} # guard with total_shards to avoid falsy eval of shard_index=0 TEST_ARGS: "${{ matrix.test_args }}" + TEMPORAL_TEST_LOG_FILE: ${{ github.workspace }}/.testoutput/debug.log + TEMPORAL_TEST_LOG_LEVEL: info + + - name: Dump container logs + if: ${{ failure() && toJson(matrix.containers) != '[]' }} + run: docker compose -f ${{ env.DOCKER_COMPOSE_FILE }} logs --no-color - name: Print memory snapshot if: always() - run: if [ -f /tmp/memory_snapshot.txt ]; then cat /tmp/memory_snapshot.txt; fi + run: cat /tmp/memory_snapshot.txt || true - name: Generate crash report if: failure() # if the tests failed, we would expect one JUnit XML report per attempt; otherwise it must have crashed @@ -548,8 +568,8 @@ jobs: CRASH_REPORT_NAME="$GITHUB_JOB" make report-test-crash - name: Generate test summary - uses: mikepenz/action-junit-report@v5.0.0-rc01 - if: failure() + uses: mikepenz/action-junit-report@v6 + if: ${{ !cancelled() }} with: report_paths: ./.testoutput/junit.*.xml detailed_summary: true @@ -575,7 +595,7 @@ jobs: - name: Upload test results to GitHub # Can't pin to major because the action linter doesn't recognize the include-hidden-files flag. - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v6 if: ${{ !cancelled() }} with: name: junit-xml--${{ github.run_id }}--${{ steps.get_job_id.outputs.job_id }}--${{ github.run_attempt }}--${{ matrix.name }}--${{ matrix.display_name }}--functional-test @@ -583,6 +603,76 @@ jobs: include-hidden-files: true retention-days: 28 + - name: Upload debug logs + uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: debug-logs--${{ github.run_id }}--${{ steps.get_job_id.outputs.job_id }}--${{ github.run_attempt }}--${{ matrix.name }}--${{ matrix.display_name }}--functional-test + path: ${{ github.workspace }}/.testoutput/debug.log + if-no-files-found: ignore + retention-days: 14 + + mixed-brain-test: + name: Mixed brain test + needs: [pre-build, test-setup] + runs-on: ${{ needs.test-setup.outputs.runner_arm }} + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.COMMIT }} + + - name: Start PostgreSQL + uses: hoverkraft-tech/compose-action@v2.0.1 + with: + compose-file: ${{ env.DOCKER_COMPOSE_FILE }} + services: postgresql + down-flags: -v + + - uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + cache: false + + - name: Restore dependencies + uses: actions/cache/restore@v5 + with: + path: ~/go/pkg/mod + key: go-${{ runner.os }}${{ runner.arch }}-${{ hashFiles('go.mod') }}-deps-${{ hashFiles('go.sum') }} + + - name: Restore build outputs + uses: actions/cache/restore@v5 + with: + path: ~/.cache/go-build + key: go-${{ runner.os }}${{ runner.arch }}-build-${{ env.COMMIT }} + + - name: Install PostgreSQL schema + run: make install-schema-postgresql12 + + - name: Run mixed brain test + timeout-minutes: 20 + run: ./develop/github/monitor_test.sh make mixed-brain-test + env: + TEST_TIMEOUT: 18m + MIXED_BRAIN_TEST_DURATION: 5m + PERSISTENCE_DRIVER: postgres12 + + - name: Print memory snapshot + if: always() + run: cat /tmp/memory_snapshot.txt || true + + - name: Print current server logs + if: always() + run: cat .testoutput/mixedbrain_process-current.log || true + + - name: Print release server logs + if: always() + run: cat .testoutput/mixedbrain_process-release.log || true + + - name: Print Omes logs + if: always() + run: cat .testoutput/mixedbrain_omes.log || true + test-status: if: always() name: Test Status @@ -615,10 +705,10 @@ jobs: actions: read steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: "go.mod" cache: true diff --git a/.github/workflows/trigger-publish.yml b/.github/workflows/trigger-publish.yml deleted file mode 100644 index 8c3c38c6cf2..00000000000 --- a/.github/workflows/trigger-publish.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: 'Trigger Docker image build' - -on: - push: - branches: - - main - - cloud/* - - feature/* - - release/* -permissions: - contents: read - -jobs: - trigger: - name: 'trigger Docker image build' - runs-on: ubuntu-latest - - defaults: - run: - shell: bash - - steps: - - name: Get git branch name - id: get_branch - run: | - echo branch="${GITHUB_REF#refs/heads/}" >> "$GITHUB_OUTPUT" - - - name: Generate a token - id: generate_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} - private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} - owner: temporalio - repositories: | - temporal - docker-builds - - - name: Dispatch docker builds Github Action - if: ${{ vars.SHOULD_TRIGGER_DOCKER_BUILD == 'true' }} - env: - PAT: ${{ steps.generate_token.outputs.token }} - PARENT_REPO: temporalio/docker-builds - PARENT_BRANCH: ${{ toJSON('main') }} - WORKFLOW_ID: update-submodules.yml - REPO: ${{ toJSON('temporal') }} - BRANCH: ${{ toJSON(steps.get_branch.outputs.branch) }} - run: | - curl -fL -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $PAT" "https://api.github.com/repos/$PARENT_REPO/actions/workflows/$WORKFLOW_ID/dispatches" -d '{"ref":'"$PARENT_BRANCH"', "inputs": { "repo":'"$REPO"', "branch":'"$BRANCH"' }}' diff --git a/.github/workflows/trigger-version-info-service.yml b/.github/workflows/trigger-version-info-service.yml index caa615bef56..3058242ede2 100644 --- a/.github/workflows/trigger-version-info-service.yml +++ b/.github/workflows/trigger-version-info-service.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} diff --git a/.gitignore b/.gitignore index 5aba3e6fe67..f6e1955edc7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ /temporal-* /tctl* /tdbg +/fairsim # proto images /proto/image.bin @@ -43,4 +44,7 @@ /proto.tmp **/.venv/ -**/.ruff_cache/ \ No newline at end of file +**/.ruff_cache/ + +# Ignoring AI agent files +.agents/ diff --git a/AGENTS.md b/AGENTS.md index dbbba33a4c5..a4807125da7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,8 @@ Before starting the implementation of any request, you MUST REVIEW the following - Run tests after altering code or tests - Start with unit tests for fastest feedback - Prefer `require` over `assert`, avoid testify suites in unit tests (functional tests require suites for test cluster setup), use `require.Eventually` instead of `time.Sleep` (forbidden by linter) +- For float comparisons in tests, use `InDelta` or `InEpsilon` instead of `Equal` (enforced by `testifylint`) +- For error assertions in testify suites, use `s.Require().NoError(err)` instead of `s.NoError(err)` (enforced by `testifylint`) # Primary Workflows ## Software Engineering Tasks diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81381419825..ca0cb4ef502 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,7 +149,7 @@ make start-sqlite-file To run with Postgres: ```bash make install-schema-postgresql -make start-postgresql +make start-postgres ``` To run with MySQL: @@ -259,11 +259,7 @@ All PR titles should start with Upper case and have no dot at the end. ## Go version update -1. In this repository, update `go` in `go.mod`. -2. ~~In [docker-builds](https://github.com/temporalio/docker-builds/), update the base images: -[base-ci-builder](https://github.com/temporalio/docker-builds/blob/main/docker/base-images/base-ci-builder.Dockerfile) -and [base-builder](https://github.com/temporalio/docker-builds/blob/main/docker/base-images/base-builder.Dockerfile)~~ -**Note:** The docker-builds repository is now deprecated and will be archived. +To update the Go version, update the `go` directive in `go.mod`. CI workflows automatically pick up the version from `go.mod`. ## License diff --git a/Makefile b/Makefile index 3b6995c088d..893c5bf5712 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,7 @@ ifeq ($(OTEL),true) export OTEL_EXPORTER_OTLP_TRACES_INSECURE=true export OTEL_TRACES_EXPORTER=otlp export TEMPORAL_OTEL_DEBUG=true + export TEMPORAL_TEST_DATA_ENCODING=json endif MODULE_ROOT := $(lastword $(shell grep -e "^module " go.mod)) @@ -120,11 +121,12 @@ TEST_DIRS := $(sort $(dir $(filter %_test.go,$(ALL_SRC)))) FUNCTIONAL_TEST_ROOT := ./tests FUNCTIONAL_TEST_XDC_ROOT := ./tests/xdc FUNCTIONAL_TEST_NDC_ROOT := ./tests/ndc +MIXED_BRAIN_TEST_ROOT := ./tests/mixedbrain DB_INTEGRATION_TEST_ROOT := ./common/persistence/tests DB_TOOL_INTEGRATION_TEST_ROOT := ./tools/tests INTEGRATION_TEST_DIRS := $(DB_INTEGRATION_TEST_ROOT) $(DB_TOOL_INTEGRATION_TEST_ROOT) ./temporaltest ifeq ($(UNIT_TEST_DIRS),) -UNIT_TEST_DIRS := $(filter-out $(FUNCTIONAL_TEST_ROOT)% $(FUNCTIONAL_TEST_XDC_ROOT)% $(FUNCTIONAL_TEST_NDC_ROOT)% $(DB_INTEGRATION_TEST_ROOT)% $(DB_TOOL_INTEGRATION_TEST_ROOT)% ./temporaltest%,$(TEST_DIRS)) +UNIT_TEST_DIRS := $(filter-out $(FUNCTIONAL_TEST_ROOT)% $(FUNCTIONAL_TEST_XDC_ROOT)% $(FUNCTIONAL_TEST_NDC_ROOT)% $(MIXED_BRAIN_TEST_ROOT)% $(DB_INTEGRATION_TEST_ROOT)% $(DB_TOOL_INTEGRATION_TEST_ROOT)% ./temporaltest%,$(TEST_DIRS)) endif SYSTEM_WORKFLOWS_ROOT := ./service/worker @@ -344,6 +346,7 @@ clean-bins: @rm -f temporal-server-debug @rm -f temporal-cassandra-tool @rm -f tdbg + @rm -f fairsim @rm -f temporal-sql-tool @rm -f temporal-elasticsearch-tool @@ -355,6 +358,10 @@ tdbg: $(ALL_SRC) @printf $(COLOR) "Build tdbg with CGO_ENABLED=$(CGO_ENABLED) for $(GOOS)/$(GOARCH)..." CGO_ENABLED=$(CGO_ENABLED) go build $(BUILD_TAG_FLAG) -o tdbg ./cmd/tools/tdbg +fairsim: $(ALL_SRC) + @printf $(COLOR) "Build fairsim with CGO_ENABLED=$(CGO_ENABLED) for $(GOOS)/$(GOARCH)..." + CGO_ENABLED=$(CGO_ENABLED) go build $(BUILD_TAG_FLAG) -o fairsim ./cmd/tools/fairsim + temporal-cassandra-tool: $(ALL_SRC) @printf $(COLOR) "Build temporal-cassandra-tool with CGO_ENABLED=$(CGO_ENABLED) for $(GOOS)/$(GOARCH)..." CGO_ENABLED=$(CGO_ENABLED) go build $(BUILD_TAG_FLAG) -o temporal-cassandra-tool ./cmd/tools/cassandra @@ -402,11 +409,39 @@ lint-protos: $(BUF) $(INTERNAL_BINPB) $(CHASM_BINPB) @$(BUF) lint $(INTERNAL_BINPB) @$(BUF) lint --config chasm/lib/buf.yaml $(CHASM_BINPB) -fmt: fmt-imports fmt-yaml +fmt: fmt-gofix fmt-imports fmt-protos fmt-yaml + +# Some fixes enable others (e.g. rangeint may expose minmax opportunities), +# so - as recommended by the Go team - we run go fix in a loop until it reaches +# a fixed point. We check for "files updated" in the output rather than relying +# on the exit code alone, since go fix can exit non-zero without actually +# modifying any files (see https://github.com/golang/go/issues/77482). +# Note: go fix automatically skips generated files. +GOFIX_FLAGS ?= -any -rangeint +GOFIX_MAX_ITERATIONS ?= 5 +fmt-gofix: + @printf $(COLOR) "Run go fix..." + @n=0; while [ $$n -lt $(GOFIX_MAX_ITERATIONS) ]; do \ + output=$$(go fix $(GOFIX_FLAGS) ./... 2>&1); \ + echo "$$output"; \ + if ! echo "$$output" | grep -q "files updated"; then break; fi; \ + n=$$((n + 1)); \ + printf $(COLOR) "Re-running go fix..."; \ + done; \ + if [ $$n -ge $(GOFIX_MAX_ITERATIONS) ]; then echo "ERROR: go fix did not converge after $(GOFIX_MAX_ITERATIONS) iterations"; exit 1; fi fmt-imports: $(GCI) # Don't get confused, there is a single linter called gci, which is a part of the mega linter we use is called golangci-lint. - @printf $(COLOR) "Formatting imports..." - @$(GCI) write --skip-generated -s standard -s default ./* + @printf $(COLOR) "Formatting imports..." + @$(GCI) write --skip-generated -s standard -s default ./* + +parallelize-tests: + @printf $(COLOR) "Add t.Parallel() to tests..." + @go run ./cmd/tools/parallelize $(INTEGRATION_TEST_DIRS) + +fmt-protos: $(BUF) + @printf $(COLOR) "Formatting proto files..." + @$(BUF) format -w $(PROTO_ROOT)/internal + @$(BUF) format -w --config chasm/lib/buf.yaml chasm/lib fmt-yaml: $(YAMLFMT) @printf $(COLOR) "Formatting YAML files..." @@ -445,26 +480,36 @@ build-tests: unit-test: clean-test-output @printf $(COLOR) "Run unit tests..." @CGO_ENABLED=$(CGO_ENABLED) go test $(UNIT_TEST_DIRS) $(COMPILED_TEST_ARGS) 2>&1 | tee -a test.log - @! grep -q "^--- FAIL" test.log + @$(MAKE) verify-test-log integration-test: clean-test-output @printf $(COLOR) "Run integration tests..." @CGO_ENABLED=$(CGO_ENABLED) go test $(INTEGRATION_TEST_DIRS) $(COMPILED_TEST_ARGS) 2>&1 | tee -a test.log - @! grep -q "^--- FAIL" test.log + @$(MAKE) verify-test-log functional-test: clean-test-output @printf $(COLOR) "Run functional tests..." @CGO_ENABLED=$(CGO_ENABLED) go test $(FUNCTIONAL_TEST_ROOT) $(COMPILED_TEST_ARGS) -persistenceType=$(PERSISTENCE_TYPE) -persistenceDriver=$(PERSISTENCE_DRIVER) 2>&1 | tee -a test.log @CGO_ENABLED=$(CGO_ENABLED) go test $(FUNCTIONAL_TEST_NDC_ROOT) $(COMPILED_TEST_ARGS) -persistenceType=$(PERSISTENCE_TYPE) -persistenceDriver=$(PERSISTENCE_DRIVER) 2>&1 | tee -a test.log @CGO_ENABLED=$(CGO_ENABLED) go test $(FUNCTIONAL_TEST_XDC_ROOT) $(COMPILED_TEST_ARGS) -persistenceType=$(PERSISTENCE_TYPE) -persistenceDriver=$(PERSISTENCE_DRIVER) 2>&1 | tee -a test.log - @! grep -q "^--- FAIL" test.log + @$(MAKE) verify-test-log functional-with-fault-injection-test: clean-test-output @printf $(COLOR) "Run integration tests with fault injection..." @CGO_ENABLED=$(CGO_ENABLED) go test $(FUNCTIONAL_TEST_ROOT) $(COMPILED_TEST_ARGS) -enableFaultInjection=true -persistenceType=$(PERSISTENCE_TYPE) -persistenceDriver=$(PERSISTENCE_DRIVER) 2>&1 | tee -a test.log @CGO_ENABLED=$(CGO_ENABLED) go test $(FUNCTIONAL_TEST_NDC_ROOT) $(COMPILED_TEST_ARGS) -enableFaultInjection=true -persistenceType=$(PERSISTENCE_TYPE) -persistenceDriver=$(PERSISTENCE_DRIVER) 2>&1 | tee -a test.log @CGO_ENABLED=$(CGO_ENABLED) go test $(FUNCTIONAL_TEST_XDC_ROOT) $(COMPILED_TEST_ARGS) -enableFaultInjection=true -persistenceType=$(PERSISTENCE_TYPE) -persistenceDriver=$(PERSISTENCE_DRIVER) 2>&1 | tee -a test.log - @! grep -q "^--- FAIL" test.log + @$(MAKE) verify-test-log + +mixed-brain-test: clean-test-output + @printf $(COLOR) "Run mixed brain tests..." + @CGO_ENABLED=1 TEST_OUTPUT_ROOT=$(CURDIR)/$(TEST_OUTPUT_ROOT) go test -v $(MIXED_BRAIN_TEST_ROOT) $(COMPILED_TEST_ARGS) 2>&1 | tee -a test.log + @$(MAKE) verify-test-log + +verify-test-log: + @test -s test.log || (echo "TEST FAILURE: test.log is missing or empty" && exit 1) + @grep -q "^ok" test.log || (echo "TEST FAILURE: no passing test found in test.log" && exit 1) + @! grep -q "^--- FAIL" test.log || (echo "TEST FAILURE: failing test found in test.log" && exit 1) test: unit-test integration-test functional-test @@ -612,6 +657,9 @@ start: start-sqlite start-cass-es: temporal-server ./temporal-server --config-file config/development-cass-es.yaml --allow-no-auth start +start-cass-archival: temporal-server + ./temporal-server --config-file config/development-cass-archival.yaml --allow-no-auth start + start-cass-es-dual: temporal-server ./temporal-server --config-file config/development-cass-es-dual.yaml --allow-no-auth start @@ -680,4 +728,4 @@ ensure-no-changes: @printf $(COLOR) "Check for local changes..." @printf $(COLOR) "========================================================================" @git status --porcelain - @test -z "`git status --porcelain`" || (printf $(COLOR) "========================================================================"; printf $(RED) "Above files are not regenerated properly. Regenerate them and try again."; exit 1) + @test -z "`git status --porcelain`" || (printf $(COLOR) "========================================================================"; printf $(RED) "Above files are not regenerated properly. Regenerate them and try again."; git diff HEAD ; exit 1) diff --git a/api/adminservice/v1/request_response.pb.go b/api/adminservice/v1/request_response.pb.go index 4f6513e09db..f65c46e7e49 100644 --- a/api/adminservice/v1/request_response.pb.go +++ b/api/adminservice/v1/request_response.pb.go @@ -16,17 +16,18 @@ import ( v16 "go.temporal.io/api/enums/v1" v110 "go.temporal.io/api/namespace/v1" v111 "go.temporal.io/api/replication/v1" - v114 "go.temporal.io/api/taskqueue/v1" + v115 "go.temporal.io/api/taskqueue/v1" v19 "go.temporal.io/api/version/v1" v17 "go.temporal.io/api/workflow/v1" v18 "go.temporal.io/server/api/cluster/v1" v112 "go.temporal.io/server/api/common/v1" v14 "go.temporal.io/server/api/enums/v1" + v113 "go.temporal.io/server/api/health/v1" v11 "go.temporal.io/server/api/history/v1" v13 "go.temporal.io/server/api/namespace/v1" v12 "go.temporal.io/server/api/persistence/v1" v15 "go.temporal.io/server/api/replication/v1" - v113 "go.temporal.io/server/api/taskqueue/v1" + v114 "go.temporal.io/server/api/taskqueue/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" @@ -316,8 +317,10 @@ type DescribeMutableStateRequest struct { Execution *v1.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` SkipForceReload bool `protobuf:"varint,3,opt,name=skip_force_reload,json=skipForceReload,proto3" json:"skip_force_reload,omitempty"` Archetype string `protobuf:"bytes,4,opt,name=archetype,proto3" json:"archetype,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // (-- api-linter: core::0141::forbidden-types=disabled --) + ArchetypeId uint32 `protobuf:"varint,5,opt,name=archetype_id,json=archetypeId,proto3" json:"archetype_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DescribeMutableStateRequest) Reset() { @@ -378,6 +381,13 @@ func (x *DescribeMutableStateRequest) GetArchetype() string { return "" } +func (x *DescribeMutableStateRequest) GetArchetypeId() uint32 { + if x != nil { + return x.ArchetypeId + } + return 0 +} + type DescribeMutableStateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ShardId string `protobuf:"bytes,1,opt,name=shard_id,json=shardId,proto3" json:"shard_id,omitempty"` @@ -3148,10 +3158,12 @@ func (x *MergeDLQMessagesResponse) GetNextPageToken() []byte { } type RefreshWorkflowTasksRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - NamespaceId string `protobuf:"bytes,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` - Execution *v1.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` - Archetype string `protobuf:"bytes,4,opt,name=archetype,proto3" json:"archetype,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + Execution *v1.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` + Archetype string `protobuf:"bytes,4,opt,name=archetype,proto3" json:"archetype,omitempty"` + // (-- api-linter: core::0141::forbidden-types=disabled --) + ArchetypeId uint32 `protobuf:"varint,5,opt,name=archetype_id,json=archetypeId,proto3" json:"archetype_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3207,6 +3219,13 @@ func (x *RefreshWorkflowTasksRequest) GetArchetype() string { return "" } +func (x *RefreshWorkflowTasksRequest) GetArchetypeId() uint32 { + if x != nil { + return x.ArchetypeId + } + return 0 +} + type RefreshWorkflowTasksResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -3540,10 +3559,12 @@ func (x *GetTaskQueueTasksResponse) GetNextPageToken() []byte { } type DeleteWorkflowExecutionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - Execution *v1.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` - Archetype string `protobuf:"bytes,3,opt,name=archetype,proto3" json:"archetype,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Execution *v1.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` + Archetype string `protobuf:"bytes,3,opt,name=archetype,proto3" json:"archetype,omitempty"` + // (-- api-linter: core::0141::forbidden-types=disabled --) + ArchetypeId uint32 `protobuf:"varint,4,opt,name=archetype_id,json=archetypeId,proto3" json:"archetype_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3599,6 +3620,13 @@ func (x *DeleteWorkflowExecutionRequest) GetArchetype() string { return "" } +func (x *DeleteWorkflowExecutionRequest) GetArchetypeId() uint32 { + if x != nil { + return x.ArchetypeId + } + return 0 +} + type DeleteWorkflowExecutionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Warnings []string `protobuf:"bytes,1,rep,name=warnings,proto3" json:"warnings,omitempty"` @@ -4816,8 +4844,10 @@ func (*DeepHealthCheckRequest) Descriptor() ([]byte, []int) { } type DeepHealthCheckResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - State v14.HealthState `protobuf:"varint,1,opt,name=state,proto3,enum=temporal.server.api.enums.v1.HealthState" json:"state,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + State v14.HealthState `protobuf:"varint,1,opt,name=state,proto3,enum=temporal.server.api.enums.v1.HealthState" json:"state,omitempty"` + // Per-service diagnostic details including per-host breakdown. + Services []*v113.ServiceHealthDetail `protobuf:"bytes,2,rep,name=services,proto3" json:"services,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4859,6 +4889,13 @@ func (x *DeepHealthCheckResponse) GetState() v14.HealthState { return v14.HealthState(0) } +func (x *DeepHealthCheckResponse) GetServices() []*v113.ServiceHealthDetail { + if x != nil { + return x.Services + } + return nil +} + type SyncWorkflowStateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` @@ -4994,8 +5031,10 @@ type GenerateLastHistoryReplicationTasksRequest struct { Execution *v1.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` TargetClusters []string `protobuf:"bytes,3,rep,name=target_clusters,json=targetClusters,proto3" json:"target_clusters,omitempty"` Archetype string `protobuf:"bytes,4,opt,name=archetype,proto3" json:"archetype,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // (-- api-linter: core::0141::forbidden-types=disabled --) + ArchetypeId uint32 `protobuf:"varint,5,opt,name=archetype_id,json=archetypeId,proto3" json:"archetype_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerateLastHistoryReplicationTasksRequest) Reset() { @@ -5056,6 +5095,13 @@ func (x *GenerateLastHistoryReplicationTasksRequest) GetArchetype() string { return "" } +func (x *GenerateLastHistoryReplicationTasksRequest) GetArchetypeId() uint32 { + if x != nil { + return x.ArchetypeId + } + return 0 +} + type GenerateLastHistoryReplicationTasksResponse struct { state protoimpl.MessageState `protogen:"open.v1"` StateTransitionCount int64 `protobuf:"varint,1,opt,name=state_transition_count,json=stateTransitionCount,proto3" json:"state_transition_count,omitempty"` @@ -5111,9 +5157,9 @@ func (x *GenerateLastHistoryReplicationTasksResponse) GetHistoryLength() int64 { type DescribeTaskQueuePartitionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - TaskQueuePartition *v113.TaskQueuePartition `protobuf:"bytes,2,opt,name=task_queue_partition,json=taskQueuePartition,proto3" json:"task_queue_partition,omitempty"` + TaskQueuePartition *v114.TaskQueuePartition `protobuf:"bytes,2,opt,name=task_queue_partition,json=taskQueuePartition,proto3" json:"task_queue_partition,omitempty"` // Absent means unversioned queue. Ignored for sticky partitions. - BuildIds *v114.TaskQueueVersionSelection `protobuf:"bytes,3,opt,name=build_ids,json=buildIds,proto3" json:"build_ids,omitempty"` + BuildIds *v115.TaskQueueVersionSelection `protobuf:"bytes,3,opt,name=build_ids,json=buildIds,proto3" json:"build_ids,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5155,14 +5201,14 @@ func (x *DescribeTaskQueuePartitionRequest) GetNamespace() string { return "" } -func (x *DescribeTaskQueuePartitionRequest) GetTaskQueuePartition() *v113.TaskQueuePartition { +func (x *DescribeTaskQueuePartitionRequest) GetTaskQueuePartition() *v114.TaskQueuePartition { if x != nil { return x.TaskQueuePartition } return nil } -func (x *DescribeTaskQueuePartitionRequest) GetBuildIds() *v114.TaskQueueVersionSelection { +func (x *DescribeTaskQueuePartitionRequest) GetBuildIds() *v115.TaskQueueVersionSelection { if x != nil { return x.BuildIds } @@ -5172,7 +5218,7 @@ func (x *DescribeTaskQueuePartitionRequest) GetBuildIds() *v114.TaskQueueVersion type DescribeTaskQueuePartitionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // contains k-v pairs of the type: buildID -> TaskQueueVersionInfoInternal - VersionsInfoInternal map[string]*v113.TaskQueueVersionInfoInternal `protobuf:"bytes,1,rep,name=versions_info_internal,json=versionsInfoInternal,proto3" json:"versions_info_internal,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + VersionsInfoInternal map[string]*v114.TaskQueueVersionInfoInternal `protobuf:"bytes,1,rep,name=versions_info_internal,json=versionsInfoInternal,proto3" json:"versions_info_internal,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5207,7 +5253,7 @@ func (*DescribeTaskQueuePartitionResponse) Descriptor() ([]byte, []int) { return file_temporal_server_api_adminservice_v1_request_response_proto_rawDescGZIP(), []int{85} } -func (x *DescribeTaskQueuePartitionResponse) GetVersionsInfoInternal() map[string]*v113.TaskQueueVersionInfoInternal { +func (x *DescribeTaskQueuePartitionResponse) GetVersionsInfoInternal() map[string]*v114.TaskQueueVersionInfoInternal { if x != nil { return x.VersionsInfoInternal } @@ -5217,7 +5263,7 @@ func (x *DescribeTaskQueuePartitionResponse) GetVersionsInfoInternal() map[strin type ForceUnloadTaskQueuePartitionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - TaskQueuePartition *v113.TaskQueuePartition `protobuf:"bytes,2,opt,name=task_queue_partition,json=taskQueuePartition,proto3" json:"task_queue_partition,omitempty"` + TaskQueuePartition *v114.TaskQueuePartition `protobuf:"bytes,2,opt,name=task_queue_partition,json=taskQueuePartition,proto3" json:"task_queue_partition,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5259,7 +5305,7 @@ func (x *ForceUnloadTaskQueuePartitionRequest) GetNamespace() string { return "" } -func (x *ForceUnloadTaskQueuePartitionRequest) GetTaskQueuePartition() *v113.TaskQueuePartition { +func (x *ForceUnloadTaskQueuePartitionRequest) GetTaskQueuePartition() *v114.TaskQueuePartition { if x != nil { return x.TaskQueuePartition } @@ -5744,7 +5790,7 @@ var File_temporal_server_api_adminservice_v1_request_response_proto protoreflect const file_temporal_server_api_adminservice_v1_request_response_proto_rawDesc = "" + "\n" + - ":temporal/server/api/adminservice/v1/request_response.proto\x12#temporal.server.api.adminservice.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/version/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a'temporal/api/namespace/v1/message.proto\x1a)temporal/api/replication/v1/message.proto\x1a'temporal/api/taskqueue/v1/message.proto\x1a,temporal/server/api/cluster/v1/message.proto\x1a'temporal/server/api/common/v1/dlq.proto\x1a)temporal/server/api/enums/v1/common.proto\x1a*temporal/server/api/enums/v1/cluster.proto\x1a'temporal/server/api/enums/v1/task.proto\x1a&temporal/server/api/enums/v1/dlq.proto\x1a,temporal/server/api/history/v1/message.proto\x1a.temporal/server/api/namespace/v1/message.proto\x1a0temporal/server/api/replication/v1/message.proto\x1a9temporal/server/api/persistence/v1/cluster_metadata.proto\x1a3temporal/server/api/persistence/v1/executions.proto\x1a?temporal/server/api/persistence/v1/workflow_mutable_state.proto\x1a.temporal/server/api/persistence/v1/tasks.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a.temporal/server/api/taskqueue/v1/message.proto\"\x83\x01\n" + + ":temporal/server/api/adminservice/v1/request_response.proto\x12#temporal.server.api.adminservice.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a\"temporal/api/enums/v1/common.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a'temporal/api/namespace/v1/message.proto\x1a)temporal/api/replication/v1/message.proto\x1a'temporal/api/taskqueue/v1/message.proto\x1a%temporal/api/version/v1/message.proto\x1a&temporal/api/workflow/v1/message.proto\x1a,temporal/server/api/cluster/v1/message.proto\x1a'temporal/server/api/common/v1/dlq.proto\x1a*temporal/server/api/enums/v1/cluster.proto\x1a)temporal/server/api/enums/v1/common.proto\x1a&temporal/server/api/enums/v1/dlq.proto\x1a'temporal/server/api/enums/v1/task.proto\x1a+temporal/server/api/health/v1/message.proto\x1a,temporal/server/api/history/v1/message.proto\x1a.temporal/server/api/namespace/v1/message.proto\x1a9temporal/server/api/persistence/v1/cluster_metadata.proto\x1a3temporal/server/api/persistence/v1/executions.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a.temporal/server/api/persistence/v1/tasks.proto\x1a?temporal/server/api/persistence/v1/workflow_mutable_state.proto\x1a0temporal/server/api/replication/v1/message.proto\x1a.temporal/server/api/taskqueue/v1/message.proto\"\x83\x01\n" + "\x1aRebuildMutableStateRequest\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12G\n" + "\texecution\x18\x02 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\"\x1d\n" + @@ -5756,12 +5802,13 @@ const file_temporal_server_api_adminservice_v1_request_response_proto_rawDesc = "\x0fversion_history\x18\x04 \x01(\v2..temporal.server.api.history.v1.VersionHistoryR\x0eversionHistory\x12\x14\n" + "\x05token\x18\x05 \x01(\fR\x05token\"7\n" + "\x1fImportWorkflowExecutionResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\fR\x05token\"\xce\x01\n" + + "\x05token\x18\x01 \x01(\fR\x05token\"\xf1\x01\n" + "\x1bDescribeMutableStateRequest\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12G\n" + "\texecution\x18\x02 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\x12*\n" + "\x11skip_force_reload\x18\x03 \x01(\bR\x0fskipForceReload\x12\x1c\n" + - "\tarchetype\x18\x04 \x01(\tR\tarchetype\"\xb6\x02\n" + + "\tarchetype\x18\x04 \x01(\tR\tarchetype\x12!\n" + + "\farchetype_id\x18\x05 \x01(\rR\varchetypeId\"\xb6\x02\n" + "\x1cDescribeMutableStateResponse\x12\x19\n" + "\bshard_id\x18\x01 \x01(\tR\ashardId\x12!\n" + "\fhistory_addr\x18\x02 \x01(\tR\vhistoryAddr\x12h\n" + @@ -5977,11 +6024,12 @@ const file_temporal_server_api_adminservice_v1_request_response_proto_rawDesc = "\x11maximum_page_size\x18\x05 \x01(\x05R\x0fmaximumPageSize\x12&\n" + "\x0fnext_page_token\x18\x06 \x01(\fR\rnextPageToken\"B\n" + "\x18MergeDLQMessagesResponse\x12&\n" + - "\x0fnext_page_token\x18\x01 \x01(\fR\rnextPageToken\"\xad\x01\n" + + "\x0fnext_page_token\x18\x01 \x01(\fR\rnextPageToken\"\xd0\x01\n" + "\x1bRefreshWorkflowTasksRequest\x12!\n" + "\fnamespace_id\x18\x03 \x01(\tR\vnamespaceId\x12G\n" + "\texecution\x18\x02 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\x12\x1c\n" + - "\tarchetype\x18\x04 \x01(\tR\tarchetypeJ\x04\b\x01\x10\x02\"\x1e\n" + + "\tarchetype\x18\x04 \x01(\tR\tarchetype\x12!\n" + + "\farchetype_id\x18\x05 \x01(\rR\varchetypeIdJ\x04\b\x01\x10\x02\"\x1e\n" + "\x1cRefreshWorkflowTasksResponse\"\xaf\x02\n" + "\x1dResendReplicationTasksRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + @@ -6010,11 +6058,12 @@ const file_temporal_server_api_adminservice_v1_request_response_proto_rawDesc = "\bsubqueue\x18\b \x01(\x05R\bsubqueue\"\x90\x01\n" + "\x19GetTaskQueueTasksResponse\x12K\n" + "\x05tasks\x18\x01 \x03(\v25.temporal.server.api.persistence.v1.AllocatedTaskInfoR\x05tasks\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\fR\rnextPageToken\"\xa5\x01\n" + + "\x0fnext_page_token\x18\x02 \x01(\fR\rnextPageToken\"\xc8\x01\n" + "\x1eDeleteWorkflowExecutionRequest\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12G\n" + "\texecution\x18\x02 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\x12\x1c\n" + - "\tarchetype\x18\x03 \x01(\tR\tarchetype\"=\n" + + "\tarchetype\x18\x03 \x01(\tR\tarchetype\x12!\n" + + "\farchetype_id\x18\x04 \x01(\rR\varchetypeId\"=\n" + "\x1fDeleteWorkflowExecutionResponse\x12\x1a\n" + "\bwarnings\x18\x01 \x03(\tR\bwarnings\"\xaa\x01\n" + "(StreamWorkflowReplicationMessagesRequest\x12p\n" + @@ -6099,9 +6148,10 @@ const file_temporal_server_api_adminservice_v1_request_response_proto_rawDesc = "queue_name\x18\x01 \x01(\tR\tqueueName\x12#\n" + "\rmessage_count\x18\x02 \x01(\x03R\fmessageCount\x12&\n" + "\x0flast_message_id\x18\x03 \x01(\x03R\rlastMessageId\"\x18\n" + - "\x16DeepHealthCheckRequest\"Z\n" + + "\x16DeepHealthCheckRequest\"\xaa\x01\n" + "\x17DeepHealthCheckResponse\x12?\n" + - "\x05state\x18\x01 \x01(\x0e2).temporal.server.api.enums.v1.HealthStateR\x05state\"\xa0\x03\n" + + "\x05state\x18\x01 \x01(\x0e2).temporal.server.api.enums.v1.HealthStateR\x05state\x12N\n" + + "\bservices\x18\x02 \x03(\v22.temporal.server.api.health.v1.ServiceHealthDetailR\bservices\"\xa0\x03\n" + "\x18SyncWorkflowStateRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12G\n" + "\texecution\x18\x02 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\x12j\n" + @@ -6110,12 +6160,13 @@ const file_temporal_server_api_adminservice_v1_request_response_proto_rawDesc = "\x11target_cluster_id\x18\x05 \x01(\x05R\x0ftargetClusterId\x12!\n" + "\farchetype_id\x18\x06 \x01(\rR\varchetypeId\"\xb9\x01\n" + "\x19SyncWorkflowStateResponse\x12\x83\x01\n" + - "\x1dversioned_transition_artifact\x18\x05 \x01(\v2?.temporal.server.api.replication.v1.VersionedTransitionArtifactR\x1bversionedTransitionArtifactJ\x04\b\x01\x10\x02J\x04\b\x02\x10\x03J\x04\b\x03\x10\x04J\x04\b\x04\x10\x05\"\xda\x01\n" + + "\x1dversioned_transition_artifact\x18\x05 \x01(\v2?.temporal.server.api.replication.v1.VersionedTransitionArtifactR\x1bversionedTransitionArtifactJ\x04\b\x01\x10\x02J\x04\b\x02\x10\x03J\x04\b\x03\x10\x04J\x04\b\x04\x10\x05\"\xfd\x01\n" + "*GenerateLastHistoryReplicationTasksRequest\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12G\n" + "\texecution\x18\x02 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\x12'\n" + "\x0ftarget_clusters\x18\x03 \x03(\tR\x0etargetClusters\x12\x1c\n" + - "\tarchetype\x18\x04 \x01(\tR\tarchetype\"\x8a\x01\n" + + "\tarchetype\x18\x04 \x01(\tR\tarchetype\x12!\n" + + "\farchetype_id\x18\x05 \x01(\rR\varchetypeId\"\x8a\x01\n" + "+GenerateLastHistoryReplicationTasksResponse\x124\n" + "\x16state_transition_count\x18\x01 \x01(\x03R\x14stateTransitionCount\x12%\n" + "\x0ehistory_length\x18\x02 \x01(\x03R\rhistoryLength\"\xfc\x01\n" + @@ -6316,13 +6367,14 @@ var file_temporal_server_api_adminservice_v1_request_response_proto_goTypes = [] (v14.DLQOperationType)(0), // 136: temporal.server.api.enums.v1.DLQOperationType (v14.DLQOperationState)(0), // 137: temporal.server.api.enums.v1.DLQOperationState (v14.HealthState)(0), // 138: temporal.server.api.enums.v1.HealthState - (*v12.VersionedTransition)(nil), // 139: temporal.server.api.persistence.v1.VersionedTransition - (*v11.VersionHistories)(nil), // 140: temporal.server.api.history.v1.VersionHistories - (*v15.VersionedTransitionArtifact)(nil), // 141: temporal.server.api.replication.v1.VersionedTransitionArtifact - (*v113.TaskQueuePartition)(nil), // 142: temporal.server.api.taskqueue.v1.TaskQueuePartition - (*v114.TaskQueueVersionSelection)(nil), // 143: temporal.api.taskqueue.v1.TaskQueueVersionSelection - (v16.IndexedValueType)(0), // 144: temporal.api.enums.v1.IndexedValueType - (*v113.TaskQueueVersionInfoInternal)(nil), // 145: temporal.server.api.taskqueue.v1.TaskQueueVersionInfoInternal + (*v113.ServiceHealthDetail)(nil), // 139: temporal.server.api.health.v1.ServiceHealthDetail + (*v12.VersionedTransition)(nil), // 140: temporal.server.api.persistence.v1.VersionedTransition + (*v11.VersionHistories)(nil), // 141: temporal.server.api.history.v1.VersionHistories + (*v15.VersionedTransitionArtifact)(nil), // 142: temporal.server.api.replication.v1.VersionedTransitionArtifact + (*v114.TaskQueuePartition)(nil), // 143: temporal.server.api.taskqueue.v1.TaskQueuePartition + (*v115.TaskQueueVersionSelection)(nil), // 144: temporal.api.taskqueue.v1.TaskQueueVersionSelection + (v16.IndexedValueType)(0), // 145: temporal.api.enums.v1.IndexedValueType + (*v114.TaskQueueVersionInfoInternal)(nil), // 146: temporal.server.api.taskqueue.v1.TaskQueueVersionInfoInternal } var file_temporal_server_api_adminservice_v1_request_response_proto_depIdxs = []int32{ 104, // 0: temporal.server.api.adminservice.v1.RebuildMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution @@ -6397,29 +6449,30 @@ var file_temporal_server_api_adminservice_v1_request_response_proto_depIdxs = [] 101, // 69: temporal.server.api.adminservice.v1.AddTasksRequest.tasks:type_name -> temporal.server.api.adminservice.v1.AddTasksRequest.Task 102, // 70: temporal.server.api.adminservice.v1.ListQueuesResponse.queues:type_name -> temporal.server.api.adminservice.v1.ListQueuesResponse.QueueInfo 138, // 71: temporal.server.api.adminservice.v1.DeepHealthCheckResponse.state:type_name -> temporal.server.api.enums.v1.HealthState - 104, // 72: temporal.server.api.adminservice.v1.SyncWorkflowStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 139, // 73: temporal.server.api.adminservice.v1.SyncWorkflowStateRequest.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 140, // 74: temporal.server.api.adminservice.v1.SyncWorkflowStateRequest.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories - 141, // 75: temporal.server.api.adminservice.v1.SyncWorkflowStateResponse.versioned_transition_artifact:type_name -> temporal.server.api.replication.v1.VersionedTransitionArtifact - 104, // 76: temporal.server.api.adminservice.v1.GenerateLastHistoryReplicationTasksRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 142, // 77: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionRequest.task_queue_partition:type_name -> temporal.server.api.taskqueue.v1.TaskQueuePartition - 143, // 78: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionRequest.build_ids:type_name -> temporal.api.taskqueue.v1.TaskQueueVersionSelection - 103, // 79: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse.versions_info_internal:type_name -> temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse.VersionsInfoInternalEntry - 142, // 80: temporal.server.api.adminservice.v1.ForceUnloadTaskQueuePartitionRequest.task_queue_partition:type_name -> temporal.server.api.taskqueue.v1.TaskQueuePartition - 104, // 81: temporal.server.api.adminservice.v1.StartAdminBatchOperationRequest.executions:type_name -> temporal.api.common.v1.WorkflowExecution - 91, // 82: temporal.server.api.adminservice.v1.StartAdminBatchOperationRequest.refresh_tasks_operation:type_name -> temporal.server.api.adminservice.v1.BatchOperationRefreshTasks - 0, // 83: temporal.server.api.adminservice.v1.MigrateScheduleRequest.target:type_name -> temporal.server.api.adminservice.v1.MigrateScheduleRequest.SchedulerTarget - 114, // 84: temporal.server.api.adminservice.v1.GetReplicationMessagesResponse.ShardMessagesEntry.value:type_name -> temporal.server.api.replication.v1.ReplicationMessages - 144, // 85: temporal.server.api.adminservice.v1.AddSearchAttributesRequest.SearchAttributesEntry.value:type_name -> temporal.api.enums.v1.IndexedValueType - 144, // 86: temporal.server.api.adminservice.v1.GetSearchAttributesResponse.CustomAttributesEntry.value:type_name -> temporal.api.enums.v1.IndexedValueType - 144, // 87: temporal.server.api.adminservice.v1.GetSearchAttributesResponse.SystemAttributesEntry.value:type_name -> temporal.api.enums.v1.IndexedValueType - 105, // 88: temporal.server.api.adminservice.v1.AddTasksRequest.Task.blob:type_name -> temporal.api.common.v1.DataBlob - 145, // 89: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse.VersionsInfoInternalEntry.value:type_name -> temporal.server.api.taskqueue.v1.TaskQueueVersionInfoInternal - 90, // [90:90] is the sub-list for method output_type - 90, // [90:90] is the sub-list for method input_type - 90, // [90:90] is the sub-list for extension type_name - 90, // [90:90] is the sub-list for extension extendee - 0, // [0:90] is the sub-list for field type_name + 139, // 72: temporal.server.api.adminservice.v1.DeepHealthCheckResponse.services:type_name -> temporal.server.api.health.v1.ServiceHealthDetail + 104, // 73: temporal.server.api.adminservice.v1.SyncWorkflowStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 140, // 74: temporal.server.api.adminservice.v1.SyncWorkflowStateRequest.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 141, // 75: temporal.server.api.adminservice.v1.SyncWorkflowStateRequest.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 142, // 76: temporal.server.api.adminservice.v1.SyncWorkflowStateResponse.versioned_transition_artifact:type_name -> temporal.server.api.replication.v1.VersionedTransitionArtifact + 104, // 77: temporal.server.api.adminservice.v1.GenerateLastHistoryReplicationTasksRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 143, // 78: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionRequest.task_queue_partition:type_name -> temporal.server.api.taskqueue.v1.TaskQueuePartition + 144, // 79: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionRequest.build_ids:type_name -> temporal.api.taskqueue.v1.TaskQueueVersionSelection + 103, // 80: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse.versions_info_internal:type_name -> temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse.VersionsInfoInternalEntry + 143, // 81: temporal.server.api.adminservice.v1.ForceUnloadTaskQueuePartitionRequest.task_queue_partition:type_name -> temporal.server.api.taskqueue.v1.TaskQueuePartition + 104, // 82: temporal.server.api.adminservice.v1.StartAdminBatchOperationRequest.executions:type_name -> temporal.api.common.v1.WorkflowExecution + 91, // 83: temporal.server.api.adminservice.v1.StartAdminBatchOperationRequest.refresh_tasks_operation:type_name -> temporal.server.api.adminservice.v1.BatchOperationRefreshTasks + 0, // 84: temporal.server.api.adminservice.v1.MigrateScheduleRequest.target:type_name -> temporal.server.api.adminservice.v1.MigrateScheduleRequest.SchedulerTarget + 114, // 85: temporal.server.api.adminservice.v1.GetReplicationMessagesResponse.ShardMessagesEntry.value:type_name -> temporal.server.api.replication.v1.ReplicationMessages + 145, // 86: temporal.server.api.adminservice.v1.AddSearchAttributesRequest.SearchAttributesEntry.value:type_name -> temporal.api.enums.v1.IndexedValueType + 145, // 87: temporal.server.api.adminservice.v1.GetSearchAttributesResponse.CustomAttributesEntry.value:type_name -> temporal.api.enums.v1.IndexedValueType + 145, // 88: temporal.server.api.adminservice.v1.GetSearchAttributesResponse.SystemAttributesEntry.value:type_name -> temporal.api.enums.v1.IndexedValueType + 105, // 89: temporal.server.api.adminservice.v1.AddTasksRequest.Task.blob:type_name -> temporal.api.common.v1.DataBlob + 146, // 90: temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse.VersionsInfoInternalEntry.value:type_name -> temporal.server.api.taskqueue.v1.TaskQueueVersionInfoInternal + 91, // [91:91] is the sub-list for method output_type + 91, // [91:91] is the sub-list for method input_type + 91, // [91:91] is the sub-list for extension type_name + 91, // [91:91] is the sub-list for extension extendee + 0, // [0:91] is the sub-list for field type_name } func init() { file_temporal_server_api_adminservice_v1_request_response_proto_init() } diff --git a/api/adminservice/v1/service.pb.go b/api/adminservice/v1/service.pb.go index 286ced0d194..00096a092cc 100644 --- a/api/adminservice/v1/service.pb.go +++ b/api/adminservice/v1/service.pb.go @@ -10,6 +10,7 @@ import ( reflect "reflect" unsafe "unsafe" + _ "go.temporal.io/server/api/common/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) @@ -25,56 +26,56 @@ var File_temporal_server_api_adminservice_v1_service_proto protoreflect.FileDesc const file_temporal_server_api_adminservice_v1_service_proto_rawDesc = "" + "\n" + - "1temporal/server/api/adminservice/v1/service.proto\x12#temporal.server.api.adminservice.v1\x1a:temporal/server/api/adminservice/v1/request_response.proto2\xfc6\n" + - "\fAdminService\x12\x9a\x01\n" + - "\x13RebuildMutableState\x12?.temporal.server.api.adminservice.v1.RebuildMutableStateRequest\x1a@.temporal.server.api.adminservice.v1.RebuildMutableStateResponse\"\x00\x12\xa6\x01\n" + - "\x17ImportWorkflowExecution\x12C.temporal.server.api.adminservice.v1.ImportWorkflowExecutionRequest\x1aD.temporal.server.api.adminservice.v1.ImportWorkflowExecutionResponse\"\x00\x12\x9d\x01\n" + - "\x14DescribeMutableState\x12@.temporal.server.api.adminservice.v1.DescribeMutableStateRequest\x1aA.temporal.server.api.adminservice.v1.DescribeMutableStateResponse\"\x00\x12\x9a\x01\n" + - "\x13DescribeHistoryHost\x12?.temporal.server.api.adminservice.v1.DescribeHistoryHostRequest\x1a@.temporal.server.api.adminservice.v1.DescribeHistoryHostResponse\"\x00\x12y\n" + - "\bGetShard\x124.temporal.server.api.adminservice.v1.GetShardRequest\x1a5.temporal.server.api.adminservice.v1.GetShardResponse\"\x00\x12\x7f\n" + + "1temporal/server/api/adminservice/v1/service.proto\x12#temporal.server.api.adminservice.v1\x1a:temporal/server/api/adminservice/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto2\x8d9\n" + + "\fAdminService\x12\xa0\x01\n" + + "\x13RebuildMutableState\x12?.temporal.server.api.adminservice.v1.RebuildMutableStateRequest\x1a@.temporal.server.api.adminservice.v1.RebuildMutableStateResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xac\x01\n" + + "\x17ImportWorkflowExecution\x12C.temporal.server.api.adminservice.v1.ImportWorkflowExecutionRequest\x1aD.temporal.server.api.adminservice.v1.ImportWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa3\x01\n" + + "\x14DescribeMutableState\x12@.temporal.server.api.adminservice.v1.DescribeMutableStateRequest\x1aA.temporal.server.api.adminservice.v1.DescribeMutableStateResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa0\x01\n" + + "\x13DescribeHistoryHost\x12?.temporal.server.api.adminservice.v1.DescribeHistoryHostRequest\x1a@.temporal.server.api.adminservice.v1.DescribeHistoryHostResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x7f\n" + + "\bGetShard\x124.temporal.server.api.adminservice.v1.GetShardRequest\x1a5.temporal.server.api.adminservice.v1.GetShardResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x85\x01\n" + "\n" + - "CloseShard\x126.temporal.server.api.adminservice.v1.CloseShardRequest\x1a7.temporal.server.api.adminservice.v1.CloseShardResponse\"\x00\x12\x91\x01\n" + - "\x10ListHistoryTasks\x12<.temporal.server.api.adminservice.v1.ListHistoryTasksRequest\x1a=.temporal.server.api.adminservice.v1.ListHistoryTasksResponse\"\x00\x12\x7f\n" + + "CloseShard\x126.temporal.server.api.adminservice.v1.CloseShardRequest\x1a7.temporal.server.api.adminservice.v1.CloseShardResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x97\x01\n" + + "\x10ListHistoryTasks\x12<.temporal.server.api.adminservice.v1.ListHistoryTasksRequest\x1a=.temporal.server.api.adminservice.v1.ListHistoryTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x85\x01\n" + "\n" + - "RemoveTask\x126.temporal.server.api.adminservice.v1.RemoveTaskRequest\x1a7.temporal.server.api.adminservice.v1.RemoveTaskResponse\"\x00\x12\xc1\x01\n" + - " GetWorkflowExecutionRawHistoryV2\x12L.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request\x1aM.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response\"\x00\x12\xbb\x01\n" + - "\x1eGetWorkflowExecutionRawHistory\x12J.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest\x1aK.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse\"\x00\x12\xa3\x01\n" + - "\x16GetReplicationMessages\x12B.temporal.server.api.adminservice.v1.GetReplicationMessagesRequest\x1aC.temporal.server.api.adminservice.v1.GetReplicationMessagesResponse\"\x00\x12\xbe\x01\n" + - "\x1fGetNamespaceReplicationMessages\x12K.temporal.server.api.adminservice.v1.GetNamespaceReplicationMessagesRequest\x1aL.temporal.server.api.adminservice.v1.GetNamespaceReplicationMessagesResponse\"\x00\x12\xac\x01\n" + - "\x19GetDLQReplicationMessages\x12E.temporal.server.api.adminservice.v1.GetDLQReplicationMessagesRequest\x1aF.temporal.server.api.adminservice.v1.GetDLQReplicationMessagesResponse\"\x00\x12\x88\x01\n" + - "\rReapplyEvents\x129.temporal.server.api.adminservice.v1.ReapplyEventsRequest\x1a:.temporal.server.api.adminservice.v1.ReapplyEventsResponse\"\x00\x12\x9a\x01\n" + - "\x13AddSearchAttributes\x12?.temporal.server.api.adminservice.v1.AddSearchAttributesRequest\x1a@.temporal.server.api.adminservice.v1.AddSearchAttributesResponse\"\x00\x12\xa3\x01\n" + - "\x16RemoveSearchAttributes\x12B.temporal.server.api.adminservice.v1.RemoveSearchAttributesRequest\x1aC.temporal.server.api.adminservice.v1.RemoveSearchAttributesResponse\"\x00\x12\x9a\x01\n" + - "\x13GetSearchAttributes\x12?.temporal.server.api.adminservice.v1.GetSearchAttributesRequest\x1a@.temporal.server.api.adminservice.v1.GetSearchAttributesResponse\"\x00\x12\x8e\x01\n" + - "\x0fDescribeCluster\x12;.temporal.server.api.adminservice.v1.DescribeClusterRequest\x1a<.temporal.server.api.adminservice.v1.DescribeClusterResponse\"\x00\x12\x85\x01\n" + - "\fListClusters\x128.temporal.server.api.adminservice.v1.ListClustersRequest\x1a9.temporal.server.api.adminservice.v1.ListClustersResponse\"\x00\x12\x97\x01\n" + - "\x12ListClusterMembers\x12>.temporal.server.api.adminservice.v1.ListClusterMembersRequest\x1a?.temporal.server.api.adminservice.v1.ListClusterMembersResponse\"\x00\x12\xa9\x01\n" + - "\x18AddOrUpdateRemoteCluster\x12D.temporal.server.api.adminservice.v1.AddOrUpdateRemoteClusterRequest\x1aE.temporal.server.api.adminservice.v1.AddOrUpdateRemoteClusterResponse\"\x00\x12\x9a\x01\n" + - "\x13RemoveRemoteCluster\x12?.temporal.server.api.adminservice.v1.RemoveRemoteClusterRequest\x1a@.temporal.server.api.adminservice.v1.RemoveRemoteClusterResponse\"\x00\x12\x8b\x01\n" + - "\x0eGetDLQMessages\x12:.temporal.server.api.adminservice.v1.GetDLQMessagesRequest\x1a;.temporal.server.api.adminservice.v1.GetDLQMessagesResponse\"\x00\x12\x91\x01\n" + - "\x10PurgeDLQMessages\x12<.temporal.server.api.adminservice.v1.PurgeDLQMessagesRequest\x1a=.temporal.server.api.adminservice.v1.PurgeDLQMessagesResponse\"\x00\x12\x91\x01\n" + - "\x10MergeDLQMessages\x12<.temporal.server.api.adminservice.v1.MergeDLQMessagesRequest\x1a=.temporal.server.api.adminservice.v1.MergeDLQMessagesResponse\"\x00\x12\x9d\x01\n" + - "\x14RefreshWorkflowTasks\x12@.temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest\x1aA.temporal.server.api.adminservice.v1.RefreshWorkflowTasksResponse\"\x00\x12\xa9\x01\n" + - "\x18StartAdminBatchOperation\x12D.temporal.server.api.adminservice.v1.StartAdminBatchOperationRequest\x1aE.temporal.server.api.adminservice.v1.StartAdminBatchOperationResponse\"\x00\x12\xa3\x01\n" + - "\x16ResendReplicationTasks\x12B.temporal.server.api.adminservice.v1.ResendReplicationTasksRequest\x1aC.temporal.server.api.adminservice.v1.ResendReplicationTasksResponse\"\x00\x12\x94\x01\n" + - "\x11GetTaskQueueTasks\x12=.temporal.server.api.adminservice.v1.GetTaskQueueTasksRequest\x1a>.temporal.server.api.adminservice.v1.GetTaskQueueTasksResponse\"\x00\x12\xa6\x01\n" + - "\x17DeleteWorkflowExecution\x12C.temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest\x1aD.temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse\"\x00\x12\xc8\x01\n" + - "!StreamWorkflowReplicationMessages\x12M.temporal.server.api.adminservice.v1.StreamWorkflowReplicationMessagesRequest\x1aN.temporal.server.api.adminservice.v1.StreamWorkflowReplicationMessagesResponse\"\x00(\x010\x01\x12\x85\x01\n" + - "\fGetNamespace\x128.temporal.server.api.adminservice.v1.GetNamespaceRequest\x1a9.temporal.server.api.adminservice.v1.GetNamespaceResponse\"\x00\x12\x82\x01\n" + - "\vGetDLQTasks\x127.temporal.server.api.adminservice.v1.GetDLQTasksRequest\x1a8.temporal.server.api.adminservice.v1.GetDLQTasksResponse\"\x00\x12\x88\x01\n" + - "\rPurgeDLQTasks\x129.temporal.server.api.adminservice.v1.PurgeDLQTasksRequest\x1a:.temporal.server.api.adminservice.v1.PurgeDLQTasksResponse\"\x00\x12\x88\x01\n" + - "\rMergeDLQTasks\x129.temporal.server.api.adminservice.v1.MergeDLQTasksRequest\x1a:.temporal.server.api.adminservice.v1.MergeDLQTasksResponse\"\x00\x12\x8b\x01\n" + - "\x0eDescribeDLQJob\x12:.temporal.server.api.adminservice.v1.DescribeDLQJobRequest\x1a;.temporal.server.api.adminservice.v1.DescribeDLQJobResponse\"\x00\x12\x85\x01\n" + - "\fCancelDLQJob\x128.temporal.server.api.adminservice.v1.CancelDLQJobRequest\x1a9.temporal.server.api.adminservice.v1.CancelDLQJobResponse\"\x00\x12y\n" + - "\bAddTasks\x124.temporal.server.api.adminservice.v1.AddTasksRequest\x1a5.temporal.server.api.adminservice.v1.AddTasksResponse\"\x00\x12\x7f\n" + + "RemoveTask\x126.temporal.server.api.adminservice.v1.RemoveTaskRequest\x1a7.temporal.server.api.adminservice.v1.RemoveTaskResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xc7\x01\n" + + " GetWorkflowExecutionRawHistoryV2\x12L.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request\x1aM.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response\"\x06\x8a\xb5\x18\x02\b\x03\x12\xc1\x01\n" + + "\x1eGetWorkflowExecutionRawHistory\x12J.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest\x1aK.temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa9\x01\n" + + "\x16GetReplicationMessages\x12B.temporal.server.api.adminservice.v1.GetReplicationMessagesRequest\x1aC.temporal.server.api.adminservice.v1.GetReplicationMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xc4\x01\n" + + "\x1fGetNamespaceReplicationMessages\x12K.temporal.server.api.adminservice.v1.GetNamespaceReplicationMessagesRequest\x1aL.temporal.server.api.adminservice.v1.GetNamespaceReplicationMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xb2\x01\n" + + "\x19GetDLQReplicationMessages\x12E.temporal.server.api.adminservice.v1.GetDLQReplicationMessagesRequest\x1aF.temporal.server.api.adminservice.v1.GetDLQReplicationMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x8e\x01\n" + + "\rReapplyEvents\x129.temporal.server.api.adminservice.v1.ReapplyEventsRequest\x1a:.temporal.server.api.adminservice.v1.ReapplyEventsResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa0\x01\n" + + "\x13AddSearchAttributes\x12?.temporal.server.api.adminservice.v1.AddSearchAttributesRequest\x1a@.temporal.server.api.adminservice.v1.AddSearchAttributesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa9\x01\n" + + "\x16RemoveSearchAttributes\x12B.temporal.server.api.adminservice.v1.RemoveSearchAttributesRequest\x1aC.temporal.server.api.adminservice.v1.RemoveSearchAttributesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa0\x01\n" + + "\x13GetSearchAttributes\x12?.temporal.server.api.adminservice.v1.GetSearchAttributesRequest\x1a@.temporal.server.api.adminservice.v1.GetSearchAttributesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x94\x01\n" + + "\x0fDescribeCluster\x12;.temporal.server.api.adminservice.v1.DescribeClusterRequest\x1a<.temporal.server.api.adminservice.v1.DescribeClusterResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x8b\x01\n" + + "\fListClusters\x128.temporal.server.api.adminservice.v1.ListClustersRequest\x1a9.temporal.server.api.adminservice.v1.ListClustersResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x9d\x01\n" + + "\x12ListClusterMembers\x12>.temporal.server.api.adminservice.v1.ListClusterMembersRequest\x1a?.temporal.server.api.adminservice.v1.ListClusterMembersResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xaf\x01\n" + + "\x18AddOrUpdateRemoteCluster\x12D.temporal.server.api.adminservice.v1.AddOrUpdateRemoteClusterRequest\x1aE.temporal.server.api.adminservice.v1.AddOrUpdateRemoteClusterResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa0\x01\n" + + "\x13RemoveRemoteCluster\x12?.temporal.server.api.adminservice.v1.RemoveRemoteClusterRequest\x1a@.temporal.server.api.adminservice.v1.RemoveRemoteClusterResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x91\x01\n" + + "\x0eGetDLQMessages\x12:.temporal.server.api.adminservice.v1.GetDLQMessagesRequest\x1a;.temporal.server.api.adminservice.v1.GetDLQMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x97\x01\n" + + "\x10PurgeDLQMessages\x12<.temporal.server.api.adminservice.v1.PurgeDLQMessagesRequest\x1a=.temporal.server.api.adminservice.v1.PurgeDLQMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x97\x01\n" + + "\x10MergeDLQMessages\x12<.temporal.server.api.adminservice.v1.MergeDLQMessagesRequest\x1a=.temporal.server.api.adminservice.v1.MergeDLQMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa3\x01\n" + + "\x14RefreshWorkflowTasks\x12@.temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest\x1aA.temporal.server.api.adminservice.v1.RefreshWorkflowTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xaf\x01\n" + + "\x18StartAdminBatchOperation\x12D.temporal.server.api.adminservice.v1.StartAdminBatchOperationRequest\x1aE.temporal.server.api.adminservice.v1.StartAdminBatchOperationResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa9\x01\n" + + "\x16ResendReplicationTasks\x12B.temporal.server.api.adminservice.v1.ResendReplicationTasksRequest\x1aC.temporal.server.api.adminservice.v1.ResendReplicationTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x9a\x01\n" + + "\x11GetTaskQueueTasks\x12=.temporal.server.api.adminservice.v1.GetTaskQueueTasksRequest\x1a>.temporal.server.api.adminservice.v1.GetTaskQueueTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xac\x01\n" + + "\x17DeleteWorkflowExecution\x12C.temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest\x1aD.temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xce\x01\n" + + "!StreamWorkflowReplicationMessages\x12M.temporal.server.api.adminservice.v1.StreamWorkflowReplicationMessagesRequest\x1aN.temporal.server.api.adminservice.v1.StreamWorkflowReplicationMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03(\x010\x01\x12\x8b\x01\n" + + "\fGetNamespace\x128.temporal.server.api.adminservice.v1.GetNamespaceRequest\x1a9.temporal.server.api.adminservice.v1.GetNamespaceResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x88\x01\n" + + "\vGetDLQTasks\x127.temporal.server.api.adminservice.v1.GetDLQTasksRequest\x1a8.temporal.server.api.adminservice.v1.GetDLQTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x8e\x01\n" + + "\rPurgeDLQTasks\x129.temporal.server.api.adminservice.v1.PurgeDLQTasksRequest\x1a:.temporal.server.api.adminservice.v1.PurgeDLQTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x8e\x01\n" + + "\rMergeDLQTasks\x129.temporal.server.api.adminservice.v1.MergeDLQTasksRequest\x1a:.temporal.server.api.adminservice.v1.MergeDLQTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x91\x01\n" + + "\x0eDescribeDLQJob\x12:.temporal.server.api.adminservice.v1.DescribeDLQJobRequest\x1a;.temporal.server.api.adminservice.v1.DescribeDLQJobResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x8b\x01\n" + + "\fCancelDLQJob\x128.temporal.server.api.adminservice.v1.CancelDLQJobRequest\x1a9.temporal.server.api.adminservice.v1.CancelDLQJobResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x7f\n" + + "\bAddTasks\x124.temporal.server.api.adminservice.v1.AddTasksRequest\x1a5.temporal.server.api.adminservice.v1.AddTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x85\x01\n" + "\n" + - "ListQueues\x126.temporal.server.api.adminservice.v1.ListQueuesRequest\x1a7.temporal.server.api.adminservice.v1.ListQueuesResponse\"\x00\x12\x8e\x01\n" + - "\x0fDeepHealthCheck\x12;.temporal.server.api.adminservice.v1.DeepHealthCheckRequest\x1a<.temporal.server.api.adminservice.v1.DeepHealthCheckResponse\"\x00\x12\x94\x01\n" + - "\x11SyncWorkflowState\x12=.temporal.server.api.adminservice.v1.SyncWorkflowStateRequest\x1a>.temporal.server.api.adminservice.v1.SyncWorkflowStateResponse\"\x00\x12\xca\x01\n" + - "#GenerateLastHistoryReplicationTasks\x12O.temporal.server.api.adminservice.v1.GenerateLastHistoryReplicationTasksRequest\x1aP.temporal.server.api.adminservice.v1.GenerateLastHistoryReplicationTasksResponse\"\x00\x12\xaf\x01\n" + - "\x1aDescribeTaskQueuePartition\x12F.temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionRequest\x1aG.temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse\"\x00\x12\xb8\x01\n" + - "\x1dForceUnloadTaskQueuePartition\x12I.temporal.server.api.adminservice.v1.ForceUnloadTaskQueuePartitionRequest\x1aJ.temporal.server.api.adminservice.v1.ForceUnloadTaskQueuePartitionResponse\"\x00\x12\x8e\x01\n" + - "\x0fMigrateSchedule\x12;.temporal.server.api.adminservice.v1.MigrateScheduleRequest\x1a<.temporal.server.api.adminservice.v1.MigrateScheduleResponse\"\x00B8Z6go.temporal.io/server/api/adminservice/v1;adminserviceb\x06proto3" + "ListQueues\x126.temporal.server.api.adminservice.v1.ListQueuesRequest\x1a7.temporal.server.api.adminservice.v1.ListQueuesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x94\x01\n" + + "\x0fDeepHealthCheck\x12;.temporal.server.api.adminservice.v1.DeepHealthCheckRequest\x1a<.temporal.server.api.adminservice.v1.DeepHealthCheckResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x9a\x01\n" + + "\x11SyncWorkflowState\x12=.temporal.server.api.adminservice.v1.SyncWorkflowStateRequest\x1a>.temporal.server.api.adminservice.v1.SyncWorkflowStateResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xd0\x01\n" + + "#GenerateLastHistoryReplicationTasks\x12O.temporal.server.api.adminservice.v1.GenerateLastHistoryReplicationTasksRequest\x1aP.temporal.server.api.adminservice.v1.GenerateLastHistoryReplicationTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xb5\x01\n" + + "\x1aDescribeTaskQueuePartition\x12F.temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionRequest\x1aG.temporal.server.api.adminservice.v1.DescribeTaskQueuePartitionResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xbe\x01\n" + + "\x1dForceUnloadTaskQueuePartition\x12I.temporal.server.api.adminservice.v1.ForceUnloadTaskQueuePartitionRequest\x1aJ.temporal.server.api.adminservice.v1.ForceUnloadTaskQueuePartitionResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x94\x01\n" + + "\x0fMigrateSchedule\x12;.temporal.server.api.adminservice.v1.MigrateScheduleRequest\x1a<.temporal.server.api.adminservice.v1.MigrateScheduleResponse\"\x06\x8a\xb5\x18\x02\b\x03B8Z6go.temporal.io/server/api/adminservice/v1;adminserviceb\x06proto3" var file_temporal_server_api_adminservice_v1_service_proto_goTypes = []any{ (*RebuildMutableStateRequest)(nil), // 0: temporal.server.api.adminservice.v1.RebuildMutableStateRequest diff --git a/api/archiver/v1/message.pb.go b/api/archiver/v1/message.pb.go index cfac4f407b3..8bd8356939a 100644 --- a/api/archiver/v1/message.pb.go +++ b/api/archiver/v1/message.pb.go @@ -348,7 +348,7 @@ var File_temporal_server_api_archiver_v1_message_proto protoreflect.FileDescript const file_temporal_server_api_archiver_v1_message_proto_rawDesc = "" + "\n" + - "-temporal/server/api/archiver/v1/message.proto\x12\x1ftemporal.server.api.archiver.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/history/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\"\xfa\x02\n" + + "-temporal/server/api/archiver/v1/message.proto\x12\x1ftemporal.server.api.archiver.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/history/v1/message.proto\"\xfa\x02\n" + "\x11HistoryBlobHeader\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12!\n" + "\fnamespace_id\x18\x02 \x01(\tR\vnamespaceId\x12\x1f\n" + diff --git a/api/batch/v1/request_response.pb.go b/api/batch/v1/request_response.pb.go index d936bc4ee6f..8856ee46919 100644 --- a/api/batch/v1/request_response.pb.go +++ b/api/batch/v1/request_response.pb.go @@ -135,7 +135,7 @@ var File_temporal_server_api_batch_v1_request_response_proto protoreflect.FileDe const file_temporal_server_api_batch_v1_request_response_proto_rawDesc = "" + "\n" + - "3temporal/server/api/batch/v1/request_response.proto\x12\x1ctemporal.server.api.batch.v1\x1a6temporal/api/workflowservice/v1/request_response.proto\x1a+temporal/api/enums/v1/batch_operation.proto\x1a:temporal/server/api/adminservice/v1/request_response.proto\x1a\x1egoogle/protobuf/duration.proto\"\xb0\x04\n" + + "3temporal/server/api/batch/v1/request_response.proto\x12\x1ctemporal.server.api.batch.v1\x1a\x1egoogle/protobuf/duration.proto\x1a+temporal/api/enums/v1/batch_operation.proto\x1a6temporal/api/workflowservice/v1/request_response.proto\x1a:temporal/server/api/adminservice/v1/request_response.proto\"\xb0\x04\n" + "\x13BatchOperationInput\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12 \n" + "\vconcurrency\x18\x02 \x01(\x03R\vconcurrency\x12=\n" + diff --git a/api/chasm/v1/message.go-helpers.pb.go b/api/chasm/v1/message.go-helpers.pb.go new file mode 100644 index 00000000000..b63c13f38a1 --- /dev/null +++ b/api/chasm/v1/message.go-helpers.pb.go @@ -0,0 +1,43 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package chasm + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type VisibilityExecutionInfo to the protobuf v3 wire format +func (val *VisibilityExecutionInfo) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type VisibilityExecutionInfo from the protobuf v3 wire format +func (val *VisibilityExecutionInfo) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *VisibilityExecutionInfo) Size() int { + return proto.Size(val) +} + +// Equal returns whether two VisibilityExecutionInfo values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *VisibilityExecutionInfo) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *VisibilityExecutionInfo + switch t := that.(type) { + case *VisibilityExecutionInfo: + that1 = t + case VisibilityExecutionInfo: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/api/chasm/v1/message.pb.go b/api/chasm/v1/message.pb.go new file mode 100644 index 00000000000..6723380a971 --- /dev/null +++ b/api/chasm/v1/message.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/api/chasm/v1/message.proto + +package chasm + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + v1 "go.temporal.io/api/common/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type VisibilityExecutionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + BusinessId string `protobuf:"bytes,1,opt,name=business_id,json=businessId,proto3" json:"business_id,omitempty"` + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + StartTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + CloseTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=close_time,json=closeTime,proto3" json:"close_time,omitempty"` + HistoryLength int64 `protobuf:"varint,5,opt,name=history_length,json=historyLength,proto3" json:"history_length,omitempty"` + HistorySizeBytes int64 `protobuf:"varint,6,opt,name=history_size_bytes,json=historySizeBytes,proto3" json:"history_size_bytes,omitempty"` + StateTransitionCount int64 `protobuf:"varint,7,opt,name=state_transition_count,json=stateTransitionCount,proto3" json:"state_transition_count,omitempty"` + ChasmSearchAttributes *v1.SearchAttributes `protobuf:"bytes,8,opt,name=chasm_search_attributes,json=chasmSearchAttributes,proto3" json:"chasm_search_attributes,omitempty"` + CustomSearchAttributes *v1.SearchAttributes `protobuf:"bytes,9,opt,name=custom_search_attributes,json=customSearchAttributes,proto3" json:"custom_search_attributes,omitempty"` + Memo *v1.Memo `protobuf:"bytes,10,opt,name=memo,proto3" json:"memo,omitempty"` + ChasmMemo *v1.Payload `protobuf:"bytes,11,opt,name=chasm_memo,json=chasmMemo,proto3" json:"chasm_memo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VisibilityExecutionInfo) Reset() { + *x = VisibilityExecutionInfo{} + mi := &file_temporal_server_api_chasm_v1_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VisibilityExecutionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VisibilityExecutionInfo) ProtoMessage() {} + +func (x *VisibilityExecutionInfo) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_chasm_v1_message_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VisibilityExecutionInfo.ProtoReflect.Descriptor instead. +func (*VisibilityExecutionInfo) Descriptor() ([]byte, []int) { + return file_temporal_server_api_chasm_v1_message_proto_rawDescGZIP(), []int{0} +} + +func (x *VisibilityExecutionInfo) GetBusinessId() string { + if x != nil { + return x.BusinessId + } + return "" +} + +func (x *VisibilityExecutionInfo) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *VisibilityExecutionInfo) GetStartTime() *timestamppb.Timestamp { + if x != nil { + return x.StartTime + } + return nil +} + +func (x *VisibilityExecutionInfo) GetCloseTime() *timestamppb.Timestamp { + if x != nil { + return x.CloseTime + } + return nil +} + +func (x *VisibilityExecutionInfo) GetHistoryLength() int64 { + if x != nil { + return x.HistoryLength + } + return 0 +} + +func (x *VisibilityExecutionInfo) GetHistorySizeBytes() int64 { + if x != nil { + return x.HistorySizeBytes + } + return 0 +} + +func (x *VisibilityExecutionInfo) GetStateTransitionCount() int64 { + if x != nil { + return x.StateTransitionCount + } + return 0 +} + +func (x *VisibilityExecutionInfo) GetChasmSearchAttributes() *v1.SearchAttributes { + if x != nil { + return x.ChasmSearchAttributes + } + return nil +} + +func (x *VisibilityExecutionInfo) GetCustomSearchAttributes() *v1.SearchAttributes { + if x != nil { + return x.CustomSearchAttributes + } + return nil +} + +func (x *VisibilityExecutionInfo) GetMemo() *v1.Memo { + if x != nil { + return x.Memo + } + return nil +} + +func (x *VisibilityExecutionInfo) GetChasmMemo() *v1.Payload { + if x != nil { + return x.ChasmMemo + } + return nil +} + +var File_temporal_server_api_chasm_v1_message_proto protoreflect.FileDescriptor + +const file_temporal_server_api_chasm_v1_message_proto_rawDesc = "" + + "\n" + + "*temporal/server/api/chasm/v1/message.proto\x12\x1ctemporal.server.api.chasm.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\"\x8a\x05\n" + + "\x17VisibilityExecutionInfo\x12\x1f\n" + + "\vbusiness_id\x18\x01 \x01(\tR\n" + + "businessId\x12\x15\n" + + "\x06run_id\x18\x02 \x01(\tR\x05runId\x129\n" + + "\n" + + "start_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tstartTime\x129\n" + + "\n" + + "close_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcloseTime\x12%\n" + + "\x0ehistory_length\x18\x05 \x01(\x03R\rhistoryLength\x12,\n" + + "\x12history_size_bytes\x18\x06 \x01(\x03R\x10historySizeBytes\x124\n" + + "\x16state_transition_count\x18\a \x01(\x03R\x14stateTransitionCount\x12`\n" + + "\x17chasm_search_attributes\x18\b \x01(\v2(.temporal.api.common.v1.SearchAttributesR\x15chasmSearchAttributes\x12b\n" + + "\x18custom_search_attributes\x18\t \x01(\v2(.temporal.api.common.v1.SearchAttributesR\x16customSearchAttributes\x120\n" + + "\x04memo\x18\n" + + " \x01(\v2\x1c.temporal.api.common.v1.MemoR\x04memo\x12>\n" + + "\n" + + "chasm_memo\x18\v \x01(\v2\x1f.temporal.api.common.v1.PayloadR\tchasmMemoB*Z(go.temporal.io/server/api/chasm/v1;chasmb\x06proto3" + +var ( + file_temporal_server_api_chasm_v1_message_proto_rawDescOnce sync.Once + file_temporal_server_api_chasm_v1_message_proto_rawDescData []byte +) + +func file_temporal_server_api_chasm_v1_message_proto_rawDescGZIP() []byte { + file_temporal_server_api_chasm_v1_message_proto_rawDescOnce.Do(func() { + file_temporal_server_api_chasm_v1_message_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_api_chasm_v1_message_proto_rawDesc), len(file_temporal_server_api_chasm_v1_message_proto_rawDesc))) + }) + return file_temporal_server_api_chasm_v1_message_proto_rawDescData +} + +var file_temporal_server_api_chasm_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_temporal_server_api_chasm_v1_message_proto_goTypes = []any{ + (*VisibilityExecutionInfo)(nil), // 0: temporal.server.api.chasm.v1.VisibilityExecutionInfo + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp + (*v1.SearchAttributes)(nil), // 2: temporal.api.common.v1.SearchAttributes + (*v1.Memo)(nil), // 3: temporal.api.common.v1.Memo + (*v1.Payload)(nil), // 4: temporal.api.common.v1.Payload +} +var file_temporal_server_api_chasm_v1_message_proto_depIdxs = []int32{ + 1, // 0: temporal.server.api.chasm.v1.VisibilityExecutionInfo.start_time:type_name -> google.protobuf.Timestamp + 1, // 1: temporal.server.api.chasm.v1.VisibilityExecutionInfo.close_time:type_name -> google.protobuf.Timestamp + 2, // 2: temporal.server.api.chasm.v1.VisibilityExecutionInfo.chasm_search_attributes:type_name -> temporal.api.common.v1.SearchAttributes + 2, // 3: temporal.server.api.chasm.v1.VisibilityExecutionInfo.custom_search_attributes:type_name -> temporal.api.common.v1.SearchAttributes + 3, // 4: temporal.server.api.chasm.v1.VisibilityExecutionInfo.memo:type_name -> temporal.api.common.v1.Memo + 4, // 5: temporal.server.api.chasm.v1.VisibilityExecutionInfo.chasm_memo:type_name -> temporal.api.common.v1.Payload + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_temporal_server_api_chasm_v1_message_proto_init() } +func file_temporal_server_api_chasm_v1_message_proto_init() { + if File_temporal_server_api_chasm_v1_message_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_chasm_v1_message_proto_rawDesc), len(file_temporal_server_api_chasm_v1_message_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_api_chasm_v1_message_proto_goTypes, + DependencyIndexes: file_temporal_server_api_chasm_v1_message_proto_depIdxs, + MessageInfos: file_temporal_server_api_chasm_v1_message_proto_msgTypes, + }.Build() + File_temporal_server_api_chasm_v1_message_proto = out.File + file_temporal_server_api_chasm_v1_message_proto_goTypes = nil + file_temporal_server_api_chasm_v1_message_proto_depIdxs = nil +} diff --git a/api/checksum/v1/message.pb.go b/api/checksum/v1/message.pb.go index deda4f5b761..64fd75c37dd 100644 --- a/api/checksum/v1/message.pb.go +++ b/api/checksum/v1/message.pb.go @@ -273,7 +273,7 @@ var File_temporal_server_api_checksum_v1_message_proto protoreflect.FileDescript const file_temporal_server_api_checksum_v1_message_proto_rawDesc = "" + "\n" + - "-temporal/server/api/checksum/v1/message.proto\x12\x1ftemporal.server.api.checksum.v1\x1a$temporal/api/enums/v1/workflow.proto\x1a,temporal/server/api/history/v1/message.proto\x1a+temporal/server/api/enums/v1/workflow.proto\"\xa2\f\n" + + "-temporal/server/api/checksum/v1/message.proto\x12\x1ftemporal.server.api.checksum.v1\x1a$temporal/api/enums/v1/workflow.proto\x1a+temporal/server/api/enums/v1/workflow.proto\x1a,temporal/server/api/history/v1/message.proto\"\xa2\f\n" + "\x1bMutableStateChecksumPayload\x12)\n" + "\x10cancel_requested\x18\x01 \x01(\bR\x0fcancelRequested\x12J\n" + "\x05state\x18\x02 \x01(\x0e24.temporal.server.api.enums.v1.WorkflowExecutionStateR\x05state\x12F\n" + diff --git a/api/common/v1/api_category.go-helpers.pb.go b/api/common/v1/api_category.go-helpers.pb.go new file mode 100644 index 00000000000..d105bd7080f --- /dev/null +++ b/api/common/v1/api_category.go-helpers.pb.go @@ -0,0 +1,65 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package commonspb + +import ( + "fmt" + + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type ApiCategoryOptions to the protobuf v3 wire format +func (val *ApiCategoryOptions) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ApiCategoryOptions from the protobuf v3 wire format +func (val *ApiCategoryOptions) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ApiCategoryOptions) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ApiCategoryOptions values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ApiCategoryOptions) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ApiCategoryOptions + switch t := that.(type) { + case *ApiCategoryOptions: + that1 = t + case ApiCategoryOptions: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +var ( + ApiCategory_shorthandValue = map[string]int32{ + "Unspecified": 0, + "Standard": 1, + "LongPoll": 2, + "System": 3, + } +) + +// ApiCategoryFromString parses a ApiCategory value from either the protojson +// canonical SCREAMING_CASE enum or the traditional temporal PascalCase enum to ApiCategory +func ApiCategoryFromString(s string) (ApiCategory, error) { + if v, ok := ApiCategory_value[s]; ok { + return ApiCategory(v), nil + } else if v, ok := ApiCategory_shorthandValue[s]; ok { + return ApiCategory(v), nil + } + return ApiCategory(0), fmt.Errorf("%s is not a valid ApiCategory", s) +} diff --git a/api/common/v1/api_category.pb.go b/api/common/v1/api_category.pb.go new file mode 100644 index 00000000000..248dd81cee8 --- /dev/null +++ b/api/common/v1/api_category.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/api/common/v1/api_category.proto + +package commonspb + +import ( + reflect "reflect" + "strconv" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + descriptorpb "google.golang.org/protobuf/types/descriptorpb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ApiCategory int32 + +const ( + // Unspecified API category. Treated as standard API. + API_CATEGORY_UNSPECIFIED ApiCategory = 0 + // Standard API with typical request/response patterns. + API_CATEGORY_STANDARD ApiCategory = 1 + // Long-polling API that intentionally waits for state changes or external events. + // These APIs should be excluded from health signal tracking because their latency + // reflects client wait times and event availability rather than server health. + // Including them in health metrics would skew the data and could cause healthy + // nodes to appear unhealthy. + // + // Examples: PollMutableState, PollWorkflowExecutionUpdate, QueryWorkflow + API_CATEGORY_LONG_POLL ApiCategory = 2 + API_CATEGORY_SYSTEM ApiCategory = 3 +) + +// Enum value maps for ApiCategory. +var ( + ApiCategory_name = map[int32]string{ + 0: "API_CATEGORY_UNSPECIFIED", + 1: "API_CATEGORY_STANDARD", + 2: "API_CATEGORY_LONG_POLL", + 3: "API_CATEGORY_SYSTEM", + } + ApiCategory_value = map[string]int32{ + "API_CATEGORY_UNSPECIFIED": 0, + "API_CATEGORY_STANDARD": 1, + "API_CATEGORY_LONG_POLL": 2, + "API_CATEGORY_SYSTEM": 3, + } +) + +func (x ApiCategory) Enum() *ApiCategory { + p := new(ApiCategory) + *p = x + return p +} + +func (x ApiCategory) String() string { + switch x { + case API_CATEGORY_UNSPECIFIED: + return "Unspecified" + case API_CATEGORY_STANDARD: + return "Standard" + case API_CATEGORY_LONG_POLL: + return "LongPoll" + case API_CATEGORY_SYSTEM: + return "System" + default: + return strconv.Itoa(int(x)) + } + +} + +func (ApiCategory) Descriptor() protoreflect.EnumDescriptor { + return file_temporal_server_api_common_v1_api_category_proto_enumTypes[0].Descriptor() +} + +func (ApiCategory) Type() protoreflect.EnumType { + return &file_temporal_server_api_common_v1_api_category_proto_enumTypes[0] +} + +func (x ApiCategory) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ApiCategory.Descriptor instead. +func (ApiCategory) EnumDescriptor() ([]byte, []int) { + return file_temporal_server_api_common_v1_api_category_proto_rawDescGZIP(), []int{0} +} + +type ApiCategoryOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The category of this API for health and observability purposes. + Category ApiCategory `protobuf:"varint,1,opt,name=category,proto3,enum=temporal.server.api.common.v1.ApiCategory" json:"category,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApiCategoryOptions) Reset() { + *x = ApiCategoryOptions{} + mi := &file_temporal_server_api_common_v1_api_category_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApiCategoryOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiCategoryOptions) ProtoMessage() {} + +func (x *ApiCategoryOptions) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_common_v1_api_category_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApiCategoryOptions.ProtoReflect.Descriptor instead. +func (*ApiCategoryOptions) Descriptor() ([]byte, []int) { + return file_temporal_server_api_common_v1_api_category_proto_rawDescGZIP(), []int{0} +} + +func (x *ApiCategoryOptions) GetCategory() ApiCategory { + if x != nil { + return x.Category + } + return API_CATEGORY_UNSPECIFIED +} + +var file_temporal_server_api_common_v1_api_category_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*descriptorpb.MethodOptions)(nil), + ExtensionType: (*ApiCategoryOptions)(nil), + Field: 50001, + Name: "temporal.server.api.common.v1.api_category", + Tag: "bytes,50001,opt,name=api_category", + Filename: "temporal/server/api/common/v1/api_category.proto", + }, +} + +// Extension fields to descriptorpb.MethodOptions. +var ( + // optional temporal.server.api.common.v1.ApiCategoryOptions api_category = 50001; + E_ApiCategory = &file_temporal_server_api_common_v1_api_category_proto_extTypes[0] +) + +var File_temporal_server_api_common_v1_api_category_proto protoreflect.FileDescriptor + +const file_temporal_server_api_common_v1_api_category_proto_rawDesc = "" + + "\n" + + "0temporal/server/api/common/v1/api_category.proto\x12\x1dtemporal.server.api.common.v1\x1a google/protobuf/descriptor.proto\"\\\n" + + "\x12ApiCategoryOptions\x12F\n" + + "\bcategory\x18\x01 \x01(\x0e2*.temporal.server.api.common.v1.ApiCategoryR\bcategory*{\n" + + "\vApiCategory\x12\x1c\n" + + "\x18API_CATEGORY_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15API_CATEGORY_STANDARD\x10\x01\x12\x1a\n" + + "\x16API_CATEGORY_LONG_POLL\x10\x02\x12\x17\n" + + "\x13API_CATEGORY_SYSTEM\x10\x03:y\n" + + "\fapi_category\x12\x1e.google.protobuf.MethodOptions\x18ц\x03 \x01(\v21.temporal.server.api.common.v1.ApiCategoryOptionsR\vapiCategory\x88\x01\x01B/Z-go.temporal.io/server/api/common/v1;commonspbb\x06proto3" + +var ( + file_temporal_server_api_common_v1_api_category_proto_rawDescOnce sync.Once + file_temporal_server_api_common_v1_api_category_proto_rawDescData []byte +) + +func file_temporal_server_api_common_v1_api_category_proto_rawDescGZIP() []byte { + file_temporal_server_api_common_v1_api_category_proto_rawDescOnce.Do(func() { + file_temporal_server_api_common_v1_api_category_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_api_common_v1_api_category_proto_rawDesc), len(file_temporal_server_api_common_v1_api_category_proto_rawDesc))) + }) + return file_temporal_server_api_common_v1_api_category_proto_rawDescData +} + +var file_temporal_server_api_common_v1_api_category_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_temporal_server_api_common_v1_api_category_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_temporal_server_api_common_v1_api_category_proto_goTypes = []any{ + (ApiCategory)(0), // 0: temporal.server.api.common.v1.ApiCategory + (*ApiCategoryOptions)(nil), // 1: temporal.server.api.common.v1.ApiCategoryOptions + (*descriptorpb.MethodOptions)(nil), // 2: google.protobuf.MethodOptions +} +var file_temporal_server_api_common_v1_api_category_proto_depIdxs = []int32{ + 0, // 0: temporal.server.api.common.v1.ApiCategoryOptions.category:type_name -> temporal.server.api.common.v1.ApiCategory + 2, // 1: temporal.server.api.common.v1.api_category:extendee -> google.protobuf.MethodOptions + 1, // 2: temporal.server.api.common.v1.api_category:type_name -> temporal.server.api.common.v1.ApiCategoryOptions + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 2, // [2:3] is the sub-list for extension type_name + 1, // [1:2] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_temporal_server_api_common_v1_api_category_proto_init() } +func file_temporal_server_api_common_v1_api_category_proto_init() { + if File_temporal_server_api_common_v1_api_category_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_common_v1_api_category_proto_rawDesc), len(file_temporal_server_api_common_v1_api_category_proto_rawDesc)), + NumEnums: 1, + NumMessages: 1, + NumExtensions: 1, + NumServices: 0, + }, + GoTypes: file_temporal_server_api_common_v1_api_category_proto_goTypes, + DependencyIndexes: file_temporal_server_api_common_v1_api_category_proto_depIdxs, + EnumInfos: file_temporal_server_api_common_v1_api_category_proto_enumTypes, + MessageInfos: file_temporal_server_api_common_v1_api_category_proto_msgTypes, + ExtensionInfos: file_temporal_server_api_common_v1_api_category_proto_extTypes, + }.Build() + File_temporal_server_api_common_v1_api_category_proto = out.File + file_temporal_server_api_common_v1_api_category_proto_goTypes = nil + file_temporal_server_api_common_v1_api_category_proto_depIdxs = nil +} diff --git a/api/deployment/v1/message.go-helpers.pb.go b/api/deployment/v1/message.go-helpers.pb.go index bb90e98c601..b54743600d1 100644 --- a/api/deployment/v1/message.go-helpers.pb.go +++ b/api/deployment/v1/message.go-helpers.pb.go @@ -819,6 +819,43 @@ func (this *QueryDescribeWorkerDeploymentResponse) Equal(that interface{}) bool return proto.Equal(this, that1) } +// Marshal an object of type CreateRequestIDQueryResponse to the protobuf v3 wire format +func (val *CreateRequestIDQueryResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateRequestIDQueryResponse from the protobuf v3 wire format +func (val *CreateRequestIDQueryResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateRequestIDQueryResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateRequestIDQueryResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateRequestIDQueryResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateRequestIDQueryResponse + switch t := that.(type) { + case *CreateRequestIDQueryResponse: + that1 = t + case CreateRequestIDQueryResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type StartWorkerDeploymentRequest to the protobuf v3 wire format func (val *StartWorkerDeploymentRequest) Marshal() ([]byte, error) { return proto.Marshal(val) @@ -1226,6 +1263,154 @@ func (this *SetCurrentVersionResponse) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type CreateWorkerDeploymentArgs to the protobuf v3 wire format +func (val *CreateWorkerDeploymentArgs) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateWorkerDeploymentArgs from the protobuf v3 wire format +func (val *CreateWorkerDeploymentArgs) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateWorkerDeploymentArgs) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateWorkerDeploymentArgs values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateWorkerDeploymentArgs) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateWorkerDeploymentArgs + switch t := that.(type) { + case *CreateWorkerDeploymentArgs: + that1 = t + case CreateWorkerDeploymentArgs: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateWorkerDeploymentResponse to the protobuf v3 wire format +func (val *CreateWorkerDeploymentResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateWorkerDeploymentResponse from the protobuf v3 wire format +func (val *CreateWorkerDeploymentResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateWorkerDeploymentResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateWorkerDeploymentResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateWorkerDeploymentResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateWorkerDeploymentResponse + switch t := that.(type) { + case *CreateWorkerDeploymentResponse: + that1 = t + case CreateWorkerDeploymentResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateWorkerDeploymentVersionArgs to the protobuf v3 wire format +func (val *CreateWorkerDeploymentVersionArgs) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateWorkerDeploymentVersionArgs from the protobuf v3 wire format +func (val *CreateWorkerDeploymentVersionArgs) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateWorkerDeploymentVersionArgs) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateWorkerDeploymentVersionArgs values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateWorkerDeploymentVersionArgs) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateWorkerDeploymentVersionArgs + switch t := that.(type) { + case *CreateWorkerDeploymentVersionArgs: + that1 = t + case CreateWorkerDeploymentVersionArgs: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateWorkerDeploymentVersionResponse to the protobuf v3 wire format +func (val *CreateWorkerDeploymentVersionResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateWorkerDeploymentVersionResponse from the protobuf v3 wire format +func (val *CreateWorkerDeploymentVersionResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateWorkerDeploymentVersionResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateWorkerDeploymentVersionResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateWorkerDeploymentVersionResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateWorkerDeploymentVersionResponse + switch t := that.(type) { + case *CreateWorkerDeploymentVersionResponse: + that1 = t + case CreateWorkerDeploymentVersionResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type DeleteVersionArgs to the protobuf v3 wire format func (val *DeleteVersionArgs) Marshal() ([]byte, error) { return proto.Marshal(val) @@ -1744,6 +1929,191 @@ func (this *WorkerDeploymentSummary) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type ValidateWorkerControllerInstanceSpecInput to the protobuf v3 wire format +func (val *ValidateWorkerControllerInstanceSpecInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ValidateWorkerControllerInstanceSpecInput from the protobuf v3 wire format +func (val *ValidateWorkerControllerInstanceSpecInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ValidateWorkerControllerInstanceSpecInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ValidateWorkerControllerInstanceSpecInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ValidateWorkerControllerInstanceSpecInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ValidateWorkerControllerInstanceSpecInput + switch t := that.(type) { + case *ValidateWorkerControllerInstanceSpecInput: + that1 = t + case ValidateWorkerControllerInstanceSpecInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type UpdateWorkerControllerInstanceInput to the protobuf v3 wire format +func (val *UpdateWorkerControllerInstanceInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type UpdateWorkerControllerInstanceInput from the protobuf v3 wire format +func (val *UpdateWorkerControllerInstanceInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *UpdateWorkerControllerInstanceInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two UpdateWorkerControllerInstanceInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *UpdateWorkerControllerInstanceInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *UpdateWorkerControllerInstanceInput + switch t := that.(type) { + case *UpdateWorkerControllerInstanceInput: + that1 = t + case UpdateWorkerControllerInstanceInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteWorkerControllerInstanceInput to the protobuf v3 wire format +func (val *DeleteWorkerControllerInstanceInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteWorkerControllerInstanceInput from the protobuf v3 wire format +func (val *DeleteWorkerControllerInstanceInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteWorkerControllerInstanceInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteWorkerControllerInstanceInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeleteWorkerControllerInstanceInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteWorkerControllerInstanceInput + switch t := that.(type) { + case *DeleteWorkerControllerInstanceInput: + that1 = t + case DeleteWorkerControllerInstanceInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type UpdateComputeConfigArgs to the protobuf v3 wire format +func (val *UpdateComputeConfigArgs) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type UpdateComputeConfigArgs from the protobuf v3 wire format +func (val *UpdateComputeConfigArgs) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *UpdateComputeConfigArgs) Size() int { + return proto.Size(val) +} + +// Equal returns whether two UpdateComputeConfigArgs values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *UpdateComputeConfigArgs) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *UpdateComputeConfigArgs + switch t := that.(type) { + case *UpdateComputeConfigArgs: + that1 = t + case UpdateComputeConfigArgs: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type UpdateComputeConfigResponse to the protobuf v3 wire format +func (val *UpdateComputeConfigResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type UpdateComputeConfigResponse from the protobuf v3 wire format +func (val *UpdateComputeConfigResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *UpdateComputeConfigResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two UpdateComputeConfigResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *UpdateComputeConfigResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *UpdateComputeConfigResponse + switch t := that.(type) { + case *UpdateComputeConfigResponse: + that1 = t + case UpdateComputeConfigResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type ForceCANDeploymentSignalArgs to the protobuf v3 wire format func (val *ForceCANDeploymentSignalArgs) Marshal() ([]byte, error) { return proto.Marshal(val) diff --git a/api/deployment/v1/message.pb.go b/api/deployment/v1/message.pb.go index 9003ec3d7f8..2a65b099db2 100644 --- a/api/deployment/v1/message.pb.go +++ b/api/deployment/v1/message.pb.go @@ -11,7 +11,8 @@ import ( sync "sync" unsafe "unsafe" - v12 "go.temporal.io/api/common/v1" + v13 "go.temporal.io/api/common/v1" + v12 "go.temporal.io/api/compute/v1" v11 "go.temporal.io/api/deployment/v1" v1 "go.temporal.io/api/enums/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -200,6 +201,8 @@ type WorkerDeploymentVersionData struct { // immediately delete the version data from task queues. instead, we mark them as deleted while // keeping the revision number. // Old enough deleted versions are GCed based on update_time. + // Deprecated. This mechanism is not safe against reactivation of versions after delete. + // Use forget_version flag for synchronous deletion of the version data from TQ. Deleted bool `protobuf:"varint,3,opt,name=deleted,proto3" json:"deleted,omitempty"` Status v1.WorkerDeploymentVersionStatus `protobuf:"varint,6,opt,name=status,proto3,enum=temporal.api.enums.v1.WorkerDeploymentVersionStatus" json:"status,omitempty"` unknownFields protoimpl.UnknownFields @@ -324,8 +327,14 @@ type VersionLocalState struct { // Incremented everytime version data synced to TQ changes. Updates with lower revision number // than what is already in the TQ will be ignored to avoid stale writes during async operations. RevisionNumber int64 `protobuf:"varint,15,opt,name=revision_number,json=revisionNumber,proto3" json:"revision_number,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Identity of the last client who modified the configuration of this Version. + // Covers changes through: CreateWorkerDeploymentVersion, UpdateWorkerDeploymentVersionComputeConfig, + // UpdateWorkerDeploymentVersionMetadata. + LastModifierIdentity string `protobuf:"bytes,17,opt,name=last_modifier_identity,json=lastModifierIdentity,proto3" json:"last_modifier_identity,omitempty"` + // Cached compute config summary, kept in sync with the WCI on each compute config update. + ComputeConfig *v12.ComputeConfigSummary `protobuf:"bytes,18,opt,name=compute_config,json=computeConfig,proto3" json:"compute_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VersionLocalState) Reset() { @@ -471,6 +480,20 @@ func (x *VersionLocalState) GetRevisionNumber() int64 { return 0 } +func (x *VersionLocalState) GetLastModifierIdentity() string { + if x != nil { + return x.LastModifierIdentity + } + return "" +} + +func (x *VersionLocalState) GetComputeConfig() *v12.ComputeConfigSummary { + if x != nil { + return x.ComputeConfig + } + return nil +} + // Data specific to a task queue, from the perspective of a worker deployment version. type TaskQueueVersionData struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -653,8 +676,10 @@ type WorkerDeploymentLocalState struct { // Track async propagations in progress per build ID. Map: build_id -> revision numbers. // Used to track which propagations are still pending across continue-as-new. PropagatingRevisions map[string]*PropagatingRevisions `protobuf:"bytes,8,rep,name=propagating_revisions,json=propagatingRevisions,proto3" json:"propagating_revisions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Request ID used to create this worker deployment. + CreateRequestId string `protobuf:"bytes,9,opt,name=create_request_id,json=createRequestId,proto3" json:"create_request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WorkerDeploymentLocalState) Reset() { @@ -743,6 +768,13 @@ func (x *WorkerDeploymentLocalState) GetPropagatingRevisions() map[string]*Propa return nil } +func (x *WorkerDeploymentLocalState) GetCreateRequestId() string { + if x != nil { + return x.CreateRequestId + } + return "" +} + // Tracks revision numbers that are currently propagating for a specific build ID type PropagatingRevisions struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -819,7 +851,13 @@ type WorkerDeploymentVersionSummary struct { // Timestamp when this version last stopped being current or ramping. LastDeactivationTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=last_deactivation_time,json=lastDeactivationTime,proto3" json:"last_deactivation_time,omitempty"` // Status of the Worker Deployment Version. - Status v1.WorkerDeploymentVersionStatus `protobuf:"varint,10,opt,name=status,proto3,enum=temporal.api.enums.v1.WorkerDeploymentVersionStatus" json:"status,omitempty"` + Status v1.WorkerDeploymentVersionStatus `protobuf:"varint,10,opt,name=status,proto3,enum=temporal.api.enums.v1.WorkerDeploymentVersionStatus" json:"status,omitempty"` + // Request ID used to create this version. Used for idempotency. + // Not synced from the version workflow; only set by the deployment workflow. + CreateRequestId string `protobuf:"bytes,12,opt,name=create_request_id,json=createRequestId,proto3" json:"create_request_id,omitempty"` + // Compute config summary for this version. Synced from the version workflow on each compute config update. + // Also set by the deployment workflow at version creation time if a compute config was provided. + ComputeConfig *v12.ComputeConfigSummary `protobuf:"bytes,13,opt,name=compute_config,json=computeConfig,proto3" json:"compute_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -932,6 +970,20 @@ func (x *WorkerDeploymentVersionSummary) GetStatus() v1.WorkerDeploymentVersionS return v1.WorkerDeploymentVersionStatus(0) } +func (x *WorkerDeploymentVersionSummary) GetCreateRequestId() string { + if x != nil { + return x.CreateRequestId + } + return "" +} + +func (x *WorkerDeploymentVersionSummary) GetComputeConfig() *v12.ComputeConfigSummary { + if x != nil { + return x.ComputeConfig + } + return nil +} + // used as Worker Deployment Version workflow update input: type RegisterWorkerInVersionArgs struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1623,6 +1675,59 @@ func (x *QueryDescribeWorkerDeploymentResponse) GetState() *WorkerDeploymentLoca return nil } +// used as Worker Deployment workflow query response: +type CreateRequestIDQueryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + ConflictToken []byte `protobuf:"bytes,2,opt,name=conflict_token,json=conflictToken,proto3" json:"conflict_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRequestIDQueryResponse) Reset() { + *x = CreateRequestIDQueryResponse{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRequestIDQueryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRequestIDQueryResponse) ProtoMessage() {} + +func (x *CreateRequestIDQueryResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRequestIDQueryResponse.ProtoReflect.Descriptor instead. +func (*CreateRequestIDQueryResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{22} +} + +func (x *CreateRequestIDQueryResponse) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *CreateRequestIDQueryResponse) GetConflictToken() []byte { + if x != nil { + return x.ConflictToken + } + return nil +} + // used as Worker Deployment Version workflow activity input: type StartWorkerDeploymentRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1634,7 +1739,7 @@ type StartWorkerDeploymentRequest struct { func (x *StartWorkerDeploymentRequest) Reset() { *x = StartWorkerDeploymentRequest{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[22] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1646,7 +1751,7 @@ func (x *StartWorkerDeploymentRequest) String() string { func (*StartWorkerDeploymentRequest) ProtoMessage() {} func (x *StartWorkerDeploymentRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[22] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1659,7 +1764,7 @@ func (x *StartWorkerDeploymentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartWorkerDeploymentRequest.ProtoReflect.Descriptor instead. func (*StartWorkerDeploymentRequest) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{22} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{23} } func (x *StartWorkerDeploymentRequest) GetDeploymentName() string { @@ -1678,17 +1783,19 @@ func (x *StartWorkerDeploymentRequest) GetRequestId() string { // used as Worker Deployment workflow activity input: type StartWorkerDeploymentVersionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - DeploymentName string `protobuf:"bytes,1,opt,name=deployment_name,json=deploymentName,proto3" json:"deployment_name,omitempty"` - BuildId string `protobuf:"bytes,2,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` - RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + DeploymentName string `protobuf:"bytes,1,opt,name=deployment_name,json=deploymentName,proto3" json:"deployment_name,omitempty"` + BuildId string `protobuf:"bytes,2,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Identity string `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` + ComputeConfig *v12.ComputeConfigSummary `protobuf:"bytes,5,opt,name=compute_config,json=computeConfig,proto3" json:"compute_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StartWorkerDeploymentVersionRequest) Reset() { *x = StartWorkerDeploymentVersionRequest{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[23] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1700,7 +1807,7 @@ func (x *StartWorkerDeploymentVersionRequest) String() string { func (*StartWorkerDeploymentVersionRequest) ProtoMessage() {} func (x *StartWorkerDeploymentVersionRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[23] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1713,7 +1820,7 @@ func (x *StartWorkerDeploymentVersionRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use StartWorkerDeploymentVersionRequest.ProtoReflect.Descriptor instead. func (*StartWorkerDeploymentVersionRequest) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{23} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{24} } func (x *StartWorkerDeploymentVersionRequest) GetDeploymentName() string { @@ -1737,6 +1844,20 @@ func (x *StartWorkerDeploymentVersionRequest) GetRequestId() string { return "" } +func (x *StartWorkerDeploymentVersionRequest) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *StartWorkerDeploymentVersionRequest) GetComputeConfig() *v12.ComputeConfigSummary { + if x != nil { + return x.ComputeConfig + } + return nil +} + // used as Worker Deployment Version workflow activity input: type SyncDeploymentVersionUserDataRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1755,7 +1876,7 @@ type SyncDeploymentVersionUserDataRequest struct { func (x *SyncDeploymentVersionUserDataRequest) Reset() { *x = SyncDeploymentVersionUserDataRequest{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[24] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1767,7 +1888,7 @@ func (x *SyncDeploymentVersionUserDataRequest) String() string { func (*SyncDeploymentVersionUserDataRequest) ProtoMessage() {} func (x *SyncDeploymentVersionUserDataRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[24] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1780,7 +1901,7 @@ func (x *SyncDeploymentVersionUserDataRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use SyncDeploymentVersionUserDataRequest.ProtoReflect.Descriptor instead. func (*SyncDeploymentVersionUserDataRequest) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{24} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{25} } func (x *SyncDeploymentVersionUserDataRequest) GetDeploymentName() string { @@ -1835,7 +1956,7 @@ type SyncDeploymentVersionUserDataResponse struct { func (x *SyncDeploymentVersionUserDataResponse) Reset() { *x = SyncDeploymentVersionUserDataResponse{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[25] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1847,7 +1968,7 @@ func (x *SyncDeploymentVersionUserDataResponse) String() string { func (*SyncDeploymentVersionUserDataResponse) ProtoMessage() {} func (x *SyncDeploymentVersionUserDataResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[25] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1860,7 +1981,7 @@ func (x *SyncDeploymentVersionUserDataResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use SyncDeploymentVersionUserDataResponse.ProtoReflect.Descriptor instead. func (*SyncDeploymentVersionUserDataResponse) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{25} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{26} } func (x *SyncDeploymentVersionUserDataResponse) GetTaskQueueMaxVersions() map[string]int64 { @@ -1880,7 +2001,7 @@ type CheckWorkerDeploymentUserDataPropagationRequest struct { func (x *CheckWorkerDeploymentUserDataPropagationRequest) Reset() { *x = CheckWorkerDeploymentUserDataPropagationRequest{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[26] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1892,7 +2013,7 @@ func (x *CheckWorkerDeploymentUserDataPropagationRequest) String() string { func (*CheckWorkerDeploymentUserDataPropagationRequest) ProtoMessage() {} func (x *CheckWorkerDeploymentUserDataPropagationRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[26] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1905,7 +2026,7 @@ func (x *CheckWorkerDeploymentUserDataPropagationRequest) ProtoReflect() protore // Deprecated: Use CheckWorkerDeploymentUserDataPropagationRequest.ProtoReflect.Descriptor instead. func (*CheckWorkerDeploymentUserDataPropagationRequest) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{26} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{27} } func (x *CheckWorkerDeploymentUserDataPropagationRequest) GetTaskQueueMaxVersions() map[string]int64 { @@ -1926,7 +2047,7 @@ type SyncUnversionedRampActivityArgs struct { func (x *SyncUnversionedRampActivityArgs) Reset() { *x = SyncUnversionedRampActivityArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[27] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1938,7 +2059,7 @@ func (x *SyncUnversionedRampActivityArgs) String() string { func (*SyncUnversionedRampActivityArgs) ProtoMessage() {} func (x *SyncUnversionedRampActivityArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[27] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1951,7 +2072,7 @@ func (x *SyncUnversionedRampActivityArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use SyncUnversionedRampActivityArgs.ProtoReflect.Descriptor instead. func (*SyncUnversionedRampActivityArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{27} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{28} } func (x *SyncUnversionedRampActivityArgs) GetCurrentVersion() string { @@ -1978,7 +2099,7 @@ type SyncUnversionedRampActivityResponse struct { func (x *SyncUnversionedRampActivityResponse) Reset() { *x = SyncUnversionedRampActivityResponse{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[28] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1990,7 +2111,7 @@ func (x *SyncUnversionedRampActivityResponse) String() string { func (*SyncUnversionedRampActivityResponse) ProtoMessage() {} func (x *SyncUnversionedRampActivityResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[28] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2003,7 +2124,7 @@ func (x *SyncUnversionedRampActivityResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use SyncUnversionedRampActivityResponse.ProtoReflect.Descriptor instead. func (*SyncUnversionedRampActivityResponse) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{28} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{29} } func (x *SyncUnversionedRampActivityResponse) GetTaskQueueMaxVersions() map[string]int64 { @@ -2016,7 +2137,7 @@ func (x *SyncUnversionedRampActivityResponse) GetTaskQueueMaxVersions() map[stri // used as Worker Deployment Version workflow update input: type UpdateVersionMetadataArgs struct { state protoimpl.MessageState `protogen:"open.v1"` - UpsertEntries map[string]*v12.Payload `protobuf:"bytes,1,rep,name=upsert_entries,json=upsertEntries,proto3" json:"upsert_entries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + UpsertEntries map[string]*v13.Payload `protobuf:"bytes,1,rep,name=upsert_entries,json=upsertEntries,proto3" json:"upsert_entries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` RemoveEntries []string `protobuf:"bytes,2,rep,name=remove_entries,json=removeEntries,proto3" json:"remove_entries,omitempty"` Identity string `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` unknownFields protoimpl.UnknownFields @@ -2025,7 +2146,7 @@ type UpdateVersionMetadataArgs struct { func (x *UpdateVersionMetadataArgs) Reset() { *x = UpdateVersionMetadataArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[29] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2037,7 +2158,7 @@ func (x *UpdateVersionMetadataArgs) String() string { func (*UpdateVersionMetadataArgs) ProtoMessage() {} func (x *UpdateVersionMetadataArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[29] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2050,10 +2171,10 @@ func (x *UpdateVersionMetadataArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateVersionMetadataArgs.ProtoReflect.Descriptor instead. func (*UpdateVersionMetadataArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{29} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{30} } -func (x *UpdateVersionMetadataArgs) GetUpsertEntries() map[string]*v12.Payload { +func (x *UpdateVersionMetadataArgs) GetUpsertEntries() map[string]*v13.Payload { if x != nil { return x.UpsertEntries } @@ -2084,7 +2205,7 @@ type UpdateVersionMetadataResponse struct { func (x *UpdateVersionMetadataResponse) Reset() { *x = UpdateVersionMetadataResponse{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[30] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2096,7 +2217,7 @@ func (x *UpdateVersionMetadataResponse) String() string { func (*UpdateVersionMetadataResponse) ProtoMessage() {} func (x *UpdateVersionMetadataResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[30] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2109,7 +2230,7 @@ func (x *UpdateVersionMetadataResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateVersionMetadataResponse.ProtoReflect.Descriptor instead. func (*UpdateVersionMetadataResponse) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{30} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{31} } func (x *UpdateVersionMetadataResponse) GetMetadata() *v11.VersionMetadata { @@ -2133,7 +2254,7 @@ type SetCurrentVersionArgs struct { func (x *SetCurrentVersionArgs) Reset() { *x = SetCurrentVersionArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[31] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2145,7 +2266,7 @@ func (x *SetCurrentVersionArgs) String() string { func (*SetCurrentVersionArgs) ProtoMessage() {} func (x *SetCurrentVersionArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[31] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2158,7 +2279,7 @@ func (x *SetCurrentVersionArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use SetCurrentVersionArgs.ProtoReflect.Descriptor instead. func (*SetCurrentVersionArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{31} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{32} } func (x *SetCurrentVersionArgs) GetIdentity() string { @@ -2207,7 +2328,7 @@ type SetCurrentVersionResponse struct { func (x *SetCurrentVersionResponse) Reset() { *x = SetCurrentVersionResponse{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[32] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2219,7 +2340,7 @@ func (x *SetCurrentVersionResponse) String() string { func (*SetCurrentVersionResponse) ProtoMessage() {} func (x *SetCurrentVersionResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[32] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2232,7 +2353,7 @@ func (x *SetCurrentVersionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetCurrentVersionResponse.ProtoReflect.Descriptor instead. func (*SetCurrentVersionResponse) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{32} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{33} } func (x *SetCurrentVersionResponse) GetPreviousVersion() string { @@ -2250,36 +2371,32 @@ func (x *SetCurrentVersionResponse) GetConflictToken() []byte { } // used as Worker Deployment workflow update input: -type DeleteVersionArgs struct { - state protoimpl.MessageState `protogen:"open.v1"` - Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` - Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - SkipDrainage bool `protobuf:"varint,3,opt,name=skip_drainage,json=skipDrainage,proto3" json:"skip_drainage,omitempty"` - // If true, it would mean that the delete operation is initiated by the server internally. This is done on the - // event that the addition of a version exceeds the max number of versions allowed in a worker-deployment (defaultMaxVersions). - // False elsewhere. - ServerDelete bool `protobuf:"varint,4,opt,name=server_delete,json=serverDelete,proto3" json:"server_delete,omitempty"` - // version workflow does not block the update for tq propagation - AsyncPropagation bool `protobuf:"varint,5,opt,name=async_propagation,json=asyncPropagation,proto3" json:"async_propagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type CreateWorkerDeploymentArgs struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + // Retrying with same request id is a successful no-op. + // Retrying with different request id is an error. + // One deployment is deleted, same or different request id will re-create it. + RequestId string `protobuf:"bytes,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *DeleteVersionArgs) Reset() { - *x = DeleteVersionArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[33] +func (x *CreateWorkerDeploymentArgs) Reset() { + *x = CreateWorkerDeploymentArgs{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteVersionArgs) String() string { +func (x *CreateWorkerDeploymentArgs) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteVersionArgs) ProtoMessage() {} +func (*CreateWorkerDeploymentArgs) ProtoMessage() {} -func (x *DeleteVersionArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[33] +func (x *CreateWorkerDeploymentArgs) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2290,74 +2407,99 @@ func (x *DeleteVersionArgs) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteVersionArgs.ProtoReflect.Descriptor instead. -func (*DeleteVersionArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{33} +// Deprecated: Use CreateWorkerDeploymentArgs.ProtoReflect.Descriptor instead. +func (*CreateWorkerDeploymentArgs) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{34} } -func (x *DeleteVersionArgs) GetIdentity() string { +func (x *CreateWorkerDeploymentArgs) GetIdentity() string { if x != nil { return x.Identity } return "" } -func (x *DeleteVersionArgs) GetVersion() string { +func (x *CreateWorkerDeploymentArgs) GetRequestId() string { if x != nil { - return x.Version + return x.RequestId } return "" } -func (x *DeleteVersionArgs) GetSkipDrainage() bool { - if x != nil { - return x.SkipDrainage - } - return false +// used as Worker Deployment update response: +type CreateWorkerDeploymentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConflictToken []byte `protobuf:"bytes,1,opt,name=conflict_token,json=conflictToken,proto3" json:"conflict_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *DeleteVersionArgs) GetServerDelete() bool { +func (x *CreateWorkerDeploymentResponse) Reset() { + *x = CreateWorkerDeploymentResponse{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateWorkerDeploymentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWorkerDeploymentResponse) ProtoMessage() {} + +func (x *CreateWorkerDeploymentResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[35] if x != nil { - return x.ServerDelete + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return false + return mi.MessageOf(x) } -func (x *DeleteVersionArgs) GetAsyncPropagation() bool { +// Deprecated: Use CreateWorkerDeploymentResponse.ProtoReflect.Descriptor instead. +func (*CreateWorkerDeploymentResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{35} +} + +func (x *CreateWorkerDeploymentResponse) GetConflictToken() []byte { if x != nil { - return x.AsyncPropagation + return x.ConflictToken } - return false + return nil } -// used as Worker Deployment Activity input: -type DeleteVersionActivityArgs struct { - state protoimpl.MessageState `protogen:"open.v1"` - Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` - DeploymentName string `protobuf:"bytes,2,opt,name=deployment_name,json=deploymentName,proto3" json:"deployment_name,omitempty"` - Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - SkipDrainage bool `protobuf:"varint,5,opt,name=skip_drainage,json=skipDrainage,proto3" json:"skip_drainage,omitempty"` - AsyncPropagation bool `protobuf:"varint,6,opt,name=async_propagation,json=asyncPropagation,proto3" json:"async_propagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// used as Worker Deployment workflow update input: +type CreateWorkerDeploymentVersionArgs struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + // Retrying with same request id is a successful no-op. + // Retrying with different request id (including auto-created) is an error. + RequestId string `protobuf:"bytes,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Version string (.) + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + ComputeConfig *v12.ComputeConfig `protobuf:"bytes,4,opt,name=compute_config,json=computeConfig,proto3" json:"compute_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *DeleteVersionActivityArgs) Reset() { - *x = DeleteVersionActivityArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[34] +func (x *CreateWorkerDeploymentVersionArgs) Reset() { + *x = CreateWorkerDeploymentVersionArgs{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteVersionActivityArgs) String() string { +func (x *CreateWorkerDeploymentVersionArgs) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteVersionActivityArgs) ProtoMessage() {} +func (*CreateWorkerDeploymentVersionArgs) ProtoMessage() {} -func (x *DeleteVersionActivityArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[34] +func (x *CreateWorkerDeploymentVersionArgs) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2368,35 +2510,224 @@ func (x *DeleteVersionActivityArgs) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteVersionActivityArgs.ProtoReflect.Descriptor instead. -func (*DeleteVersionActivityArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{34} +// Deprecated: Use CreateWorkerDeploymentVersionArgs.ProtoReflect.Descriptor instead. +func (*CreateWorkerDeploymentVersionArgs) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{36} } -func (x *DeleteVersionActivityArgs) GetIdentity() string { +func (x *CreateWorkerDeploymentVersionArgs) GetIdentity() string { if x != nil { return x.Identity } return "" } -func (x *DeleteVersionActivityArgs) GetDeploymentName() string { +func (x *CreateWorkerDeploymentVersionArgs) GetRequestId() string { if x != nil { - return x.DeploymentName + return x.RequestId } return "" } -func (x *DeleteVersionActivityArgs) GetVersion() string { +func (x *CreateWorkerDeploymentVersionArgs) GetVersion() string { if x != nil { return x.Version } return "" } -func (x *DeleteVersionActivityArgs) GetRequestId() string { +func (x *CreateWorkerDeploymentVersionArgs) GetComputeConfig() *v12.ComputeConfig { if x != nil { - return x.RequestId + return x.ComputeConfig + } + return nil +} + +// used as Worker Deployment update response: +type CreateWorkerDeploymentVersionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateWorkerDeploymentVersionResponse) Reset() { + *x = CreateWorkerDeploymentVersionResponse{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateWorkerDeploymentVersionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWorkerDeploymentVersionResponse) ProtoMessage() {} + +func (x *CreateWorkerDeploymentVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateWorkerDeploymentVersionResponse.ProtoReflect.Descriptor instead. +func (*CreateWorkerDeploymentVersionResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{37} +} + +// used as Worker Deployment workflow update input: +type DeleteVersionArgs struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + SkipDrainage bool `protobuf:"varint,3,opt,name=skip_drainage,json=skipDrainage,proto3" json:"skip_drainage,omitempty"` + // If true, it would mean that the delete operation is initiated by the server internally. This is done on the + // event that the addition of a version exceeds the max number of versions allowed in a worker-deployment (defaultMaxVersions). + // False elsewhere. + ServerDelete bool `protobuf:"varint,4,opt,name=server_delete,json=serverDelete,proto3" json:"server_delete,omitempty"` + // version workflow does not block the update for tq propagation + AsyncPropagation bool `protobuf:"varint,5,opt,name=async_propagation,json=asyncPropagation,proto3" json:"async_propagation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteVersionArgs) Reset() { + *x = DeleteVersionArgs{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteVersionArgs) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteVersionArgs) ProtoMessage() {} + +func (x *DeleteVersionArgs) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteVersionArgs.ProtoReflect.Descriptor instead. +func (*DeleteVersionArgs) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{38} +} + +func (x *DeleteVersionArgs) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *DeleteVersionArgs) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *DeleteVersionArgs) GetSkipDrainage() bool { + if x != nil { + return x.SkipDrainage + } + return false +} + +func (x *DeleteVersionArgs) GetServerDelete() bool { + if x != nil { + return x.ServerDelete + } + return false +} + +func (x *DeleteVersionArgs) GetAsyncPropagation() bool { + if x != nil { + return x.AsyncPropagation + } + return false +} + +// used as Worker Deployment Activity input: +type DeleteVersionActivityArgs struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + DeploymentName string `protobuf:"bytes,2,opt,name=deployment_name,json=deploymentName,proto3" json:"deployment_name,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + SkipDrainage bool `protobuf:"varint,5,opt,name=skip_drainage,json=skipDrainage,proto3" json:"skip_drainage,omitempty"` + AsyncPropagation bool `protobuf:"varint,6,opt,name=async_propagation,json=asyncPropagation,proto3" json:"async_propagation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteVersionActivityArgs) Reset() { + *x = DeleteVersionActivityArgs{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteVersionActivityArgs) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteVersionActivityArgs) ProtoMessage() {} + +func (x *DeleteVersionActivityArgs) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteVersionActivityArgs.ProtoReflect.Descriptor instead. +func (*DeleteVersionActivityArgs) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{39} +} + +func (x *DeleteVersionActivityArgs) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *DeleteVersionActivityArgs) GetDeploymentName() string { + if x != nil { + return x.DeploymentName + } + return "" +} + +func (x *DeleteVersionActivityArgs) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *DeleteVersionActivityArgs) GetRequestId() string { + if x != nil { + return x.RequestId } return "" } @@ -2427,7 +2758,7 @@ type CheckTaskQueuesHavePollersActivityArgs struct { func (x *CheckTaskQueuesHavePollersActivityArgs) Reset() { *x = CheckTaskQueuesHavePollersActivityArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[35] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2439,7 +2770,7 @@ func (x *CheckTaskQueuesHavePollersActivityArgs) String() string { func (*CheckTaskQueuesHavePollersActivityArgs) ProtoMessage() {} func (x *CheckTaskQueuesHavePollersActivityArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[35] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2452,7 +2783,7 @@ func (x *CheckTaskQueuesHavePollersActivityArgs) ProtoReflect() protoreflect.Mes // Deprecated: Use CheckTaskQueuesHavePollersActivityArgs.ProtoReflect.Descriptor instead. func (*CheckTaskQueuesHavePollersActivityArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{35} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{40} } func (x *CheckTaskQueuesHavePollersActivityArgs) GetTaskQueuesAndTypes() map[string]*CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes { @@ -2479,7 +2810,7 @@ type DeleteDeploymentArgs struct { func (x *DeleteDeploymentArgs) Reset() { *x = DeleteDeploymentArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[36] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2491,7 +2822,7 @@ func (x *DeleteDeploymentArgs) String() string { func (*DeleteDeploymentArgs) ProtoMessage() {} func (x *DeleteDeploymentArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[36] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2504,7 +2835,7 @@ func (x *DeleteDeploymentArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteDeploymentArgs.ProtoReflect.Descriptor instead. func (*DeleteDeploymentArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{36} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{41} } func (x *DeleteDeploymentArgs) GetIdentity() string { @@ -2526,7 +2857,7 @@ type SetRampingVersionResponse struct { func (x *SetRampingVersionResponse) Reset() { *x = SetRampingVersionResponse{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[37] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2538,7 +2869,7 @@ func (x *SetRampingVersionResponse) String() string { func (*SetRampingVersionResponse) ProtoMessage() {} func (x *SetRampingVersionResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[37] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2551,7 +2882,7 @@ func (x *SetRampingVersionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRampingVersionResponse.ProtoReflect.Descriptor instead. func (*SetRampingVersionResponse) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{37} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{42} } func (x *SetRampingVersionResponse) GetPreviousVersion() string { @@ -2590,7 +2921,7 @@ type SetRampingVersionArgs struct { func (x *SetRampingVersionArgs) Reset() { *x = SetRampingVersionArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[38] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2602,7 +2933,7 @@ func (x *SetRampingVersionArgs) String() string { func (*SetRampingVersionArgs) ProtoMessage() {} func (x *SetRampingVersionArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[38] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2615,7 +2946,7 @@ func (x *SetRampingVersionArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRampingVersionArgs.ProtoReflect.Descriptor instead. func (*SetRampingVersionArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{38} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{43} } func (x *SetRampingVersionArgs) GetIdentity() string { @@ -2674,7 +3005,7 @@ type SetManagerIdentityArgs struct { func (x *SetManagerIdentityArgs) Reset() { *x = SetManagerIdentityArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[39] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2686,7 +3017,7 @@ func (x *SetManagerIdentityArgs) String() string { func (*SetManagerIdentityArgs) ProtoMessage() {} func (x *SetManagerIdentityArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[39] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2699,7 +3030,7 @@ func (x *SetManagerIdentityArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use SetManagerIdentityArgs.ProtoReflect.Descriptor instead. func (*SetManagerIdentityArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{39} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{44} } func (x *SetManagerIdentityArgs) GetIdentity() string { @@ -2734,7 +3065,7 @@ type SetManagerIdentityResponse struct { func (x *SetManagerIdentityResponse) Reset() { *x = SetManagerIdentityResponse{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[40] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2746,7 +3077,7 @@ func (x *SetManagerIdentityResponse) String() string { func (*SetManagerIdentityResponse) ProtoMessage() {} func (x *SetManagerIdentityResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[40] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2759,7 +3090,7 @@ func (x *SetManagerIdentityResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetManagerIdentityResponse.ProtoReflect.Descriptor instead. func (*SetManagerIdentityResponse) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{40} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{45} } func (x *SetManagerIdentityResponse) GetPreviousManagerIdentity() string { @@ -2790,7 +3121,7 @@ type SyncVersionStateActivityArgs struct { func (x *SyncVersionStateActivityArgs) Reset() { *x = SyncVersionStateActivityArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[41] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2802,7 +3133,7 @@ func (x *SyncVersionStateActivityArgs) String() string { func (*SyncVersionStateActivityArgs) ProtoMessage() {} func (x *SyncVersionStateActivityArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[41] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2815,7 +3146,7 @@ func (x *SyncVersionStateActivityArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use SyncVersionStateActivityArgs.ProtoReflect.Descriptor instead. func (*SyncVersionStateActivityArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{41} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{46} } func (x *SyncVersionStateActivityArgs) GetDeploymentName() string { @@ -2858,7 +3189,7 @@ type SyncVersionStateActivityResult struct { func (x *SyncVersionStateActivityResult) Reset() { *x = SyncVersionStateActivityResult{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[42] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2870,7 +3201,7 @@ func (x *SyncVersionStateActivityResult) String() string { func (*SyncVersionStateActivityResult) ProtoMessage() {} func (x *SyncVersionStateActivityResult) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[42] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2883,7 +3214,7 @@ func (x *SyncVersionStateActivityResult) ProtoReflect() protoreflect.Message { // Deprecated: Use SyncVersionStateActivityResult.ProtoReflect.Descriptor instead. func (*SyncVersionStateActivityResult) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{42} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{47} } // Deprecated: Marked as deprecated in temporal/server/api/deployment/v1/message.proto. @@ -2912,7 +3243,7 @@ type IsVersionMissingTaskQueuesArgs struct { func (x *IsVersionMissingTaskQueuesArgs) Reset() { *x = IsVersionMissingTaskQueuesArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[43] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2924,7 +3255,7 @@ func (x *IsVersionMissingTaskQueuesArgs) String() string { func (*IsVersionMissingTaskQueuesArgs) ProtoMessage() {} func (x *IsVersionMissingTaskQueuesArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[43] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2937,7 +3268,7 @@ func (x *IsVersionMissingTaskQueuesArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use IsVersionMissingTaskQueuesArgs.ProtoReflect.Descriptor instead. func (*IsVersionMissingTaskQueuesArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{43} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{48} } func (x *IsVersionMissingTaskQueuesArgs) GetPrevCurrentVersion() string { @@ -2964,7 +3295,7 @@ type IsVersionMissingTaskQueuesResult struct { func (x *IsVersionMissingTaskQueuesResult) Reset() { *x = IsVersionMissingTaskQueuesResult{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[44] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2976,7 +3307,7 @@ func (x *IsVersionMissingTaskQueuesResult) String() string { func (*IsVersionMissingTaskQueuesResult) ProtoMessage() {} func (x *IsVersionMissingTaskQueuesResult) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[44] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2989,7 +3320,7 @@ func (x *IsVersionMissingTaskQueuesResult) ProtoReflect() protoreflect.Message { // Deprecated: Use IsVersionMissingTaskQueuesResult.ProtoReflect.Descriptor instead. func (*IsVersionMissingTaskQueuesResult) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{44} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{49} } func (x *IsVersionMissingTaskQueuesResult) GetIsMissingTaskQueues() bool { @@ -3014,7 +3345,7 @@ type WorkerDeploymentWorkflowMemo struct { func (x *WorkerDeploymentWorkflowMemo) Reset() { *x = WorkerDeploymentWorkflowMemo{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[45] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3026,7 +3357,7 @@ func (x *WorkerDeploymentWorkflowMemo) String() string { func (*WorkerDeploymentWorkflowMemo) ProtoMessage() {} func (x *WorkerDeploymentWorkflowMemo) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[45] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3039,7 +3370,7 @@ func (x *WorkerDeploymentWorkflowMemo) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkerDeploymentWorkflowMemo.ProtoReflect.Descriptor instead. func (*WorkerDeploymentWorkflowMemo) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{45} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{50} } func (x *WorkerDeploymentWorkflowMemo) GetDeploymentName() string { @@ -3099,7 +3430,7 @@ type WorkerDeploymentSummary struct { func (x *WorkerDeploymentSummary) Reset() { *x = WorkerDeploymentSummary{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[46] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3111,7 +3442,7 @@ func (x *WorkerDeploymentSummary) String() string { func (*WorkerDeploymentSummary) ProtoMessage() {} func (x *WorkerDeploymentSummary) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[46] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3124,7 +3455,7 @@ func (x *WorkerDeploymentSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkerDeploymentSummary.ProtoReflect.Descriptor instead. func (*WorkerDeploymentSummary) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{46} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{51} } func (x *WorkerDeploymentSummary) GetName() string { @@ -3169,6 +3500,284 @@ func (x *WorkerDeploymentSummary) GetRampingVersionSummary() *v11.WorkerDeployme return nil } +// Input for the activity that validates compute config scaling groups via +// the Worker Controller Instance client. +type ValidateWorkerControllerInstanceSpecInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + ScalingGroups map[string]*v12.ComputeConfigScalingGroup `protobuf:"bytes,1,rep,name=scaling_groups,json=scalingGroups,proto3" json:"scaling_groups,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateWorkerControllerInstanceSpecInput) Reset() { + *x = ValidateWorkerControllerInstanceSpecInput{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateWorkerControllerInstanceSpecInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateWorkerControllerInstanceSpecInput) ProtoMessage() {} + +func (x *ValidateWorkerControllerInstanceSpecInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateWorkerControllerInstanceSpecInput.ProtoReflect.Descriptor instead. +func (*ValidateWorkerControllerInstanceSpecInput) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{52} +} + +func (x *ValidateWorkerControllerInstanceSpecInput) GetScalingGroups() map[string]*v12.ComputeConfigScalingGroup { + if x != nil { + return x.ScalingGroups + } + return nil +} + +// used as activity input for creating or updating a Worker Controller Instance +// via the WCI client. +type UpdateWorkerControllerInstanceInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version *v11.WorkerDeploymentVersion `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Identity string `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` + UpsertScalingGroups map[string]*v12.ComputeConfigScalingGroupUpdate `protobuf:"bytes,3,rep,name=upsert_scaling_groups,json=upsertScalingGroups,proto3" json:"upsert_scaling_groups,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + RemoveScalingGroups []string `protobuf:"bytes,4,rep,name=remove_scaling_groups,json=removeScalingGroups,proto3" json:"remove_scaling_groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateWorkerControllerInstanceInput) Reset() { + *x = UpdateWorkerControllerInstanceInput{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateWorkerControllerInstanceInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateWorkerControllerInstanceInput) ProtoMessage() {} + +func (x *UpdateWorkerControllerInstanceInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateWorkerControllerInstanceInput.ProtoReflect.Descriptor instead. +func (*UpdateWorkerControllerInstanceInput) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{53} +} + +func (x *UpdateWorkerControllerInstanceInput) GetVersion() *v11.WorkerDeploymentVersion { + if x != nil { + return x.Version + } + return nil +} + +func (x *UpdateWorkerControllerInstanceInput) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *UpdateWorkerControllerInstanceInput) GetUpsertScalingGroups() map[string]*v12.ComputeConfigScalingGroupUpdate { + if x != nil { + return x.UpsertScalingGroups + } + return nil +} + +func (x *UpdateWorkerControllerInstanceInput) GetRemoveScalingGroups() []string { + if x != nil { + return x.RemoveScalingGroups + } + return nil +} + +// used as activity input for deleting a Worker Controller Instance +// via the WCI client. +type DeleteWorkerControllerInstanceInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version *v11.WorkerDeploymentVersion `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Identity string `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkerControllerInstanceInput) Reset() { + *x = DeleteWorkerControllerInstanceInput{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkerControllerInstanceInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkerControllerInstanceInput) ProtoMessage() {} + +func (x *DeleteWorkerControllerInstanceInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWorkerControllerInstanceInput.ProtoReflect.Descriptor instead. +func (*DeleteWorkerControllerInstanceInput) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{54} +} + +func (x *DeleteWorkerControllerInstanceInput) GetVersion() *v11.WorkerDeploymentVersion { + if x != nil { + return x.Version + } + return nil +} + +func (x *DeleteWorkerControllerInstanceInput) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +// Input for the UpdateComputeConfig workflow update on a version workflow. +type UpdateComputeConfigArgs struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + RequestId string `protobuf:"bytes,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Scaling groups to add or update. + UpsertScalingGroups map[string]*v12.ComputeConfigScalingGroupUpdate `protobuf:"bytes,3,rep,name=upsert_scaling_groups,json=upsertScalingGroups,proto3" json:"upsert_scaling_groups,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Names of scaling groups to remove. Names that don't match an existing group are ignored. + RemoveScalingGroups []string `protobuf:"bytes,4,rep,name=remove_scaling_groups,json=removeScalingGroups,proto3" json:"remove_scaling_groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateComputeConfigArgs) Reset() { + *x = UpdateComputeConfigArgs{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateComputeConfigArgs) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateComputeConfigArgs) ProtoMessage() {} + +func (x *UpdateComputeConfigArgs) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateComputeConfigArgs.ProtoReflect.Descriptor instead. +func (*UpdateComputeConfigArgs) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{55} +} + +func (x *UpdateComputeConfigArgs) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *UpdateComputeConfigArgs) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *UpdateComputeConfigArgs) GetUpsertScalingGroups() map[string]*v12.ComputeConfigScalingGroupUpdate { + if x != nil { + return x.UpsertScalingGroups + } + return nil +} + +func (x *UpdateComputeConfigArgs) GetRemoveScalingGroups() []string { + if x != nil { + return x.RemoveScalingGroups + } + return nil +} + +// Response for the UpdateComputeConfig workflow update on a version workflow. +type UpdateComputeConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateComputeConfigResponse) Reset() { + *x = UpdateComputeConfigResponse{} + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateComputeConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateComputeConfigResponse) ProtoMessage() {} + +func (x *UpdateComputeConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateComputeConfigResponse.ProtoReflect.Descriptor instead. +func (*UpdateComputeConfigResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{56} +} + // Signal input for force-continue-as-new on Deployment workflow type ForceCANDeploymentSignalArgs struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3181,7 +3790,7 @@ type ForceCANDeploymentSignalArgs struct { func (x *ForceCANDeploymentSignalArgs) Reset() { *x = ForceCANDeploymentSignalArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[47] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3193,7 +3802,7 @@ func (x *ForceCANDeploymentSignalArgs) String() string { func (*ForceCANDeploymentSignalArgs) ProtoMessage() {} func (x *ForceCANDeploymentSignalArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[47] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3206,7 +3815,7 @@ func (x *ForceCANDeploymentSignalArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use ForceCANDeploymentSignalArgs.ProtoReflect.Descriptor instead. func (*ForceCANDeploymentSignalArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{47} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{57} } func (x *ForceCANDeploymentSignalArgs) GetOverrideState() *WorkerDeploymentLocalState { @@ -3228,7 +3837,7 @@ type ForceCANVersionSignalArgs struct { func (x *ForceCANVersionSignalArgs) Reset() { *x = ForceCANVersionSignalArgs{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[48] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3240,7 +3849,7 @@ func (x *ForceCANVersionSignalArgs) String() string { func (*ForceCANVersionSignalArgs) ProtoMessage() {} func (x *ForceCANVersionSignalArgs) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[48] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3253,7 +3862,7 @@ func (x *ForceCANVersionSignalArgs) ProtoReflect() protoreflect.Message { // Deprecated: Use ForceCANVersionSignalArgs.ProtoReflect.Descriptor instead. func (*ForceCANVersionSignalArgs) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{48} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{58} } func (x *ForceCANVersionSignalArgs) GetOverrideState() *VersionLocalState { @@ -3273,7 +3882,7 @@ type VersionLocalState_TaskQueueFamilyData struct { func (x *VersionLocalState_TaskQueueFamilyData) Reset() { *x = VersionLocalState_TaskQueueFamilyData{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[50] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3285,7 +3894,7 @@ func (x *VersionLocalState_TaskQueueFamilyData) String() string { func (*VersionLocalState_TaskQueueFamilyData) ProtoMessage() {} func (x *VersionLocalState_TaskQueueFamilyData) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[50] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3319,7 +3928,7 @@ type SyncDeploymentVersionUserDataRequest_SyncUserData struct { func (x *SyncDeploymentVersionUserDataRequest_SyncUserData) Reset() { *x = SyncDeploymentVersionUserDataRequest_SyncUserData{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[54] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3331,7 +3940,7 @@ func (x *SyncDeploymentVersionUserDataRequest_SyncUserData) String() string { func (*SyncDeploymentVersionUserDataRequest_SyncUserData) ProtoMessage() {} func (x *SyncDeploymentVersionUserDataRequest_SyncUserData) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[54] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3344,7 +3953,7 @@ func (x *SyncDeploymentVersionUserDataRequest_SyncUserData) ProtoReflect() proto // Deprecated: Use SyncDeploymentVersionUserDataRequest_SyncUserData.ProtoReflect.Descriptor instead. func (*SyncDeploymentVersionUserDataRequest_SyncUserData) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{24, 0} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{25, 0} } func (x *SyncDeploymentVersionUserDataRequest_SyncUserData) GetName() string { @@ -3377,7 +3986,7 @@ type CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes struct { func (x *CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes) Reset() { *x = CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes{} - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[60] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3389,7 +3998,7 @@ func (x *CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes) String() string func (*CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes) ProtoMessage() {} func (x *CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[60] + mi := &file_temporal_server_api_deployment_v1_message_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3402,7 +4011,7 @@ func (x *CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes) ProtoReflect() p // Deprecated: Use CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes.ProtoReflect.Descriptor instead. func (*CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes) Descriptor() ([]byte, []int) { - return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{35, 1} + return file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP(), []int{40, 1} } func (x *CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes) GetTypes() []v1.TaskQueueType { @@ -3416,7 +4025,7 @@ var File_temporal_server_api_deployment_v1_message_proto protoreflect.FileDescri const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\n" + - "/temporal/server/api/deployment/v1/message.proto\x12!temporal.server.api.deployment.v1\x1a&temporal/api/enums/v1/task_queue.proto\x1a&temporal/api/enums/v1/deployment.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a(temporal/api/deployment/v1/message.proto\x1a$temporal/api/common/v1/message.proto\"]\n" + + "/temporal/server/api/deployment/v1/message.proto\x12!temporal.server.api.deployment.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/compute/v1/config.proto\x1a(temporal/api/deployment/v1/message.proto\x1a&temporal/api/enums/v1/deployment.proto\x1a&temporal/api/enums/v1/task_queue.proto\"]\n" + "\x17WorkerDeploymentVersion\x12'\n" + "\x0fdeployment_name\x18\x01 \x01(\tR\x0edeploymentName\x12\x19\n" + "\bbuild_id\x18\x02 \x01(\tR\abuildId\"\xc4\x03\n" + @@ -3432,7 +4041,7 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\vupdate_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + "updateTime\x12\x18\n" + "\adeleted\x18\x03 \x01(\bR\adeleted\x12L\n" + - "\x06status\x18\x06 \x01(\x0e24.temporal.api.enums.v1.WorkerDeploymentVersionStatusR\x06status\"\xb4\f\n" + + "\x06status\x18\x06 \x01(\x0e24.temporal.api.enums.v1.WorkerDeploymentVersionStatusR\x06status\"\xc0\r\n" + "\x11VersionLocalState\x12T\n" + "\aversion\x18\x01 \x01(\v2:.temporal.server.api.deployment.v1.WorkerDeploymentVersionR\aversion\x12;\n" + "\vcreate_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + @@ -3451,7 +4060,9 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + " \x03(\v2K.temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamiliesEntryR\x11taskQueueFamilies\x12&\n" + "\x0fsync_batch_size\x18\v \x01(\x05R\rsyncBatchSize\x12L\n" + "\x06status\x18\x0e \x01(\x0e24.temporal.api.enums.v1.WorkerDeploymentVersionStatusR\x06status\x12'\n" + - "\x0frevision_number\x18\x0f \x01(\x03R\x0erevisionNumber\x1a\x8e\x01\n" + + "\x0frevision_number\x18\x0f \x01(\x03R\x0erevisionNumber\x124\n" + + "\x16last_modifier_identity\x18\x11 \x01(\tR\x14lastModifierIdentity\x12T\n" + + "\x0ecompute_config\x18\x12 \x01(\v2-.temporal.api.compute.v1.ComputeConfigSummaryR\rcomputeConfig\x1a\x8e\x01\n" + "\x16TaskQueueFamiliesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12^\n" + "\x05value\x18\x02 \x01(\v2H.temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyDataR\x05value:\x028\x01\x1a\x88\x02\n" + @@ -3470,7 +4081,7 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\x0enamespace_name\x18\x01 \x01(\tR\rnamespaceName\x12!\n" + "\fnamespace_id\x18\x02 \x01(\tR\vnamespaceId\x12'\n" + "\x0fdeployment_name\x18\x03 \x01(\tR\x0edeploymentName\x12S\n" + - "\x05state\x18\x04 \x01(\v2=.temporal.server.api.deployment.v1.WorkerDeploymentLocalStateR\x05state\"\xd6\x06\n" + + "\x05state\x18\x04 \x01(\v2=.temporal.server.api.deployment.v1.WorkerDeploymentLocalStateR\x05state\"\x82\a\n" + "\x1aWorkerDeploymentLocalState\x12;\n" + "\vcreate_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\n" + "createTime\x12P\n" + @@ -3480,7 +4091,8 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\x16last_modifier_identity\x18\x05 \x01(\tR\x14lastModifierIdentity\x12&\n" + "\x0fsync_batch_size\x18\x06 \x01(\x05R\rsyncBatchSize\x12)\n" + "\x10manager_identity\x18\a \x01(\tR\x0fmanagerIdentity\x12\x8c\x01\n" + - "\x15propagating_revisions\x18\b \x03(\v2W.temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntryR\x14propagatingRevisions\x1a~\n" + + "\x15propagating_revisions\x18\b \x03(\v2W.temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntryR\x14propagatingRevisions\x12*\n" + + "\x11create_request_id\x18\t \x01(\tR\x0fcreateRequestId\x1a~\n" + "\rVersionsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12W\n" + "\x05value\x18\x02 \x01(\v2A.temporal.server.api.deployment.v1.WorkerDeploymentVersionSummaryR\x05value:\x028\x01\x1a\x80\x01\n" + @@ -3488,7 +4100,7 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\tR\x03key\x12M\n" + "\x05value\x18\x02 \x01(\v27.temporal.server.api.deployment.v1.PropagatingRevisionsR\x05value:\x028\x01\"A\n" + "\x14PropagatingRevisions\x12)\n" + - "\x10revision_numbers\x18\x01 \x03(\x03R\x0frevisionNumbers\"\xc0\x06\n" + + "\x10revision_numbers\x18\x01 \x03(\x03R\x0frevisionNumbers\"\xc2\a\n" + "\x1eWorkerDeploymentVersionSummary\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12;\n" + "\vcreate_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + @@ -3502,7 +4114,9 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\x11last_current_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x0flastCurrentTime\x12P\n" + "\x16last_deactivation_time\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\x14lastDeactivationTime\x12L\n" + "\x06status\x18\n" + - " \x01(\x0e24.temporal.api.enums.v1.WorkerDeploymentVersionStatusR\x06status\"\xa7\x02\n" + + " \x01(\x0e24.temporal.api.enums.v1.WorkerDeploymentVersionStatusR\x06status\x12*\n" + + "\x11create_request_id\x18\f \x01(\tR\x0fcreateRequestId\x12T\n" + + "\x0ecompute_config\x18\r \x01(\v2-.temporal.api.compute.v1.ComputeConfigSummaryR\rcomputeConfig\"\xa7\x02\n" + "\x1bRegisterWorkerInVersionArgs\x12&\n" + "\x0ftask_queue_name\x18\x01 \x01(\tR\rtaskQueueName\x12L\n" + "\x0ftask_queue_type\x18\x02 \x01(\x0e2$.temporal.api.enums.v1.TaskQueueTypeR\rtaskQueueType\x12&\n" + @@ -3542,16 +4156,22 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\x1cQueryDescribeVersionResponse\x12Y\n" + "\rversion_state\x18\x01 \x01(\v24.temporal.server.api.deployment.v1.VersionLocalStateR\fversionState\"|\n" + "%QueryDescribeWorkerDeploymentResponse\x12S\n" + - "\x05state\x18\x01 \x01(\v2=.temporal.server.api.deployment.v1.WorkerDeploymentLocalStateR\x05state\"f\n" + + "\x05state\x18\x01 \x01(\v2=.temporal.server.api.deployment.v1.WorkerDeploymentLocalStateR\x05state\"d\n" + + "\x1cCreateRequestIDQueryResponse\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12%\n" + + "\x0econflict_token\x18\x02 \x01(\fR\rconflictToken\"f\n" + "\x1cStartWorkerDeploymentRequest\x12'\n" + "\x0fdeployment_name\x18\x01 \x01(\tR\x0edeploymentName\x12\x1d\n" + "\n" + - "request_id\x18\x02 \x01(\tR\trequestId\"\x88\x01\n" + + "request_id\x18\x02 \x01(\tR\trequestId\"\xfa\x01\n" + "#StartWorkerDeploymentVersionRequest\x12'\n" + "\x0fdeployment_name\x18\x01 \x01(\tR\x0edeploymentName\x12\x19\n" + "\bbuild_id\x18\x02 \x01(\tR\abuildId\x12\x1d\n" + "\n" + - "request_id\x18\x03 \x01(\tR\trequestId\"\xb4\x05\n" + + "request_id\x18\x03 \x01(\tR\trequestId\x12\x1a\n" + + "\bidentity\x18\x04 \x01(\tR\bidentity\x12T\n" + + "\x0ecompute_config\x18\x05 \x01(\v2-.temporal.api.compute.v1.ComputeConfigSummaryR\rcomputeConfig\"\xb4\x05\n" + "$SyncDeploymentVersionUserDataRequest\x12'\n" + "\x0fdeployment_name\x18\x04 \x01(\tR\x0edeploymentName\x12T\n" + "\aversion\x18\x01 \x01(\v2:.temporal.server.api.deployment.v1.WorkerDeploymentVersionR\aversion\x12h\n" + @@ -3599,7 +4219,20 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\x10allow_no_pollers\x18\x05 \x01(\bR\x0eallowNoPollers\"m\n" + "\x19SetCurrentVersionResponse\x12)\n" + "\x10previous_version\x18\x01 \x01(\tR\x0fpreviousVersion\x12%\n" + - "\x0econflict_token\x18\x02 \x01(\fR\rconflictToken\"\xc0\x01\n" + + "\x0econflict_token\x18\x02 \x01(\fR\rconflictToken\"W\n" + + "\x1aCreateWorkerDeploymentArgs\x12\x1a\n" + + "\bidentity\x18\x01 \x01(\tR\bidentity\x12\x1d\n" + + "\n" + + "request_id\x18\x02 \x01(\tR\trequestId\"G\n" + + "\x1eCreateWorkerDeploymentResponse\x12%\n" + + "\x0econflict_token\x18\x01 \x01(\fR\rconflictToken\"\xc7\x01\n" + + "!CreateWorkerDeploymentVersionArgs\x12\x1a\n" + + "\bidentity\x18\x01 \x01(\tR\bidentity\x12\x1d\n" + + "\n" + + "request_id\x18\x02 \x01(\tR\trequestId\x12\x18\n" + + "\aversion\x18\x03 \x01(\tR\aversion\x12M\n" + + "\x0ecompute_config\x18\x04 \x01(\v2&.temporal.api.compute.v1.ComputeConfigR\rcomputeConfig\"'\n" + + "%CreateWorkerDeploymentVersionResponse\"\xc0\x01\n" + "\x11DeleteVersionArgs\x12\x1a\n" + "\bidentity\x18\x01 \x01(\tR\bidentity\x12\x18\n" + "\aversion\x18\x02 \x01(\tR\aversion\x12#\n" + @@ -3674,7 +4307,33 @@ const file_temporal_server_api_deployment_v1_message_proto_rawDesc = "" + "\x0erouting_config\x18\x03 \x01(\v2).temporal.api.deployment.v1.RoutingConfigR\rroutingConfig\x12\x85\x01\n" + "\x16latest_version_summary\x18\x04 \x01(\v2O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummaryR\x14latestVersionSummary\x12\x87\x01\n" + "\x17current_version_summary\x18\x05 \x01(\v2O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummaryR\x15currentVersionSummary\x12\x87\x01\n" + - "\x17ramping_version_summary\x18\x06 \x01(\v2O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummaryR\x15rampingVersionSummary\"\x84\x01\n" + + "\x17ramping_version_summary\x18\x06 \x01(\v2O.temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummaryR\x15rampingVersionSummary\"\xaa\x02\n" + + ")ValidateWorkerControllerInstanceSpecInput\x12\x86\x01\n" + + "\x0escaling_groups\x18\x01 \x03(\v2_.temporal.server.api.deployment.v1.ValidateWorkerControllerInstanceSpecInput.ScalingGroupsEntryR\rscalingGroups\x1at\n" + + "\x12ScalingGroupsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12H\n" + + "\x05value\x18\x02 \x01(\v22.temporal.api.compute.v1.ComputeConfigScalingGroupR\x05value:\x028\x01\"\xdd\x03\n" + + "#UpdateWorkerControllerInstanceInput\x12M\n" + + "\aversion\x18\x01 \x01(\v23.temporal.api.deployment.v1.WorkerDeploymentVersionR\aversion\x12\x1a\n" + + "\bidentity\x18\x02 \x01(\tR\bidentity\x12\x93\x01\n" + + "\x15upsert_scaling_groups\x18\x03 \x03(\v2_.temporal.server.api.deployment.v1.UpdateWorkerControllerInstanceInput.UpsertScalingGroupsEntryR\x13upsertScalingGroups\x122\n" + + "\x15remove_scaling_groups\x18\x04 \x03(\tR\x13removeScalingGroups\x1a\x80\x01\n" + + "\x18UpsertScalingGroupsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12N\n" + + "\x05value\x18\x02 \x01(\v28.temporal.api.compute.v1.ComputeConfigScalingGroupUpdateR\x05value:\x028\x01\"\x90\x01\n" + + "#DeleteWorkerControllerInstanceInput\x12M\n" + + "\aversion\x18\x01 \x01(\v23.temporal.api.deployment.v1.WorkerDeploymentVersionR\aversion\x12\x1a\n" + + "\bidentity\x18\x02 \x01(\tR\bidentity\"\x95\x03\n" + + "\x17UpdateComputeConfigArgs\x12\x1a\n" + + "\bidentity\x18\x01 \x01(\tR\bidentity\x12\x1d\n" + + "\n" + + "request_id\x18\x02 \x01(\tR\trequestId\x12\x87\x01\n" + + "\x15upsert_scaling_groups\x18\x03 \x03(\v2S.temporal.server.api.deployment.v1.UpdateComputeConfigArgs.UpsertScalingGroupsEntryR\x13upsertScalingGroups\x122\n" + + "\x15remove_scaling_groups\x18\x04 \x03(\tR\x13removeScalingGroups\x1a\x80\x01\n" + + "\x18UpsertScalingGroupsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12N\n" + + "\x05value\x18\x02 \x01(\v28.temporal.api.compute.v1.ComputeConfigScalingGroupUpdateR\x05value:\x028\x01\"\x1d\n" + + "\x1bUpdateComputeConfigResponse\"\x84\x01\n" + "\x1cForceCANDeploymentSignalArgs\x12d\n" + "\x0eoverride_state\x18\x01 \x01(\v2=.temporal.server.api.deployment.v1.WorkerDeploymentLocalStateR\roverrideState\"x\n" + "\x19ForceCANVersionSignalArgs\x12[\n" + @@ -3692,7 +4351,7 @@ func file_temporal_server_api_deployment_v1_message_proto_rawDescGZIP() []byte { return file_temporal_server_api_deployment_v1_message_proto_rawDescData } -var file_temporal_server_api_deployment_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 61) +var file_temporal_server_api_deployment_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 74) var file_temporal_server_api_deployment_v1_message_proto_goTypes = []any{ (*WorkerDeploymentVersion)(nil), // 0: temporal.server.api.deployment.v1.WorkerDeploymentVersion (*DeploymentVersionData)(nil), // 1: temporal.server.api.deployment.v1.DeploymentVersionData @@ -3716,150 +4375,180 @@ var file_temporal_server_api_deployment_v1_message_proto_goTypes = []any{ (*PropagationCompletionInfo)(nil), // 19: temporal.server.api.deployment.v1.PropagationCompletionInfo (*QueryDescribeVersionResponse)(nil), // 20: temporal.server.api.deployment.v1.QueryDescribeVersionResponse (*QueryDescribeWorkerDeploymentResponse)(nil), // 21: temporal.server.api.deployment.v1.QueryDescribeWorkerDeploymentResponse - (*StartWorkerDeploymentRequest)(nil), // 22: temporal.server.api.deployment.v1.StartWorkerDeploymentRequest - (*StartWorkerDeploymentVersionRequest)(nil), // 23: temporal.server.api.deployment.v1.StartWorkerDeploymentVersionRequest - (*SyncDeploymentVersionUserDataRequest)(nil), // 24: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest - (*SyncDeploymentVersionUserDataResponse)(nil), // 25: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse - (*CheckWorkerDeploymentUserDataPropagationRequest)(nil), // 26: temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest - (*SyncUnversionedRampActivityArgs)(nil), // 27: temporal.server.api.deployment.v1.SyncUnversionedRampActivityArgs - (*SyncUnversionedRampActivityResponse)(nil), // 28: temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse - (*UpdateVersionMetadataArgs)(nil), // 29: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs - (*UpdateVersionMetadataResponse)(nil), // 30: temporal.server.api.deployment.v1.UpdateVersionMetadataResponse - (*SetCurrentVersionArgs)(nil), // 31: temporal.server.api.deployment.v1.SetCurrentVersionArgs - (*SetCurrentVersionResponse)(nil), // 32: temporal.server.api.deployment.v1.SetCurrentVersionResponse - (*DeleteVersionArgs)(nil), // 33: temporal.server.api.deployment.v1.DeleteVersionArgs - (*DeleteVersionActivityArgs)(nil), // 34: temporal.server.api.deployment.v1.DeleteVersionActivityArgs - (*CheckTaskQueuesHavePollersActivityArgs)(nil), // 35: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs - (*DeleteDeploymentArgs)(nil), // 36: temporal.server.api.deployment.v1.DeleteDeploymentArgs - (*SetRampingVersionResponse)(nil), // 37: temporal.server.api.deployment.v1.SetRampingVersionResponse - (*SetRampingVersionArgs)(nil), // 38: temporal.server.api.deployment.v1.SetRampingVersionArgs - (*SetManagerIdentityArgs)(nil), // 39: temporal.server.api.deployment.v1.SetManagerIdentityArgs - (*SetManagerIdentityResponse)(nil), // 40: temporal.server.api.deployment.v1.SetManagerIdentityResponse - (*SyncVersionStateActivityArgs)(nil), // 41: temporal.server.api.deployment.v1.SyncVersionStateActivityArgs - (*SyncVersionStateActivityResult)(nil), // 42: temporal.server.api.deployment.v1.SyncVersionStateActivityResult - (*IsVersionMissingTaskQueuesArgs)(nil), // 43: temporal.server.api.deployment.v1.IsVersionMissingTaskQueuesArgs - (*IsVersionMissingTaskQueuesResult)(nil), // 44: temporal.server.api.deployment.v1.IsVersionMissingTaskQueuesResult - (*WorkerDeploymentWorkflowMemo)(nil), // 45: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo - (*WorkerDeploymentSummary)(nil), // 46: temporal.server.api.deployment.v1.WorkerDeploymentSummary - (*ForceCANDeploymentSignalArgs)(nil), // 47: temporal.server.api.deployment.v1.ForceCANDeploymentSignalArgs - (*ForceCANVersionSignalArgs)(nil), // 48: temporal.server.api.deployment.v1.ForceCANVersionSignalArgs - nil, // 49: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamiliesEntry - (*VersionLocalState_TaskQueueFamilyData)(nil), // 50: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData - nil, // 51: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.TaskQueuesEntry - nil, // 52: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.VersionsEntry - nil, // 53: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntry - (*SyncDeploymentVersionUserDataRequest_SyncUserData)(nil), // 54: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData - nil, // 55: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse.TaskQueueMaxVersionsEntry - nil, // 56: temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest.TaskQueueMaxVersionsEntry - nil, // 57: temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse.TaskQueueMaxVersionsEntry - nil, // 58: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.UpsertEntriesEntry - nil, // 59: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueuesAndTypesEntry - (*CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes)(nil), // 60: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueueTypes - (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp - (v1.WorkerDeploymentVersionStatus)(0), // 62: temporal.api.enums.v1.WorkerDeploymentVersionStatus - (*v11.VersionDrainageInfo)(nil), // 63: temporal.api.deployment.v1.VersionDrainageInfo - (*v11.VersionMetadata)(nil), // 64: temporal.api.deployment.v1.VersionMetadata - (*v11.RoutingConfig)(nil), // 65: temporal.api.deployment.v1.RoutingConfig - (v1.VersionDrainageStatus)(0), // 66: temporal.api.enums.v1.VersionDrainageStatus - (v1.TaskQueueType)(0), // 67: temporal.api.enums.v1.TaskQueueType - (*v11.WorkerDeploymentVersionInfo_VersionTaskQueueInfo)(nil), // 68: temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo - (*v11.WorkerDeploymentInfo_WorkerDeploymentVersionSummary)(nil), // 69: temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary - (*v12.Payload)(nil), // 70: temporal.api.common.v1.Payload + (*CreateRequestIDQueryResponse)(nil), // 22: temporal.server.api.deployment.v1.CreateRequestIDQueryResponse + (*StartWorkerDeploymentRequest)(nil), // 23: temporal.server.api.deployment.v1.StartWorkerDeploymentRequest + (*StartWorkerDeploymentVersionRequest)(nil), // 24: temporal.server.api.deployment.v1.StartWorkerDeploymentVersionRequest + (*SyncDeploymentVersionUserDataRequest)(nil), // 25: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest + (*SyncDeploymentVersionUserDataResponse)(nil), // 26: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse + (*CheckWorkerDeploymentUserDataPropagationRequest)(nil), // 27: temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest + (*SyncUnversionedRampActivityArgs)(nil), // 28: temporal.server.api.deployment.v1.SyncUnversionedRampActivityArgs + (*SyncUnversionedRampActivityResponse)(nil), // 29: temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse + (*UpdateVersionMetadataArgs)(nil), // 30: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs + (*UpdateVersionMetadataResponse)(nil), // 31: temporal.server.api.deployment.v1.UpdateVersionMetadataResponse + (*SetCurrentVersionArgs)(nil), // 32: temporal.server.api.deployment.v1.SetCurrentVersionArgs + (*SetCurrentVersionResponse)(nil), // 33: temporal.server.api.deployment.v1.SetCurrentVersionResponse + (*CreateWorkerDeploymentArgs)(nil), // 34: temporal.server.api.deployment.v1.CreateWorkerDeploymentArgs + (*CreateWorkerDeploymentResponse)(nil), // 35: temporal.server.api.deployment.v1.CreateWorkerDeploymentResponse + (*CreateWorkerDeploymentVersionArgs)(nil), // 36: temporal.server.api.deployment.v1.CreateWorkerDeploymentVersionArgs + (*CreateWorkerDeploymentVersionResponse)(nil), // 37: temporal.server.api.deployment.v1.CreateWorkerDeploymentVersionResponse + (*DeleteVersionArgs)(nil), // 38: temporal.server.api.deployment.v1.DeleteVersionArgs + (*DeleteVersionActivityArgs)(nil), // 39: temporal.server.api.deployment.v1.DeleteVersionActivityArgs + (*CheckTaskQueuesHavePollersActivityArgs)(nil), // 40: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs + (*DeleteDeploymentArgs)(nil), // 41: temporal.server.api.deployment.v1.DeleteDeploymentArgs + (*SetRampingVersionResponse)(nil), // 42: temporal.server.api.deployment.v1.SetRampingVersionResponse + (*SetRampingVersionArgs)(nil), // 43: temporal.server.api.deployment.v1.SetRampingVersionArgs + (*SetManagerIdentityArgs)(nil), // 44: temporal.server.api.deployment.v1.SetManagerIdentityArgs + (*SetManagerIdentityResponse)(nil), // 45: temporal.server.api.deployment.v1.SetManagerIdentityResponse + (*SyncVersionStateActivityArgs)(nil), // 46: temporal.server.api.deployment.v1.SyncVersionStateActivityArgs + (*SyncVersionStateActivityResult)(nil), // 47: temporal.server.api.deployment.v1.SyncVersionStateActivityResult + (*IsVersionMissingTaskQueuesArgs)(nil), // 48: temporal.server.api.deployment.v1.IsVersionMissingTaskQueuesArgs + (*IsVersionMissingTaskQueuesResult)(nil), // 49: temporal.server.api.deployment.v1.IsVersionMissingTaskQueuesResult + (*WorkerDeploymentWorkflowMemo)(nil), // 50: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo + (*WorkerDeploymentSummary)(nil), // 51: temporal.server.api.deployment.v1.WorkerDeploymentSummary + (*ValidateWorkerControllerInstanceSpecInput)(nil), // 52: temporal.server.api.deployment.v1.ValidateWorkerControllerInstanceSpecInput + (*UpdateWorkerControllerInstanceInput)(nil), // 53: temporal.server.api.deployment.v1.UpdateWorkerControllerInstanceInput + (*DeleteWorkerControllerInstanceInput)(nil), // 54: temporal.server.api.deployment.v1.DeleteWorkerControllerInstanceInput + (*UpdateComputeConfigArgs)(nil), // 55: temporal.server.api.deployment.v1.UpdateComputeConfigArgs + (*UpdateComputeConfigResponse)(nil), // 56: temporal.server.api.deployment.v1.UpdateComputeConfigResponse + (*ForceCANDeploymentSignalArgs)(nil), // 57: temporal.server.api.deployment.v1.ForceCANDeploymentSignalArgs + (*ForceCANVersionSignalArgs)(nil), // 58: temporal.server.api.deployment.v1.ForceCANVersionSignalArgs + nil, // 59: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamiliesEntry + (*VersionLocalState_TaskQueueFamilyData)(nil), // 60: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData + nil, // 61: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.TaskQueuesEntry + nil, // 62: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.VersionsEntry + nil, // 63: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntry + (*SyncDeploymentVersionUserDataRequest_SyncUserData)(nil), // 64: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData + nil, // 65: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse.TaskQueueMaxVersionsEntry + nil, // 66: temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest.TaskQueueMaxVersionsEntry + nil, // 67: temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse.TaskQueueMaxVersionsEntry + nil, // 68: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.UpsertEntriesEntry + nil, // 69: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueuesAndTypesEntry + (*CheckTaskQueuesHavePollersActivityArgs_TaskQueueTypes)(nil), // 70: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueueTypes + nil, // 71: temporal.server.api.deployment.v1.ValidateWorkerControllerInstanceSpecInput.ScalingGroupsEntry + nil, // 72: temporal.server.api.deployment.v1.UpdateWorkerControllerInstanceInput.UpsertScalingGroupsEntry + nil, // 73: temporal.server.api.deployment.v1.UpdateComputeConfigArgs.UpsertScalingGroupsEntry + (*timestamppb.Timestamp)(nil), // 74: google.protobuf.Timestamp + (v1.WorkerDeploymentVersionStatus)(0), // 75: temporal.api.enums.v1.WorkerDeploymentVersionStatus + (*v11.VersionDrainageInfo)(nil), // 76: temporal.api.deployment.v1.VersionDrainageInfo + (*v11.VersionMetadata)(nil), // 77: temporal.api.deployment.v1.VersionMetadata + (*v12.ComputeConfigSummary)(nil), // 78: temporal.api.compute.v1.ComputeConfigSummary + (*v11.RoutingConfig)(nil), // 79: temporal.api.deployment.v1.RoutingConfig + (v1.VersionDrainageStatus)(0), // 80: temporal.api.enums.v1.VersionDrainageStatus + (v1.TaskQueueType)(0), // 81: temporal.api.enums.v1.TaskQueueType + (*v11.WorkerDeploymentVersionInfo_VersionTaskQueueInfo)(nil), // 82: temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo + (*v12.ComputeConfig)(nil), // 83: temporal.api.compute.v1.ComputeConfig + (*v11.WorkerDeploymentInfo_WorkerDeploymentVersionSummary)(nil), // 84: temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary + (*v11.WorkerDeploymentVersion)(nil), // 85: temporal.api.deployment.v1.WorkerDeploymentVersion + (*v13.Payload)(nil), // 86: temporal.api.common.v1.Payload + (*v12.ComputeConfigScalingGroup)(nil), // 87: temporal.api.compute.v1.ComputeConfigScalingGroup + (*v12.ComputeConfigScalingGroupUpdate)(nil), // 88: temporal.api.compute.v1.ComputeConfigScalingGroupUpdate } var file_temporal_server_api_deployment_v1_message_proto_depIdxs = []int32{ - 0, // 0: temporal.server.api.deployment.v1.DeploymentVersionData.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion - 61, // 1: temporal.server.api.deployment.v1.DeploymentVersionData.routing_update_time:type_name -> google.protobuf.Timestamp - 61, // 2: temporal.server.api.deployment.v1.DeploymentVersionData.current_since_time:type_name -> google.protobuf.Timestamp - 61, // 3: temporal.server.api.deployment.v1.DeploymentVersionData.ramping_since_time:type_name -> google.protobuf.Timestamp - 62, // 4: temporal.server.api.deployment.v1.DeploymentVersionData.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus - 61, // 5: temporal.server.api.deployment.v1.WorkerDeploymentVersionData.update_time:type_name -> google.protobuf.Timestamp - 62, // 6: temporal.server.api.deployment.v1.WorkerDeploymentVersionData.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus - 0, // 7: temporal.server.api.deployment.v1.VersionLocalState.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion - 61, // 8: temporal.server.api.deployment.v1.VersionLocalState.create_time:type_name -> google.protobuf.Timestamp - 61, // 9: temporal.server.api.deployment.v1.VersionLocalState.routing_update_time:type_name -> google.protobuf.Timestamp - 61, // 10: temporal.server.api.deployment.v1.VersionLocalState.current_since_time:type_name -> google.protobuf.Timestamp - 61, // 11: temporal.server.api.deployment.v1.VersionLocalState.ramping_since_time:type_name -> google.protobuf.Timestamp - 61, // 12: temporal.server.api.deployment.v1.VersionLocalState.first_activation_time:type_name -> google.protobuf.Timestamp - 61, // 13: temporal.server.api.deployment.v1.VersionLocalState.last_current_time:type_name -> google.protobuf.Timestamp - 61, // 14: temporal.server.api.deployment.v1.VersionLocalState.last_deactivation_time:type_name -> google.protobuf.Timestamp - 63, // 15: temporal.server.api.deployment.v1.VersionLocalState.drainage_info:type_name -> temporal.api.deployment.v1.VersionDrainageInfo - 64, // 16: temporal.server.api.deployment.v1.VersionLocalState.metadata:type_name -> temporal.api.deployment.v1.VersionMetadata - 49, // 17: temporal.server.api.deployment.v1.VersionLocalState.task_queue_families:type_name -> temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamiliesEntry - 62, // 18: temporal.server.api.deployment.v1.VersionLocalState.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus - 3, // 19: temporal.server.api.deployment.v1.WorkerDeploymentVersionWorkflowArgs.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState - 7, // 20: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowArgs.state:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState - 61, // 21: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.create_time:type_name -> google.protobuf.Timestamp - 65, // 22: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig - 52, // 23: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.versions:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState.VersionsEntry - 53, // 24: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.propagating_revisions:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntry - 61, // 25: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.create_time:type_name -> google.protobuf.Timestamp - 66, // 26: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.drainage_status:type_name -> temporal.api.enums.v1.VersionDrainageStatus - 63, // 27: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.drainage_info:type_name -> temporal.api.deployment.v1.VersionDrainageInfo - 61, // 28: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.routing_update_time:type_name -> google.protobuf.Timestamp - 61, // 29: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.current_since_time:type_name -> google.protobuf.Timestamp - 61, // 30: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.ramping_since_time:type_name -> google.protobuf.Timestamp - 61, // 31: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.first_activation_time:type_name -> google.protobuf.Timestamp - 61, // 32: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.last_current_time:type_name -> google.protobuf.Timestamp - 61, // 33: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.last_deactivation_time:type_name -> google.protobuf.Timestamp - 62, // 34: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus - 67, // 35: temporal.server.api.deployment.v1.RegisterWorkerInVersionArgs.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType - 65, // 36: temporal.server.api.deployment.v1.RegisterWorkerInVersionArgs.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig - 67, // 37: temporal.server.api.deployment.v1.RegisterWorkerInWorkerDeploymentArgs.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType - 0, // 38: temporal.server.api.deployment.v1.RegisterWorkerInWorkerDeploymentArgs.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion - 68, // 39: temporal.server.api.deployment.v1.DescribeVersionFromWorkerDeploymentActivityResult.task_queue_infos:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo - 61, // 40: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.routing_update_time:type_name -> google.protobuf.Timestamp - 61, // 41: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.current_since_time:type_name -> google.protobuf.Timestamp - 61, // 42: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.ramping_since_time:type_name -> google.protobuf.Timestamp - 65, // 43: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig - 3, // 44: temporal.server.api.deployment.v1.SyncVersionStateResponse.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState - 9, // 45: temporal.server.api.deployment.v1.SyncVersionStateResponse.summary:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary - 61, // 46: temporal.server.api.deployment.v1.AddVersionUpdateArgs.create_time:type_name -> google.protobuf.Timestamp - 63, // 47: temporal.server.api.deployment.v1.SyncDrainageInfoSignalArgs.drainage_info:type_name -> temporal.api.deployment.v1.VersionDrainageInfo - 66, // 48: temporal.server.api.deployment.v1.SyncDrainageStatusSignalArgs.drainage_status:type_name -> temporal.api.enums.v1.VersionDrainageStatus - 3, // 49: temporal.server.api.deployment.v1.QueryDescribeVersionResponse.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState - 7, // 50: temporal.server.api.deployment.v1.QueryDescribeWorkerDeploymentResponse.state:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState - 0, // 51: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion - 54, // 52: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.sync:type_name -> temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData - 65, // 53: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.update_routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig - 2, // 54: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.upsert_version_data:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionData - 55, // 55: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse.task_queue_max_versions:type_name -> temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse.TaskQueueMaxVersionsEntry - 56, // 56: temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest.task_queue_max_versions:type_name -> temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest.TaskQueueMaxVersionsEntry - 14, // 57: temporal.server.api.deployment.v1.SyncUnversionedRampActivityArgs.update_args:type_name -> temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs - 57, // 58: temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse.task_queue_max_versions:type_name -> temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse.TaskQueueMaxVersionsEntry - 58, // 59: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.upsert_entries:type_name -> temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.UpsertEntriesEntry - 64, // 60: temporal.server.api.deployment.v1.UpdateVersionMetadataResponse.metadata:type_name -> temporal.api.deployment.v1.VersionMetadata - 59, // 61: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.task_queues_and_types:type_name -> temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueuesAndTypesEntry - 0, // 62: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.worker_deployment_version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion - 14, // 63: temporal.server.api.deployment.v1.SyncVersionStateActivityArgs.update_args:type_name -> temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs - 3, // 64: temporal.server.api.deployment.v1.SyncVersionStateActivityResult.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState - 9, // 65: temporal.server.api.deployment.v1.SyncVersionStateActivityResult.summary:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary - 61, // 66: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.create_time:type_name -> google.protobuf.Timestamp - 65, // 67: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig - 69, // 68: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.latest_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary - 69, // 69: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.current_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary - 69, // 70: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.ramping_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary - 61, // 71: temporal.server.api.deployment.v1.WorkerDeploymentSummary.create_time:type_name -> google.protobuf.Timestamp - 65, // 72: temporal.server.api.deployment.v1.WorkerDeploymentSummary.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig - 69, // 73: temporal.server.api.deployment.v1.WorkerDeploymentSummary.latest_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary - 69, // 74: temporal.server.api.deployment.v1.WorkerDeploymentSummary.current_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary - 69, // 75: temporal.server.api.deployment.v1.WorkerDeploymentSummary.ramping_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary - 7, // 76: temporal.server.api.deployment.v1.ForceCANDeploymentSignalArgs.override_state:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState - 3, // 77: temporal.server.api.deployment.v1.ForceCANVersionSignalArgs.override_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState - 50, // 78: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamiliesEntry.value:type_name -> temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData - 51, // 79: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.task_queues:type_name -> temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.TaskQueuesEntry - 4, // 80: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.TaskQueuesEntry.value:type_name -> temporal.server.api.deployment.v1.TaskQueueVersionData - 9, // 81: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.VersionsEntry.value:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary - 8, // 82: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntry.value:type_name -> temporal.server.api.deployment.v1.PropagatingRevisions - 67, // 83: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData.types:type_name -> temporal.api.enums.v1.TaskQueueType - 1, // 84: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData.data:type_name -> temporal.server.api.deployment.v1.DeploymentVersionData - 70, // 85: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.UpsertEntriesEntry.value:type_name -> temporal.api.common.v1.Payload - 60, // 86: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueuesAndTypesEntry.value:type_name -> temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueueTypes - 67, // 87: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueueTypes.types:type_name -> temporal.api.enums.v1.TaskQueueType - 88, // [88:88] is the sub-list for method output_type - 88, // [88:88] is the sub-list for method input_type - 88, // [88:88] is the sub-list for extension type_name - 88, // [88:88] is the sub-list for extension extendee - 0, // [0:88] is the sub-list for field type_name + 0, // 0: temporal.server.api.deployment.v1.DeploymentVersionData.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion + 74, // 1: temporal.server.api.deployment.v1.DeploymentVersionData.routing_update_time:type_name -> google.protobuf.Timestamp + 74, // 2: temporal.server.api.deployment.v1.DeploymentVersionData.current_since_time:type_name -> google.protobuf.Timestamp + 74, // 3: temporal.server.api.deployment.v1.DeploymentVersionData.ramping_since_time:type_name -> google.protobuf.Timestamp + 75, // 4: temporal.server.api.deployment.v1.DeploymentVersionData.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus + 74, // 5: temporal.server.api.deployment.v1.WorkerDeploymentVersionData.update_time:type_name -> google.protobuf.Timestamp + 75, // 6: temporal.server.api.deployment.v1.WorkerDeploymentVersionData.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus + 0, // 7: temporal.server.api.deployment.v1.VersionLocalState.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion + 74, // 8: temporal.server.api.deployment.v1.VersionLocalState.create_time:type_name -> google.protobuf.Timestamp + 74, // 9: temporal.server.api.deployment.v1.VersionLocalState.routing_update_time:type_name -> google.protobuf.Timestamp + 74, // 10: temporal.server.api.deployment.v1.VersionLocalState.current_since_time:type_name -> google.protobuf.Timestamp + 74, // 11: temporal.server.api.deployment.v1.VersionLocalState.ramping_since_time:type_name -> google.protobuf.Timestamp + 74, // 12: temporal.server.api.deployment.v1.VersionLocalState.first_activation_time:type_name -> google.protobuf.Timestamp + 74, // 13: temporal.server.api.deployment.v1.VersionLocalState.last_current_time:type_name -> google.protobuf.Timestamp + 74, // 14: temporal.server.api.deployment.v1.VersionLocalState.last_deactivation_time:type_name -> google.protobuf.Timestamp + 76, // 15: temporal.server.api.deployment.v1.VersionLocalState.drainage_info:type_name -> temporal.api.deployment.v1.VersionDrainageInfo + 77, // 16: temporal.server.api.deployment.v1.VersionLocalState.metadata:type_name -> temporal.api.deployment.v1.VersionMetadata + 59, // 17: temporal.server.api.deployment.v1.VersionLocalState.task_queue_families:type_name -> temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamiliesEntry + 75, // 18: temporal.server.api.deployment.v1.VersionLocalState.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus + 78, // 19: temporal.server.api.deployment.v1.VersionLocalState.compute_config:type_name -> temporal.api.compute.v1.ComputeConfigSummary + 3, // 20: temporal.server.api.deployment.v1.WorkerDeploymentVersionWorkflowArgs.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState + 7, // 21: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowArgs.state:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState + 74, // 22: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.create_time:type_name -> google.protobuf.Timestamp + 79, // 23: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig + 62, // 24: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.versions:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState.VersionsEntry + 63, // 25: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.propagating_revisions:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntry + 74, // 26: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.create_time:type_name -> google.protobuf.Timestamp + 80, // 27: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.drainage_status:type_name -> temporal.api.enums.v1.VersionDrainageStatus + 76, // 28: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.drainage_info:type_name -> temporal.api.deployment.v1.VersionDrainageInfo + 74, // 29: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.routing_update_time:type_name -> google.protobuf.Timestamp + 74, // 30: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.current_since_time:type_name -> google.protobuf.Timestamp + 74, // 31: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.ramping_since_time:type_name -> google.protobuf.Timestamp + 74, // 32: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.first_activation_time:type_name -> google.protobuf.Timestamp + 74, // 33: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.last_current_time:type_name -> google.protobuf.Timestamp + 74, // 34: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.last_deactivation_time:type_name -> google.protobuf.Timestamp + 75, // 35: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.status:type_name -> temporal.api.enums.v1.WorkerDeploymentVersionStatus + 78, // 36: temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary.compute_config:type_name -> temporal.api.compute.v1.ComputeConfigSummary + 81, // 37: temporal.server.api.deployment.v1.RegisterWorkerInVersionArgs.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType + 79, // 38: temporal.server.api.deployment.v1.RegisterWorkerInVersionArgs.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig + 81, // 39: temporal.server.api.deployment.v1.RegisterWorkerInWorkerDeploymentArgs.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType + 0, // 40: temporal.server.api.deployment.v1.RegisterWorkerInWorkerDeploymentArgs.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion + 82, // 41: temporal.server.api.deployment.v1.DescribeVersionFromWorkerDeploymentActivityResult.task_queue_infos:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo + 74, // 42: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.routing_update_time:type_name -> google.protobuf.Timestamp + 74, // 43: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.current_since_time:type_name -> google.protobuf.Timestamp + 74, // 44: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.ramping_since_time:type_name -> google.protobuf.Timestamp + 79, // 45: temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig + 3, // 46: temporal.server.api.deployment.v1.SyncVersionStateResponse.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState + 9, // 47: temporal.server.api.deployment.v1.SyncVersionStateResponse.summary:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary + 74, // 48: temporal.server.api.deployment.v1.AddVersionUpdateArgs.create_time:type_name -> google.protobuf.Timestamp + 76, // 49: temporal.server.api.deployment.v1.SyncDrainageInfoSignalArgs.drainage_info:type_name -> temporal.api.deployment.v1.VersionDrainageInfo + 80, // 50: temporal.server.api.deployment.v1.SyncDrainageStatusSignalArgs.drainage_status:type_name -> temporal.api.enums.v1.VersionDrainageStatus + 3, // 51: temporal.server.api.deployment.v1.QueryDescribeVersionResponse.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState + 7, // 52: temporal.server.api.deployment.v1.QueryDescribeWorkerDeploymentResponse.state:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState + 78, // 53: temporal.server.api.deployment.v1.StartWorkerDeploymentVersionRequest.compute_config:type_name -> temporal.api.compute.v1.ComputeConfigSummary + 0, // 54: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion + 64, // 55: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.sync:type_name -> temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData + 79, // 56: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.update_routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig + 2, // 57: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.upsert_version_data:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionData + 65, // 58: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse.task_queue_max_versions:type_name -> temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataResponse.TaskQueueMaxVersionsEntry + 66, // 59: temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest.task_queue_max_versions:type_name -> temporal.server.api.deployment.v1.CheckWorkerDeploymentUserDataPropagationRequest.TaskQueueMaxVersionsEntry + 14, // 60: temporal.server.api.deployment.v1.SyncUnversionedRampActivityArgs.update_args:type_name -> temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs + 67, // 61: temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse.task_queue_max_versions:type_name -> temporal.server.api.deployment.v1.SyncUnversionedRampActivityResponse.TaskQueueMaxVersionsEntry + 68, // 62: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.upsert_entries:type_name -> temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.UpsertEntriesEntry + 77, // 63: temporal.server.api.deployment.v1.UpdateVersionMetadataResponse.metadata:type_name -> temporal.api.deployment.v1.VersionMetadata + 83, // 64: temporal.server.api.deployment.v1.CreateWorkerDeploymentVersionArgs.compute_config:type_name -> temporal.api.compute.v1.ComputeConfig + 69, // 65: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.task_queues_and_types:type_name -> temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueuesAndTypesEntry + 0, // 66: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.worker_deployment_version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion + 14, // 67: temporal.server.api.deployment.v1.SyncVersionStateActivityArgs.update_args:type_name -> temporal.server.api.deployment.v1.SyncVersionStateUpdateArgs + 3, // 68: temporal.server.api.deployment.v1.SyncVersionStateActivityResult.version_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState + 9, // 69: temporal.server.api.deployment.v1.SyncVersionStateActivityResult.summary:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary + 74, // 70: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.create_time:type_name -> google.protobuf.Timestamp + 79, // 71: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig + 84, // 72: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.latest_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary + 84, // 73: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.current_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary + 84, // 74: temporal.server.api.deployment.v1.WorkerDeploymentWorkflowMemo.ramping_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary + 74, // 75: temporal.server.api.deployment.v1.WorkerDeploymentSummary.create_time:type_name -> google.protobuf.Timestamp + 79, // 76: temporal.server.api.deployment.v1.WorkerDeploymentSummary.routing_config:type_name -> temporal.api.deployment.v1.RoutingConfig + 84, // 77: temporal.server.api.deployment.v1.WorkerDeploymentSummary.latest_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary + 84, // 78: temporal.server.api.deployment.v1.WorkerDeploymentSummary.current_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary + 84, // 79: temporal.server.api.deployment.v1.WorkerDeploymentSummary.ramping_version_summary:type_name -> temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary + 71, // 80: temporal.server.api.deployment.v1.ValidateWorkerControllerInstanceSpecInput.scaling_groups:type_name -> temporal.server.api.deployment.v1.ValidateWorkerControllerInstanceSpecInput.ScalingGroupsEntry + 85, // 81: temporal.server.api.deployment.v1.UpdateWorkerControllerInstanceInput.version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 72, // 82: temporal.server.api.deployment.v1.UpdateWorkerControllerInstanceInput.upsert_scaling_groups:type_name -> temporal.server.api.deployment.v1.UpdateWorkerControllerInstanceInput.UpsertScalingGroupsEntry + 85, // 83: temporal.server.api.deployment.v1.DeleteWorkerControllerInstanceInput.version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 73, // 84: temporal.server.api.deployment.v1.UpdateComputeConfigArgs.upsert_scaling_groups:type_name -> temporal.server.api.deployment.v1.UpdateComputeConfigArgs.UpsertScalingGroupsEntry + 7, // 85: temporal.server.api.deployment.v1.ForceCANDeploymentSignalArgs.override_state:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentLocalState + 3, // 86: temporal.server.api.deployment.v1.ForceCANVersionSignalArgs.override_state:type_name -> temporal.server.api.deployment.v1.VersionLocalState + 60, // 87: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamiliesEntry.value:type_name -> temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData + 61, // 88: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.task_queues:type_name -> temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.TaskQueuesEntry + 4, // 89: temporal.server.api.deployment.v1.VersionLocalState.TaskQueueFamilyData.TaskQueuesEntry.value:type_name -> temporal.server.api.deployment.v1.TaskQueueVersionData + 9, // 90: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.VersionsEntry.value:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionSummary + 8, // 91: temporal.server.api.deployment.v1.WorkerDeploymentLocalState.PropagatingRevisionsEntry.value:type_name -> temporal.server.api.deployment.v1.PropagatingRevisions + 81, // 92: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData.types:type_name -> temporal.api.enums.v1.TaskQueueType + 1, // 93: temporal.server.api.deployment.v1.SyncDeploymentVersionUserDataRequest.SyncUserData.data:type_name -> temporal.server.api.deployment.v1.DeploymentVersionData + 86, // 94: temporal.server.api.deployment.v1.UpdateVersionMetadataArgs.UpsertEntriesEntry.value:type_name -> temporal.api.common.v1.Payload + 70, // 95: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueuesAndTypesEntry.value:type_name -> temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueueTypes + 81, // 96: temporal.server.api.deployment.v1.CheckTaskQueuesHavePollersActivityArgs.TaskQueueTypes.types:type_name -> temporal.api.enums.v1.TaskQueueType + 87, // 97: temporal.server.api.deployment.v1.ValidateWorkerControllerInstanceSpecInput.ScalingGroupsEntry.value:type_name -> temporal.api.compute.v1.ComputeConfigScalingGroup + 88, // 98: temporal.server.api.deployment.v1.UpdateWorkerControllerInstanceInput.UpsertScalingGroupsEntry.value:type_name -> temporal.api.compute.v1.ComputeConfigScalingGroupUpdate + 88, // 99: temporal.server.api.deployment.v1.UpdateComputeConfigArgs.UpsertScalingGroupsEntry.value:type_name -> temporal.api.compute.v1.ComputeConfigScalingGroupUpdate + 100, // [100:100] is the sub-list for method output_type + 100, // [100:100] is the sub-list for method input_type + 100, // [100:100] is the sub-list for extension type_name + 100, // [100:100] is the sub-list for extension extendee + 0, // [0:100] is the sub-list for field type_name } func init() { file_temporal_server_api_deployment_v1_message_proto_init() } @@ -3873,7 +4562,7 @@ func file_temporal_server_api_deployment_v1_message_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_deployment_v1_message_proto_rawDesc), len(file_temporal_server_api_deployment_v1_message_proto_rawDesc)), NumEnums: 0, - NumMessages: 61, + NumMessages: 74, NumExtensions: 0, NumServices: 0, }, diff --git a/api/enums/v1/cluster.go-helpers.pb.go b/api/enums/v1/cluster.go-helpers.pb.go index 653bfd9907a..fcb46ddaa75 100644 --- a/api/enums/v1/cluster.go-helpers.pb.go +++ b/api/enums/v1/cluster.go-helpers.pb.go @@ -32,6 +32,7 @@ var ( "Serving": 1, "NotServing": 2, "DeclinedServing": 3, + "InternalError": 4, } ) diff --git a/api/enums/v1/cluster.pb.go b/api/enums/v1/cluster.pb.go index d19cbdb6d39..ff2b2ff84b9 100644 --- a/api/enums/v1/cluster.pb.go +++ b/api/enums/v1/cluster.pb.go @@ -102,6 +102,8 @@ const ( HEALTH_STATE_NOT_SERVING HealthState = 2 // The host has marked itself as not ready to serve traffic. HEALTH_STATE_DECLINED_SERVING HealthState = 3 + // An internal error occurred while checking health (e.g. resolver failure). + HEALTH_STATE_INTERNAL_ERROR HealthState = 4 ) // Enum value maps for HealthState. @@ -111,12 +113,14 @@ var ( 1: "HEALTH_STATE_SERVING", 2: "HEALTH_STATE_NOT_SERVING", 3: "HEALTH_STATE_DECLINED_SERVING", + 4: "HEALTH_STATE_INTERNAL_ERROR", } HealthState_value = map[string]int32{ "HEALTH_STATE_UNSPECIFIED": 0, "HEALTH_STATE_SERVING": 1, "HEALTH_STATE_NOT_SERVING": 2, "HEALTH_STATE_DECLINED_SERVING": 3, + "HEALTH_STATE_INTERNAL_ERROR": 4, } ) @@ -136,6 +140,8 @@ func (x HealthState) String() string { return "NotServing" case HEALTH_STATE_DECLINED_SERVING: return "DeclinedServing" + case HEALTH_STATE_INTERNAL_ERROR: + return "InternalError" default: return strconv.Itoa(int(x)) } @@ -169,12 +175,13 @@ const file_temporal_server_api_enums_v1_cluster_proto_rawDesc = "" + "\x1cCLUSTER_MEMBER_ROLE_FRONTEND\x10\x01\x12\x1f\n" + "\x1bCLUSTER_MEMBER_ROLE_HISTORY\x10\x02\x12 \n" + "\x1cCLUSTER_MEMBER_ROLE_MATCHING\x10\x03\x12\x1e\n" + - "\x1aCLUSTER_MEMBER_ROLE_WORKER\x10\x04*\x86\x01\n" + + "\x1aCLUSTER_MEMBER_ROLE_WORKER\x10\x04*\xa7\x01\n" + "\vHealthState\x12\x1c\n" + "\x18HEALTH_STATE_UNSPECIFIED\x10\x00\x12\x18\n" + "\x14HEALTH_STATE_SERVING\x10\x01\x12\x1c\n" + "\x18HEALTH_STATE_NOT_SERVING\x10\x02\x12!\n" + - "\x1dHEALTH_STATE_DECLINED_SERVING\x10\x03B*Z(go.temporal.io/server/api/enums/v1;enumsb\x06proto3" + "\x1dHEALTH_STATE_DECLINED_SERVING\x10\x03\x12\x1f\n" + + "\x1bHEALTH_STATE_INTERNAL_ERROR\x10\x04B*Z(go.temporal.io/server/api/enums/v1;enumsb\x06proto3" var ( file_temporal_server_api_enums_v1_cluster_proto_rawDescOnce sync.Once diff --git a/api/enums/v1/replication.go-helpers.pb.go b/api/enums/v1/replication.go-helpers.pb.go index 36a725cad14..33656860401 100644 --- a/api/enums/v1/replication.go-helpers.pb.go +++ b/api/enums/v1/replication.go-helpers.pb.go @@ -20,6 +20,7 @@ var ( "BackfillHistoryTask": 10, "VerifyVersionedTransitionTask": 11, "SyncVersionedTransitionTask": 12, + "DeleteExecutionTask": 13, } ) diff --git a/api/enums/v1/replication.pb.go b/api/enums/v1/replication.pb.go index 557e587aad9..49cf16cc13d 100644 --- a/api/enums/v1/replication.pb.go +++ b/api/enums/v1/replication.pb.go @@ -39,6 +39,7 @@ const ( REPLICATION_TASK_TYPE_BACKFILL_HISTORY_TASK ReplicationTaskType = 10 REPLICATION_TASK_TYPE_VERIFY_VERSIONED_TRANSITION_TASK ReplicationTaskType = 11 REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK ReplicationTaskType = 12 + REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK ReplicationTaskType = 13 ) // Enum value maps for ReplicationTaskType. @@ -57,6 +58,7 @@ var ( 10: "REPLICATION_TASK_TYPE_BACKFILL_HISTORY_TASK", 11: "REPLICATION_TASK_TYPE_VERIFY_VERSIONED_TRANSITION_TASK", 12: "REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK", + 13: "REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK", } ReplicationTaskType_value = map[string]int32{ "REPLICATION_TASK_TYPE_UNSPECIFIED": 0, @@ -72,6 +74,7 @@ var ( "REPLICATION_TASK_TYPE_BACKFILL_HISTORY_TASK": 10, "REPLICATION_TASK_TYPE_VERIFY_VERSIONED_TRANSITION_TASK": 11, "REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK": 12, + "REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK": 13, } ) @@ -111,11 +114,12 @@ func (x ReplicationTaskType) String() string { return "VerifyVersionedTransitionTask" case REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK: return "SyncVersionedTransitionTask" - default: - return strconv. + case REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK: - // Enum value maps for NamespaceOperation. - Itoa(int(x)) + // Enum value maps for NamespaceOperation. + return "DeleteExecutionTask" + default: + return strconv.Itoa(int(x)) } } @@ -257,7 +261,7 @@ var File_temporal_server_api_enums_v1_replication_proto protoreflect.FileDescrip const file_temporal_server_api_enums_v1_replication_proto_rawDesc = "" + "\n" + - ".temporal/server/api/enums/v1/replication.proto\x12\x1ctemporal.server.api.enums.v1*\xfe\x04\n" + + ".temporal/server/api/enums/v1/replication.proto\x12\x1ctemporal.server.api.enums.v1*\xaf\x05\n" + "\x13ReplicationTaskType\x12%\n" + "!REPLICATION_TASK_TYPE_UNSPECIFIED\x10\x00\x12(\n" + "$REPLICATION_TASK_TYPE_NAMESPACE_TASK\x10\x01\x12&\n" + @@ -272,7 +276,8 @@ const file_temporal_server_api_enums_v1_replication_proto_rawDesc = "" + "+REPLICATION_TASK_TYPE_BACKFILL_HISTORY_TASK\x10\n" + "\x12:\n" + "6REPLICATION_TASK_TYPE_VERIFY_VERSIONED_TRANSITION_TASK\x10\v\x128\n" + - "4REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK\x10\f*y\n" + + "4REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK\x10\f\x12/\n" + + "+REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK\x10\r*y\n" + "\x12NamespaceOperation\x12#\n" + "\x1fNAMESPACE_OPERATION_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aNAMESPACE_OPERATION_CREATE\x10\x01\x12\x1e\n" + diff --git a/api/enums/v1/task.go-helpers.pb.go b/api/enums/v1/task.go-helpers.pb.go index 30573a5d7af..2eba00b5c49 100644 --- a/api/enums/v1/task.go-helpers.pb.go +++ b/api/enums/v1/task.go-helpers.pb.go @@ -57,7 +57,8 @@ var ( "ReplicationSyncVersionedTransition": 31, "ChasmPure": 32, "Chasm": 33, - "WorkerCommands": 34, + "ReplicationDeleteExecution": 34, + "WorkerCommands": 35, } ) diff --git a/api/enums/v1/task.pb.go b/api/enums/v1/task.pb.go index cbec1ef4b44..726074b41ee 100644 --- a/api/enums/v1/task.pb.go +++ b/api/enums/v1/task.pb.go @@ -126,8 +126,10 @@ const ( TASK_TYPE_CHASM_PURE TaskType = 32 // A task with side effects generated by a CHASM component. TASK_TYPE_CHASM TaskType = 33 + // A replication task that deletes workflow on passive cluster(s). + TASK_TYPE_REPLICATION_DELETE_EXECUTION TaskType = 34 // A task to send worker commands via Nexus. - TASK_TYPE_WORKER_COMMANDS TaskType = 34 + TASK_TYPE_WORKER_COMMANDS TaskType = 35 ) // Enum value maps for TaskType. @@ -164,7 +166,8 @@ var ( 31: "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION", 32: "TASK_TYPE_CHASM_PURE", 33: "TASK_TYPE_CHASM", - 34: "TASK_TYPE_WORKER_COMMANDS", + 34: "TASK_TYPE_REPLICATION_DELETE_EXECUTION", + 35: "TASK_TYPE_WORKER_COMMANDS", } TaskType_value = map[string]int32{ "TASK_TYPE_UNSPECIFIED": 0, @@ -198,7 +201,8 @@ var ( "TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION": 31, "TASK_TYPE_CHASM_PURE": 32, "TASK_TYPE_CHASM": 33, - "TASK_TYPE_WORKER_COMMANDS": 34, + "TASK_TYPE_REPLICATION_DELETE_EXECUTION": 34, + "TASK_TYPE_WORKER_COMMANDS": 35, } ) @@ -282,6 +286,8 @@ func (x TaskType) String() string { return "ChasmPure" case TASK_TYPE_CHASM: return "Chasm" + case TASK_TYPE_REPLICATION_DELETE_EXECUTION: + return "ReplicationDeleteExecution" case TASK_TYPE_WORKER_COMMANDS: return "WorkerCommands" default: @@ -373,7 +379,8 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "TaskSource\x12\x1b\n" + "\x17TASK_SOURCE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13TASK_SOURCE_HISTORY\x10\x01\x12\x1a\n" + - "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\xd5\t\n" + + "\x16TASK_SOURCE_DB_BACKLOG\x10\x02*\x81\n" + + "\n" + "\bTaskType\x12\x19\n" + "\x15TASK_TYPE_UNSPECIFIED\x10\x00\x12!\n" + "\x1dTASK_TYPE_REPLICATION_HISTORY\x10\x01\x12'\n" + @@ -406,8 +413,9 @@ const file_temporal_server_api_enums_v1_task_proto_rawDesc = "" + "\x1eTASK_TYPE_REPLICATION_SYNC_HSM\x10\x1e\x123\n" + "/TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION\x10\x1f\x12\x18\n" + "\x14TASK_TYPE_CHASM_PURE\x10 \x12\x13\n" + - "\x0fTASK_TYPE_CHASM\x10!\x12\x1d\n" + - "\x19TASK_TYPE_WORKER_COMMANDS\x10\"\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*\\\n" + + "\x0fTASK_TYPE_CHASM\x10!\x12*\n" + + "&TASK_TYPE_REPLICATION_DELETE_EXECUTION\x10\"\x12\x1d\n" + + "\x19TASK_TYPE_WORKER_COMMANDS\x10#\"\x04\b\t\x10\t\"\x04\b\v\x10\v\"\x04\b\x17\x10\x17*\\\n" + "\fTaskPriority\x12\x1d\n" + "\x19TASK_PRIORITY_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12TASK_PRIORITY_HIGH\x10\x01\x12\x15\n" + diff --git a/api/health/v1/message.go-helpers.pb.go b/api/health/v1/message.go-helpers.pb.go new file mode 100644 index 00000000000..f35c7bec0b5 --- /dev/null +++ b/api/health/v1/message.go-helpers.pb.go @@ -0,0 +1,117 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package health + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type HealthCheck to the protobuf v3 wire format +func (val *HealthCheck) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type HealthCheck from the protobuf v3 wire format +func (val *HealthCheck) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *HealthCheck) Size() int { + return proto.Size(val) +} + +// Equal returns whether two HealthCheck values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *HealthCheck) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *HealthCheck + switch t := that.(type) { + case *HealthCheck: + that1 = t + case HealthCheck: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type HostHealthDetail to the protobuf v3 wire format +func (val *HostHealthDetail) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type HostHealthDetail from the protobuf v3 wire format +func (val *HostHealthDetail) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *HostHealthDetail) Size() int { + return proto.Size(val) +} + +// Equal returns whether two HostHealthDetail values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *HostHealthDetail) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *HostHealthDetail + switch t := that.(type) { + case *HostHealthDetail: + that1 = t + case HostHealthDetail: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ServiceHealthDetail to the protobuf v3 wire format +func (val *ServiceHealthDetail) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ServiceHealthDetail from the protobuf v3 wire format +func (val *ServiceHealthDetail) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ServiceHealthDetail) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ServiceHealthDetail values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ServiceHealthDetail) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ServiceHealthDetail + switch t := that.(type) { + case *ServiceHealthDetail: + that1 = t + case ServiceHealthDetail: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/api/health/v1/message.pb.go b/api/health/v1/message.pb.go new file mode 100644 index 00000000000..72e1ae78ef4 --- /dev/null +++ b/api/health/v1/message.pb.go @@ -0,0 +1,324 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/api/health/v1/message.proto + +package health + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + v1 "go.temporal.io/server/api/enums/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Individual health check result. +// The check_type field uses human-readable strings rather than an enum for extensibility. +type HealthCheck struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Machine-readable check type identifier for programmatic matching. + // Known values defined as Go constants in api/health/v1/types.go: + // + // "grpc_health", "rpc_latency", "rpc_error_ratio", + // "persistence_latency", "persistence_error_ratio", + // "host_availability", "task_queue_backlog" + // + // We use strings instead of an enum for flexibility: new check types can be + // added without proto changes. See HealthCheck.message for human-readable details. + CheckType string `protobuf:"bytes,1,opt,name=check_type,json=checkType,proto3" json:"check_type,omitempty"` + State v1.HealthState `protobuf:"varint,2,opt,name=state,proto3,enum=temporal.server.api.enums.v1.HealthState" json:"state,omitempty"` + // Actual observed value (0 if N/A). + Value float64 `protobuf:"fixed64,3,opt,name=value,proto3" json:"value,omitempty"` + // Threshold that was exceeded (0 if N/A). + Threshold float64 `protobuf:"fixed64,4,opt,name=threshold,proto3" json:"threshold,omitempty"` + // Human-readable detail describing what happened, e.g. + // "RPC latency 850.00ms exceeded 500.00ms threshold". + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheck) Reset() { + *x = HealthCheck{} + mi := &file_temporal_server_api_health_v1_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheck) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheck) ProtoMessage() {} + +func (x *HealthCheck) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_health_v1_message_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheck.ProtoReflect.Descriptor instead. +func (*HealthCheck) Descriptor() ([]byte, []int) { + return file_temporal_server_api_health_v1_message_proto_rawDescGZIP(), []int{0} +} + +func (x *HealthCheck) GetCheckType() string { + if x != nil { + return x.CheckType + } + return "" +} + +func (x *HealthCheck) GetState() v1.HealthState { + if x != nil { + return x.State + } + return v1.HealthState(0) +} + +func (x *HealthCheck) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *HealthCheck) GetThreshold() float64 { + if x != nil { + return x.Threshold + } + return 0 +} + +func (x *HealthCheck) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// Health details for a single host. +type HostHealthDetail struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + State v1.HealthState `protobuf:"varint,2,opt,name=state,proto3,enum=temporal.server.api.enums.v1.HealthState" json:"state,omitempty"` + Checks []*HealthCheck `protobuf:"bytes,3,rep,name=checks,proto3" json:"checks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostHealthDetail) Reset() { + *x = HostHealthDetail{} + mi := &file_temporal_server_api_health_v1_message_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostHealthDetail) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostHealthDetail) ProtoMessage() {} + +func (x *HostHealthDetail) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_health_v1_message_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostHealthDetail.ProtoReflect.Descriptor instead. +func (*HostHealthDetail) Descriptor() ([]byte, []int) { + return file_temporal_server_api_health_v1_message_proto_rawDescGZIP(), []int{1} +} + +func (x *HostHealthDetail) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *HostHealthDetail) GetState() v1.HealthState { + if x != nil { + return x.State + } + return v1.HealthState(0) +} + +func (x *HostHealthDetail) GetChecks() []*HealthCheck { + if x != nil { + return x.Checks + } + return nil +} + +// Health details for a service (history, frontend, matching). +type ServiceHealthDetail struct { + state protoimpl.MessageState `protogen:"open.v1"` + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + State v1.HealthState `protobuf:"varint,2,opt,name=state,proto3,enum=temporal.server.api.enums.v1.HealthState" json:"state,omitempty"` + Hosts []*HostHealthDetail `protobuf:"bytes,3,rep,name=hosts,proto3" json:"hosts,omitempty"` + // Service-level diagnostic message (e.g. "no available hosts", "resolver error"). + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServiceHealthDetail) Reset() { + *x = ServiceHealthDetail{} + mi := &file_temporal_server_api_health_v1_message_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServiceHealthDetail) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceHealthDetail) ProtoMessage() {} + +func (x *ServiceHealthDetail) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_health_v1_message_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServiceHealthDetail.ProtoReflect.Descriptor instead. +func (*ServiceHealthDetail) Descriptor() ([]byte, []int) { + return file_temporal_server_api_health_v1_message_proto_rawDescGZIP(), []int{2} +} + +func (x *ServiceHealthDetail) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *ServiceHealthDetail) GetState() v1.HealthState { + if x != nil { + return x.State + } + return v1.HealthState(0) +} + +func (x *ServiceHealthDetail) GetHosts() []*HostHealthDetail { + if x != nil { + return x.Hosts + } + return nil +} + +func (x *ServiceHealthDetail) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_temporal_server_api_health_v1_message_proto protoreflect.FileDescriptor + +const file_temporal_server_api_health_v1_message_proto_rawDesc = "" + + "\n" + + "+temporal/server/api/health/v1/message.proto\x12\x1dtemporal.server.api.health.v1\x1a*temporal/server/api/enums/v1/cluster.proto\"\xbb\x01\n" + + "\vHealthCheck\x12\x1d\n" + + "\n" + + "check_type\x18\x01 \x01(\tR\tcheckType\x12?\n" + + "\x05state\x18\x02 \x01(\x0e2).temporal.server.api.enums.v1.HealthStateR\x05state\x12\x14\n" + + "\x05value\x18\x03 \x01(\x01R\x05value\x12\x1c\n" + + "\tthreshold\x18\x04 \x01(\x01R\tthreshold\x12\x18\n" + + "\amessage\x18\x05 \x01(\tR\amessage\"\xb1\x01\n" + + "\x10HostHealthDetail\x12\x18\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\x12?\n" + + "\x05state\x18\x02 \x01(\x0e2).temporal.server.api.enums.v1.HealthStateR\x05state\x12B\n" + + "\x06checks\x18\x03 \x03(\v2*.temporal.server.api.health.v1.HealthCheckR\x06checks\"\xd1\x01\n" + + "\x13ServiceHealthDetail\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12?\n" + + "\x05state\x18\x02 \x01(\x0e2).temporal.server.api.enums.v1.HealthStateR\x05state\x12E\n" + + "\x05hosts\x18\x03 \x03(\v2/.temporal.server.api.health.v1.HostHealthDetailR\x05hosts\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessageB,Z*go.temporal.io/server/api/health/v1;healthb\x06proto3" + +var ( + file_temporal_server_api_health_v1_message_proto_rawDescOnce sync.Once + file_temporal_server_api_health_v1_message_proto_rawDescData []byte +) + +func file_temporal_server_api_health_v1_message_proto_rawDescGZIP() []byte { + file_temporal_server_api_health_v1_message_proto_rawDescOnce.Do(func() { + file_temporal_server_api_health_v1_message_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_api_health_v1_message_proto_rawDesc), len(file_temporal_server_api_health_v1_message_proto_rawDesc))) + }) + return file_temporal_server_api_health_v1_message_proto_rawDescData +} + +var file_temporal_server_api_health_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_temporal_server_api_health_v1_message_proto_goTypes = []any{ + (*HealthCheck)(nil), // 0: temporal.server.api.health.v1.HealthCheck + (*HostHealthDetail)(nil), // 1: temporal.server.api.health.v1.HostHealthDetail + (*ServiceHealthDetail)(nil), // 2: temporal.server.api.health.v1.ServiceHealthDetail + (v1.HealthState)(0), // 3: temporal.server.api.enums.v1.HealthState +} +var file_temporal_server_api_health_v1_message_proto_depIdxs = []int32{ + 3, // 0: temporal.server.api.health.v1.HealthCheck.state:type_name -> temporal.server.api.enums.v1.HealthState + 3, // 1: temporal.server.api.health.v1.HostHealthDetail.state:type_name -> temporal.server.api.enums.v1.HealthState + 0, // 2: temporal.server.api.health.v1.HostHealthDetail.checks:type_name -> temporal.server.api.health.v1.HealthCheck + 3, // 3: temporal.server.api.health.v1.ServiceHealthDetail.state:type_name -> temporal.server.api.enums.v1.HealthState + 1, // 4: temporal.server.api.health.v1.ServiceHealthDetail.hosts:type_name -> temporal.server.api.health.v1.HostHealthDetail + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_temporal_server_api_health_v1_message_proto_init() } +func file_temporal_server_api_health_v1_message_proto_init() { + if File_temporal_server_api_health_v1_message_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_health_v1_message_proto_rawDesc), len(file_temporal_server_api_health_v1_message_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_api_health_v1_message_proto_goTypes, + DependencyIndexes: file_temporal_server_api_health_v1_message_proto_depIdxs, + MessageInfos: file_temporal_server_api_health_v1_message_proto_msgTypes, + }.Build() + File_temporal_server_api_health_v1_message_proto = out.File + file_temporal_server_api_health_v1_message_proto_goTypes = nil + file_temporal_server_api_health_v1_message_proto_depIdxs = nil +} diff --git a/api/historyservice/v1/request_response.pb.go b/api/historyservice/v1/request_response.pb.go index 80bf52baa53..e4f8c7b9e1c 100644 --- a/api/historyservice/v1/request_response.pb.go +++ b/api/historyservice/v1/request_response.pb.go @@ -11,27 +11,28 @@ import ( sync "sync" unsafe "unsafe" - v122 "go.temporal.io/api/activity/v1" + v123 "go.temporal.io/api/activity/v1" v14 "go.temporal.io/api/common/v1" v16 "go.temporal.io/api/deployment/v1" v12 "go.temporal.io/api/enums/v1" v13 "go.temporal.io/api/failure/v1" - v115 "go.temporal.io/api/history/v1" + v17 "go.temporal.io/api/history/v1" v121 "go.temporal.io/api/nexus/v1" - v114 "go.temporal.io/api/protocol/v1" - v113 "go.temporal.io/api/query/v1" - v110 "go.temporal.io/api/taskqueue/v1" + v115 "go.temporal.io/api/protocol/v1" + v114 "go.temporal.io/api/query/v1" + v111 "go.temporal.io/api/taskqueue/v1" v15 "go.temporal.io/api/workflow/v1" v1 "go.temporal.io/api/workflowservice/v1" v118 "go.temporal.io/server/api/adminservice/v1" - v17 "go.temporal.io/server/api/clock/v1" + v18 "go.temporal.io/server/api/clock/v1" v119 "go.temporal.io/server/api/common/v1" - v111 "go.temporal.io/server/api/enums/v1" - v18 "go.temporal.io/server/api/history/v1" + v112 "go.temporal.io/server/api/enums/v1" + v122 "go.temporal.io/server/api/health/v1" + v19 "go.temporal.io/server/api/history/v1" v116 "go.temporal.io/server/api/namespace/v1" - v19 "go.temporal.io/server/api/persistence/v1" + v110 "go.temporal.io/server/api/persistence/v1" v117 "go.temporal.io/server/api/replication/v1" - v112 "go.temporal.io/server/api/taskqueue/v1" + v113 "go.temporal.io/server/api/taskqueue/v1" v120 "go.temporal.io/server/api/token/v1" v11 "go.temporal.io/server/api/workflow/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -194,8 +195,14 @@ type StartWorkflowExecutionRequest struct { // After the first workflow task, the effective behavior of the workflow is determined by worker-sent values in // subsequent workflow tasks. InheritedAutoUpgradeInfo *v16.InheritedAutoUpgradeInfo `protobuf:"bytes,16,opt,name=inherited_auto_upgrade_info,json=inheritedAutoUpgradeInfo,proto3" json:"inherited_auto_upgrade_info,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The target version that the previous run implicitly declined to upgrade to. + // Computed at continue-as-new time from the previous run's last_notified_target_version + // (if set) or its existing declined value (CaN chain). For retries, passed through + // directly from the started event. Written onto the new run's + // WorkflowExecutionStartedEvent. + DeclinedTargetVersionUpgrade *v17.DeclinedTargetVersionUpgrade `protobuf:"bytes,17,opt,name=declined_target_version_upgrade,json=declinedTargetVersionUpgrade,proto3" json:"declined_target_version_upgrade,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StartWorkflowExecutionRequest) Reset() { @@ -340,10 +347,17 @@ func (x *StartWorkflowExecutionRequest) GetInheritedAutoUpgradeInfo() *v16.Inher return nil } +func (x *StartWorkflowExecutionRequest) GetDeclinedTargetVersionUpgrade() *v17.DeclinedTargetVersionUpgrade { + if x != nil { + return x.DeclinedTargetVersionUpgrade + } + return nil +} + type StartWorkflowExecutionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,2,opt,name=clock,proto3" json:"clock,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,2,opt,name=clock,proto3" json:"clock,omitempty"` // Set if request_eager_execution is set on the start request EagerWorkflowTask *v1.PollWorkflowTaskQueueResponse `protobuf:"bytes,3,opt,name=eager_workflow_task,json=eagerWorkflowTask,proto3" json:"eager_workflow_task,omitempty"` Started bool `protobuf:"varint,4,opt,name=started,proto3" json:"started,omitempty"` @@ -390,7 +404,7 @@ func (x *StartWorkflowExecutionResponse) GetRunId() string { return "" } -func (x *StartWorkflowExecutionResponse) GetClock() *v17.VectorClock { +func (x *StartWorkflowExecutionResponse) GetClock() *v18.VectorClock { if x != nil { return x.Clock } @@ -426,13 +440,13 @@ func (x *StartWorkflowExecutionResponse) GetLink() *v14.Link { } type GetMutableStateRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` - Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` - ExpectedNextEventId int64 `protobuf:"varint,3,opt,name=expected_next_event_id,json=expectedNextEventId,proto3" json:"expected_next_event_id,omitempty"` - CurrentBranchToken []byte `protobuf:"bytes,4,opt,name=current_branch_token,json=currentBranchToken,proto3" json:"current_branch_token,omitempty"` - VersionHistoryItem *v18.VersionHistoryItem `protobuf:"bytes,5,opt,name=version_history_item,json=versionHistoryItem,proto3" json:"version_history_item,omitempty"` - VersionedTransition *v19.VersionedTransition `protobuf:"bytes,6,opt,name=versioned_transition,json=versionedTransition,proto3" json:"versioned_transition,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` + ExpectedNextEventId int64 `protobuf:"varint,3,opt,name=expected_next_event_id,json=expectedNextEventId,proto3" json:"expected_next_event_id,omitempty"` + CurrentBranchToken []byte `protobuf:"bytes,4,opt,name=current_branch_token,json=currentBranchToken,proto3" json:"current_branch_token,omitempty"` + VersionHistoryItem *v19.VersionHistoryItem `protobuf:"bytes,5,opt,name=version_history_item,json=versionHistoryItem,proto3" json:"version_history_item,omitempty"` + VersionedTransition *v110.VersionedTransition `protobuf:"bytes,6,opt,name=versioned_transition,json=versionedTransition,proto3" json:"versioned_transition,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -495,14 +509,14 @@ func (x *GetMutableStateRequest) GetCurrentBranchToken() []byte { return nil } -func (x *GetMutableStateRequest) GetVersionHistoryItem() *v18.VersionHistoryItem { +func (x *GetMutableStateRequest) GetVersionHistoryItem() *v19.VersionHistoryItem { if x != nil { return x.VersionHistoryItem } return nil } -func (x *GetMutableStateRequest) GetVersionedTransition() *v19.VersionedTransition { +func (x *GetMutableStateRequest) GetVersionedTransition() *v110.VersionedTransition { if x != nil { return x.VersionedTransition } @@ -516,16 +530,16 @@ type GetMutableStateResponse struct { NextEventId int64 `protobuf:"varint,3,opt,name=next_event_id,json=nextEventId,proto3" json:"next_event_id,omitempty"` PreviousStartedEventId int64 `protobuf:"varint,4,opt,name=previous_started_event_id,json=previousStartedEventId,proto3" json:"previous_started_event_id,omitempty"` LastFirstEventId int64 `protobuf:"varint,5,opt,name=last_first_event_id,json=lastFirstEventId,proto3" json:"last_first_event_id,omitempty"` - TaskQueue *v110.TaskQueue `protobuf:"bytes,6,opt,name=task_queue,json=taskQueue,proto3" json:"task_queue,omitempty"` - StickyTaskQueue *v110.TaskQueue `protobuf:"bytes,7,opt,name=sticky_task_queue,json=stickyTaskQueue,proto3" json:"sticky_task_queue,omitempty"` + TaskQueue *v111.TaskQueue `protobuf:"bytes,6,opt,name=task_queue,json=taskQueue,proto3" json:"task_queue,omitempty"` + StickyTaskQueue *v111.TaskQueue `protobuf:"bytes,7,opt,name=sticky_task_queue,json=stickyTaskQueue,proto3" json:"sticky_task_queue,omitempty"` // (-- api-linter: core::0140::prepositions=disabled // // aip.dev/not-precedent: "to" is used to indicate interval. --) StickyTaskQueueScheduleToStartTimeout *durationpb.Duration `protobuf:"bytes,11,opt,name=sticky_task_queue_schedule_to_start_timeout,json=stickyTaskQueueScheduleToStartTimeout,proto3" json:"sticky_task_queue_schedule_to_start_timeout,omitempty"` CurrentBranchToken []byte `protobuf:"bytes,13,opt,name=current_branch_token,json=currentBranchToken,proto3" json:"current_branch_token,omitempty"` - WorkflowState v111.WorkflowExecutionState `protobuf:"varint,15,opt,name=workflow_state,json=workflowState,proto3,enum=temporal.server.api.enums.v1.WorkflowExecutionState" json:"workflow_state,omitempty"` + WorkflowState v112.WorkflowExecutionState `protobuf:"varint,15,opt,name=workflow_state,json=workflowState,proto3,enum=temporal.server.api.enums.v1.WorkflowExecutionState" json:"workflow_state,omitempty"` WorkflowStatus v12.WorkflowExecutionStatus `protobuf:"varint,16,opt,name=workflow_status,json=workflowStatus,proto3,enum=temporal.api.enums.v1.WorkflowExecutionStatus" json:"workflow_status,omitempty"` - VersionHistories *v18.VersionHistories `protobuf:"bytes,17,opt,name=version_histories,json=versionHistories,proto3" json:"version_histories,omitempty"` + VersionHistories *v19.VersionHistories `protobuf:"bytes,17,opt,name=version_histories,json=versionHistories,proto3" json:"version_histories,omitempty"` IsStickyTaskQueueEnabled bool `protobuf:"varint,18,opt,name=is_sticky_task_queue_enabled,json=isStickyTaskQueueEnabled,proto3" json:"is_sticky_task_queue_enabled,omitempty"` LastFirstEventTxnId int64 `protobuf:"varint,19,opt,name=last_first_event_txn_id,json=lastFirstEventTxnId,proto3" json:"last_first_event_txn_id,omitempty"` FirstExecutionRunId string `protobuf:"bytes,20,opt,name=first_execution_run_id,json=firstExecutionRunId,proto3" json:"first_execution_run_id,omitempty"` @@ -536,10 +550,13 @@ type GetMutableStateResponse struct { // for this execution. AssignedBuildId string `protobuf:"bytes,22,opt,name=assigned_build_id,json=assignedBuildId,proto3" json:"assigned_build_id,omitempty"` InheritedBuildId string `protobuf:"bytes,23,opt,name=inherited_build_id,json=inheritedBuildId,proto3" json:"inherited_build_id,omitempty"` - TransitionHistory []*v19.VersionedTransition `protobuf:"bytes,24,rep,name=transition_history,json=transitionHistory,proto3" json:"transition_history,omitempty"` + TransitionHistory []*v110.VersionedTransition `protobuf:"bytes,24,rep,name=transition_history,json=transitionHistory,proto3" json:"transition_history,omitempty"` VersioningInfo *v15.WorkflowExecutionVersioningInfo `protobuf:"bytes,25,opt,name=versioning_info,json=versioningInfo,proto3" json:"versioning_info,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Transient or speculative workflow task events which are not yet persisted in the history. + // These events should be appended to the history when it is returned to the worker. + TransientOrSpeculativeTasks *v19.TransientWorkflowTaskInfo `protobuf:"bytes,26,opt,name=transient_or_speculative_tasks,json=transientOrSpeculativeTasks,proto3" json:"transient_or_speculative_tasks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetMutableStateResponse) Reset() { @@ -607,14 +624,14 @@ func (x *GetMutableStateResponse) GetLastFirstEventId() int64 { return 0 } -func (x *GetMutableStateResponse) GetTaskQueue() *v110.TaskQueue { +func (x *GetMutableStateResponse) GetTaskQueue() *v111.TaskQueue { if x != nil { return x.TaskQueue } return nil } -func (x *GetMutableStateResponse) GetStickyTaskQueue() *v110.TaskQueue { +func (x *GetMutableStateResponse) GetStickyTaskQueue() *v111.TaskQueue { if x != nil { return x.StickyTaskQueue } @@ -635,11 +652,11 @@ func (x *GetMutableStateResponse) GetCurrentBranchToken() []byte { return nil } -func (x *GetMutableStateResponse) GetWorkflowState() v111.WorkflowExecutionState { +func (x *GetMutableStateResponse) GetWorkflowState() v112.WorkflowExecutionState { if x != nil { return x.WorkflowState } - return v111.WorkflowExecutionState(0) + return v112.WorkflowExecutionState(0) } func (x *GetMutableStateResponse) GetWorkflowStatus() v12.WorkflowExecutionStatus { @@ -649,7 +666,7 @@ func (x *GetMutableStateResponse) GetWorkflowStatus() v12.WorkflowExecutionStatu return v12.WorkflowExecutionStatus(0) } -func (x *GetMutableStateResponse) GetVersionHistories() *v18.VersionHistories { +func (x *GetMutableStateResponse) GetVersionHistories() *v19.VersionHistories { if x != nil { return x.VersionHistories } @@ -698,7 +715,7 @@ func (x *GetMutableStateResponse) GetInheritedBuildId() string { return "" } -func (x *GetMutableStateResponse) GetTransitionHistory() []*v19.VersionedTransition { +func (x *GetMutableStateResponse) GetTransitionHistory() []*v110.VersionedTransition { if x != nil { return x.TransitionHistory } @@ -712,13 +729,20 @@ func (x *GetMutableStateResponse) GetVersioningInfo() *v15.WorkflowExecutionVers return nil } +func (x *GetMutableStateResponse) GetTransientOrSpeculativeTasks() *v19.TransientWorkflowTaskInfo { + if x != nil { + return x.TransientOrSpeculativeTasks + } + return nil +} + type PollMutableStateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` ExpectedNextEventId int64 `protobuf:"varint,3,opt,name=expected_next_event_id,json=expectedNextEventId,proto3" json:"expected_next_event_id,omitempty"` CurrentBranchToken []byte `protobuf:"bytes,4,opt,name=current_branch_token,json=currentBranchToken,proto3" json:"current_branch_token,omitempty"` - VersionHistoryItem *v18.VersionHistoryItem `protobuf:"bytes,5,opt,name=version_history_item,json=versionHistoryItem,proto3" json:"version_history_item,omitempty"` + VersionHistoryItem *v19.VersionHistoryItem `protobuf:"bytes,5,opt,name=version_history_item,json=versionHistoryItem,proto3" json:"version_history_item,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -781,7 +805,7 @@ func (x *PollMutableStateRequest) GetCurrentBranchToken() []byte { return nil } -func (x *PollMutableStateRequest) GetVersionHistoryItem() *v18.VersionHistoryItem { +func (x *PollMutableStateRequest) GetVersionHistoryItem() *v19.VersionHistoryItem { if x != nil { return x.VersionHistoryItem } @@ -795,15 +819,15 @@ type PollMutableStateResponse struct { NextEventId int64 `protobuf:"varint,3,opt,name=next_event_id,json=nextEventId,proto3" json:"next_event_id,omitempty"` PreviousStartedEventId int64 `protobuf:"varint,4,opt,name=previous_started_event_id,json=previousStartedEventId,proto3" json:"previous_started_event_id,omitempty"` LastFirstEventId int64 `protobuf:"varint,5,opt,name=last_first_event_id,json=lastFirstEventId,proto3" json:"last_first_event_id,omitempty"` - TaskQueue *v110.TaskQueue `protobuf:"bytes,6,opt,name=task_queue,json=taskQueue,proto3" json:"task_queue,omitempty"` - StickyTaskQueue *v110.TaskQueue `protobuf:"bytes,7,opt,name=sticky_task_queue,json=stickyTaskQueue,proto3" json:"sticky_task_queue,omitempty"` + TaskQueue *v111.TaskQueue `protobuf:"bytes,6,opt,name=task_queue,json=taskQueue,proto3" json:"task_queue,omitempty"` + StickyTaskQueue *v111.TaskQueue `protobuf:"bytes,7,opt,name=sticky_task_queue,json=stickyTaskQueue,proto3" json:"sticky_task_queue,omitempty"` // (-- api-linter: core::0140::prepositions=disabled // // aip.dev/not-precedent: "to" is used to indicate interval. --) StickyTaskQueueScheduleToStartTimeout *durationpb.Duration `protobuf:"bytes,11,opt,name=sticky_task_queue_schedule_to_start_timeout,json=stickyTaskQueueScheduleToStartTimeout,proto3" json:"sticky_task_queue_schedule_to_start_timeout,omitempty"` CurrentBranchToken []byte `protobuf:"bytes,12,opt,name=current_branch_token,json=currentBranchToken,proto3" json:"current_branch_token,omitempty"` - VersionHistories *v18.VersionHistories `protobuf:"bytes,14,opt,name=version_histories,json=versionHistories,proto3" json:"version_histories,omitempty"` - WorkflowState v111.WorkflowExecutionState `protobuf:"varint,15,opt,name=workflow_state,json=workflowState,proto3,enum=temporal.server.api.enums.v1.WorkflowExecutionState" json:"workflow_state,omitempty"` + VersionHistories *v19.VersionHistories `protobuf:"bytes,14,opt,name=version_histories,json=versionHistories,proto3" json:"version_histories,omitempty"` + WorkflowState v112.WorkflowExecutionState `protobuf:"varint,15,opt,name=workflow_state,json=workflowState,proto3,enum=temporal.server.api.enums.v1.WorkflowExecutionState" json:"workflow_state,omitempty"` WorkflowStatus v12.WorkflowExecutionStatus `protobuf:"varint,16,opt,name=workflow_status,json=workflowStatus,proto3,enum=temporal.api.enums.v1.WorkflowExecutionStatus" json:"workflow_status,omitempty"` LastFirstEventTxnId int64 `protobuf:"varint,17,opt,name=last_first_event_txn_id,json=lastFirstEventTxnId,proto3" json:"last_first_event_txn_id,omitempty"` FirstExecutionRunId string `protobuf:"bytes,18,opt,name=first_execution_run_id,json=firstExecutionRunId,proto3" json:"first_execution_run_id,omitempty"` @@ -876,14 +900,14 @@ func (x *PollMutableStateResponse) GetLastFirstEventId() int64 { return 0 } -func (x *PollMutableStateResponse) GetTaskQueue() *v110.TaskQueue { +func (x *PollMutableStateResponse) GetTaskQueue() *v111.TaskQueue { if x != nil { return x.TaskQueue } return nil } -func (x *PollMutableStateResponse) GetStickyTaskQueue() *v110.TaskQueue { +func (x *PollMutableStateResponse) GetStickyTaskQueue() *v111.TaskQueue { if x != nil { return x.StickyTaskQueue } @@ -904,18 +928,18 @@ func (x *PollMutableStateResponse) GetCurrentBranchToken() []byte { return nil } -func (x *PollMutableStateResponse) GetVersionHistories() *v18.VersionHistories { +func (x *PollMutableStateResponse) GetVersionHistories() *v19.VersionHistories { if x != nil { return x.VersionHistories } return nil } -func (x *PollMutableStateResponse) GetWorkflowState() v111.WorkflowExecutionState { +func (x *PollMutableStateResponse) GetWorkflowState() v112.WorkflowExecutionState { if x != nil { return x.WorkflowState } - return v111.WorkflowExecutionState(0) + return v112.WorkflowExecutionState(0) } func (x *PollMutableStateResponse) GetWorkflowStatus() v12.WorkflowExecutionStatus { @@ -1139,13 +1163,13 @@ type RecordWorkflowTaskStartedRequest struct { // Unique id of each poll request. Used to ensure at most once delivery of tasks. RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` PollRequest *v1.PollWorkflowTaskQueueRequest `protobuf:"bytes,6,opt,name=poll_request,json=pollRequest,proto3" json:"poll_request,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,7,opt,name=clock,proto3" json:"clock,omitempty"` - BuildIdRedirectInfo *v112.BuildIdRedirectInfo `protobuf:"bytes,8,opt,name=build_id_redirect_info,json=buildIdRedirectInfo,proto3" json:"build_id_redirect_info,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,7,opt,name=clock,proto3" json:"clock,omitempty"` + BuildIdRedirectInfo *v113.BuildIdRedirectInfo `protobuf:"bytes,8,opt,name=build_id_redirect_info,json=buildIdRedirectInfo,proto3" json:"build_id_redirect_info,omitempty"` // The deployment passed by History when the task was scheduled. // Deprecated. use `version_directive.deployment`. ScheduledDeployment *v16.Deployment `protobuf:"bytes,9,opt,name=scheduled_deployment,json=scheduledDeployment,proto3" json:"scheduled_deployment,omitempty"` // Versioning directive that was sent by history when scheduling the task. - VersionDirective *v112.TaskVersionDirective `protobuf:"bytes,10,opt,name=version_directive,json=versionDirective,proto3" json:"version_directive,omitempty"` + VersionDirective *v113.TaskVersionDirective `protobuf:"bytes,10,opt,name=version_directive,json=versionDirective,proto3" json:"version_directive,omitempty"` // Stamp value from when the workflow task was scheduled. Used to validate the task is still relevant. Stamp int32 `protobuf:"varint,11,opt,name=stamp,proto3" json:"stamp,omitempty"` // Revision number that was sent by matching when the task was dispatched. Used to resolve eventual consistency issues @@ -1224,14 +1248,14 @@ func (x *RecordWorkflowTaskStartedRequest) GetPollRequest() *v1.PollWorkflowTask return nil } -func (x *RecordWorkflowTaskStartedRequest) GetClock() *v17.VectorClock { +func (x *RecordWorkflowTaskStartedRequest) GetClock() *v18.VectorClock { if x != nil { return x.Clock } return nil } -func (x *RecordWorkflowTaskStartedRequest) GetBuildIdRedirectInfo() *v112.BuildIdRedirectInfo { +func (x *RecordWorkflowTaskStartedRequest) GetBuildIdRedirectInfo() *v113.BuildIdRedirectInfo { if x != nil { return x.BuildIdRedirectInfo } @@ -1245,7 +1269,7 @@ func (x *RecordWorkflowTaskStartedRequest) GetScheduledDeployment() *v16.Deploym return nil } -func (x *RecordWorkflowTaskStartedRequest) GetVersionDirective() *v112.TaskVersionDirective { +func (x *RecordWorkflowTaskStartedRequest) GetVersionDirective() *v113.TaskVersionDirective { if x != nil { return x.VersionDirective } @@ -1282,16 +1306,16 @@ type RecordWorkflowTaskStartedResponse struct { NextEventId int64 `protobuf:"varint,5,opt,name=next_event_id,json=nextEventId,proto3" json:"next_event_id,omitempty"` Attempt int32 `protobuf:"varint,6,opt,name=attempt,proto3" json:"attempt,omitempty"` StickyExecutionEnabled bool `protobuf:"varint,7,opt,name=sticky_execution_enabled,json=stickyExecutionEnabled,proto3" json:"sticky_execution_enabled,omitempty"` - TransientWorkflowTask *v18.TransientWorkflowTaskInfo `protobuf:"bytes,8,opt,name=transient_workflow_task,json=transientWorkflowTask,proto3" json:"transient_workflow_task,omitempty"` - WorkflowExecutionTaskQueue *v110.TaskQueue `protobuf:"bytes,9,opt,name=workflow_execution_task_queue,json=workflowExecutionTaskQueue,proto3" json:"workflow_execution_task_queue,omitempty"` + TransientWorkflowTask *v19.TransientWorkflowTaskInfo `protobuf:"bytes,8,opt,name=transient_workflow_task,json=transientWorkflowTask,proto3" json:"transient_workflow_task,omitempty"` + WorkflowExecutionTaskQueue *v111.TaskQueue `protobuf:"bytes,9,opt,name=workflow_execution_task_queue,json=workflowExecutionTaskQueue,proto3" json:"workflow_execution_task_queue,omitempty"` BranchToken []byte `protobuf:"bytes,11,opt,name=branch_token,json=branchToken,proto3" json:"branch_token,omitempty"` ScheduledTime *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=scheduled_time,json=scheduledTime,proto3" json:"scheduled_time,omitempty"` StartedTime *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=started_time,json=startedTime,proto3" json:"started_time,omitempty"` - Queries map[string]*v113.WorkflowQuery `protobuf:"bytes,14,rep,name=queries,proto3" json:"queries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Clock *v17.VectorClock `protobuf:"bytes,15,opt,name=clock,proto3" json:"clock,omitempty"` - Messages []*v114.Message `protobuf:"bytes,16,rep,name=messages,proto3" json:"messages,omitempty"` + Queries map[string]*v114.WorkflowQuery `protobuf:"bytes,14,rep,name=queries,proto3" json:"queries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Clock *v18.VectorClock `protobuf:"bytes,15,opt,name=clock,proto3" json:"clock,omitempty"` + Messages []*v115.Message `protobuf:"bytes,16,rep,name=messages,proto3" json:"messages,omitempty"` Version int64 `protobuf:"varint,17,opt,name=version,proto3" json:"version,omitempty"` - History *v115.History `protobuf:"bytes,18,opt,name=history,proto3" json:"history,omitempty"` + History *v17.History `protobuf:"bytes,18,opt,name=history,proto3" json:"history,omitempty"` NextPageToken []byte `protobuf:"bytes,19,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` // Deprecated: This field is being replaced by raw_history_bytes which sends raw bytes // instead of a proto-decoded History. This avoids matching service having to decode history. @@ -1308,8 +1332,8 @@ type RecordWorkflowTaskStartedResponse struct { // as raw_history_bytes (field 21) will be the only field used. // // Deprecated: Marked as deprecated in temporal/server/api/historyservice/v1/request_response.proto. - RawHistory *v115.History `protobuf:"bytes,20,opt,name=raw_history,json=rawHistory,proto3" json:"raw_history,omitempty"` - RawHistoryBytes [][]byte `protobuf:"bytes,21,rep,name=raw_history_bytes,json=rawHistoryBytes,proto3" json:"raw_history_bytes,omitempty"` + RawHistory *v17.History `protobuf:"bytes,20,opt,name=raw_history,json=rawHistory,proto3" json:"raw_history,omitempty"` + RawHistoryBytes [][]byte `protobuf:"bytes,21,rep,name=raw_history_bytes,json=rawHistoryBytes,proto3" json:"raw_history_bytes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1393,14 +1417,14 @@ func (x *RecordWorkflowTaskStartedResponse) GetStickyExecutionEnabled() bool { return false } -func (x *RecordWorkflowTaskStartedResponse) GetTransientWorkflowTask() *v18.TransientWorkflowTaskInfo { +func (x *RecordWorkflowTaskStartedResponse) GetTransientWorkflowTask() *v19.TransientWorkflowTaskInfo { if x != nil { return x.TransientWorkflowTask } return nil } -func (x *RecordWorkflowTaskStartedResponse) GetWorkflowExecutionTaskQueue() *v110.TaskQueue { +func (x *RecordWorkflowTaskStartedResponse) GetWorkflowExecutionTaskQueue() *v111.TaskQueue { if x != nil { return x.WorkflowExecutionTaskQueue } @@ -1428,21 +1452,21 @@ func (x *RecordWorkflowTaskStartedResponse) GetStartedTime() *timestamppb.Timest return nil } -func (x *RecordWorkflowTaskStartedResponse) GetQueries() map[string]*v113.WorkflowQuery { +func (x *RecordWorkflowTaskStartedResponse) GetQueries() map[string]*v114.WorkflowQuery { if x != nil { return x.Queries } return nil } -func (x *RecordWorkflowTaskStartedResponse) GetClock() *v17.VectorClock { +func (x *RecordWorkflowTaskStartedResponse) GetClock() *v18.VectorClock { if x != nil { return x.Clock } return nil } -func (x *RecordWorkflowTaskStartedResponse) GetMessages() []*v114.Message { +func (x *RecordWorkflowTaskStartedResponse) GetMessages() []*v115.Message { if x != nil { return x.Messages } @@ -1456,7 +1480,7 @@ func (x *RecordWorkflowTaskStartedResponse) GetVersion() int64 { return 0 } -func (x *RecordWorkflowTaskStartedResponse) GetHistory() *v115.History { +func (x *RecordWorkflowTaskStartedResponse) GetHistory() *v17.History { if x != nil { return x.History } @@ -1471,7 +1495,7 @@ func (x *RecordWorkflowTaskStartedResponse) GetNextPageToken() []byte { } // Deprecated: Marked as deprecated in temporal/server/api/historyservice/v1/request_response.proto. -func (x *RecordWorkflowTaskStartedResponse) GetRawHistory() *v115.History { +func (x *RecordWorkflowTaskStartedResponse) GetRawHistory() *v17.History { if x != nil { return x.RawHistory } @@ -1512,16 +1536,16 @@ type RecordWorkflowTaskStartedResponseWithRawHistory struct { NextEventId int64 `protobuf:"varint,5,opt,name=next_event_id,json=nextEventId,proto3" json:"next_event_id,omitempty"` Attempt int32 `protobuf:"varint,6,opt,name=attempt,proto3" json:"attempt,omitempty"` StickyExecutionEnabled bool `protobuf:"varint,7,opt,name=sticky_execution_enabled,json=stickyExecutionEnabled,proto3" json:"sticky_execution_enabled,omitempty"` - TransientWorkflowTask *v18.TransientWorkflowTaskInfo `protobuf:"bytes,8,opt,name=transient_workflow_task,json=transientWorkflowTask,proto3" json:"transient_workflow_task,omitempty"` - WorkflowExecutionTaskQueue *v110.TaskQueue `protobuf:"bytes,9,opt,name=workflow_execution_task_queue,json=workflowExecutionTaskQueue,proto3" json:"workflow_execution_task_queue,omitempty"` + TransientWorkflowTask *v19.TransientWorkflowTaskInfo `protobuf:"bytes,8,opt,name=transient_workflow_task,json=transientWorkflowTask,proto3" json:"transient_workflow_task,omitempty"` + WorkflowExecutionTaskQueue *v111.TaskQueue `protobuf:"bytes,9,opt,name=workflow_execution_task_queue,json=workflowExecutionTaskQueue,proto3" json:"workflow_execution_task_queue,omitempty"` BranchToken []byte `protobuf:"bytes,11,opt,name=branch_token,json=branchToken,proto3" json:"branch_token,omitempty"` ScheduledTime *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=scheduled_time,json=scheduledTime,proto3" json:"scheduled_time,omitempty"` StartedTime *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=started_time,json=startedTime,proto3" json:"started_time,omitempty"` - Queries map[string]*v113.WorkflowQuery `protobuf:"bytes,14,rep,name=queries,proto3" json:"queries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Clock *v17.VectorClock `protobuf:"bytes,15,opt,name=clock,proto3" json:"clock,omitempty"` - Messages []*v114.Message `protobuf:"bytes,16,rep,name=messages,proto3" json:"messages,omitempty"` + Queries map[string]*v114.WorkflowQuery `protobuf:"bytes,14,rep,name=queries,proto3" json:"queries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Clock *v18.VectorClock `protobuf:"bytes,15,opt,name=clock,proto3" json:"clock,omitempty"` + Messages []*v115.Message `protobuf:"bytes,16,rep,name=messages,proto3" json:"messages,omitempty"` Version int64 `protobuf:"varint,17,opt,name=version,proto3" json:"version,omitempty"` - History *v115.History `protobuf:"bytes,18,opt,name=history,proto3" json:"history,omitempty"` + History *v17.History `protobuf:"bytes,18,opt,name=history,proto3" json:"history,omitempty"` NextPageToken []byte `protobuf:"bytes,19,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` // Deprecated: This field is being replaced by raw_history_bytes which sends raw bytes // instead of a proto-decoded History. This avoids matching service having to decode history. @@ -1612,14 +1636,14 @@ func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetStickyExecutionEnab return false } -func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetTransientWorkflowTask() *v18.TransientWorkflowTaskInfo { +func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetTransientWorkflowTask() *v19.TransientWorkflowTaskInfo { if x != nil { return x.TransientWorkflowTask } return nil } -func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetWorkflowExecutionTaskQueue() *v110.TaskQueue { +func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetWorkflowExecutionTaskQueue() *v111.TaskQueue { if x != nil { return x.WorkflowExecutionTaskQueue } @@ -1647,21 +1671,21 @@ func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetStartedTime() *time return nil } -func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetQueries() map[string]*v113.WorkflowQuery { +func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetQueries() map[string]*v114.WorkflowQuery { if x != nil { return x.Queries } return nil } -func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetClock() *v17.VectorClock { +func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetClock() *v18.VectorClock { if x != nil { return x.Clock } return nil } -func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetMessages() []*v114.Message { +func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetMessages() []*v115.Message { if x != nil { return x.Messages } @@ -1675,7 +1699,7 @@ func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetVersion() int64 { return 0 } -func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetHistory() *v115.History { +func (x *RecordWorkflowTaskStartedResponseWithRawHistory) GetHistory() *v17.History { if x != nil { return x.History } @@ -1712,15 +1736,15 @@ type RecordActivityTaskStartedRequest struct { // Unique id of each poll request. Used to ensure at most once delivery of tasks. RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` PollRequest *v1.PollActivityTaskQueueRequest `protobuf:"bytes,6,opt,name=poll_request,json=pollRequest,proto3" json:"poll_request,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,7,opt,name=clock,proto3" json:"clock,omitempty"` - BuildIdRedirectInfo *v112.BuildIdRedirectInfo `protobuf:"bytes,8,opt,name=build_id_redirect_info,json=buildIdRedirectInfo,proto3" json:"build_id_redirect_info,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,7,opt,name=clock,proto3" json:"clock,omitempty"` + BuildIdRedirectInfo *v113.BuildIdRedirectInfo `protobuf:"bytes,8,opt,name=build_id_redirect_info,json=buildIdRedirectInfo,proto3" json:"build_id_redirect_info,omitempty"` // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. Stamp int32 `protobuf:"varint,9,opt,name=stamp,proto3" json:"stamp,omitempty"` // The deployment passed by History when the task was scheduled. // Deprecated. use `version_directive.deployment`. ScheduledDeployment *v16.Deployment `protobuf:"bytes,10,opt,name=scheduled_deployment,json=scheduledDeployment,proto3" json:"scheduled_deployment,omitempty"` // Versioning directive that was sent by history when scheduling the task. - VersionDirective *v112.TaskVersionDirective `protobuf:"bytes,12,opt,name=version_directive,json=versionDirective,proto3" json:"version_directive,omitempty"` + VersionDirective *v113.TaskVersionDirective `protobuf:"bytes,12,opt,name=version_directive,json=versionDirective,proto3" json:"version_directive,omitempty"` // Revision number that was sent by matching when the task was dispatched. Used to resolve eventual consistency issues // that may arise due to stale routing configs in task queue partitions. TaskDispatchRevisionNumber int64 `protobuf:"varint,13,opt,name=task_dispatch_revision_number,json=taskDispatchRevisionNumber,proto3" json:"task_dispatch_revision_number,omitempty"` @@ -1797,14 +1821,14 @@ func (x *RecordActivityTaskStartedRequest) GetPollRequest() *v1.PollActivityTask return nil } -func (x *RecordActivityTaskStartedRequest) GetClock() *v17.VectorClock { +func (x *RecordActivityTaskStartedRequest) GetClock() *v18.VectorClock { if x != nil { return x.Clock } return nil } -func (x *RecordActivityTaskStartedRequest) GetBuildIdRedirectInfo() *v112.BuildIdRedirectInfo { +func (x *RecordActivityTaskStartedRequest) GetBuildIdRedirectInfo() *v113.BuildIdRedirectInfo { if x != nil { return x.BuildIdRedirectInfo } @@ -1825,7 +1849,7 @@ func (x *RecordActivityTaskStartedRequest) GetScheduledDeployment() *v16.Deploym return nil } -func (x *RecordActivityTaskStartedRequest) GetVersionDirective() *v112.TaskVersionDirective { +func (x *RecordActivityTaskStartedRequest) GetVersionDirective() *v113.TaskVersionDirective { if x != nil { return x.VersionDirective } @@ -1848,14 +1872,14 @@ func (x *RecordActivityTaskStartedRequest) GetComponentRef() []byte { type RecordActivityTaskStartedResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - ScheduledEvent *v115.HistoryEvent `protobuf:"bytes,1,opt,name=scheduled_event,json=scheduledEvent,proto3" json:"scheduled_event,omitempty"` + ScheduledEvent *v17.HistoryEvent `protobuf:"bytes,1,opt,name=scheduled_event,json=scheduledEvent,proto3" json:"scheduled_event,omitempty"` StartedTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=started_time,json=startedTime,proto3" json:"started_time,omitempty"` Attempt int32 `protobuf:"varint,3,opt,name=attempt,proto3" json:"attempt,omitempty"` CurrentAttemptScheduledTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=current_attempt_scheduled_time,json=currentAttemptScheduledTime,proto3" json:"current_attempt_scheduled_time,omitempty"` HeartbeatDetails *v14.Payloads `protobuf:"bytes,5,opt,name=heartbeat_details,json=heartbeatDetails,proto3" json:"heartbeat_details,omitempty"` WorkflowType *v14.WorkflowType `protobuf:"bytes,6,opt,name=workflow_type,json=workflowType,proto3" json:"workflow_type,omitempty"` WorkflowNamespace string `protobuf:"bytes,7,opt,name=workflow_namespace,json=workflowNamespace,proto3" json:"workflow_namespace,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,8,opt,name=clock,proto3" json:"clock,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,8,opt,name=clock,proto3" json:"clock,omitempty"` Version int64 `protobuf:"varint,9,opt,name=version,proto3" json:"version,omitempty"` Priority *v14.Priority `protobuf:"bytes,10,opt,name=priority,proto3" json:"priority,omitempty"` RetryPolicy *v14.RetryPolicy `protobuf:"bytes,11,opt,name=retry_policy,json=retryPolicy,proto3" json:"retry_policy,omitempty"` @@ -1896,7 +1920,7 @@ func (*RecordActivityTaskStartedResponse) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{15} } -func (x *RecordActivityTaskStartedResponse) GetScheduledEvent() *v115.HistoryEvent { +func (x *RecordActivityTaskStartedResponse) GetScheduledEvent() *v17.HistoryEvent { if x != nil { return x.ScheduledEvent } @@ -1945,7 +1969,7 @@ func (x *RecordActivityTaskStartedResponse) GetWorkflowNamespace() string { return "" } -func (x *RecordActivityTaskStartedResponse) GetClock() *v17.VectorClock { +func (x *RecordActivityTaskStartedResponse) GetClock() *v18.VectorClock { if x != nil { return x.Clock } @@ -2201,7 +2225,7 @@ type IsWorkflowTaskValidRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,3,opt,name=clock,proto3" json:"clock,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,3,opt,name=clock,proto3" json:"clock,omitempty"` ScheduledEventId int64 `protobuf:"varint,4,opt,name=scheduled_event_id,json=scheduledEventId,proto3" json:"scheduled_event_id,omitempty"` Stamp int32 `protobuf:"varint,5,opt,name=stamp,proto3" json:"stamp,omitempty"` unknownFields protoimpl.UnknownFields @@ -2252,7 +2276,7 @@ func (x *IsWorkflowTaskValidRequest) GetExecution() *v14.WorkflowExecution { return nil } -func (x *IsWorkflowTaskValidRequest) GetClock() *v17.VectorClock { +func (x *IsWorkflowTaskValidRequest) GetClock() *v18.VectorClock { if x != nil { return x.Clock } @@ -2698,7 +2722,7 @@ type IsActivityTaskValidRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,3,opt,name=clock,proto3" json:"clock,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,3,opt,name=clock,proto3" json:"clock,omitempty"` ScheduledEventId int64 `protobuf:"varint,4,opt,name=scheduled_event_id,json=scheduledEventId,proto3" json:"scheduled_event_id,omitempty"` // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. Stamp int32 `protobuf:"varint,5,opt,name=stamp,proto3" json:"stamp,omitempty"` @@ -2750,7 +2774,7 @@ func (x *IsActivityTaskValidRequest) GetExecution() *v14.WorkflowExecution { return nil } -func (x *IsActivityTaskValidRequest) GetClock() *v17.VectorClock { +func (x *IsActivityTaskValidRequest) GetClock() *v18.VectorClock { if x != nil { return x.Clock } @@ -3536,8 +3560,8 @@ type ScheduleWorkflowTaskRequest struct { NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` WorkflowExecution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` IsFirstWorkflowTask bool `protobuf:"varint,3,opt,name=is_first_workflow_task,json=isFirstWorkflowTask,proto3" json:"is_first_workflow_task,omitempty"` - ChildClock *v17.VectorClock `protobuf:"bytes,4,opt,name=child_clock,json=childClock,proto3" json:"child_clock,omitempty"` - ParentClock *v17.VectorClock `protobuf:"bytes,5,opt,name=parent_clock,json=parentClock,proto3" json:"parent_clock,omitempty"` + ChildClock *v18.VectorClock `protobuf:"bytes,4,opt,name=child_clock,json=childClock,proto3" json:"child_clock,omitempty"` + ParentClock *v18.VectorClock `protobuf:"bytes,5,opt,name=parent_clock,json=parentClock,proto3" json:"parent_clock,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3593,14 +3617,14 @@ func (x *ScheduleWorkflowTaskRequest) GetIsFirstWorkflowTask() bool { return false } -func (x *ScheduleWorkflowTaskRequest) GetChildClock() *v17.VectorClock { +func (x *ScheduleWorkflowTaskRequest) GetChildClock() *v18.VectorClock { if x != nil { return x.ChildClock } return nil } -func (x *ScheduleWorkflowTaskRequest) GetParentClock() *v17.VectorClock { +func (x *ScheduleWorkflowTaskRequest) GetParentClock() *v18.VectorClock { if x != nil { return x.ParentClock } @@ -3647,7 +3671,7 @@ type VerifyFirstWorkflowTaskScheduledRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` WorkflowExecution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,3,opt,name=clock,proto3" json:"clock,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,3,opt,name=clock,proto3" json:"clock,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3696,7 +3720,7 @@ func (x *VerifyFirstWorkflowTaskScheduledRequest) GetWorkflowExecution() *v14.Wo return nil } -func (x *VerifyFirstWorkflowTaskScheduledRequest) GetClock() *v17.VectorClock { +func (x *VerifyFirstWorkflowTaskScheduledRequest) GetClock() *v18.VectorClock { if x != nil { return x.Clock } @@ -3751,8 +3775,8 @@ type RecordChildExecutionCompletedRequest struct { ParentExecution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=parent_execution,json=parentExecution,proto3" json:"parent_execution,omitempty"` ParentInitiatedId int64 `protobuf:"varint,3,opt,name=parent_initiated_id,json=parentInitiatedId,proto3" json:"parent_initiated_id,omitempty"` ChildExecution *v14.WorkflowExecution `protobuf:"bytes,4,opt,name=child_execution,json=childExecution,proto3" json:"child_execution,omitempty"` - CompletionEvent *v115.HistoryEvent `protobuf:"bytes,5,opt,name=completion_event,json=completionEvent,proto3" json:"completion_event,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,6,opt,name=clock,proto3" json:"clock,omitempty"` + CompletionEvent *v17.HistoryEvent `protobuf:"bytes,5,opt,name=completion_event,json=completionEvent,proto3" json:"completion_event,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,6,opt,name=clock,proto3" json:"clock,omitempty"` ParentInitiatedVersion int64 `protobuf:"varint,7,opt,name=parent_initiated_version,json=parentInitiatedVersion,proto3" json:"parent_initiated_version,omitempty"` ChildFirstExecutionRunId string `protobuf:"bytes,8,opt,name=child_first_execution_run_id,json=childFirstExecutionRunId,proto3" json:"child_first_execution_run_id,omitempty"` unknownFields protoimpl.UnknownFields @@ -3817,14 +3841,14 @@ func (x *RecordChildExecutionCompletedRequest) GetChildExecution() *v14.Workflow return nil } -func (x *RecordChildExecutionCompletedRequest) GetCompletionEvent() *v115.HistoryEvent { +func (x *RecordChildExecutionCompletedRequest) GetCompletionEvent() *v17.HistoryEvent { if x != nil { return x.CompletionEvent } return nil } -func (x *RecordChildExecutionCompletedRequest) GetClock() *v17.VectorClock { +func (x *RecordChildExecutionCompletedRequest) GetClock() *v18.VectorClock { if x != nil { return x.Clock } @@ -3888,7 +3912,7 @@ type VerifyChildExecutionCompletionRecordedRequest struct { ChildExecution *v14.WorkflowExecution `protobuf:"bytes,3,opt,name=child_execution,json=childExecution,proto3" json:"child_execution,omitempty"` ParentInitiatedId int64 `protobuf:"varint,4,opt,name=parent_initiated_id,json=parentInitiatedId,proto3" json:"parent_initiated_id,omitempty"` ParentInitiatedVersion int64 `protobuf:"varint,5,opt,name=parent_initiated_version,json=parentInitiatedVersion,proto3" json:"parent_initiated_version,omitempty"` - Clock *v17.VectorClock `protobuf:"bytes,6,opt,name=clock,proto3" json:"clock,omitempty"` + Clock *v18.VectorClock `protobuf:"bytes,6,opt,name=clock,proto3" json:"clock,omitempty"` ResendParent bool `protobuf:"varint,7,opt,name=resend_parent,json=resendParent,proto3" json:"resend_parent,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3959,7 +3983,7 @@ func (x *VerifyChildExecutionCompletionRecordedRequest) GetParentInitiatedVersio return 0 } -func (x *VerifyChildExecutionCompletionRecordedRequest) GetClock() *v17.VectorClock { +func (x *VerifyChildExecutionCompletionRecordedRequest) GetClock() *v18.VectorClock { if x != nil { return x.Clock } @@ -4165,7 +4189,7 @@ type ReplicateEventsV2Request struct { state protoimpl.MessageState `protogen:"open.v1"` NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` WorkflowExecution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` - VersionHistoryItems []*v18.VersionHistoryItem `protobuf:"bytes,3,rep,name=version_history_items,json=versionHistoryItems,proto3" json:"version_history_items,omitempty"` + VersionHistoryItems []*v19.VersionHistoryItem `protobuf:"bytes,3,rep,name=version_history_items,json=versionHistoryItems,proto3" json:"version_history_items,omitempty"` Events *v14.DataBlob `protobuf:"bytes,4,opt,name=events,proto3" json:"events,omitempty"` // New run events does not need version history since there is no prior events. NewRunEvents *v14.DataBlob `protobuf:"bytes,5,opt,name=new_run_events,json=newRunEvents,proto3" json:"new_run_events,omitempty"` @@ -4219,7 +4243,7 @@ func (x *ReplicateEventsV2Request) GetWorkflowExecution() *v14.WorkflowExecution return nil } -func (x *ReplicateEventsV2Request) GetVersionHistoryItems() []*v18.VersionHistoryItem { +func (x *ReplicateEventsV2Request) GetVersionHistoryItems() []*v19.VersionHistoryItem { if x != nil { return x.VersionHistoryItems } @@ -4291,12 +4315,12 @@ func (*ReplicateEventsV2Response) Descriptor() ([]byte, []int) { } type ReplicateWorkflowStateRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkflowState *v19.WorkflowMutableState `protobuf:"bytes,1,opt,name=workflow_state,json=workflowState,proto3" json:"workflow_state,omitempty"` - RemoteCluster string `protobuf:"bytes,2,opt,name=remote_cluster,json=remoteCluster,proto3" json:"remote_cluster,omitempty"` - NamespaceId string `protobuf:"bytes,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` - IsForceReplication bool `protobuf:"varint,4,opt,name=is_force_replication,json=isForceReplication,proto3" json:"is_force_replication,omitempty"` - IsCloseTransferTaskAcked bool `protobuf:"varint,5,opt,name=is_close_transfer_task_acked,json=isCloseTransferTaskAcked,proto3" json:"is_close_transfer_task_acked,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowState *v110.WorkflowMutableState `protobuf:"bytes,1,opt,name=workflow_state,json=workflowState,proto3" json:"workflow_state,omitempty"` + RemoteCluster string `protobuf:"bytes,2,opt,name=remote_cluster,json=remoteCluster,proto3" json:"remote_cluster,omitempty"` + NamespaceId string `protobuf:"bytes,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + IsForceReplication bool `protobuf:"varint,4,opt,name=is_force_replication,json=isForceReplication,proto3" json:"is_force_replication,omitempty"` + IsCloseTransferTaskAcked bool `protobuf:"varint,5,opt,name=is_close_transfer_task_acked,json=isCloseTransferTaskAcked,proto3" json:"is_close_transfer_task_acked,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4331,7 +4355,7 @@ func (*ReplicateWorkflowStateRequest) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{58} } -func (x *ReplicateWorkflowStateRequest) GetWorkflowState() *v19.WorkflowMutableState { +func (x *ReplicateWorkflowStateRequest) GetWorkflowState() *v110.WorkflowMutableState { if x != nil { return x.WorkflowState } @@ -4513,7 +4537,7 @@ type SyncActivityRequest struct { Attempt int32 `protobuf:"varint,11,opt,name=attempt,proto3" json:"attempt,omitempty"` LastFailure *v13.Failure `protobuf:"bytes,12,opt,name=last_failure,json=lastFailure,proto3" json:"last_failure,omitempty"` LastWorkerIdentity string `protobuf:"bytes,13,opt,name=last_worker_identity,json=lastWorkerIdentity,proto3" json:"last_worker_identity,omitempty"` - VersionHistory *v18.VersionHistory `protobuf:"bytes,14,opt,name=version_history,json=versionHistory,proto3" json:"version_history,omitempty"` + VersionHistory *v19.VersionHistory `protobuf:"bytes,14,opt,name=version_history,json=versionHistory,proto3" json:"version_history,omitempty"` BaseExecutionInfo *v11.BaseExecutionInfo `protobuf:"bytes,15,opt,name=base_execution_info,json=baseExecutionInfo,proto3" json:"base_execution_info,omitempty"` // build ID of the worker who received this activity last time LastStartedBuildId string `protobuf:"bytes,16,opt,name=last_started_build_id,json=lastStartedBuildId,proto3" json:"last_started_build_id,omitempty"` @@ -4658,7 +4682,7 @@ func (x *SyncActivityRequest) GetLastWorkerIdentity() string { return "" } -func (x *SyncActivityRequest) GetVersionHistory() *v18.VersionHistory { +func (x *SyncActivityRequest) GetVersionHistory() *v19.VersionHistory { if x != nil { return x.VersionHistory } @@ -4829,7 +4853,7 @@ type ActivitySyncInfo struct { Attempt int32 `protobuf:"varint,8,opt,name=attempt,proto3" json:"attempt,omitempty"` LastFailure *v13.Failure `protobuf:"bytes,9,opt,name=last_failure,json=lastFailure,proto3" json:"last_failure,omitempty"` LastWorkerIdentity string `protobuf:"bytes,10,opt,name=last_worker_identity,json=lastWorkerIdentity,proto3" json:"last_worker_identity,omitempty"` - VersionHistory *v18.VersionHistory `protobuf:"bytes,11,opt,name=version_history,json=versionHistory,proto3" json:"version_history,omitempty"` + VersionHistory *v19.VersionHistory `protobuf:"bytes,11,opt,name=version_history,json=versionHistory,proto3" json:"version_history,omitempty"` // build ID of the worker who received this activity last time LastStartedBuildId string `protobuf:"bytes,12,opt,name=last_started_build_id,json=lastStartedBuildId,proto3" json:"last_started_build_id,omitempty"` // workflows redirect_counter value when this activity started last time @@ -4952,7 +4976,7 @@ func (x *ActivitySyncInfo) GetLastWorkerIdentity() string { return "" } -func (x *ActivitySyncInfo) GetVersionHistory() *v18.VersionHistory { +func (x *ActivitySyncInfo) GetVersionHistory() *v19.VersionHistory { if x != nil { return x.VersionHistory } @@ -5144,10 +5168,10 @@ func (x *DescribeMutableStateRequest) GetArchetypeId() uint32 { type DescribeMutableStateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // CacheMutableState is only available when mutable state is in cache. - CacheMutableState *v19.WorkflowMutableState `protobuf:"bytes,1,opt,name=cache_mutable_state,json=cacheMutableState,proto3" json:"cache_mutable_state,omitempty"` + CacheMutableState *v110.WorkflowMutableState `protobuf:"bytes,1,opt,name=cache_mutable_state,json=cacheMutableState,proto3" json:"cache_mutable_state,omitempty"` // DatabaseMutableState is always available, // but only loaded from database when mutable state is NOT in cache or skip_force_reload is false. - DatabaseMutableState *v19.WorkflowMutableState `protobuf:"bytes,2,opt,name=database_mutable_state,json=databaseMutableState,proto3" json:"database_mutable_state,omitempty"` + DatabaseMutableState *v110.WorkflowMutableState `protobuf:"bytes,2,opt,name=database_mutable_state,json=databaseMutableState,proto3" json:"database_mutable_state,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5182,14 +5206,14 @@ func (*DescribeMutableStateResponse) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{67} } -func (x *DescribeMutableStateResponse) GetCacheMutableState() *v19.WorkflowMutableState { +func (x *DescribeMutableStateResponse) GetCacheMutableState() *v110.WorkflowMutableState { if x != nil { return x.CacheMutableState } return nil } -func (x *DescribeMutableStateResponse) GetDatabaseMutableState() *v19.WorkflowMutableState { +func (x *DescribeMutableStateResponse) GetDatabaseMutableState() *v110.WorkflowMutableState { if x != nil { return x.DatabaseMutableState } @@ -5460,7 +5484,7 @@ func (x *GetShardRequest) GetShardId() int32 { type GetShardResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - ShardInfo *v19.ShardInfo `protobuf:"bytes,1,opt,name=shard_info,json=shardInfo,proto3" json:"shard_info,omitempty"` + ShardInfo *v110.ShardInfo `protobuf:"bytes,1,opt,name=shard_info,json=shardInfo,proto3" json:"shard_info,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5495,7 +5519,7 @@ func (*GetShardResponse) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{73} } -func (x *GetShardResponse) GetShardInfo() *v19.ShardInfo { +func (x *GetShardResponse) GetShardInfo() *v110.ShardInfo { if x != nil { return x.ShardInfo } @@ -5977,7 +6001,7 @@ func (*ReapplyEventsResponse) Descriptor() ([]byte, []int) { type GetDLQMessagesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Type v111.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` + Type v112.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` ShardId int32 `protobuf:"varint,2,opt,name=shard_id,json=shardId,proto3" json:"shard_id,omitempty"` SourceCluster string `protobuf:"bytes,3,opt,name=source_cluster,json=sourceCluster,proto3" json:"source_cluster,omitempty"` InclusiveEndMessageId int64 `protobuf:"varint,4,opt,name=inclusive_end_message_id,json=inclusiveEndMessageId,proto3" json:"inclusive_end_message_id,omitempty"` @@ -6017,11 +6041,11 @@ func (*GetDLQMessagesRequest) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{84} } -func (x *GetDLQMessagesRequest) GetType() v111.DeadLetterQueueType { +func (x *GetDLQMessagesRequest) GetType() v112.DeadLetterQueueType { if x != nil { return x.Type } - return v111.DeadLetterQueueType(0) + return v112.DeadLetterQueueType(0) } func (x *GetDLQMessagesRequest) GetShardId() int32 { @@ -6061,7 +6085,7 @@ func (x *GetDLQMessagesRequest) GetNextPageToken() []byte { type GetDLQMessagesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Type v111.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` + Type v112.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` ReplicationTasks []*v117.ReplicationTask `protobuf:"bytes,2,rep,name=replication_tasks,json=replicationTasks,proto3" json:"replication_tasks,omitempty"` NextPageToken []byte `protobuf:"bytes,3,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` ReplicationTasksInfo []*v117.ReplicationTaskInfo `protobuf:"bytes,4,rep,name=replication_tasks_info,json=replicationTasksInfo,proto3" json:"replication_tasks_info,omitempty"` @@ -6099,11 +6123,11 @@ func (*GetDLQMessagesResponse) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{85} } -func (x *GetDLQMessagesResponse) GetType() v111.DeadLetterQueueType { +func (x *GetDLQMessagesResponse) GetType() v112.DeadLetterQueueType { if x != nil { return x.Type } - return v111.DeadLetterQueueType(0) + return v112.DeadLetterQueueType(0) } func (x *GetDLQMessagesResponse) GetReplicationTasks() []*v117.ReplicationTask { @@ -6129,7 +6153,7 @@ func (x *GetDLQMessagesResponse) GetReplicationTasksInfo() []*v117.ReplicationTa type PurgeDLQMessagesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Type v111.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` + Type v112.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` ShardId int32 `protobuf:"varint,2,opt,name=shard_id,json=shardId,proto3" json:"shard_id,omitempty"` SourceCluster string `protobuf:"bytes,3,opt,name=source_cluster,json=sourceCluster,proto3" json:"source_cluster,omitempty"` InclusiveEndMessageId int64 `protobuf:"varint,4,opt,name=inclusive_end_message_id,json=inclusiveEndMessageId,proto3" json:"inclusive_end_message_id,omitempty"` @@ -6167,11 +6191,11 @@ func (*PurgeDLQMessagesRequest) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{86} } -func (x *PurgeDLQMessagesRequest) GetType() v111.DeadLetterQueueType { +func (x *PurgeDLQMessagesRequest) GetType() v112.DeadLetterQueueType { if x != nil { return x.Type } - return v111.DeadLetterQueueType(0) + return v112.DeadLetterQueueType(0) } func (x *PurgeDLQMessagesRequest) GetShardId() int32 { @@ -6233,7 +6257,7 @@ func (*PurgeDLQMessagesResponse) Descriptor() ([]byte, []int) { type MergeDLQMessagesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Type v111.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` + Type v112.DeadLetterQueueType `protobuf:"varint,1,opt,name=type,proto3,enum=temporal.server.api.enums.v1.DeadLetterQueueType" json:"type,omitempty"` ShardId int32 `protobuf:"varint,2,opt,name=shard_id,json=shardId,proto3" json:"shard_id,omitempty"` SourceCluster string `protobuf:"bytes,3,opt,name=source_cluster,json=sourceCluster,proto3" json:"source_cluster,omitempty"` InclusiveEndMessageId int64 `protobuf:"varint,4,opt,name=inclusive_end_message_id,json=inclusiveEndMessageId,proto3" json:"inclusive_end_message_id,omitempty"` @@ -6273,11 +6297,11 @@ func (*MergeDLQMessagesRequest) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{88} } -func (x *MergeDLQMessagesRequest) GetType() v111.DeadLetterQueueType { +func (x *MergeDLQMessagesRequest) GetType() v112.DeadLetterQueueType { if x != nil { return x.Type } - return v111.DeadLetterQueueType(0) + return v112.DeadLetterQueueType(0) } func (x *MergeDLQMessagesRequest) GetShardId() int32 { @@ -6944,7 +6968,7 @@ type ImportWorkflowExecutionRequest struct { NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` HistoryBatches []*v14.DataBlob `protobuf:"bytes,3,rep,name=history_batches,json=historyBatches,proto3" json:"history_batches,omitempty"` - VersionHistory *v18.VersionHistory `protobuf:"bytes,4,opt,name=version_history,json=versionHistory,proto3" json:"version_history,omitempty"` + VersionHistory *v19.VersionHistory `protobuf:"bytes,4,opt,name=version_history,json=versionHistory,proto3" json:"version_history,omitempty"` Token []byte `protobuf:"bytes,5,opt,name=token,proto3" json:"token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -7001,7 +7025,7 @@ func (x *ImportWorkflowExecutionRequest) GetHistoryBatches() []*v14.DataBlob { return nil } -func (x *ImportWorkflowExecutionRequest) GetVersionHistory() *v18.VersionHistory { +func (x *ImportWorkflowExecutionRequest) GetVersionHistory() *v19.VersionHistory { if x != nil { return x.VersionHistory } @@ -7555,7 +7579,7 @@ func (x *GetWorkflowExecutionHistoryRequest) GetRequest() *v1.GetWorkflowExecuti type GetWorkflowExecutionHistoryResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Response *v1.GetWorkflowExecutionHistoryResponse `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` - History *v115.History `protobuf:"bytes,2,opt,name=history,proto3" json:"history,omitempty"` + History *v17.History `protobuf:"bytes,2,opt,name=history,proto3" json:"history,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -7597,7 +7621,7 @@ func (x *GetWorkflowExecutionHistoryResponse) GetResponse() *v1.GetWorkflowExecu return nil } -func (x *GetWorkflowExecutionHistoryResponse) GetHistory() *v115.History { +func (x *GetWorkflowExecutionHistoryResponse) GetHistory() *v17.History { if x != nil { return x.History } @@ -8873,7 +8897,7 @@ type InvokeStateMachineMethodRequest struct { RunId string `protobuf:"bytes,3,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` // Reference including the path to the backing Operation state machine and a version + transition count for // staleness checks. - Ref *v19.StateMachineRef `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"` + Ref *v110.StateMachineRef `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"` // The method name to invoke. Methods must be explicitly registered for the target state machine in the state // machine registry, and accept an argument type of HistoryEvent that is the completion event of the completed // workflow. @@ -8935,7 +8959,7 @@ func (x *InvokeStateMachineMethodRequest) GetRunId() string { return "" } -func (x *InvokeStateMachineMethodRequest) GetRef() *v19.StateMachineRef { +func (x *InvokeStateMachineMethodRequest) GetRef() *v110.StateMachineRef { if x != nil { return x.Ref } @@ -9046,8 +9070,10 @@ func (x *DeepHealthCheckRequest) GetHostAddress() string { } type DeepHealthCheckResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - State v111.HealthState `protobuf:"varint,1,opt,name=state,proto3,enum=temporal.server.api.enums.v1.HealthState" json:"state,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + State v112.HealthState `protobuf:"varint,1,opt,name=state,proto3,enum=temporal.server.api.enums.v1.HealthState" json:"state,omitempty"` + // Per-check diagnostic results. Populated for all checks regardless of state. + Checks []*v122.HealthCheck `protobuf:"bytes,2,rep,name=checks,proto3" json:"checks,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -9082,20 +9108,27 @@ func (*DeepHealthCheckResponse) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{139} } -func (x *DeepHealthCheckResponse) GetState() v111.HealthState { +func (x *DeepHealthCheckResponse) GetState() v112.HealthState { if x != nil { return x.State } - return v111.HealthState(0) + return v112.HealthState(0) +} + +func (x *DeepHealthCheckResponse) GetChecks() []*v122.HealthCheck { + if x != nil { + return x.Checks + } + return nil } type SyncWorkflowStateRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` - Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` - VersionedTransition *v19.VersionedTransition `protobuf:"bytes,3,opt,name=versioned_transition,json=versionedTransition,proto3" json:"versioned_transition,omitempty"` - VersionHistories *v18.VersionHistories `protobuf:"bytes,4,opt,name=version_histories,json=versionHistories,proto3" json:"version_histories,omitempty"` - TargetClusterId int32 `protobuf:"varint,5,opt,name=target_cluster_id,json=targetClusterId,proto3" json:"target_cluster_id,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + Execution *v14.WorkflowExecution `protobuf:"bytes,2,opt,name=execution,proto3" json:"execution,omitempty"` + VersionedTransition *v110.VersionedTransition `protobuf:"bytes,3,opt,name=versioned_transition,json=versionedTransition,proto3" json:"versioned_transition,omitempty"` + VersionHistories *v19.VersionHistories `protobuf:"bytes,4,opt,name=version_histories,json=versionHistories,proto3" json:"version_histories,omitempty"` + TargetClusterId int32 `protobuf:"varint,5,opt,name=target_cluster_id,json=targetClusterId,proto3" json:"target_cluster_id,omitempty"` // (-- api-linter: core::0141::forbidden-types=disabled --) ArchetypeId uint32 `protobuf:"varint,6,opt,name=archetype_id,json=archetypeId,proto3" json:"archetype_id,omitempty"` unknownFields protoimpl.UnknownFields @@ -9146,14 +9179,14 @@ func (x *SyncWorkflowStateRequest) GetExecution() *v14.WorkflowExecution { return nil } -func (x *SyncWorkflowStateRequest) GetVersionedTransition() *v19.VersionedTransition { +func (x *SyncWorkflowStateRequest) GetVersionedTransition() *v110.VersionedTransition { if x != nil { return x.VersionedTransition } return nil } -func (x *SyncWorkflowStateRequest) GetVersionHistories() *v18.VersionHistories { +func (x *SyncWorkflowStateRequest) GetVersionHistories() *v19.VersionHistories { if x != nil { return x.VersionHistories } @@ -9276,7 +9309,7 @@ func (x *UpdateActivityOptionsRequest) GetUpdateRequest() *v1.UpdateActivityOpti type UpdateActivityOptionsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Activity options after an update - ActivityOptions *v122.ActivityOptions `protobuf:"bytes,1,opt,name=activity_options,json=activityOptions,proto3" json:"activity_options,omitempty"` + ActivityOptions *v123.ActivityOptions `protobuf:"bytes,1,opt,name=activity_options,json=activityOptions,proto3" json:"activity_options,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -9311,7 +9344,7 @@ func (*UpdateActivityOptionsResponse) Descriptor() ([]byte, []int) { return file_temporal_server_api_historyservice_v1_request_response_proto_rawDescGZIP(), []int{143} } -func (x *UpdateActivityOptionsResponse) GetActivityOptions() *v122.ActivityOptions { +func (x *UpdateActivityOptionsResponse) GetActivityOptions() *v123.ActivityOptions { if x != nil { return x.ActivityOptions } @@ -10375,7 +10408,7 @@ var File_temporal_server_api_historyservice_v1_request_response_proto protorefle const file_temporal_server_api_historyservice_v1_request_response_proto_rawDesc = "" + "\n" + - ".temporal.api.workflowservice.v1.StartWorkflowExecutionRequestR\fstartRequest\x12h\n" + @@ -10405,7 +10438,8 @@ const file_temporal_server_api_historyservice_v1_request_response_proto_rawDesc "\x13versioning_override\x18\r \x01(\v2,.temporal.api.workflow.v1.VersioningOverrideR\x12versioningOverride\x12.\n" + "\x13child_workflow_only\x18\x0e \x01(\bR\x11childWorkflowOnly\x12m\n" + "\x18inherited_pinned_version\x18\x0f \x01(\v23.temporal.api.deployment.v1.WorkerDeploymentVersionR\x16inheritedPinnedVersion\x12s\n" + - "\x1binherited_auto_upgrade_info\x18\x10 \x01(\v24.temporal.api.deployment.v1.InheritedAutoUpgradeInfoR\x18inheritedAutoUpgradeInfo:\x1f\x92\xc4\x03\x1b*\x19start_request.workflow_id\"\xfc\x02\n" + + "\x1binherited_auto_upgrade_info\x18\x10 \x01(\v24.temporal.api.deployment.v1.InheritedAutoUpgradeInfoR\x18inheritedAutoUpgradeInfo\x12|\n" + + "\x1fdeclined_target_version_upgrade\x18\x11 \x01(\v25.temporal.api.history.v1.DeclinedTargetVersionUpgradeR\x1cdeclinedTargetVersionUpgrade:\x1f\x92\xc4\x03\x1b*\x19start_request.workflow_id\"\xfc\x02\n" + "\x1eStartWorkflowExecutionResponse\x12\x15\n" + "\x06run_id\x18\x01 \x01(\tR\x05runId\x12?\n" + "\x05clock\x18\x02 \x01(\v2).temporal.server.api.clock.v1.VectorClockR\x05clock\x12n\n" + @@ -10419,7 +10453,7 @@ const file_temporal_server_api_historyservice_v1_request_response_proto_rawDesc "\x16expected_next_event_id\x18\x03 \x01(\x03R\x13expectedNextEventId\x120\n" + "\x14current_branch_token\x18\x04 \x01(\fR\x12currentBranchToken\x12d\n" + "\x14version_history_item\x18\x05 \x01(\v22.temporal.server.api.history.v1.VersionHistoryItemR\x12versionHistoryItem\x12j\n" + - "\x14versioned_transition\x18\x06 \x01(\v27.temporal.server.api.persistence.v1.VersionedTransitionR\x13versionedTransition:\x1b\x92\xc4\x03\x17*\x15execution.workflow_id\"\xf3\v\n" + + "\x14versioned_transition\x18\x06 \x01(\v27.temporal.server.api.persistence.v1.VersionedTransitionR\x13versionedTransition:\x1b\x92\xc4\x03\x17*\x15execution.workflow_id\"\xf3\f\n" + "\x17GetMutableStateResponse\x12G\n" + "\texecution\x18\x01 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\x12I\n" + "\rworkflow_type\x18\x02 \x01(\v2$.temporal.api.common.v1.WorkflowTypeR\fworkflowType\x12\"\n" + @@ -10441,7 +10475,8 @@ const file_temporal_server_api_historyservice_v1_request_response_proto_rawDesc "\x11assigned_build_id\x18\x16 \x01(\tR\x0fassignedBuildId\x12,\n" + "\x12inherited_build_id\x18\x17 \x01(\tR\x10inheritedBuildId\x12f\n" + "\x12transition_history\x18\x18 \x03(\v27.temporal.server.api.persistence.v1.VersionedTransitionR\x11transitionHistory\x12b\n" + - "\x0fversioning_info\x18\x19 \x01(\v29.temporal.api.workflow.v1.WorkflowExecutionVersioningInfoR\x0eversioningInfoJ\x04\b\b\x10\tJ\x04\b\t\x10\n" + + "\x0fversioning_info\x18\x19 \x01(\v29.temporal.api.workflow.v1.WorkflowExecutionVersioningInfoR\x0eversioningInfo\x12~\n" + + "\x1etransient_or_speculative_tasks\x18\x1a \x01(\v29.temporal.server.api.history.v1.TransientWorkflowTaskInfoR\x1btransientOrSpeculativeTasksJ\x04\b\b\x10\tJ\x04\b\t\x10\n" + "J\x04\b\n" + "\x10\vJ\x04\b\f\x10\rJ\x04\b\x0e\x10\x0f\"\xef\x02\n" + "\x17PollMutableStateRequest\x12!\n" + @@ -11068,9 +11103,10 @@ const file_temporal_server_api_historyservice_v1_request_response_proto_rawDesc " InvokeStateMachineMethodResponse\x12\x16\n" + "\x06output\x18\x01 \x01(\fR\x06output\"C\n" + "\x16DeepHealthCheckRequest\x12!\n" + - "\fhost_address\x18\x01 \x01(\tR\vhostAddress:\x06\x92\xc4\x03\x02\b\x01\"Z\n" + + "\fhost_address\x18\x01 \x01(\tR\vhostAddress:\x06\x92\xc4\x03\x02\b\x01\"\x9e\x01\n" + "\x17DeepHealthCheckResponse\x12?\n" + - "\x05state\x18\x01 \x01(\x0e2).temporal.server.api.enums.v1.HealthStateR\x05state\"\xbd\x03\n" + + "\x05state\x18\x01 \x01(\x0e2).temporal.server.api.enums.v1.HealthStateR\x05state\x12B\n" + + "\x06checks\x18\x02 \x03(\v2*.temporal.server.api.health.v1.HealthCheckR\x06checks\"\xbd\x03\n" + "\x18SyncWorkflowStateRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12G\n" + "\texecution\x18\x02 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\texecution\x12j\n" + @@ -11321,107 +11357,109 @@ var file_temporal_server_api_historyservice_v1_request_response_proto_goTypes = (*v15.VersioningOverride)(nil), // 178: temporal.api.workflow.v1.VersioningOverride (*v16.WorkerDeploymentVersion)(nil), // 179: temporal.api.deployment.v1.WorkerDeploymentVersion (*v16.InheritedAutoUpgradeInfo)(nil), // 180: temporal.api.deployment.v1.InheritedAutoUpgradeInfo - (*v17.VectorClock)(nil), // 181: temporal.server.api.clock.v1.VectorClock - (*v1.PollWorkflowTaskQueueResponse)(nil), // 182: temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse - (v12.WorkflowExecutionStatus)(0), // 183: temporal.api.enums.v1.WorkflowExecutionStatus - (*v14.Link)(nil), // 184: temporal.api.common.v1.Link - (*v14.WorkflowExecution)(nil), // 185: temporal.api.common.v1.WorkflowExecution - (*v18.VersionHistoryItem)(nil), // 186: temporal.server.api.history.v1.VersionHistoryItem - (*v19.VersionedTransition)(nil), // 187: temporal.server.api.persistence.v1.VersionedTransition - (*v14.WorkflowType)(nil), // 188: temporal.api.common.v1.WorkflowType - (*v110.TaskQueue)(nil), // 189: temporal.api.taskqueue.v1.TaskQueue - (v111.WorkflowExecutionState)(0), // 190: temporal.server.api.enums.v1.WorkflowExecutionState - (*v18.VersionHistories)(nil), // 191: temporal.server.api.history.v1.VersionHistories - (*v15.WorkflowExecutionVersioningInfo)(nil), // 192: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - (*v1.PollWorkflowTaskQueueRequest)(nil), // 193: temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest - (*v112.BuildIdRedirectInfo)(nil), // 194: temporal.server.api.taskqueue.v1.BuildIdRedirectInfo - (*v16.Deployment)(nil), // 195: temporal.api.deployment.v1.Deployment - (*v112.TaskVersionDirective)(nil), // 196: temporal.server.api.taskqueue.v1.TaskVersionDirective - (*v18.TransientWorkflowTaskInfo)(nil), // 197: temporal.server.api.history.v1.TransientWorkflowTaskInfo - (*v114.Message)(nil), // 198: temporal.api.protocol.v1.Message - (*v115.History)(nil), // 199: temporal.api.history.v1.History - (*v1.PollActivityTaskQueueRequest)(nil), // 200: temporal.api.workflowservice.v1.PollActivityTaskQueueRequest - (*v115.HistoryEvent)(nil), // 201: temporal.api.history.v1.HistoryEvent - (*v14.Priority)(nil), // 202: temporal.api.common.v1.Priority - (*v14.RetryPolicy)(nil), // 203: temporal.api.common.v1.RetryPolicy - (*v1.RespondWorkflowTaskCompletedRequest)(nil), // 204: temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest - (*v1.PollActivityTaskQueueResponse)(nil), // 205: temporal.api.workflowservice.v1.PollActivityTaskQueueResponse - (*v1.RespondWorkflowTaskFailedRequest)(nil), // 206: temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest - (*v1.RecordActivityTaskHeartbeatRequest)(nil), // 207: temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest - (*v1.RespondActivityTaskCompletedRequest)(nil), // 208: temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest - (*v1.RespondActivityTaskFailedRequest)(nil), // 209: temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest - (*v1.RespondActivityTaskCanceledRequest)(nil), // 210: temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest - (*v1.SignalWorkflowExecutionRequest)(nil), // 211: temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest - (*v1.SignalWithStartWorkflowExecutionRequest)(nil), // 212: temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - (*v1.TerminateWorkflowExecutionRequest)(nil), // 213: temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest - (*v1.ResetWorkflowExecutionRequest)(nil), // 214: temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest - (*v1.RequestCancelWorkflowExecutionRequest)(nil), // 215: temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest - (*v1.DescribeWorkflowExecutionRequest)(nil), // 216: temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest - (*v15.WorkflowExecutionConfig)(nil), // 217: temporal.api.workflow.v1.WorkflowExecutionConfig - (*v15.WorkflowExecutionInfo)(nil), // 218: temporal.api.workflow.v1.WorkflowExecutionInfo - (*v15.PendingActivityInfo)(nil), // 219: temporal.api.workflow.v1.PendingActivityInfo - (*v15.PendingChildExecutionInfo)(nil), // 220: temporal.api.workflow.v1.PendingChildExecutionInfo - (*v15.PendingWorkflowTaskInfo)(nil), // 221: temporal.api.workflow.v1.PendingWorkflowTaskInfo - (*v15.CallbackInfo)(nil), // 222: temporal.api.workflow.v1.CallbackInfo - (*v15.PendingNexusOperationInfo)(nil), // 223: temporal.api.workflow.v1.PendingNexusOperationInfo - (*v15.WorkflowExecutionExtendedInfo)(nil), // 224: temporal.api.workflow.v1.WorkflowExecutionExtendedInfo - (*v14.DataBlob)(nil), // 225: temporal.api.common.v1.DataBlob - (*v11.BaseExecutionInfo)(nil), // 226: temporal.server.api.workflow.v1.BaseExecutionInfo - (*v19.WorkflowMutableState)(nil), // 227: temporal.server.api.persistence.v1.WorkflowMutableState - (*v18.VersionHistory)(nil), // 228: temporal.server.api.history.v1.VersionHistory - (*v116.NamespaceCacheInfo)(nil), // 229: temporal.server.api.namespace.v1.NamespaceCacheInfo - (*v19.ShardInfo)(nil), // 230: temporal.server.api.persistence.v1.ShardInfo - (*v117.ReplicationToken)(nil), // 231: temporal.server.api.replication.v1.ReplicationToken - (*v117.ReplicationTaskInfo)(nil), // 232: temporal.server.api.replication.v1.ReplicationTaskInfo - (*v117.ReplicationTask)(nil), // 233: temporal.server.api.replication.v1.ReplicationTask - (*v1.QueryWorkflowRequest)(nil), // 234: temporal.api.workflowservice.v1.QueryWorkflowRequest - (*v1.QueryWorkflowResponse)(nil), // 235: temporal.api.workflowservice.v1.QueryWorkflowResponse - (*v118.ReapplyEventsRequest)(nil), // 236: temporal.server.api.adminservice.v1.ReapplyEventsRequest - (v111.DeadLetterQueueType)(0), // 237: temporal.server.api.enums.v1.DeadLetterQueueType - (*v118.RefreshWorkflowTasksRequest)(nil), // 238: temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest - (*v1.UpdateWorkflowExecutionRequest)(nil), // 239: temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest - (*v1.UpdateWorkflowExecutionResponse)(nil), // 240: temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse - (*v117.SyncReplicationState)(nil), // 241: temporal.server.api.replication.v1.SyncReplicationState - (*v117.WorkflowReplicationMessages)(nil), // 242: temporal.server.api.replication.v1.WorkflowReplicationMessages - (*v1.PollWorkflowExecutionUpdateRequest)(nil), // 243: temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest - (*v1.PollWorkflowExecutionUpdateResponse)(nil), // 244: temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse - (*v1.GetWorkflowExecutionHistoryRequest)(nil), // 245: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest - (*v1.GetWorkflowExecutionHistoryResponse)(nil), // 246: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse - (*v1.GetWorkflowExecutionHistoryReverseRequest)(nil), // 247: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest - (*v1.GetWorkflowExecutionHistoryReverseResponse)(nil), // 248: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse - (*v118.GetWorkflowExecutionRawHistoryV2Request)(nil), // 249: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request - (*v118.GetWorkflowExecutionRawHistoryV2Response)(nil), // 250: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response - (*v118.GetWorkflowExecutionRawHistoryRequest)(nil), // 251: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest - (*v118.GetWorkflowExecutionRawHistoryResponse)(nil), // 252: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse - (*v118.DeleteWorkflowExecutionRequest)(nil), // 253: temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest - (*v118.DeleteWorkflowExecutionResponse)(nil), // 254: temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse - (*v119.HistoryDLQKey)(nil), // 255: temporal.server.api.common.v1.HistoryDLQKey - (*v119.HistoryDLQTask)(nil), // 256: temporal.server.api.common.v1.HistoryDLQTask - (*v119.HistoryDLQTaskMetadata)(nil), // 257: temporal.server.api.common.v1.HistoryDLQTaskMetadata - (*v118.ListHistoryTasksRequest)(nil), // 258: temporal.server.api.adminservice.v1.ListHistoryTasksRequest - (*v118.ListHistoryTasksResponse)(nil), // 259: temporal.server.api.adminservice.v1.ListHistoryTasksResponse - (*v120.NexusOperationCompletion)(nil), // 260: temporal.server.api.token.v1.NexusOperationCompletion - (*v14.Payload)(nil), // 261: temporal.api.common.v1.Payload - (*v121.Failure)(nil), // 262: temporal.api.nexus.v1.Failure - (*v19.StateMachineRef)(nil), // 263: temporal.server.api.persistence.v1.StateMachineRef - (v111.HealthState)(0), // 264: temporal.server.api.enums.v1.HealthState - (*v117.VersionedTransitionArtifact)(nil), // 265: temporal.server.api.replication.v1.VersionedTransitionArtifact - (*v1.UpdateActivityOptionsRequest)(nil), // 266: temporal.api.workflowservice.v1.UpdateActivityOptionsRequest - (*v122.ActivityOptions)(nil), // 267: temporal.api.activity.v1.ActivityOptions - (*v1.PauseActivityRequest)(nil), // 268: temporal.api.workflowservice.v1.PauseActivityRequest - (*v1.UnpauseActivityRequest)(nil), // 269: temporal.api.workflowservice.v1.UnpauseActivityRequest - (*v1.ResetActivityRequest)(nil), // 270: temporal.api.workflowservice.v1.ResetActivityRequest - (*v1.UpdateWorkflowExecutionOptionsRequest)(nil), // 271: temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest - (*v15.WorkflowExecutionOptions)(nil), // 272: temporal.api.workflow.v1.WorkflowExecutionOptions - (*v1.PauseWorkflowExecutionRequest)(nil), // 273: temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest - (*v1.UnpauseWorkflowExecutionRequest)(nil), // 274: temporal.api.workflowservice.v1.UnpauseWorkflowExecutionRequest - (*v121.StartOperationRequest)(nil), // 275: temporal.api.nexus.v1.StartOperationRequest - (*v121.StartOperationResponse)(nil), // 276: temporal.api.nexus.v1.StartOperationResponse - (*v121.CancelOperationRequest)(nil), // 277: temporal.api.nexus.v1.CancelOperationRequest - (*v121.CancelOperationResponse)(nil), // 278: temporal.api.nexus.v1.CancelOperationResponse - (*v113.WorkflowQuery)(nil), // 279: temporal.api.query.v1.WorkflowQuery - (*v117.ReplicationMessages)(nil), // 280: temporal.server.api.replication.v1.ReplicationMessages - (*descriptorpb.MessageOptions)(nil), // 281: google.protobuf.MessageOptions + (*v17.DeclinedTargetVersionUpgrade)(nil), // 181: temporal.api.history.v1.DeclinedTargetVersionUpgrade + (*v18.VectorClock)(nil), // 182: temporal.server.api.clock.v1.VectorClock + (*v1.PollWorkflowTaskQueueResponse)(nil), // 183: temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse + (v12.WorkflowExecutionStatus)(0), // 184: temporal.api.enums.v1.WorkflowExecutionStatus + (*v14.Link)(nil), // 185: temporal.api.common.v1.Link + (*v14.WorkflowExecution)(nil), // 186: temporal.api.common.v1.WorkflowExecution + (*v19.VersionHistoryItem)(nil), // 187: temporal.server.api.history.v1.VersionHistoryItem + (*v110.VersionedTransition)(nil), // 188: temporal.server.api.persistence.v1.VersionedTransition + (*v14.WorkflowType)(nil), // 189: temporal.api.common.v1.WorkflowType + (*v111.TaskQueue)(nil), // 190: temporal.api.taskqueue.v1.TaskQueue + (v112.WorkflowExecutionState)(0), // 191: temporal.server.api.enums.v1.WorkflowExecutionState + (*v19.VersionHistories)(nil), // 192: temporal.server.api.history.v1.VersionHistories + (*v15.WorkflowExecutionVersioningInfo)(nil), // 193: temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + (*v19.TransientWorkflowTaskInfo)(nil), // 194: temporal.server.api.history.v1.TransientWorkflowTaskInfo + (*v1.PollWorkflowTaskQueueRequest)(nil), // 195: temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest + (*v113.BuildIdRedirectInfo)(nil), // 196: temporal.server.api.taskqueue.v1.BuildIdRedirectInfo + (*v16.Deployment)(nil), // 197: temporal.api.deployment.v1.Deployment + (*v113.TaskVersionDirective)(nil), // 198: temporal.server.api.taskqueue.v1.TaskVersionDirective + (*v115.Message)(nil), // 199: temporal.api.protocol.v1.Message + (*v17.History)(nil), // 200: temporal.api.history.v1.History + (*v1.PollActivityTaskQueueRequest)(nil), // 201: temporal.api.workflowservice.v1.PollActivityTaskQueueRequest + (*v17.HistoryEvent)(nil), // 202: temporal.api.history.v1.HistoryEvent + (*v14.Priority)(nil), // 203: temporal.api.common.v1.Priority + (*v14.RetryPolicy)(nil), // 204: temporal.api.common.v1.RetryPolicy + (*v1.RespondWorkflowTaskCompletedRequest)(nil), // 205: temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest + (*v1.PollActivityTaskQueueResponse)(nil), // 206: temporal.api.workflowservice.v1.PollActivityTaskQueueResponse + (*v1.RespondWorkflowTaskFailedRequest)(nil), // 207: temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest + (*v1.RecordActivityTaskHeartbeatRequest)(nil), // 208: temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest + (*v1.RespondActivityTaskCompletedRequest)(nil), // 209: temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest + (*v1.RespondActivityTaskFailedRequest)(nil), // 210: temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest + (*v1.RespondActivityTaskCanceledRequest)(nil), // 211: temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest + (*v1.SignalWorkflowExecutionRequest)(nil), // 212: temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest + (*v1.SignalWithStartWorkflowExecutionRequest)(nil), // 213: temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + (*v1.TerminateWorkflowExecutionRequest)(nil), // 214: temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest + (*v1.ResetWorkflowExecutionRequest)(nil), // 215: temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest + (*v1.RequestCancelWorkflowExecutionRequest)(nil), // 216: temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest + (*v1.DescribeWorkflowExecutionRequest)(nil), // 217: temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest + (*v15.WorkflowExecutionConfig)(nil), // 218: temporal.api.workflow.v1.WorkflowExecutionConfig + (*v15.WorkflowExecutionInfo)(nil), // 219: temporal.api.workflow.v1.WorkflowExecutionInfo + (*v15.PendingActivityInfo)(nil), // 220: temporal.api.workflow.v1.PendingActivityInfo + (*v15.PendingChildExecutionInfo)(nil), // 221: temporal.api.workflow.v1.PendingChildExecutionInfo + (*v15.PendingWorkflowTaskInfo)(nil), // 222: temporal.api.workflow.v1.PendingWorkflowTaskInfo + (*v15.CallbackInfo)(nil), // 223: temporal.api.workflow.v1.CallbackInfo + (*v15.PendingNexusOperationInfo)(nil), // 224: temporal.api.workflow.v1.PendingNexusOperationInfo + (*v15.WorkflowExecutionExtendedInfo)(nil), // 225: temporal.api.workflow.v1.WorkflowExecutionExtendedInfo + (*v14.DataBlob)(nil), // 226: temporal.api.common.v1.DataBlob + (*v11.BaseExecutionInfo)(nil), // 227: temporal.server.api.workflow.v1.BaseExecutionInfo + (*v110.WorkflowMutableState)(nil), // 228: temporal.server.api.persistence.v1.WorkflowMutableState + (*v19.VersionHistory)(nil), // 229: temporal.server.api.history.v1.VersionHistory + (*v116.NamespaceCacheInfo)(nil), // 230: temporal.server.api.namespace.v1.NamespaceCacheInfo + (*v110.ShardInfo)(nil), // 231: temporal.server.api.persistence.v1.ShardInfo + (*v117.ReplicationToken)(nil), // 232: temporal.server.api.replication.v1.ReplicationToken + (*v117.ReplicationTaskInfo)(nil), // 233: temporal.server.api.replication.v1.ReplicationTaskInfo + (*v117.ReplicationTask)(nil), // 234: temporal.server.api.replication.v1.ReplicationTask + (*v1.QueryWorkflowRequest)(nil), // 235: temporal.api.workflowservice.v1.QueryWorkflowRequest + (*v1.QueryWorkflowResponse)(nil), // 236: temporal.api.workflowservice.v1.QueryWorkflowResponse + (*v118.ReapplyEventsRequest)(nil), // 237: temporal.server.api.adminservice.v1.ReapplyEventsRequest + (v112.DeadLetterQueueType)(0), // 238: temporal.server.api.enums.v1.DeadLetterQueueType + (*v118.RefreshWorkflowTasksRequest)(nil), // 239: temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest + (*v1.UpdateWorkflowExecutionRequest)(nil), // 240: temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest + (*v1.UpdateWorkflowExecutionResponse)(nil), // 241: temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse + (*v117.SyncReplicationState)(nil), // 242: temporal.server.api.replication.v1.SyncReplicationState + (*v117.WorkflowReplicationMessages)(nil), // 243: temporal.server.api.replication.v1.WorkflowReplicationMessages + (*v1.PollWorkflowExecutionUpdateRequest)(nil), // 244: temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest + (*v1.PollWorkflowExecutionUpdateResponse)(nil), // 245: temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse + (*v1.GetWorkflowExecutionHistoryRequest)(nil), // 246: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest + (*v1.GetWorkflowExecutionHistoryResponse)(nil), // 247: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse + (*v1.GetWorkflowExecutionHistoryReverseRequest)(nil), // 248: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest + (*v1.GetWorkflowExecutionHistoryReverseResponse)(nil), // 249: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse + (*v118.GetWorkflowExecutionRawHistoryV2Request)(nil), // 250: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request + (*v118.GetWorkflowExecutionRawHistoryV2Response)(nil), // 251: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response + (*v118.GetWorkflowExecutionRawHistoryRequest)(nil), // 252: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest + (*v118.GetWorkflowExecutionRawHistoryResponse)(nil), // 253: temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse + (*v118.DeleteWorkflowExecutionRequest)(nil), // 254: temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest + (*v118.DeleteWorkflowExecutionResponse)(nil), // 255: temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse + (*v119.HistoryDLQKey)(nil), // 256: temporal.server.api.common.v1.HistoryDLQKey + (*v119.HistoryDLQTask)(nil), // 257: temporal.server.api.common.v1.HistoryDLQTask + (*v119.HistoryDLQTaskMetadata)(nil), // 258: temporal.server.api.common.v1.HistoryDLQTaskMetadata + (*v118.ListHistoryTasksRequest)(nil), // 259: temporal.server.api.adminservice.v1.ListHistoryTasksRequest + (*v118.ListHistoryTasksResponse)(nil), // 260: temporal.server.api.adminservice.v1.ListHistoryTasksResponse + (*v120.NexusOperationCompletion)(nil), // 261: temporal.server.api.token.v1.NexusOperationCompletion + (*v14.Payload)(nil), // 262: temporal.api.common.v1.Payload + (*v121.Failure)(nil), // 263: temporal.api.nexus.v1.Failure + (*v110.StateMachineRef)(nil), // 264: temporal.server.api.persistence.v1.StateMachineRef + (v112.HealthState)(0), // 265: temporal.server.api.enums.v1.HealthState + (*v122.HealthCheck)(nil), // 266: temporal.server.api.health.v1.HealthCheck + (*v117.VersionedTransitionArtifact)(nil), // 267: temporal.server.api.replication.v1.VersionedTransitionArtifact + (*v1.UpdateActivityOptionsRequest)(nil), // 268: temporal.api.workflowservice.v1.UpdateActivityOptionsRequest + (*v123.ActivityOptions)(nil), // 269: temporal.api.activity.v1.ActivityOptions + (*v1.PauseActivityRequest)(nil), // 270: temporal.api.workflowservice.v1.PauseActivityRequest + (*v1.UnpauseActivityRequest)(nil), // 271: temporal.api.workflowservice.v1.UnpauseActivityRequest + (*v1.ResetActivityRequest)(nil), // 272: temporal.api.workflowservice.v1.ResetActivityRequest + (*v1.UpdateWorkflowExecutionOptionsRequest)(nil), // 273: temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest + (*v15.WorkflowExecutionOptions)(nil), // 274: temporal.api.workflow.v1.WorkflowExecutionOptions + (*v1.PauseWorkflowExecutionRequest)(nil), // 275: temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest + (*v1.UnpauseWorkflowExecutionRequest)(nil), // 276: temporal.api.workflowservice.v1.UnpauseWorkflowExecutionRequest + (*v121.StartOperationRequest)(nil), // 277: temporal.api.nexus.v1.StartOperationRequest + (*v121.StartOperationResponse)(nil), // 278: temporal.api.nexus.v1.StartOperationResponse + (*v121.CancelOperationRequest)(nil), // 279: temporal.api.nexus.v1.CancelOperationRequest + (*v121.CancelOperationResponse)(nil), // 280: temporal.api.nexus.v1.CancelOperationResponse + (*v114.WorkflowQuery)(nil), // 281: temporal.api.query.v1.WorkflowQuery + (*v117.ReplicationMessages)(nil), // 282: temporal.server.api.replication.v1.ReplicationMessages + (*descriptorpb.MessageOptions)(nil), // 283: google.protobuf.MessageOptions } var file_temporal_server_api_historyservice_v1_request_response_proto_depIdxs = []int32{ 169, // 0: temporal.server.api.historyservice.v1.StartWorkflowExecutionRequest.start_request:type_name -> temporal.api.workflowservice.v1.StartWorkflowExecutionRequest @@ -11436,256 +11474,259 @@ var file_temporal_server_api_historyservice_v1_request_response_proto_depIdxs = 178, // 9: temporal.server.api.historyservice.v1.StartWorkflowExecutionRequest.versioning_override:type_name -> temporal.api.workflow.v1.VersioningOverride 179, // 10: temporal.server.api.historyservice.v1.StartWorkflowExecutionRequest.inherited_pinned_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion 180, // 11: temporal.server.api.historyservice.v1.StartWorkflowExecutionRequest.inherited_auto_upgrade_info:type_name -> temporal.api.deployment.v1.InheritedAutoUpgradeInfo - 181, // 12: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 182, // 13: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.eager_workflow_task:type_name -> temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse - 183, // 14: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus - 184, // 15: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.link:type_name -> temporal.api.common.v1.Link - 185, // 16: temporal.server.api.historyservice.v1.GetMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 186, // 17: temporal.server.api.historyservice.v1.GetMutableStateRequest.version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 187, // 18: temporal.server.api.historyservice.v1.GetMutableStateRequest.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 185, // 19: temporal.server.api.historyservice.v1.GetMutableStateResponse.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 188, // 20: temporal.server.api.historyservice.v1.GetMutableStateResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType - 189, // 21: temporal.server.api.historyservice.v1.GetMutableStateResponse.task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue - 189, // 22: temporal.server.api.historyservice.v1.GetMutableStateResponse.sticky_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue - 175, // 23: temporal.server.api.historyservice.v1.GetMutableStateResponse.sticky_task_queue_schedule_to_start_timeout:type_name -> google.protobuf.Duration - 190, // 24: temporal.server.api.historyservice.v1.GetMutableStateResponse.workflow_state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState - 183, // 25: temporal.server.api.historyservice.v1.GetMutableStateResponse.workflow_status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus - 191, // 26: temporal.server.api.historyservice.v1.GetMutableStateResponse.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories - 176, // 27: temporal.server.api.historyservice.v1.GetMutableStateResponse.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 187, // 28: temporal.server.api.historyservice.v1.GetMutableStateResponse.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 192, // 29: temporal.server.api.historyservice.v1.GetMutableStateResponse.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - 185, // 30: temporal.server.api.historyservice.v1.PollMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 186, // 31: temporal.server.api.historyservice.v1.PollMutableStateRequest.version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 185, // 32: temporal.server.api.historyservice.v1.PollMutableStateResponse.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 188, // 33: temporal.server.api.historyservice.v1.PollMutableStateResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType - 189, // 34: temporal.server.api.historyservice.v1.PollMutableStateResponse.task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue - 189, // 35: temporal.server.api.historyservice.v1.PollMutableStateResponse.sticky_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue - 175, // 36: temporal.server.api.historyservice.v1.PollMutableStateResponse.sticky_task_queue_schedule_to_start_timeout:type_name -> google.protobuf.Duration - 191, // 37: temporal.server.api.historyservice.v1.PollMutableStateResponse.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories - 190, // 38: temporal.server.api.historyservice.v1.PollMutableStateResponse.workflow_state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState - 183, // 39: temporal.server.api.historyservice.v1.PollMutableStateResponse.workflow_status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus - 185, // 40: temporal.server.api.historyservice.v1.ResetStickyTaskQueueRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 160, // 41: temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.operations:type_name -> temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.Operation - 161, // 42: temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.responses:type_name -> temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.Response - 185, // 43: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 193, // 44: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.poll_request:type_name -> temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest - 181, // 45: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 194, // 46: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.build_id_redirect_info:type_name -> temporal.server.api.taskqueue.v1.BuildIdRedirectInfo - 195, // 47: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.scheduled_deployment:type_name -> temporal.api.deployment.v1.Deployment - 196, // 48: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.version_directive:type_name -> temporal.server.api.taskqueue.v1.TaskVersionDirective - 179, // 49: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.target_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion - 188, // 50: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType - 197, // 51: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.transient_workflow_task:type_name -> temporal.server.api.history.v1.TransientWorkflowTaskInfo - 189, // 52: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.workflow_execution_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue - 171, // 53: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.scheduled_time:type_name -> google.protobuf.Timestamp - 171, // 54: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.started_time:type_name -> google.protobuf.Timestamp - 162, // 55: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.queries:type_name -> temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.QueriesEntry - 181, // 56: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 198, // 57: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.messages:type_name -> temporal.api.protocol.v1.Message - 199, // 58: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.history:type_name -> temporal.api.history.v1.History - 199, // 59: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.raw_history:type_name -> temporal.api.history.v1.History - 188, // 60: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.workflow_type:type_name -> temporal.api.common.v1.WorkflowType - 197, // 61: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.transient_workflow_task:type_name -> temporal.server.api.history.v1.TransientWorkflowTaskInfo - 189, // 62: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.workflow_execution_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue - 171, // 63: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.scheduled_time:type_name -> google.protobuf.Timestamp - 171, // 64: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.started_time:type_name -> google.protobuf.Timestamp - 163, // 65: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.queries:type_name -> temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.QueriesEntry - 181, // 66: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 198, // 67: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.messages:type_name -> temporal.api.protocol.v1.Message - 199, // 68: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.history:type_name -> temporal.api.history.v1.History - 185, // 69: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 200, // 70: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.poll_request:type_name -> temporal.api.workflowservice.v1.PollActivityTaskQueueRequest - 181, // 71: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 194, // 72: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.build_id_redirect_info:type_name -> temporal.server.api.taskqueue.v1.BuildIdRedirectInfo - 195, // 73: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.scheduled_deployment:type_name -> temporal.api.deployment.v1.Deployment - 196, // 74: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.version_directive:type_name -> temporal.server.api.taskqueue.v1.TaskVersionDirective - 201, // 75: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.scheduled_event:type_name -> temporal.api.history.v1.HistoryEvent - 171, // 76: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.started_time:type_name -> google.protobuf.Timestamp - 171, // 77: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.current_attempt_scheduled_time:type_name -> google.protobuf.Timestamp - 174, // 78: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.heartbeat_details:type_name -> temporal.api.common.v1.Payloads - 188, // 79: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType - 181, // 80: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 202, // 81: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.priority:type_name -> temporal.api.common.v1.Priority - 203, // 82: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.retry_policy:type_name -> temporal.api.common.v1.RetryPolicy - 204, // 83: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedRequest.complete_request:type_name -> temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest - 12, // 84: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse.started_response:type_name -> temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse - 205, // 85: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse.activity_tasks:type_name -> temporal.api.workflowservice.v1.PollActivityTaskQueueResponse - 182, // 86: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse.new_workflow_task:type_name -> temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse - 206, // 87: temporal.server.api.historyservice.v1.RespondWorkflowTaskFailedRequest.failed_request:type_name -> temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest - 185, // 88: temporal.server.api.historyservice.v1.IsWorkflowTaskValidRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 181, // 89: temporal.server.api.historyservice.v1.IsWorkflowTaskValidRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 207, // 90: temporal.server.api.historyservice.v1.RecordActivityTaskHeartbeatRequest.heartbeat_request:type_name -> temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest - 208, // 91: temporal.server.api.historyservice.v1.RespondActivityTaskCompletedRequest.complete_request:type_name -> temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest - 209, // 92: temporal.server.api.historyservice.v1.RespondActivityTaskFailedRequest.failed_request:type_name -> temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest - 210, // 93: temporal.server.api.historyservice.v1.RespondActivityTaskCanceledRequest.cancel_request:type_name -> temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest - 185, // 94: temporal.server.api.historyservice.v1.IsActivityTaskValidRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 181, // 95: temporal.server.api.historyservice.v1.IsActivityTaskValidRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 211, // 96: temporal.server.api.historyservice.v1.SignalWorkflowExecutionRequest.signal_request:type_name -> temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest - 185, // 97: temporal.server.api.historyservice.v1.SignalWorkflowExecutionRequest.external_workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 212, // 98: temporal.server.api.historyservice.v1.SignalWithStartWorkflowExecutionRequest.signal_with_start_request:type_name -> temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - 185, // 99: temporal.server.api.historyservice.v1.RemoveSignalMutableStateRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 213, // 100: temporal.server.api.historyservice.v1.TerminateWorkflowExecutionRequest.terminate_request:type_name -> temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest - 185, // 101: temporal.server.api.historyservice.v1.TerminateWorkflowExecutionRequest.external_workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 185, // 102: temporal.server.api.historyservice.v1.DeleteWorkflowExecutionRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 214, // 103: temporal.server.api.historyservice.v1.ResetWorkflowExecutionRequest.reset_request:type_name -> temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest - 215, // 104: temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionRequest.cancel_request:type_name -> temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest - 185, // 105: temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionRequest.external_workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 185, // 106: temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 181, // 107: temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest.child_clock:type_name -> temporal.server.api.clock.v1.VectorClock - 181, // 108: temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock - 185, // 109: temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 181, // 110: temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 185, // 111: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.parent_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 185, // 112: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.child_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 201, // 113: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.completion_event:type_name -> temporal.api.history.v1.HistoryEvent - 181, // 114: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 185, // 115: temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest.parent_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 185, // 116: temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest.child_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 181, // 117: temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 216, // 118: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionRequest.request:type_name -> temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest - 217, // 119: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.execution_config:type_name -> temporal.api.workflow.v1.WorkflowExecutionConfig - 218, // 120: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.workflow_execution_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionInfo - 219, // 121: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_activities:type_name -> temporal.api.workflow.v1.PendingActivityInfo - 220, // 122: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_children:type_name -> temporal.api.workflow.v1.PendingChildExecutionInfo - 221, // 123: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_workflow_task:type_name -> temporal.api.workflow.v1.PendingWorkflowTaskInfo - 222, // 124: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.callbacks:type_name -> temporal.api.workflow.v1.CallbackInfo - 223, // 125: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_nexus_operations:type_name -> temporal.api.workflow.v1.PendingNexusOperationInfo - 224, // 126: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.workflow_extended_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionExtendedInfo - 185, // 127: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 186, // 128: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.version_history_items:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 225, // 129: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.events:type_name -> temporal.api.common.v1.DataBlob - 225, // 130: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.new_run_events:type_name -> temporal.api.common.v1.DataBlob - 226, // 131: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo - 227, // 132: temporal.server.api.historyservice.v1.ReplicateWorkflowStateRequest.workflow_state:type_name -> temporal.server.api.persistence.v1.WorkflowMutableState - 171, // 133: temporal.server.api.historyservice.v1.SyncShardStatusRequest.status_time:type_name -> google.protobuf.Timestamp - 171, // 134: temporal.server.api.historyservice.v1.SyncActivityRequest.scheduled_time:type_name -> google.protobuf.Timestamp - 171, // 135: temporal.server.api.historyservice.v1.SyncActivityRequest.started_time:type_name -> google.protobuf.Timestamp - 171, // 136: temporal.server.api.historyservice.v1.SyncActivityRequest.last_heartbeat_time:type_name -> google.protobuf.Timestamp - 174, // 137: temporal.server.api.historyservice.v1.SyncActivityRequest.details:type_name -> temporal.api.common.v1.Payloads - 173, // 138: temporal.server.api.historyservice.v1.SyncActivityRequest.last_failure:type_name -> temporal.api.failure.v1.Failure - 228, // 139: temporal.server.api.historyservice.v1.SyncActivityRequest.version_history:type_name -> temporal.server.api.history.v1.VersionHistory - 226, // 140: temporal.server.api.historyservice.v1.SyncActivityRequest.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo - 171, // 141: temporal.server.api.historyservice.v1.SyncActivityRequest.first_scheduled_time:type_name -> google.protobuf.Timestamp - 171, // 142: temporal.server.api.historyservice.v1.SyncActivityRequest.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 175, // 143: temporal.server.api.historyservice.v1.SyncActivityRequest.retry_initial_interval:type_name -> google.protobuf.Duration - 175, // 144: temporal.server.api.historyservice.v1.SyncActivityRequest.retry_maximum_interval:type_name -> google.protobuf.Duration - 64, // 145: temporal.server.api.historyservice.v1.SyncActivitiesRequest.activities_info:type_name -> temporal.server.api.historyservice.v1.ActivitySyncInfo - 171, // 146: temporal.server.api.historyservice.v1.ActivitySyncInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 171, // 147: temporal.server.api.historyservice.v1.ActivitySyncInfo.started_time:type_name -> google.protobuf.Timestamp - 171, // 148: temporal.server.api.historyservice.v1.ActivitySyncInfo.last_heartbeat_time:type_name -> google.protobuf.Timestamp - 174, // 149: temporal.server.api.historyservice.v1.ActivitySyncInfo.details:type_name -> temporal.api.common.v1.Payloads - 173, // 150: temporal.server.api.historyservice.v1.ActivitySyncInfo.last_failure:type_name -> temporal.api.failure.v1.Failure - 228, // 151: temporal.server.api.historyservice.v1.ActivitySyncInfo.version_history:type_name -> temporal.server.api.history.v1.VersionHistory - 171, // 152: temporal.server.api.historyservice.v1.ActivitySyncInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp - 171, // 153: temporal.server.api.historyservice.v1.ActivitySyncInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 175, // 154: temporal.server.api.historyservice.v1.ActivitySyncInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 175, // 155: temporal.server.api.historyservice.v1.ActivitySyncInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 185, // 156: temporal.server.api.historyservice.v1.DescribeMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 227, // 157: temporal.server.api.historyservice.v1.DescribeMutableStateResponse.cache_mutable_state:type_name -> temporal.server.api.persistence.v1.WorkflowMutableState - 227, // 158: temporal.server.api.historyservice.v1.DescribeMutableStateResponse.database_mutable_state:type_name -> temporal.server.api.persistence.v1.WorkflowMutableState - 185, // 159: temporal.server.api.historyservice.v1.DescribeHistoryHostRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution - 229, // 160: temporal.server.api.historyservice.v1.DescribeHistoryHostResponse.namespace_cache:type_name -> temporal.server.api.namespace.v1.NamespaceCacheInfo - 230, // 161: temporal.server.api.historyservice.v1.GetShardResponse.shard_info:type_name -> temporal.server.api.persistence.v1.ShardInfo - 171, // 162: temporal.server.api.historyservice.v1.RemoveTaskRequest.visibility_time:type_name -> google.protobuf.Timestamp - 231, // 163: temporal.server.api.historyservice.v1.GetReplicationMessagesRequest.tokens:type_name -> temporal.server.api.replication.v1.ReplicationToken - 164, // 164: temporal.server.api.historyservice.v1.GetReplicationMessagesResponse.shard_messages:type_name -> temporal.server.api.historyservice.v1.GetReplicationMessagesResponse.ShardMessagesEntry - 232, // 165: temporal.server.api.historyservice.v1.GetDLQReplicationMessagesRequest.task_infos:type_name -> temporal.server.api.replication.v1.ReplicationTaskInfo - 233, // 166: temporal.server.api.historyservice.v1.GetDLQReplicationMessagesResponse.replication_tasks:type_name -> temporal.server.api.replication.v1.ReplicationTask - 234, // 167: temporal.server.api.historyservice.v1.QueryWorkflowRequest.request:type_name -> temporal.api.workflowservice.v1.QueryWorkflowRequest - 235, // 168: temporal.server.api.historyservice.v1.QueryWorkflowResponse.response:type_name -> temporal.api.workflowservice.v1.QueryWorkflowResponse - 236, // 169: temporal.server.api.historyservice.v1.ReapplyEventsRequest.request:type_name -> temporal.server.api.adminservice.v1.ReapplyEventsRequest - 237, // 170: temporal.server.api.historyservice.v1.GetDLQMessagesRequest.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType - 237, // 171: temporal.server.api.historyservice.v1.GetDLQMessagesResponse.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType - 233, // 172: temporal.server.api.historyservice.v1.GetDLQMessagesResponse.replication_tasks:type_name -> temporal.server.api.replication.v1.ReplicationTask - 232, // 173: temporal.server.api.historyservice.v1.GetDLQMessagesResponse.replication_tasks_info:type_name -> temporal.server.api.replication.v1.ReplicationTaskInfo - 237, // 174: temporal.server.api.historyservice.v1.PurgeDLQMessagesRequest.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType - 237, // 175: temporal.server.api.historyservice.v1.MergeDLQMessagesRequest.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType - 238, // 176: temporal.server.api.historyservice.v1.RefreshWorkflowTasksRequest.request:type_name -> temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest - 185, // 177: temporal.server.api.historyservice.v1.GenerateLastHistoryReplicationTasksRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 96, // 178: temporal.server.api.historyservice.v1.GetReplicationStatusResponse.shards:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatus - 171, // 179: temporal.server.api.historyservice.v1.ShardReplicationStatus.shard_local_time:type_name -> google.protobuf.Timestamp - 165, // 180: temporal.server.api.historyservice.v1.ShardReplicationStatus.remote_clusters:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatus.RemoteClustersEntry - 166, // 181: temporal.server.api.historyservice.v1.ShardReplicationStatus.handover_namespaces:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatus.HandoverNamespacesEntry - 171, // 182: temporal.server.api.historyservice.v1.ShardReplicationStatus.max_replication_task_visibility_time:type_name -> google.protobuf.Timestamp - 171, // 183: temporal.server.api.historyservice.v1.ShardReplicationStatusPerCluster.acked_task_visibility_time:type_name -> google.protobuf.Timestamp - 185, // 184: temporal.server.api.historyservice.v1.RebuildMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 185, // 185: temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 225, // 186: temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest.history_batches:type_name -> temporal.api.common.v1.DataBlob - 228, // 187: temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest.version_history:type_name -> temporal.server.api.history.v1.VersionHistory - 185, // 188: temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 171, // 189: temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest.workflow_start_time:type_name -> google.protobuf.Timestamp - 171, // 190: temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest.workflow_close_time:type_name -> google.protobuf.Timestamp - 239, // 191: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionRequest.request:type_name -> temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest - 240, // 192: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionResponse.response:type_name -> temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse - 241, // 193: temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesRequest.sync_replication_state:type_name -> temporal.server.api.replication.v1.SyncReplicationState - 242, // 194: temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesResponse.messages:type_name -> temporal.server.api.replication.v1.WorkflowReplicationMessages - 243, // 195: temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateRequest.request:type_name -> temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest - 244, // 196: temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateResponse.response:type_name -> temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse - 245, // 197: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryRequest.request:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest - 246, // 198: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponse.response:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse - 199, // 199: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponse.history:type_name -> temporal.api.history.v1.History - 246, // 200: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponseWithRaw.response:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse - 247, // 201: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseRequest.request:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest - 248, // 202: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseResponse.response:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse - 249, // 203: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Request.request:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request - 250, // 204: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Response.response:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response - 251, // 205: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryRequest.request:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest - 252, // 206: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryResponse.response:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse - 253, // 207: temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionRequest.request:type_name -> temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest - 254, // 208: temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionResponse.response:type_name -> temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse - 255, // 209: temporal.server.api.historyservice.v1.GetDLQTasksRequest.dlq_key:type_name -> temporal.server.api.common.v1.HistoryDLQKey - 256, // 210: temporal.server.api.historyservice.v1.GetDLQTasksResponse.dlq_tasks:type_name -> temporal.server.api.common.v1.HistoryDLQTask - 255, // 211: temporal.server.api.historyservice.v1.DeleteDLQTasksRequest.dlq_key:type_name -> temporal.server.api.common.v1.HistoryDLQKey - 257, // 212: temporal.server.api.historyservice.v1.DeleteDLQTasksRequest.inclusive_max_task_metadata:type_name -> temporal.server.api.common.v1.HistoryDLQTaskMetadata - 167, // 213: temporal.server.api.historyservice.v1.ListQueuesResponse.queues:type_name -> temporal.server.api.historyservice.v1.ListQueuesResponse.QueueInfo - 168, // 214: temporal.server.api.historyservice.v1.AddTasksRequest.tasks:type_name -> temporal.server.api.historyservice.v1.AddTasksRequest.Task - 258, // 215: temporal.server.api.historyservice.v1.ListTasksRequest.request:type_name -> temporal.server.api.adminservice.v1.ListHistoryTasksRequest - 259, // 216: temporal.server.api.historyservice.v1.ListTasksResponse.response:type_name -> temporal.server.api.adminservice.v1.ListHistoryTasksResponse - 260, // 217: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.completion:type_name -> temporal.server.api.token.v1.NexusOperationCompletion - 261, // 218: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.success:type_name -> temporal.api.common.v1.Payload - 173, // 219: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.failure:type_name -> temporal.api.failure.v1.Failure - 171, // 220: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.close_time:type_name -> google.protobuf.Timestamp - 260, // 221: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.completion:type_name -> temporal.server.api.token.v1.NexusOperationCompletion - 261, // 222: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.success:type_name -> temporal.api.common.v1.Payload - 262, // 223: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.failure:type_name -> temporal.api.nexus.v1.Failure - 171, // 224: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.start_time:type_name -> google.protobuf.Timestamp - 184, // 225: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.links:type_name -> temporal.api.common.v1.Link - 263, // 226: temporal.server.api.historyservice.v1.InvokeStateMachineMethodRequest.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef - 264, // 227: temporal.server.api.historyservice.v1.DeepHealthCheckResponse.state:type_name -> temporal.server.api.enums.v1.HealthState - 185, // 228: temporal.server.api.historyservice.v1.SyncWorkflowStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution - 187, // 229: temporal.server.api.historyservice.v1.SyncWorkflowStateRequest.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 191, // 230: temporal.server.api.historyservice.v1.SyncWorkflowStateRequest.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories - 265, // 231: temporal.server.api.historyservice.v1.SyncWorkflowStateResponse.versioned_transition_artifact:type_name -> temporal.server.api.replication.v1.VersionedTransitionArtifact - 266, // 232: temporal.server.api.historyservice.v1.UpdateActivityOptionsRequest.update_request:type_name -> temporal.api.workflowservice.v1.UpdateActivityOptionsRequest - 267, // 233: temporal.server.api.historyservice.v1.UpdateActivityOptionsResponse.activity_options:type_name -> temporal.api.activity.v1.ActivityOptions - 268, // 234: temporal.server.api.historyservice.v1.PauseActivityRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PauseActivityRequest - 269, // 235: temporal.server.api.historyservice.v1.UnpauseActivityRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.UnpauseActivityRequest - 270, // 236: temporal.server.api.historyservice.v1.ResetActivityRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.ResetActivityRequest - 271, // 237: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsRequest.update_request:type_name -> temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest - 272, // 238: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsResponse.workflow_execution_options:type_name -> temporal.api.workflow.v1.WorkflowExecutionOptions - 273, // 239: temporal.server.api.historyservice.v1.PauseWorkflowExecutionRequest.pause_request:type_name -> temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest - 274, // 240: temporal.server.api.historyservice.v1.UnpauseWorkflowExecutionRequest.unpause_request:type_name -> temporal.api.workflowservice.v1.UnpauseWorkflowExecutionRequest - 275, // 241: temporal.server.api.historyservice.v1.StartNexusOperationRequest.request:type_name -> temporal.api.nexus.v1.StartOperationRequest - 276, // 242: temporal.server.api.historyservice.v1.StartNexusOperationResponse.response:type_name -> temporal.api.nexus.v1.StartOperationResponse - 277, // 243: temporal.server.api.historyservice.v1.CancelNexusOperationRequest.request:type_name -> temporal.api.nexus.v1.CancelOperationRequest - 278, // 244: temporal.server.api.historyservice.v1.CancelNexusOperationResponse.response:type_name -> temporal.api.nexus.v1.CancelOperationResponse - 1, // 245: temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.Operation.start_workflow:type_name -> temporal.server.api.historyservice.v1.StartWorkflowExecutionRequest - 105, // 246: temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.Operation.update_workflow:type_name -> temporal.server.api.historyservice.v1.UpdateWorkflowExecutionRequest - 2, // 247: temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.Response.start_workflow:type_name -> temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse - 106, // 248: temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.Response.update_workflow:type_name -> temporal.server.api.historyservice.v1.UpdateWorkflowExecutionResponse - 279, // 249: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery - 279, // 250: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery - 280, // 251: temporal.server.api.historyservice.v1.GetReplicationMessagesResponse.ShardMessagesEntry.value:type_name -> temporal.server.api.replication.v1.ReplicationMessages - 98, // 252: temporal.server.api.historyservice.v1.ShardReplicationStatus.RemoteClustersEntry.value:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatusPerCluster - 97, // 253: temporal.server.api.historyservice.v1.ShardReplicationStatus.HandoverNamespacesEntry.value:type_name -> temporal.server.api.historyservice.v1.HandoverNamespaceInfo - 225, // 254: temporal.server.api.historyservice.v1.AddTasksRequest.Task.blob:type_name -> temporal.api.common.v1.DataBlob - 281, // 255: temporal.server.api.historyservice.v1.routing:extendee -> google.protobuf.MessageOptions - 0, // 256: temporal.server.api.historyservice.v1.routing:type_name -> temporal.server.api.historyservice.v1.RoutingOptions - 257, // [257:257] is the sub-list for method output_type - 257, // [257:257] is the sub-list for method input_type - 256, // [256:257] is the sub-list for extension type_name - 255, // [255:256] is the sub-list for extension extendee - 0, // [0:255] is the sub-list for field type_name + 181, // 12: temporal.server.api.historyservice.v1.StartWorkflowExecutionRequest.declined_target_version_upgrade:type_name -> temporal.api.history.v1.DeclinedTargetVersionUpgrade + 182, // 13: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 183, // 14: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.eager_workflow_task:type_name -> temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse + 184, // 15: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus + 185, // 16: temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse.link:type_name -> temporal.api.common.v1.Link + 186, // 17: temporal.server.api.historyservice.v1.GetMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 187, // 18: temporal.server.api.historyservice.v1.GetMutableStateRequest.version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 188, // 19: temporal.server.api.historyservice.v1.GetMutableStateRequest.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 186, // 20: temporal.server.api.historyservice.v1.GetMutableStateResponse.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 189, // 21: temporal.server.api.historyservice.v1.GetMutableStateResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType + 190, // 22: temporal.server.api.historyservice.v1.GetMutableStateResponse.task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue + 190, // 23: temporal.server.api.historyservice.v1.GetMutableStateResponse.sticky_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue + 175, // 24: temporal.server.api.historyservice.v1.GetMutableStateResponse.sticky_task_queue_schedule_to_start_timeout:type_name -> google.protobuf.Duration + 191, // 25: temporal.server.api.historyservice.v1.GetMutableStateResponse.workflow_state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState + 184, // 26: temporal.server.api.historyservice.v1.GetMutableStateResponse.workflow_status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus + 192, // 27: temporal.server.api.historyservice.v1.GetMutableStateResponse.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 176, // 28: temporal.server.api.historyservice.v1.GetMutableStateResponse.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 188, // 29: temporal.server.api.historyservice.v1.GetMutableStateResponse.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 193, // 30: temporal.server.api.historyservice.v1.GetMutableStateResponse.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + 194, // 31: temporal.server.api.historyservice.v1.GetMutableStateResponse.transient_or_speculative_tasks:type_name -> temporal.server.api.history.v1.TransientWorkflowTaskInfo + 186, // 32: temporal.server.api.historyservice.v1.PollMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 187, // 33: temporal.server.api.historyservice.v1.PollMutableStateRequest.version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 186, // 34: temporal.server.api.historyservice.v1.PollMutableStateResponse.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 189, // 35: temporal.server.api.historyservice.v1.PollMutableStateResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType + 190, // 36: temporal.server.api.historyservice.v1.PollMutableStateResponse.task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue + 190, // 37: temporal.server.api.historyservice.v1.PollMutableStateResponse.sticky_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue + 175, // 38: temporal.server.api.historyservice.v1.PollMutableStateResponse.sticky_task_queue_schedule_to_start_timeout:type_name -> google.protobuf.Duration + 192, // 39: temporal.server.api.historyservice.v1.PollMutableStateResponse.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 191, // 40: temporal.server.api.historyservice.v1.PollMutableStateResponse.workflow_state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState + 184, // 41: temporal.server.api.historyservice.v1.PollMutableStateResponse.workflow_status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus + 186, // 42: temporal.server.api.historyservice.v1.ResetStickyTaskQueueRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 160, // 43: temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.operations:type_name -> temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.Operation + 161, // 44: temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.responses:type_name -> temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.Response + 186, // 45: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 195, // 46: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.poll_request:type_name -> temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest + 182, // 47: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 196, // 48: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.build_id_redirect_info:type_name -> temporal.server.api.taskqueue.v1.BuildIdRedirectInfo + 197, // 49: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.scheduled_deployment:type_name -> temporal.api.deployment.v1.Deployment + 198, // 50: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.version_directive:type_name -> temporal.server.api.taskqueue.v1.TaskVersionDirective + 179, // 51: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest.target_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 189, // 52: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType + 194, // 53: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.transient_workflow_task:type_name -> temporal.server.api.history.v1.TransientWorkflowTaskInfo + 190, // 54: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.workflow_execution_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue + 171, // 55: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.scheduled_time:type_name -> google.protobuf.Timestamp + 171, // 56: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.started_time:type_name -> google.protobuf.Timestamp + 162, // 57: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.queries:type_name -> temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.QueriesEntry + 182, // 58: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 199, // 59: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.messages:type_name -> temporal.api.protocol.v1.Message + 200, // 60: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.history:type_name -> temporal.api.history.v1.History + 200, // 61: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.raw_history:type_name -> temporal.api.history.v1.History + 189, // 62: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.workflow_type:type_name -> temporal.api.common.v1.WorkflowType + 194, // 63: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.transient_workflow_task:type_name -> temporal.server.api.history.v1.TransientWorkflowTaskInfo + 190, // 64: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.workflow_execution_task_queue:type_name -> temporal.api.taskqueue.v1.TaskQueue + 171, // 65: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.scheduled_time:type_name -> google.protobuf.Timestamp + 171, // 66: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.started_time:type_name -> google.protobuf.Timestamp + 163, // 67: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.queries:type_name -> temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.QueriesEntry + 182, // 68: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 199, // 69: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.messages:type_name -> temporal.api.protocol.v1.Message + 200, // 70: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.history:type_name -> temporal.api.history.v1.History + 186, // 71: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 201, // 72: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.poll_request:type_name -> temporal.api.workflowservice.v1.PollActivityTaskQueueRequest + 182, // 73: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 196, // 74: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.build_id_redirect_info:type_name -> temporal.server.api.taskqueue.v1.BuildIdRedirectInfo + 197, // 75: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.scheduled_deployment:type_name -> temporal.api.deployment.v1.Deployment + 198, // 76: temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest.version_directive:type_name -> temporal.server.api.taskqueue.v1.TaskVersionDirective + 202, // 77: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.scheduled_event:type_name -> temporal.api.history.v1.HistoryEvent + 171, // 78: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.started_time:type_name -> google.protobuf.Timestamp + 171, // 79: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.current_attempt_scheduled_time:type_name -> google.protobuf.Timestamp + 174, // 80: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.heartbeat_details:type_name -> temporal.api.common.v1.Payloads + 189, // 81: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.workflow_type:type_name -> temporal.api.common.v1.WorkflowType + 182, // 82: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 203, // 83: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.priority:type_name -> temporal.api.common.v1.Priority + 204, // 84: temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse.retry_policy:type_name -> temporal.api.common.v1.RetryPolicy + 205, // 85: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedRequest.complete_request:type_name -> temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest + 12, // 86: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse.started_response:type_name -> temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse + 206, // 87: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse.activity_tasks:type_name -> temporal.api.workflowservice.v1.PollActivityTaskQueueResponse + 183, // 88: temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse.new_workflow_task:type_name -> temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse + 207, // 89: temporal.server.api.historyservice.v1.RespondWorkflowTaskFailedRequest.failed_request:type_name -> temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest + 186, // 90: temporal.server.api.historyservice.v1.IsWorkflowTaskValidRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 182, // 91: temporal.server.api.historyservice.v1.IsWorkflowTaskValidRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 208, // 92: temporal.server.api.historyservice.v1.RecordActivityTaskHeartbeatRequest.heartbeat_request:type_name -> temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest + 209, // 93: temporal.server.api.historyservice.v1.RespondActivityTaskCompletedRequest.complete_request:type_name -> temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest + 210, // 94: temporal.server.api.historyservice.v1.RespondActivityTaskFailedRequest.failed_request:type_name -> temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest + 211, // 95: temporal.server.api.historyservice.v1.RespondActivityTaskCanceledRequest.cancel_request:type_name -> temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest + 186, // 96: temporal.server.api.historyservice.v1.IsActivityTaskValidRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 182, // 97: temporal.server.api.historyservice.v1.IsActivityTaskValidRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 212, // 98: temporal.server.api.historyservice.v1.SignalWorkflowExecutionRequest.signal_request:type_name -> temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest + 186, // 99: temporal.server.api.historyservice.v1.SignalWorkflowExecutionRequest.external_workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 213, // 100: temporal.server.api.historyservice.v1.SignalWithStartWorkflowExecutionRequest.signal_with_start_request:type_name -> temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + 186, // 101: temporal.server.api.historyservice.v1.RemoveSignalMutableStateRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 214, // 102: temporal.server.api.historyservice.v1.TerminateWorkflowExecutionRequest.terminate_request:type_name -> temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest + 186, // 103: temporal.server.api.historyservice.v1.TerminateWorkflowExecutionRequest.external_workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 186, // 104: temporal.server.api.historyservice.v1.DeleteWorkflowExecutionRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 215, // 105: temporal.server.api.historyservice.v1.ResetWorkflowExecutionRequest.reset_request:type_name -> temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest + 216, // 106: temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionRequest.cancel_request:type_name -> temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest + 186, // 107: temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionRequest.external_workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 186, // 108: temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 182, // 109: temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest.child_clock:type_name -> temporal.server.api.clock.v1.VectorClock + 182, // 110: temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock + 186, // 111: temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 182, // 112: temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 186, // 113: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.parent_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 186, // 114: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.child_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 202, // 115: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.completion_event:type_name -> temporal.api.history.v1.HistoryEvent + 182, // 116: temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 186, // 117: temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest.parent_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 186, // 118: temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest.child_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 182, // 119: temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 217, // 120: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionRequest.request:type_name -> temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest + 218, // 121: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.execution_config:type_name -> temporal.api.workflow.v1.WorkflowExecutionConfig + 219, // 122: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.workflow_execution_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionInfo + 220, // 123: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_activities:type_name -> temporal.api.workflow.v1.PendingActivityInfo + 221, // 124: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_children:type_name -> temporal.api.workflow.v1.PendingChildExecutionInfo + 222, // 125: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_workflow_task:type_name -> temporal.api.workflow.v1.PendingWorkflowTaskInfo + 223, // 126: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.callbacks:type_name -> temporal.api.workflow.v1.CallbackInfo + 224, // 127: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.pending_nexus_operations:type_name -> temporal.api.workflow.v1.PendingNexusOperationInfo + 225, // 128: temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse.workflow_extended_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionExtendedInfo + 186, // 129: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 187, // 130: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.version_history_items:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 226, // 131: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.events:type_name -> temporal.api.common.v1.DataBlob + 226, // 132: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.new_run_events:type_name -> temporal.api.common.v1.DataBlob + 227, // 133: temporal.server.api.historyservice.v1.ReplicateEventsV2Request.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo + 228, // 134: temporal.server.api.historyservice.v1.ReplicateWorkflowStateRequest.workflow_state:type_name -> temporal.server.api.persistence.v1.WorkflowMutableState + 171, // 135: temporal.server.api.historyservice.v1.SyncShardStatusRequest.status_time:type_name -> google.protobuf.Timestamp + 171, // 136: temporal.server.api.historyservice.v1.SyncActivityRequest.scheduled_time:type_name -> google.protobuf.Timestamp + 171, // 137: temporal.server.api.historyservice.v1.SyncActivityRequest.started_time:type_name -> google.protobuf.Timestamp + 171, // 138: temporal.server.api.historyservice.v1.SyncActivityRequest.last_heartbeat_time:type_name -> google.protobuf.Timestamp + 174, // 139: temporal.server.api.historyservice.v1.SyncActivityRequest.details:type_name -> temporal.api.common.v1.Payloads + 173, // 140: temporal.server.api.historyservice.v1.SyncActivityRequest.last_failure:type_name -> temporal.api.failure.v1.Failure + 229, // 141: temporal.server.api.historyservice.v1.SyncActivityRequest.version_history:type_name -> temporal.server.api.history.v1.VersionHistory + 227, // 142: temporal.server.api.historyservice.v1.SyncActivityRequest.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo + 171, // 143: temporal.server.api.historyservice.v1.SyncActivityRequest.first_scheduled_time:type_name -> google.protobuf.Timestamp + 171, // 144: temporal.server.api.historyservice.v1.SyncActivityRequest.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 175, // 145: temporal.server.api.historyservice.v1.SyncActivityRequest.retry_initial_interval:type_name -> google.protobuf.Duration + 175, // 146: temporal.server.api.historyservice.v1.SyncActivityRequest.retry_maximum_interval:type_name -> google.protobuf.Duration + 64, // 147: temporal.server.api.historyservice.v1.SyncActivitiesRequest.activities_info:type_name -> temporal.server.api.historyservice.v1.ActivitySyncInfo + 171, // 148: temporal.server.api.historyservice.v1.ActivitySyncInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 171, // 149: temporal.server.api.historyservice.v1.ActivitySyncInfo.started_time:type_name -> google.protobuf.Timestamp + 171, // 150: temporal.server.api.historyservice.v1.ActivitySyncInfo.last_heartbeat_time:type_name -> google.protobuf.Timestamp + 174, // 151: temporal.server.api.historyservice.v1.ActivitySyncInfo.details:type_name -> temporal.api.common.v1.Payloads + 173, // 152: temporal.server.api.historyservice.v1.ActivitySyncInfo.last_failure:type_name -> temporal.api.failure.v1.Failure + 229, // 153: temporal.server.api.historyservice.v1.ActivitySyncInfo.version_history:type_name -> temporal.server.api.history.v1.VersionHistory + 171, // 154: temporal.server.api.historyservice.v1.ActivitySyncInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp + 171, // 155: temporal.server.api.historyservice.v1.ActivitySyncInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 175, // 156: temporal.server.api.historyservice.v1.ActivitySyncInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 175, // 157: temporal.server.api.historyservice.v1.ActivitySyncInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 186, // 158: temporal.server.api.historyservice.v1.DescribeMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 228, // 159: temporal.server.api.historyservice.v1.DescribeMutableStateResponse.cache_mutable_state:type_name -> temporal.server.api.persistence.v1.WorkflowMutableState + 228, // 160: temporal.server.api.historyservice.v1.DescribeMutableStateResponse.database_mutable_state:type_name -> temporal.server.api.persistence.v1.WorkflowMutableState + 186, // 161: temporal.server.api.historyservice.v1.DescribeHistoryHostRequest.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 230, // 162: temporal.server.api.historyservice.v1.DescribeHistoryHostResponse.namespace_cache:type_name -> temporal.server.api.namespace.v1.NamespaceCacheInfo + 231, // 163: temporal.server.api.historyservice.v1.GetShardResponse.shard_info:type_name -> temporal.server.api.persistence.v1.ShardInfo + 171, // 164: temporal.server.api.historyservice.v1.RemoveTaskRequest.visibility_time:type_name -> google.protobuf.Timestamp + 232, // 165: temporal.server.api.historyservice.v1.GetReplicationMessagesRequest.tokens:type_name -> temporal.server.api.replication.v1.ReplicationToken + 164, // 166: temporal.server.api.historyservice.v1.GetReplicationMessagesResponse.shard_messages:type_name -> temporal.server.api.historyservice.v1.GetReplicationMessagesResponse.ShardMessagesEntry + 233, // 167: temporal.server.api.historyservice.v1.GetDLQReplicationMessagesRequest.task_infos:type_name -> temporal.server.api.replication.v1.ReplicationTaskInfo + 234, // 168: temporal.server.api.historyservice.v1.GetDLQReplicationMessagesResponse.replication_tasks:type_name -> temporal.server.api.replication.v1.ReplicationTask + 235, // 169: temporal.server.api.historyservice.v1.QueryWorkflowRequest.request:type_name -> temporal.api.workflowservice.v1.QueryWorkflowRequest + 236, // 170: temporal.server.api.historyservice.v1.QueryWorkflowResponse.response:type_name -> temporal.api.workflowservice.v1.QueryWorkflowResponse + 237, // 171: temporal.server.api.historyservice.v1.ReapplyEventsRequest.request:type_name -> temporal.server.api.adminservice.v1.ReapplyEventsRequest + 238, // 172: temporal.server.api.historyservice.v1.GetDLQMessagesRequest.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType + 238, // 173: temporal.server.api.historyservice.v1.GetDLQMessagesResponse.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType + 234, // 174: temporal.server.api.historyservice.v1.GetDLQMessagesResponse.replication_tasks:type_name -> temporal.server.api.replication.v1.ReplicationTask + 233, // 175: temporal.server.api.historyservice.v1.GetDLQMessagesResponse.replication_tasks_info:type_name -> temporal.server.api.replication.v1.ReplicationTaskInfo + 238, // 176: temporal.server.api.historyservice.v1.PurgeDLQMessagesRequest.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType + 238, // 177: temporal.server.api.historyservice.v1.MergeDLQMessagesRequest.type:type_name -> temporal.server.api.enums.v1.DeadLetterQueueType + 239, // 178: temporal.server.api.historyservice.v1.RefreshWorkflowTasksRequest.request:type_name -> temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest + 186, // 179: temporal.server.api.historyservice.v1.GenerateLastHistoryReplicationTasksRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 96, // 180: temporal.server.api.historyservice.v1.GetReplicationStatusResponse.shards:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatus + 171, // 181: temporal.server.api.historyservice.v1.ShardReplicationStatus.shard_local_time:type_name -> google.protobuf.Timestamp + 165, // 182: temporal.server.api.historyservice.v1.ShardReplicationStatus.remote_clusters:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatus.RemoteClustersEntry + 166, // 183: temporal.server.api.historyservice.v1.ShardReplicationStatus.handover_namespaces:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatus.HandoverNamespacesEntry + 171, // 184: temporal.server.api.historyservice.v1.ShardReplicationStatus.max_replication_task_visibility_time:type_name -> google.protobuf.Timestamp + 171, // 185: temporal.server.api.historyservice.v1.ShardReplicationStatusPerCluster.acked_task_visibility_time:type_name -> google.protobuf.Timestamp + 186, // 186: temporal.server.api.historyservice.v1.RebuildMutableStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 186, // 187: temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 226, // 188: temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest.history_batches:type_name -> temporal.api.common.v1.DataBlob + 229, // 189: temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest.version_history:type_name -> temporal.server.api.history.v1.VersionHistory + 186, // 190: temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 171, // 191: temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest.workflow_start_time:type_name -> google.protobuf.Timestamp + 171, // 192: temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest.workflow_close_time:type_name -> google.protobuf.Timestamp + 240, // 193: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionRequest.request:type_name -> temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest + 241, // 194: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionResponse.response:type_name -> temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse + 242, // 195: temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesRequest.sync_replication_state:type_name -> temporal.server.api.replication.v1.SyncReplicationState + 243, // 196: temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesResponse.messages:type_name -> temporal.server.api.replication.v1.WorkflowReplicationMessages + 244, // 197: temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateRequest.request:type_name -> temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest + 245, // 198: temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateResponse.response:type_name -> temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse + 246, // 199: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryRequest.request:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest + 247, // 200: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponse.response:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse + 200, // 201: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponse.history:type_name -> temporal.api.history.v1.History + 247, // 202: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponseWithRaw.response:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse + 248, // 203: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseRequest.request:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest + 249, // 204: temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseResponse.response:type_name -> temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse + 250, // 205: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Request.request:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request + 251, // 206: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Response.response:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response + 252, // 207: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryRequest.request:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest + 253, // 208: temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryResponse.response:type_name -> temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse + 254, // 209: temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionRequest.request:type_name -> temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest + 255, // 210: temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionResponse.response:type_name -> temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse + 256, // 211: temporal.server.api.historyservice.v1.GetDLQTasksRequest.dlq_key:type_name -> temporal.server.api.common.v1.HistoryDLQKey + 257, // 212: temporal.server.api.historyservice.v1.GetDLQTasksResponse.dlq_tasks:type_name -> temporal.server.api.common.v1.HistoryDLQTask + 256, // 213: temporal.server.api.historyservice.v1.DeleteDLQTasksRequest.dlq_key:type_name -> temporal.server.api.common.v1.HistoryDLQKey + 258, // 214: temporal.server.api.historyservice.v1.DeleteDLQTasksRequest.inclusive_max_task_metadata:type_name -> temporal.server.api.common.v1.HistoryDLQTaskMetadata + 167, // 215: temporal.server.api.historyservice.v1.ListQueuesResponse.queues:type_name -> temporal.server.api.historyservice.v1.ListQueuesResponse.QueueInfo + 168, // 216: temporal.server.api.historyservice.v1.AddTasksRequest.tasks:type_name -> temporal.server.api.historyservice.v1.AddTasksRequest.Task + 259, // 217: temporal.server.api.historyservice.v1.ListTasksRequest.request:type_name -> temporal.server.api.adminservice.v1.ListHistoryTasksRequest + 260, // 218: temporal.server.api.historyservice.v1.ListTasksResponse.response:type_name -> temporal.server.api.adminservice.v1.ListHistoryTasksResponse + 261, // 219: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.completion:type_name -> temporal.server.api.token.v1.NexusOperationCompletion + 262, // 220: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.success:type_name -> temporal.api.common.v1.Payload + 173, // 221: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.failure:type_name -> temporal.api.failure.v1.Failure + 171, // 222: temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest.close_time:type_name -> google.protobuf.Timestamp + 261, // 223: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.completion:type_name -> temporal.server.api.token.v1.NexusOperationCompletion + 262, // 224: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.success:type_name -> temporal.api.common.v1.Payload + 263, // 225: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.failure:type_name -> temporal.api.nexus.v1.Failure + 171, // 226: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.start_time:type_name -> google.protobuf.Timestamp + 185, // 227: temporal.server.api.historyservice.v1.CompleteNexusOperationRequest.links:type_name -> temporal.api.common.v1.Link + 264, // 228: temporal.server.api.historyservice.v1.InvokeStateMachineMethodRequest.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef + 265, // 229: temporal.server.api.historyservice.v1.DeepHealthCheckResponse.state:type_name -> temporal.server.api.enums.v1.HealthState + 266, // 230: temporal.server.api.historyservice.v1.DeepHealthCheckResponse.checks:type_name -> temporal.server.api.health.v1.HealthCheck + 186, // 231: temporal.server.api.historyservice.v1.SyncWorkflowStateRequest.execution:type_name -> temporal.api.common.v1.WorkflowExecution + 188, // 232: temporal.server.api.historyservice.v1.SyncWorkflowStateRequest.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 192, // 233: temporal.server.api.historyservice.v1.SyncWorkflowStateRequest.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 267, // 234: temporal.server.api.historyservice.v1.SyncWorkflowStateResponse.versioned_transition_artifact:type_name -> temporal.server.api.replication.v1.VersionedTransitionArtifact + 268, // 235: temporal.server.api.historyservice.v1.UpdateActivityOptionsRequest.update_request:type_name -> temporal.api.workflowservice.v1.UpdateActivityOptionsRequest + 269, // 236: temporal.server.api.historyservice.v1.UpdateActivityOptionsResponse.activity_options:type_name -> temporal.api.activity.v1.ActivityOptions + 270, // 237: temporal.server.api.historyservice.v1.PauseActivityRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PauseActivityRequest + 271, // 238: temporal.server.api.historyservice.v1.UnpauseActivityRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.UnpauseActivityRequest + 272, // 239: temporal.server.api.historyservice.v1.ResetActivityRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.ResetActivityRequest + 273, // 240: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsRequest.update_request:type_name -> temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest + 274, // 241: temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsResponse.workflow_execution_options:type_name -> temporal.api.workflow.v1.WorkflowExecutionOptions + 275, // 242: temporal.server.api.historyservice.v1.PauseWorkflowExecutionRequest.pause_request:type_name -> temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest + 276, // 243: temporal.server.api.historyservice.v1.UnpauseWorkflowExecutionRequest.unpause_request:type_name -> temporal.api.workflowservice.v1.UnpauseWorkflowExecutionRequest + 277, // 244: temporal.server.api.historyservice.v1.StartNexusOperationRequest.request:type_name -> temporal.api.nexus.v1.StartOperationRequest + 278, // 245: temporal.server.api.historyservice.v1.StartNexusOperationResponse.response:type_name -> temporal.api.nexus.v1.StartOperationResponse + 279, // 246: temporal.server.api.historyservice.v1.CancelNexusOperationRequest.request:type_name -> temporal.api.nexus.v1.CancelOperationRequest + 280, // 247: temporal.server.api.historyservice.v1.CancelNexusOperationResponse.response:type_name -> temporal.api.nexus.v1.CancelOperationResponse + 1, // 248: temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.Operation.start_workflow:type_name -> temporal.server.api.historyservice.v1.StartWorkflowExecutionRequest + 105, // 249: temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest.Operation.update_workflow:type_name -> temporal.server.api.historyservice.v1.UpdateWorkflowExecutionRequest + 2, // 250: temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.Response.start_workflow:type_name -> temporal.server.api.historyservice.v1.StartWorkflowExecutionResponse + 106, // 251: temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse.Response.update_workflow:type_name -> temporal.server.api.historyservice.v1.UpdateWorkflowExecutionResponse + 281, // 252: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery + 281, // 253: temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponseWithRawHistory.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery + 282, // 254: temporal.server.api.historyservice.v1.GetReplicationMessagesResponse.ShardMessagesEntry.value:type_name -> temporal.server.api.replication.v1.ReplicationMessages + 98, // 255: temporal.server.api.historyservice.v1.ShardReplicationStatus.RemoteClustersEntry.value:type_name -> temporal.server.api.historyservice.v1.ShardReplicationStatusPerCluster + 97, // 256: temporal.server.api.historyservice.v1.ShardReplicationStatus.HandoverNamespacesEntry.value:type_name -> temporal.server.api.historyservice.v1.HandoverNamespaceInfo + 226, // 257: temporal.server.api.historyservice.v1.AddTasksRequest.Task.blob:type_name -> temporal.api.common.v1.DataBlob + 283, // 258: temporal.server.api.historyservice.v1.routing:extendee -> google.protobuf.MessageOptions + 0, // 259: temporal.server.api.historyservice.v1.routing:type_name -> temporal.server.api.historyservice.v1.RoutingOptions + 260, // [260:260] is the sub-list for method output_type + 260, // [260:260] is the sub-list for method input_type + 259, // [259:260] is the sub-list for extension type_name + 258, // [258:259] is the sub-list for extension extendee + 0, // [0:258] is the sub-list for field type_name } func init() { file_temporal_server_api_historyservice_v1_request_response_proto_init() } diff --git a/api/historyservice/v1/service.pb.go b/api/historyservice/v1/service.pb.go index 1e625333116..634ddb51a15 100644 --- a/api/historyservice/v1/service.pb.go +++ b/api/historyservice/v1/service.pb.go @@ -10,6 +10,7 @@ import ( reflect "reflect" unsafe "unsafe" + _ "go.temporal.io/server/api/common/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) @@ -25,87 +26,87 @@ var File_temporal_server_api_historyservice_v1_service_proto protoreflect.FileDe const file_temporal_server_api_historyservice_v1_service_proto_rawDesc = "" + "\n" + - "3temporal/server/api/historyservice/v1/service.proto\x12%temporal.server.api.historyservice.v1\x1a.temporal.server.api.historyservice.v1.GetMutableStateResponse\"\x00\x12\x95\x01\n" + - "\x10PollMutableState\x12>.temporal.server.api.historyservice.v1.PollMutableStateRequest\x1a?.temporal.server.api.historyservice.v1.PollMutableStateResponse\"\x00\x12\xa1\x01\n" + - "\x14ResetStickyTaskQueue\x12B.temporal.server.api.historyservice.v1.ResetStickyTaskQueueRequest\x1aC.temporal.server.api.historyservice.v1.ResetStickyTaskQueueResponse\"\x00\x12\xb0\x01\n" + - "\x19RecordWorkflowTaskStarted\x12G.temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest\x1aH.temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse\"\x00\x12\xb0\x01\n" + - "\x19RecordActivityTaskStarted\x12G.temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest\x1aH.temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse\"\x00\x12\xb9\x01\n" + - "\x1cRespondWorkflowTaskCompleted\x12J.temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedRequest\x1aK.temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse\"\x00\x12\xb0\x01\n" + - "\x19RespondWorkflowTaskFailed\x12G.temporal.server.api.historyservice.v1.RespondWorkflowTaskFailedRequest\x1aH.temporal.server.api.historyservice.v1.RespondWorkflowTaskFailedResponse\"\x00\x12\x9e\x01\n" + - "\x13IsWorkflowTaskValid\x12A.temporal.server.api.historyservice.v1.IsWorkflowTaskValidRequest\x1aB.temporal.server.api.historyservice.v1.IsWorkflowTaskValidResponse\"\x00\x12\xb6\x01\n" + - "\x1bRecordActivityTaskHeartbeat\x12I.temporal.server.api.historyservice.v1.RecordActivityTaskHeartbeatRequest\x1aJ.temporal.server.api.historyservice.v1.RecordActivityTaskHeartbeatResponse\"\x00\x12\xb9\x01\n" + - "\x1cRespondActivityTaskCompleted\x12J.temporal.server.api.historyservice.v1.RespondActivityTaskCompletedRequest\x1aK.temporal.server.api.historyservice.v1.RespondActivityTaskCompletedResponse\"\x00\x12\xb0\x01\n" + - "\x19RespondActivityTaskFailed\x12G.temporal.server.api.historyservice.v1.RespondActivityTaskFailedRequest\x1aH.temporal.server.api.historyservice.v1.RespondActivityTaskFailedResponse\"\x00\x12\xb6\x01\n" + - "\x1bRespondActivityTaskCanceled\x12I.temporal.server.api.historyservice.v1.RespondActivityTaskCanceledRequest\x1aJ.temporal.server.api.historyservice.v1.RespondActivityTaskCanceledResponse\"\x00\x12\x9e\x01\n" + - "\x13IsActivityTaskValid\x12A.temporal.server.api.historyservice.v1.IsActivityTaskValidRequest\x1aB.temporal.server.api.historyservice.v1.IsActivityTaskValidResponse\"\x00\x12\xaa\x01\n" + - "\x17SignalWorkflowExecution\x12E.temporal.server.api.historyservice.v1.SignalWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.SignalWorkflowExecutionResponse\"\x00\x12\xc5\x01\n" + - " SignalWithStartWorkflowExecution\x12N.temporal.server.api.historyservice.v1.SignalWithStartWorkflowExecutionRequest\x1aO.temporal.server.api.historyservice.v1.SignalWithStartWorkflowExecutionResponse\"\x00\x12\xa4\x01\n" + - "\x15ExecuteMultiOperation\x12C.temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest\x1aD.temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse\"\x00\x12\xad\x01\n" + - "\x18RemoveSignalMutableState\x12F.temporal.server.api.historyservice.v1.RemoveSignalMutableStateRequest\x1aG.temporal.server.api.historyservice.v1.RemoveSignalMutableStateResponse\"\x00\x12\xb3\x01\n" + - "\x1aTerminateWorkflowExecution\x12H.temporal.server.api.historyservice.v1.TerminateWorkflowExecutionRequest\x1aI.temporal.server.api.historyservice.v1.TerminateWorkflowExecutionResponse\"\x00\x12\xaa\x01\n" + - "\x17DeleteWorkflowExecution\x12E.temporal.server.api.historyservice.v1.DeleteWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.DeleteWorkflowExecutionResponse\"\x00\x12\xa7\x01\n" + - "\x16ResetWorkflowExecution\x12D.temporal.server.api.historyservice.v1.ResetWorkflowExecutionRequest\x1aE.temporal.server.api.historyservice.v1.ResetWorkflowExecutionResponse\"\x00\x12\xbf\x01\n" + - "\x1eUpdateWorkflowExecutionOptions\x12L.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsRequest\x1aM.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsResponse\"\x00\x12\xbf\x01\n" + - "\x1eRequestCancelWorkflowExecution\x12L.temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionRequest\x1aM.temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionResponse\"\x00\x12\xa1\x01\n" + - "\x14ScheduleWorkflowTask\x12B.temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest\x1aC.temporal.server.api.historyservice.v1.ScheduleWorkflowTaskResponse\"\x00\x12\xc5\x01\n" + - " VerifyFirstWorkflowTaskScheduled\x12N.temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledRequest\x1aO.temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledResponse\"\x00\x12\xbc\x01\n" + - "\x1dRecordChildExecutionCompleted\x12K.temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest\x1aL.temporal.server.api.historyservice.v1.RecordChildExecutionCompletedResponse\"\x00\x12\xd7\x01\n" + - "&VerifyChildExecutionCompletionRecorded\x12T.temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest\x1aU.temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedResponse\"\x00\x12\xb0\x01\n" + - "\x19DescribeWorkflowExecution\x12G.temporal.server.api.historyservice.v1.DescribeWorkflowExecutionRequest\x1aH.temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse\"\x00\x12\x98\x01\n" + - "\x11ReplicateEventsV2\x12?.temporal.server.api.historyservice.v1.ReplicateEventsV2Request\x1a@.temporal.server.api.historyservice.v1.ReplicateEventsV2Response\"\x00\x12\xa7\x01\n" + - "\x16ReplicateWorkflowState\x12D.temporal.server.api.historyservice.v1.ReplicateWorkflowStateRequest\x1aE.temporal.server.api.historyservice.v1.ReplicateWorkflowStateResponse\"\x00\x12\x92\x01\n" + - "\x0fSyncShardStatus\x12=.temporal.server.api.historyservice.v1.SyncShardStatusRequest\x1a>.temporal.server.api.historyservice.v1.SyncShardStatusResponse\"\x00\x12\x89\x01\n" + - "\fSyncActivity\x12:.temporal.server.api.historyservice.v1.SyncActivityRequest\x1a;.temporal.server.api.historyservice.v1.SyncActivityResponse\"\x00\x12\xa1\x01\n" + - "\x14DescribeMutableState\x12B.temporal.server.api.historyservice.v1.DescribeMutableStateRequest\x1aC.temporal.server.api.historyservice.v1.DescribeMutableStateResponse\"\x00\x12\x9e\x01\n" + - "\x13DescribeHistoryHost\x12A.temporal.server.api.historyservice.v1.DescribeHistoryHostRequest\x1aB.temporal.server.api.historyservice.v1.DescribeHistoryHostResponse\"\x00\x12\x83\x01\n" + + "3temporal/server/api/historyservice/v1/service.proto\x12%temporal.server.api.historyservice.v1\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal.server.api.historyservice.v1.GetMutableStateResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x9b\x01\n" + + "\x10PollMutableState\x12>.temporal.server.api.historyservice.v1.PollMutableStateRequest\x1a?.temporal.server.api.historyservice.v1.PollMutableStateResponse\"\x06\x8a\xb5\x18\x02\b\x02\x12\xa7\x01\n" + + "\x14ResetStickyTaskQueue\x12B.temporal.server.api.historyservice.v1.ResetStickyTaskQueueRequest\x1aC.temporal.server.api.historyservice.v1.ResetStickyTaskQueueResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb6\x01\n" + + "\x19RecordWorkflowTaskStarted\x12G.temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedRequest\x1aH.temporal.server.api.historyservice.v1.RecordWorkflowTaskStartedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb6\x01\n" + + "\x19RecordActivityTaskStarted\x12G.temporal.server.api.historyservice.v1.RecordActivityTaskStartedRequest\x1aH.temporal.server.api.historyservice.v1.RecordActivityTaskStartedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbf\x01\n" + + "\x1cRespondWorkflowTaskCompleted\x12J.temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedRequest\x1aK.temporal.server.api.historyservice.v1.RespondWorkflowTaskCompletedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb6\x01\n" + + "\x19RespondWorkflowTaskFailed\x12G.temporal.server.api.historyservice.v1.RespondWorkflowTaskFailedRequest\x1aH.temporal.server.api.historyservice.v1.RespondWorkflowTaskFailedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa4\x01\n" + + "\x13IsWorkflowTaskValid\x12A.temporal.server.api.historyservice.v1.IsWorkflowTaskValidRequest\x1aB.temporal.server.api.historyservice.v1.IsWorkflowTaskValidResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbc\x01\n" + + "\x1bRecordActivityTaskHeartbeat\x12I.temporal.server.api.historyservice.v1.RecordActivityTaskHeartbeatRequest\x1aJ.temporal.server.api.historyservice.v1.RecordActivityTaskHeartbeatResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbf\x01\n" + + "\x1cRespondActivityTaskCompleted\x12J.temporal.server.api.historyservice.v1.RespondActivityTaskCompletedRequest\x1aK.temporal.server.api.historyservice.v1.RespondActivityTaskCompletedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb6\x01\n" + + "\x19RespondActivityTaskFailed\x12G.temporal.server.api.historyservice.v1.RespondActivityTaskFailedRequest\x1aH.temporal.server.api.historyservice.v1.RespondActivityTaskFailedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbc\x01\n" + + "\x1bRespondActivityTaskCanceled\x12I.temporal.server.api.historyservice.v1.RespondActivityTaskCanceledRequest\x1aJ.temporal.server.api.historyservice.v1.RespondActivityTaskCanceledResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa4\x01\n" + + "\x13IsActivityTaskValid\x12A.temporal.server.api.historyservice.v1.IsActivityTaskValidRequest\x1aB.temporal.server.api.historyservice.v1.IsActivityTaskValidResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb0\x01\n" + + "\x17SignalWorkflowExecution\x12E.temporal.server.api.historyservice.v1.SignalWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.SignalWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xcb\x01\n" + + " SignalWithStartWorkflowExecution\x12N.temporal.server.api.historyservice.v1.SignalWithStartWorkflowExecutionRequest\x1aO.temporal.server.api.historyservice.v1.SignalWithStartWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xaa\x01\n" + + "\x15ExecuteMultiOperation\x12C.temporal.server.api.historyservice.v1.ExecuteMultiOperationRequest\x1aD.temporal.server.api.historyservice.v1.ExecuteMultiOperationResponse\"\x06\x8a\xb5\x18\x02\b\x02\x12\xb3\x01\n" + + "\x18RemoveSignalMutableState\x12F.temporal.server.api.historyservice.v1.RemoveSignalMutableStateRequest\x1aG.temporal.server.api.historyservice.v1.RemoveSignalMutableStateResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb9\x01\n" + + "\x1aTerminateWorkflowExecution\x12H.temporal.server.api.historyservice.v1.TerminateWorkflowExecutionRequest\x1aI.temporal.server.api.historyservice.v1.TerminateWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb0\x01\n" + + "\x17DeleteWorkflowExecution\x12E.temporal.server.api.historyservice.v1.DeleteWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.DeleteWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xad\x01\n" + + "\x16ResetWorkflowExecution\x12D.temporal.server.api.historyservice.v1.ResetWorkflowExecutionRequest\x1aE.temporal.server.api.historyservice.v1.ResetWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc5\x01\n" + + "\x1eUpdateWorkflowExecutionOptions\x12L.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsRequest\x1aM.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionOptionsResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc5\x01\n" + + "\x1eRequestCancelWorkflowExecution\x12L.temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionRequest\x1aM.temporal.server.api.historyservice.v1.RequestCancelWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa7\x01\n" + + "\x14ScheduleWorkflowTask\x12B.temporal.server.api.historyservice.v1.ScheduleWorkflowTaskRequest\x1aC.temporal.server.api.historyservice.v1.ScheduleWorkflowTaskResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xcb\x01\n" + + " VerifyFirstWorkflowTaskScheduled\x12N.temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledRequest\x1aO.temporal.server.api.historyservice.v1.VerifyFirstWorkflowTaskScheduledResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc2\x01\n" + + "\x1dRecordChildExecutionCompleted\x12K.temporal.server.api.historyservice.v1.RecordChildExecutionCompletedRequest\x1aL.temporal.server.api.historyservice.v1.RecordChildExecutionCompletedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xdd\x01\n" + + "&VerifyChildExecutionCompletionRecorded\x12T.temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedRequest\x1aU.temporal.server.api.historyservice.v1.VerifyChildExecutionCompletionRecordedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb6\x01\n" + + "\x19DescribeWorkflowExecution\x12G.temporal.server.api.historyservice.v1.DescribeWorkflowExecutionRequest\x1aH.temporal.server.api.historyservice.v1.DescribeWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x9e\x01\n" + + "\x11ReplicateEventsV2\x12?.temporal.server.api.historyservice.v1.ReplicateEventsV2Request\x1a@.temporal.server.api.historyservice.v1.ReplicateEventsV2Response\"\x06\x8a\xb5\x18\x02\b\x01\x12\xad\x01\n" + + "\x16ReplicateWorkflowState\x12D.temporal.server.api.historyservice.v1.ReplicateWorkflowStateRequest\x1aE.temporal.server.api.historyservice.v1.ReplicateWorkflowStateResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x98\x01\n" + + "\x0fSyncShardStatus\x12=.temporal.server.api.historyservice.v1.SyncShardStatusRequest\x1a>.temporal.server.api.historyservice.v1.SyncShardStatusResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x8f\x01\n" + + "\fSyncActivity\x12:.temporal.server.api.historyservice.v1.SyncActivityRequest\x1a;.temporal.server.api.historyservice.v1.SyncActivityResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa7\x01\n" + + "\x14DescribeMutableState\x12B.temporal.server.api.historyservice.v1.DescribeMutableStateRequest\x1aC.temporal.server.api.historyservice.v1.DescribeMutableStateResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa4\x01\n" + + "\x13DescribeHistoryHost\x12A.temporal.server.api.historyservice.v1.DescribeHistoryHostRequest\x1aB.temporal.server.api.historyservice.v1.DescribeHistoryHostResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x89\x01\n" + "\n" + - "CloseShard\x128.temporal.server.api.historyservice.v1.CloseShardRequest\x1a9.temporal.server.api.historyservice.v1.CloseShardResponse\"\x00\x12}\n" + - "\bGetShard\x126.temporal.server.api.historyservice.v1.GetShardRequest\x1a7.temporal.server.api.historyservice.v1.GetShardResponse\"\x00\x12\x83\x01\n" + + "CloseShard\x128.temporal.server.api.historyservice.v1.CloseShardRequest\x1a9.temporal.server.api.historyservice.v1.CloseShardResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x83\x01\n" + + "\bGetShard\x126.temporal.server.api.historyservice.v1.GetShardRequest\x1a7.temporal.server.api.historyservice.v1.GetShardResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x89\x01\n" + "\n" + - "RemoveTask\x128.temporal.server.api.historyservice.v1.RemoveTaskRequest\x1a9.temporal.server.api.historyservice.v1.RemoveTaskResponse\"\x00\x12\xa7\x01\n" + - "\x16GetReplicationMessages\x12D.temporal.server.api.historyservice.v1.GetReplicationMessagesRequest\x1aE.temporal.server.api.historyservice.v1.GetReplicationMessagesResponse\"\x00\x12\xb0\x01\n" + - "\x19GetDLQReplicationMessages\x12G.temporal.server.api.historyservice.v1.GetDLQReplicationMessagesRequest\x1aH.temporal.server.api.historyservice.v1.GetDLQReplicationMessagesResponse\"\x00\x12\x8c\x01\n" + - "\rQueryWorkflow\x12;.temporal.server.api.historyservice.v1.QueryWorkflowRequest\x1a<.temporal.server.api.historyservice.v1.QueryWorkflowResponse\"\x00\x12\x8c\x01\n" + - "\rReapplyEvents\x12;.temporal.server.api.historyservice.v1.ReapplyEventsRequest\x1a<.temporal.server.api.historyservice.v1.ReapplyEventsResponse\"\x00\x12\x8f\x01\n" + - "\x0eGetDLQMessages\x12<.temporal.server.api.historyservice.v1.GetDLQMessagesRequest\x1a=.temporal.server.api.historyservice.v1.GetDLQMessagesResponse\"\x00\x12\x95\x01\n" + - "\x10PurgeDLQMessages\x12>.temporal.server.api.historyservice.v1.PurgeDLQMessagesRequest\x1a?.temporal.server.api.historyservice.v1.PurgeDLQMessagesResponse\"\x00\x12\x95\x01\n" + - "\x10MergeDLQMessages\x12>.temporal.server.api.historyservice.v1.MergeDLQMessagesRequest\x1a?.temporal.server.api.historyservice.v1.MergeDLQMessagesResponse\"\x00\x12\xa1\x01\n" + - "\x14RefreshWorkflowTasks\x12B.temporal.server.api.historyservice.v1.RefreshWorkflowTasksRequest\x1aC.temporal.server.api.historyservice.v1.RefreshWorkflowTasksResponse\"\x00\x12\xce\x01\n" + - "#GenerateLastHistoryReplicationTasks\x12Q.temporal.server.api.historyservice.v1.GenerateLastHistoryReplicationTasksRequest\x1aR.temporal.server.api.historyservice.v1.GenerateLastHistoryReplicationTasksResponse\"\x00\x12\xa1\x01\n" + - "\x14GetReplicationStatus\x12B.temporal.server.api.historyservice.v1.GetReplicationStatusRequest\x1aC.temporal.server.api.historyservice.v1.GetReplicationStatusResponse\"\x00\x12\x9e\x01\n" + - "\x13RebuildMutableState\x12A.temporal.server.api.historyservice.v1.RebuildMutableStateRequest\x1aB.temporal.server.api.historyservice.v1.RebuildMutableStateResponse\"\x00\x12\xaa\x01\n" + - "\x17ImportWorkflowExecution\x12E.temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.ImportWorkflowExecutionResponse\"\x00\x12\xbf\x01\n" + - "\x1eDeleteWorkflowVisibilityRecord\x12L.temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest\x1aM.temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordResponse\"\x00\x12\xaa\x01\n" + - "\x17UpdateWorkflowExecution\x12E.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionResponse\"\x00\x12\xb6\x01\n" + - "\x1bPollWorkflowExecutionUpdate\x12I.temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateRequest\x1aJ.temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateResponse\"\x00\x12\xcc\x01\n" + - "!StreamWorkflowReplicationMessages\x12O.temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesRequest\x1aP.temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesResponse\"\x00(\x010\x01\x12\xb6\x01\n" + - "\x1bGetWorkflowExecutionHistory\x12I.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryRequest\x1aJ.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponse\"\x00\x12\xcb\x01\n" + - "\"GetWorkflowExecutionHistoryReverse\x12P.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseRequest\x1aQ.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseResponse\"\x00\x12\xc5\x01\n" + - " GetWorkflowExecutionRawHistoryV2\x12N.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Request\x1aO.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Response\"\x00\x12\xbf\x01\n" + - "\x1eGetWorkflowExecutionRawHistory\x12L.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryRequest\x1aM.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryResponse\"\x00\x12\xb9\x01\n" + - "\x1cForceDeleteWorkflowExecution\x12J.temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionRequest\x1aK.temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionResponse\"\x00\x12\x86\x01\n" + - "\vGetDLQTasks\x129.temporal.server.api.historyservice.v1.GetDLQTasksRequest\x1a:.temporal.server.api.historyservice.v1.GetDLQTasksResponse\"\x00\x12\x8f\x01\n" + - "\x0eDeleteDLQTasks\x12<.temporal.server.api.historyservice.v1.DeleteDLQTasksRequest\x1a=.temporal.server.api.historyservice.v1.DeleteDLQTasksResponse\"\x00\x12\x83\x01\n" + + "RemoveTask\x128.temporal.server.api.historyservice.v1.RemoveTaskRequest\x1a9.temporal.server.api.historyservice.v1.RemoveTaskResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xad\x01\n" + + "\x16GetReplicationMessages\x12D.temporal.server.api.historyservice.v1.GetReplicationMessagesRequest\x1aE.temporal.server.api.historyservice.v1.GetReplicationMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb6\x01\n" + + "\x19GetDLQReplicationMessages\x12G.temporal.server.api.historyservice.v1.GetDLQReplicationMessagesRequest\x1aH.temporal.server.api.historyservice.v1.GetDLQReplicationMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x92\x01\n" + + "\rQueryWorkflow\x12;.temporal.server.api.historyservice.v1.QueryWorkflowRequest\x1a<.temporal.server.api.historyservice.v1.QueryWorkflowResponse\"\x06\x8a\xb5\x18\x02\b\x02\x12\x92\x01\n" + + "\rReapplyEvents\x12;.temporal.server.api.historyservice.v1.ReapplyEventsRequest\x1a<.temporal.server.api.historyservice.v1.ReapplyEventsResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x95\x01\n" + + "\x0eGetDLQMessages\x12<.temporal.server.api.historyservice.v1.GetDLQMessagesRequest\x1a=.temporal.server.api.historyservice.v1.GetDLQMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x9b\x01\n" + + "\x10PurgeDLQMessages\x12>.temporal.server.api.historyservice.v1.PurgeDLQMessagesRequest\x1a?.temporal.server.api.historyservice.v1.PurgeDLQMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x9b\x01\n" + + "\x10MergeDLQMessages\x12>.temporal.server.api.historyservice.v1.MergeDLQMessagesRequest\x1a?.temporal.server.api.historyservice.v1.MergeDLQMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xa7\x01\n" + + "\x14RefreshWorkflowTasks\x12B.temporal.server.api.historyservice.v1.RefreshWorkflowTasksRequest\x1aC.temporal.server.api.historyservice.v1.RefreshWorkflowTasksResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xd4\x01\n" + + "#GenerateLastHistoryReplicationTasks\x12Q.temporal.server.api.historyservice.v1.GenerateLastHistoryReplicationTasksRequest\x1aR.temporal.server.api.historyservice.v1.GenerateLastHistoryReplicationTasksResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa7\x01\n" + + "\x14GetReplicationStatus\x12B.temporal.server.api.historyservice.v1.GetReplicationStatusRequest\x1aC.temporal.server.api.historyservice.v1.GetReplicationStatusResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa4\x01\n" + + "\x13RebuildMutableState\x12A.temporal.server.api.historyservice.v1.RebuildMutableStateRequest\x1aB.temporal.server.api.historyservice.v1.RebuildMutableStateResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb0\x01\n" + + "\x17ImportWorkflowExecution\x12E.temporal.server.api.historyservice.v1.ImportWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.ImportWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc5\x01\n" + + "\x1eDeleteWorkflowVisibilityRecord\x12L.temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordRequest\x1aM.temporal.server.api.historyservice.v1.DeleteWorkflowVisibilityRecordResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\xb0\x01\n" + + "\x17UpdateWorkflowExecution\x12E.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionRequest\x1aF.temporal.server.api.historyservice.v1.UpdateWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x02\x12\xbc\x01\n" + + "\x1bPollWorkflowExecutionUpdate\x12I.temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateRequest\x1aJ.temporal.server.api.historyservice.v1.PollWorkflowExecutionUpdateResponse\"\x06\x8a\xb5\x18\x02\b\x02\x12\xd2\x01\n" + + "!StreamWorkflowReplicationMessages\x12O.temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesRequest\x1aP.temporal.server.api.historyservice.v1.StreamWorkflowReplicationMessagesResponse\"\x06\x8a\xb5\x18\x02\b\x03(\x010\x01\x12\xbc\x01\n" + + "\x1bGetWorkflowExecutionHistory\x12I.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryRequest\x1aJ.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xd1\x01\n" + + "\"GetWorkflowExecutionHistoryReverse\x12P.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseRequest\x1aQ.temporal.server.api.historyservice.v1.GetWorkflowExecutionHistoryReverseResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xcb\x01\n" + + " GetWorkflowExecutionRawHistoryV2\x12N.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Request\x1aO.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryV2Response\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc5\x01\n" + + "\x1eGetWorkflowExecutionRawHistory\x12L.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryRequest\x1aM.temporal.server.api.historyservice.v1.GetWorkflowExecutionRawHistoryResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbf\x01\n" + + "\x1cForceDeleteWorkflowExecution\x12J.temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionRequest\x1aK.temporal.server.api.historyservice.v1.ForceDeleteWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x8c\x01\n" + + "\vGetDLQTasks\x129.temporal.server.api.historyservice.v1.GetDLQTasksRequest\x1a:.temporal.server.api.historyservice.v1.GetDLQTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x95\x01\n" + + "\x0eDeleteDLQTasks\x12<.temporal.server.api.historyservice.v1.DeleteDLQTasksRequest\x1a=.temporal.server.api.historyservice.v1.DeleteDLQTasksResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x89\x01\n" + "\n" + - "ListQueues\x128.temporal.server.api.historyservice.v1.ListQueuesRequest\x1a9.temporal.server.api.historyservice.v1.ListQueuesResponse\"\x00\x12}\n" + - "\bAddTasks\x126.temporal.server.api.historyservice.v1.AddTasksRequest\x1a7.temporal.server.api.historyservice.v1.AddTasksResponse\"\x00\x12\x80\x01\n" + - "\tListTasks\x127.temporal.server.api.historyservice.v1.ListTasksRequest\x1a8.temporal.server.api.historyservice.v1.ListTasksResponse\"\x00\x12\xa7\x01\n" + - "\x16CompleteNexusOperation\x12D.temporal.server.api.historyservice.v1.CompleteNexusOperationRequest\x1aE.temporal.server.api.historyservice.v1.CompleteNexusOperationResponse\"\x00\x12\xb6\x01\n" + - "\x1bCompleteNexusOperationChasm\x12I.temporal.server.api.historyservice.v1.CompleteNexusOperationChasmRequest\x1aJ.temporal.server.api.historyservice.v1.CompleteNexusOperationChasmResponse\"\x00\x12\xad\x01\n" + - "\x18InvokeStateMachineMethod\x12F.temporal.server.api.historyservice.v1.InvokeStateMachineMethodRequest\x1aG.temporal.server.api.historyservice.v1.InvokeStateMachineMethodResponse\"\x00\x12\x92\x01\n" + - "\x0fDeepHealthCheck\x12=.temporal.server.api.historyservice.v1.DeepHealthCheckRequest\x1a>.temporal.server.api.historyservice.v1.DeepHealthCheckResponse\"\x00\x12\x98\x01\n" + - "\x11SyncWorkflowState\x12?.temporal.server.api.historyservice.v1.SyncWorkflowStateRequest\x1a@.temporal.server.api.historyservice.v1.SyncWorkflowStateResponse\"\x00\x12\xa4\x01\n" + - "\x15UpdateActivityOptions\x12C.temporal.server.api.historyservice.v1.UpdateActivityOptionsRequest\x1aD.temporal.server.api.historyservice.v1.UpdateActivityOptionsResponse\"\x00\x12\x8c\x01\n" + - "\rPauseActivity\x12;.temporal.server.api.historyservice.v1.PauseActivityRequest\x1a<.temporal.server.api.historyservice.v1.PauseActivityResponse\"\x00\x12\x92\x01\n" + - "\x0fUnpauseActivity\x12=.temporal.server.api.historyservice.v1.UnpauseActivityRequest\x1a>.temporal.server.api.historyservice.v1.UnpauseActivityResponse\"\x00\x12\x8c\x01\n" + - "\rResetActivity\x12;.temporal.server.api.historyservice.v1.ResetActivityRequest\x1a<.temporal.server.api.historyservice.v1.ResetActivityResponse\"\x00\x12\xa7\x01\n" + - "\x16PauseWorkflowExecution\x12D.temporal.server.api.historyservice.v1.PauseWorkflowExecutionRequest\x1aE.temporal.server.api.historyservice.v1.PauseWorkflowExecutionResponse\"\x00\x12\xad\x01\n" + - "\x18UnpauseWorkflowExecution\x12F.temporal.server.api.historyservice.v1.UnpauseWorkflowExecutionRequest\x1aG.temporal.server.api.historyservice.v1.UnpauseWorkflowExecutionResponse\"\x00\x12\x9e\x01\n" + - "\x13StartNexusOperation\x12A.temporal.server.api.historyservice.v1.StartNexusOperationRequest\x1aB.temporal.server.api.historyservice.v1.StartNexusOperationResponse\"\x00\x12\xa1\x01\n" + - "\x14CancelNexusOperation\x12B.temporal.server.api.historyservice.v1.CancelNexusOperationRequest\x1aC.temporal.server.api.historyservice.v1.CancelNexusOperationResponse\"\x00B.temporal.server.api.historyservice.v1.DeepHealthCheckResponse\"\x06\x8a\xb5\x18\x02\b\x03\x12\x9e\x01\n" + + "\x11SyncWorkflowState\x12?.temporal.server.api.historyservice.v1.SyncWorkflowStateRequest\x1a@.temporal.server.api.historyservice.v1.SyncWorkflowStateResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xaa\x01\n" + + "\x15UpdateActivityOptions\x12C.temporal.server.api.historyservice.v1.UpdateActivityOptionsRequest\x1aD.temporal.server.api.historyservice.v1.UpdateActivityOptionsResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x92\x01\n" + + "\rPauseActivity\x12;.temporal.server.api.historyservice.v1.PauseActivityRequest\x1a<.temporal.server.api.historyservice.v1.PauseActivityResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x98\x01\n" + + "\x0fUnpauseActivity\x12=.temporal.server.api.historyservice.v1.UnpauseActivityRequest\x1a>.temporal.server.api.historyservice.v1.UnpauseActivityResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x92\x01\n" + + "\rResetActivity\x12;.temporal.server.api.historyservice.v1.ResetActivityRequest\x1a<.temporal.server.api.historyservice.v1.ResetActivityResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xad\x01\n" + + "\x16PauseWorkflowExecution\x12D.temporal.server.api.historyservice.v1.PauseWorkflowExecutionRequest\x1aE.temporal.server.api.historyservice.v1.PauseWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb3\x01\n" + + "\x18UnpauseWorkflowExecution\x12F.temporal.server.api.historyservice.v1.UnpauseWorkflowExecutionRequest\x1aG.temporal.server.api.historyservice.v1.UnpauseWorkflowExecutionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa4\x01\n" + + "\x13StartNexusOperation\x12A.temporal.server.api.historyservice.v1.StartNexusOperationRequest\x1aB.temporal.server.api.historyservice.v1.StartNexusOperationResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa7\x01\n" + + "\x14CancelNexusOperation\x12B.temporal.server.api.historyservice.v1.CancelNexusOperationRequest\x1aC.temporal.server.api.historyservice.v1.CancelNexusOperationResponse\"\x06\x8a\xb5\x18\x02\b\x01B temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest @@ -6448,29 +6465,30 @@ var file_temporal_server_api_matchingservice_v1_request_response_proto_depIdxs = 143, // 124: temporal.server.api.matchingservice.v1.RecordWorkerHeartbeatRequest.heartbeart_request:type_name -> temporal.api.workflowservice.v1.RecordWorkerHeartbeatRequest 144, // 125: temporal.server.api.matchingservice.v1.ListWorkersRequest.list_request:type_name -> temporal.api.workflowservice.v1.ListWorkersRequest 145, // 126: temporal.server.api.matchingservice.v1.ListWorkersResponse.workers_info:type_name -> temporal.api.worker.v1.WorkerInfo - 146, // 127: temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigRequest.update_taskqueue_config:type_name -> temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest - 147, // 128: temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigResponse.updated_taskqueue_config:type_name -> temporal.api.taskqueue.v1.TaskQueueConfig - 148, // 129: temporal.server.api.matchingservice.v1.DescribeWorkerRequest.request:type_name -> temporal.api.workflowservice.v1.DescribeWorkerRequest - 145, // 130: temporal.server.api.matchingservice.v1.DescribeWorkerResponse.worker_info:type_name -> temporal.api.worker.v1.WorkerInfo - 115, // 131: temporal.server.api.matchingservice.v1.UpdateFairnessStateRequest.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType - 149, // 132: temporal.server.api.matchingservice.v1.UpdateFairnessStateRequest.fairness_state:type_name -> temporal.server.api.enums.v1.FairnessState - 115, // 133: temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipRequest.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType - 117, // 134: temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipRequest.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion - 95, // 135: temporal.server.api.matchingservice.v1.PollWorkflowTaskQueueResponse.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery - 95, // 136: temporal.server.api.matchingservice.v1.PollWorkflowTaskQueueResponseWithRawHistory.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery - 115, // 137: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesRequest.VersionTaskQueue.type:type_name -> temporal.api.enums.v1.TaskQueueType - 115, // 138: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.type:type_name -> temporal.api.enums.v1.TaskQueueType - 150, // 139: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.stats:type_name -> temporal.api.taskqueue.v1.TaskQueueStats - 86, // 140: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.stats_by_priority_key:type_name -> temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.StatsByPriorityKeyEntry - 150, // 141: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.StatsByPriorityKeyEntry.value:type_name -> temporal.api.taskqueue.v1.TaskQueueStats - 151, // 142: temporal.server.api.matchingservice.v1.DescribeTaskQueuePartitionResponse.VersionsInfoInternalEntry.value:type_name -> temporal.server.api.taskqueue.v1.TaskQueueVersionInfoInternal - 152, // 143: temporal.server.api.matchingservice.v1.UpdateWorkerBuildIdCompatibilityRequest.ApplyPublicRequest.request:type_name -> temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest - 153, // 144: temporal.server.api.matchingservice.v1.SyncDeploymentUserDataRequest.UpsertVersionsDataEntry.value:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionData - 145, // [145:145] is the sub-list for method output_type - 145, // [145:145] is the sub-list for method input_type - 145, // [145:145] is the sub-list for extension type_name - 145, // [145:145] is the sub-list for extension extendee - 0, // [0:145] is the sub-list for field type_name + 146, // 127: temporal.server.api.matchingservice.v1.ListWorkersResponse.workers:type_name -> temporal.api.worker.v1.WorkerListInfo + 147, // 128: temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigRequest.update_taskqueue_config:type_name -> temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest + 148, // 129: temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigResponse.updated_taskqueue_config:type_name -> temporal.api.taskqueue.v1.TaskQueueConfig + 149, // 130: temporal.server.api.matchingservice.v1.DescribeWorkerRequest.request:type_name -> temporal.api.workflowservice.v1.DescribeWorkerRequest + 145, // 131: temporal.server.api.matchingservice.v1.DescribeWorkerResponse.worker_info:type_name -> temporal.api.worker.v1.WorkerInfo + 115, // 132: temporal.server.api.matchingservice.v1.UpdateFairnessStateRequest.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType + 150, // 133: temporal.server.api.matchingservice.v1.UpdateFairnessStateRequest.fairness_state:type_name -> temporal.server.api.enums.v1.FairnessState + 115, // 134: temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipRequest.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType + 117, // 135: temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipRequest.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion + 95, // 136: temporal.server.api.matchingservice.v1.PollWorkflowTaskQueueResponse.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery + 95, // 137: temporal.server.api.matchingservice.v1.PollWorkflowTaskQueueResponseWithRawHistory.QueriesEntry.value:type_name -> temporal.api.query.v1.WorkflowQuery + 115, // 138: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesRequest.VersionTaskQueue.type:type_name -> temporal.api.enums.v1.TaskQueueType + 115, // 139: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.type:type_name -> temporal.api.enums.v1.TaskQueueType + 151, // 140: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.stats:type_name -> temporal.api.taskqueue.v1.TaskQueueStats + 86, // 141: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.stats_by_priority_key:type_name -> temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.StatsByPriorityKeyEntry + 151, // 142: temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse.VersionTaskQueue.StatsByPriorityKeyEntry.value:type_name -> temporal.api.taskqueue.v1.TaskQueueStats + 152, // 143: temporal.server.api.matchingservice.v1.DescribeTaskQueuePartitionResponse.VersionsInfoInternalEntry.value:type_name -> temporal.server.api.taskqueue.v1.TaskQueueVersionInfoInternal + 153, // 144: temporal.server.api.matchingservice.v1.UpdateWorkerBuildIdCompatibilityRequest.ApplyPublicRequest.request:type_name -> temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest + 154, // 145: temporal.server.api.matchingservice.v1.SyncDeploymentUserDataRequest.UpsertVersionsDataEntry.value:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersionData + 146, // [146:146] is the sub-list for method output_type + 146, // [146:146] is the sub-list for method input_type + 146, // [146:146] is the sub-list for extension type_name + 146, // [146:146] is the sub-list for extension extendee + 0, // [0:146] is the sub-list for field type_name } func init() { file_temporal_server_api_matchingservice_v1_request_response_proto_init() } diff --git a/api/matchingservice/v1/service.pb.go b/api/matchingservice/v1/service.pb.go index d92d98f4732..10479d1ca5c 100644 --- a/api/matchingservice/v1/service.pb.go +++ b/api/matchingservice/v1/service.pb.go @@ -10,6 +10,7 @@ import ( reflect "reflect" unsafe "unsafe" + _ "go.temporal.io/server/api/common/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) @@ -25,48 +26,48 @@ var File_temporal_server_api_matchingservice_v1_service_proto protoreflect.FileD const file_temporal_server_api_matchingservice_v1_service_proto_rawDesc = "" + "\n" + - "4temporal/server/api/matchingservice/v1/service.proto\x12&temporal.server.api.matchingservice.v1\x1a=temporal/server/api/matchingservice/v1/request_response.proto2\xb36\n" + - "\x0fMatchingService\x12\xa6\x01\n" + - "\x15PollWorkflowTaskQueue\x12D.temporal.server.api.matchingservice.v1.PollWorkflowTaskQueueRequest\x1aE.temporal.server.api.matchingservice.v1.PollWorkflowTaskQueueResponse\"\x00\x12\xa6\x01\n" + - "\x15PollActivityTaskQueue\x12D.temporal.server.api.matchingservice.v1.PollActivityTaskQueueRequest\x1aE.temporal.server.api.matchingservice.v1.PollActivityTaskQueueResponse\"\x00\x12\x94\x01\n" + - "\x0fAddWorkflowTask\x12>.temporal.server.api.matchingservice.v1.AddWorkflowTaskRequest\x1a?.temporal.server.api.matchingservice.v1.AddWorkflowTaskResponse\"\x00\x12\x94\x01\n" + - "\x0fAddActivityTask\x12>.temporal.server.api.matchingservice.v1.AddActivityTaskRequest\x1a?.temporal.server.api.matchingservice.v1.AddActivityTaskResponse\"\x00\x12\x8e\x01\n" + - "\rQueryWorkflow\x12<.temporal.server.api.matchingservice.v1.QueryWorkflowRequest\x1a=.temporal.server.api.matchingservice.v1.QueryWorkflowResponse\"\x00\x12\xb2\x01\n" + - "\x19RespondQueryTaskCompleted\x12H.temporal.server.api.matchingservice.v1.RespondQueryTaskCompletedRequest\x1aI.temporal.server.api.matchingservice.v1.RespondQueryTaskCompletedResponse\"\x00\x12\x9a\x01\n" + - "\x11DispatchNexusTask\x12@.temporal.server.api.matchingservice.v1.DispatchNexusTaskRequest\x1aA.temporal.server.api.matchingservice.v1.DispatchNexusTaskResponse\"\x00\x12\x9d\x01\n" + - "\x12PollNexusTaskQueue\x12A.temporal.server.api.matchingservice.v1.PollNexusTaskQueueRequest\x1aB.temporal.server.api.matchingservice.v1.PollNexusTaskQueueResponse\"\x00\x12\xb2\x01\n" + - "\x19RespondNexusTaskCompleted\x12H.temporal.server.api.matchingservice.v1.RespondNexusTaskCompletedRequest\x1aI.temporal.server.api.matchingservice.v1.RespondNexusTaskCompletedResponse\"\x00\x12\xa9\x01\n" + - "\x16RespondNexusTaskFailed\x12E.temporal.server.api.matchingservice.v1.RespondNexusTaskFailedRequest\x1aF.temporal.server.api.matchingservice.v1.RespondNexusTaskFailedResponse\"\x00\x12\xa6\x01\n" + - "\x15CancelOutstandingPoll\x12D.temporal.server.api.matchingservice.v1.CancelOutstandingPollRequest\x1aE.temporal.server.api.matchingservice.v1.CancelOutstandingPollResponse\"\x00\x12\xbb\x01\n" + - "\x1cCancelOutstandingWorkerPolls\x12K.temporal.server.api.matchingservice.v1.CancelOutstandingWorkerPollsRequest\x1aL.temporal.server.api.matchingservice.v1.CancelOutstandingWorkerPollsResponse\"\x00\x12\x9a\x01\n" + - "\x11DescribeTaskQueue\x12@.temporal.server.api.matchingservice.v1.DescribeTaskQueueRequest\x1aA.temporal.server.api.matchingservice.v1.DescribeTaskQueueResponse\"\x00\x12\xb5\x01\n" + - "\x1aDescribeTaskQueuePartition\x12I.temporal.server.api.matchingservice.v1.DescribeTaskQueuePartitionRequest\x1aJ.temporal.server.api.matchingservice.v1.DescribeTaskQueuePartitionResponse\"\x00\x12\xb8\x01\n" + - "\x1bDescribeVersionedTaskQueues\x12J.temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesRequest\x1aK.temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse\"\x00\x12\xac\x01\n" + - "\x17ListTaskQueuePartitions\x12F.temporal.server.api.matchingservice.v1.ListTaskQueuePartitionsRequest\x1aG.temporal.server.api.matchingservice.v1.ListTaskQueuePartitionsResponse\"\x00\x12\xc7\x01\n" + - " UpdateWorkerBuildIdCompatibility\x12O.temporal.server.api.matchingservice.v1.UpdateWorkerBuildIdCompatibilityRequest\x1aP.temporal.server.api.matchingservice.v1.UpdateWorkerBuildIdCompatibilityResponse\"\x00\x12\xbe\x01\n" + - "\x1dGetWorkerBuildIdCompatibility\x12L.temporal.server.api.matchingservice.v1.GetWorkerBuildIdCompatibilityRequest\x1aM.temporal.server.api.matchingservice.v1.GetWorkerBuildIdCompatibilityResponse\"\x00\x12\xa3\x01\n" + - "\x14GetTaskQueueUserData\x12C.temporal.server.api.matchingservice.v1.GetTaskQueueUserDataRequest\x1aD.temporal.server.api.matchingservice.v1.GetTaskQueueUserDataResponse\"\x00\x12\xb8\x01\n" + - "\x1bUpdateWorkerVersioningRules\x12J.temporal.server.api.matchingservice.v1.UpdateWorkerVersioningRulesRequest\x1aK.temporal.server.api.matchingservice.v1.UpdateWorkerVersioningRulesResponse\"\x00\x12\xaf\x01\n" + - "\x18GetWorkerVersioningRules\x12G.temporal.server.api.matchingservice.v1.GetWorkerVersioningRulesRequest\x1aH.temporal.server.api.matchingservice.v1.GetWorkerVersioningRulesResponse\"\x00\x12\xa9\x01\n" + - "\x16SyncDeploymentUserData\x12E.temporal.server.api.matchingservice.v1.SyncDeploymentUserDataRequest\x1aF.temporal.server.api.matchingservice.v1.SyncDeploymentUserDataResponse\"\x00\x12\xd9\x01\n" + - "&ApplyTaskQueueUserDataReplicationEvent\x12U.temporal.server.api.matchingservice.v1.ApplyTaskQueueUserDataReplicationEventRequest\x1aV.temporal.server.api.matchingservice.v1.ApplyTaskQueueUserDataReplicationEventResponse\"\x00\x12\xb5\x01\n" + - "\x1aGetBuildIdTaskQueueMapping\x12I.temporal.server.api.matchingservice.v1.GetBuildIdTaskQueueMappingRequest\x1aJ.temporal.server.api.matchingservice.v1.GetBuildIdTaskQueueMappingResponse\"\x00\x12\xb8\x01\n" + - "\x1bForceLoadTaskQueuePartition\x12J.temporal.server.api.matchingservice.v1.ForceLoadTaskQueuePartitionRequest\x1aK.temporal.server.api.matchingservice.v1.ForceLoadTaskQueuePartitionResponse\"\x00\x12\xa3\x01\n" + - "\x14ForceUnloadTaskQueue\x12C.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueueRequest\x1aD.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueueResponse\"\x00\x12\xbe\x01\n" + - "\x1dForceUnloadTaskQueuePartition\x12L.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueuePartitionRequest\x1aM.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueuePartitionResponse\"\x00\x12\xac\x01\n" + - "\x17UpdateTaskQueueUserData\x12F.temporal.server.api.matchingservice.v1.UpdateTaskQueueUserDataRequest\x1aG.temporal.server.api.matchingservice.v1.UpdateTaskQueueUserDataResponse\"\x00\x12\xb5\x01\n" + - "\x1aReplicateTaskQueueUserData\x12I.temporal.server.api.matchingservice.v1.ReplicateTaskQueueUserDataRequest\x1aJ.temporal.server.api.matchingservice.v1.ReplicateTaskQueueUserDataResponse\"\x00\x12\xca\x01\n" + - "!CheckTaskQueueUserDataPropagation\x12P.temporal.server.api.matchingservice.v1.CheckTaskQueueUserDataPropagationRequest\x1aQ.temporal.server.api.matchingservice.v1.CheckTaskQueueUserDataPropagationResponse\"\x00\x12\xa0\x01\n" + - "\x13CreateNexusEndpoint\x12B.temporal.server.api.matchingservice.v1.CreateNexusEndpointRequest\x1aC.temporal.server.api.matchingservice.v1.CreateNexusEndpointResponse\"\x00\x12\xa0\x01\n" + - "\x13UpdateNexusEndpoint\x12B.temporal.server.api.matchingservice.v1.UpdateNexusEndpointRequest\x1aC.temporal.server.api.matchingservice.v1.UpdateNexusEndpointResponse\"\x00\x12\xa0\x01\n" + - "\x13DeleteNexusEndpoint\x12B.temporal.server.api.matchingservice.v1.DeleteNexusEndpointRequest\x1aC.temporal.server.api.matchingservice.v1.DeleteNexusEndpointResponse\"\x00\x12\x9d\x01\n" + - "\x12ListNexusEndpoints\x12A.temporal.server.api.matchingservice.v1.ListNexusEndpointsRequest\x1aB.temporal.server.api.matchingservice.v1.ListNexusEndpointsResponse\"\x00\x12\xa6\x01\n" + - "\x15RecordWorkerHeartbeat\x12D.temporal.server.api.matchingservice.v1.RecordWorkerHeartbeatRequest\x1aE.temporal.server.api.matchingservice.v1.RecordWorkerHeartbeatResponse\"\x00\x12\x88\x01\n" + - "\vListWorkers\x12:.temporal.server.api.matchingservice.v1.ListWorkersRequest\x1a;.temporal.server.api.matchingservice.v1.ListWorkersResponse\"\x00\x12\xa6\x01\n" + - "\x15UpdateTaskQueueConfig\x12D.temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigRequest\x1aE.temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigResponse\"\x00\x12\x91\x01\n" + - "\x0eDescribeWorker\x12=.temporal.server.api.matchingservice.v1.DescribeWorkerRequest\x1a>.temporal.server.api.matchingservice.v1.DescribeWorkerResponse\"\x00\x12\xa0\x01\n" + - "\x13UpdateFairnessState\x12B.temporal.server.api.matchingservice.v1.UpdateFairnessStateRequest\x1aC.temporal.server.api.matchingservice.v1.UpdateFairnessStateResponse\"\x00\x12\xc4\x01\n" + - "\x1fCheckTaskQueueVersionMembership\x12N.temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipRequest\x1aO.temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipResponse\"\x00B>Z.temporal.server.api.matchingservice.v1.AddWorkflowTaskRequest\x1a?.temporal.server.api.matchingservice.v1.AddWorkflowTaskResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x9a\x01\n" + + "\x0fAddActivityTask\x12>.temporal.server.api.matchingservice.v1.AddActivityTaskRequest\x1a?.temporal.server.api.matchingservice.v1.AddActivityTaskResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x94\x01\n" + + "\rQueryWorkflow\x12<.temporal.server.api.matchingservice.v1.QueryWorkflowRequest\x1a=.temporal.server.api.matchingservice.v1.QueryWorkflowResponse\"\x06\x8a\xb5\x18\x02\b\x02\x12\xb8\x01\n" + + "\x19RespondQueryTaskCompleted\x12H.temporal.server.api.matchingservice.v1.RespondQueryTaskCompletedRequest\x1aI.temporal.server.api.matchingservice.v1.RespondQueryTaskCompletedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa0\x01\n" + + "\x11DispatchNexusTask\x12@.temporal.server.api.matchingservice.v1.DispatchNexusTaskRequest\x1aA.temporal.server.api.matchingservice.v1.DispatchNexusTaskResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa3\x01\n" + + "\x12PollNexusTaskQueue\x12A.temporal.server.api.matchingservice.v1.PollNexusTaskQueueRequest\x1aB.temporal.server.api.matchingservice.v1.PollNexusTaskQueueResponse\"\x06\x8a\xb5\x18\x02\b\x02\x12\xb8\x01\n" + + "\x19RespondNexusTaskCompleted\x12H.temporal.server.api.matchingservice.v1.RespondNexusTaskCompletedRequest\x1aI.temporal.server.api.matchingservice.v1.RespondNexusTaskCompletedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xaf\x01\n" + + "\x16RespondNexusTaskFailed\x12E.temporal.server.api.matchingservice.v1.RespondNexusTaskFailedRequest\x1aF.temporal.server.api.matchingservice.v1.RespondNexusTaskFailedResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xac\x01\n" + + "\x15CancelOutstandingPoll\x12D.temporal.server.api.matchingservice.v1.CancelOutstandingPollRequest\x1aE.temporal.server.api.matchingservice.v1.CancelOutstandingPollResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc1\x01\n" + + "\x1cCancelOutstandingWorkerPolls\x12K.temporal.server.api.matchingservice.v1.CancelOutstandingWorkerPollsRequest\x1aL.temporal.server.api.matchingservice.v1.CancelOutstandingWorkerPollsResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa0\x01\n" + + "\x11DescribeTaskQueue\x12@.temporal.server.api.matchingservice.v1.DescribeTaskQueueRequest\x1aA.temporal.server.api.matchingservice.v1.DescribeTaskQueueResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbb\x01\n" + + "\x1aDescribeTaskQueuePartition\x12I.temporal.server.api.matchingservice.v1.DescribeTaskQueuePartitionRequest\x1aJ.temporal.server.api.matchingservice.v1.DescribeTaskQueuePartitionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbe\x01\n" + + "\x1bDescribeVersionedTaskQueues\x12J.temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesRequest\x1aK.temporal.server.api.matchingservice.v1.DescribeVersionedTaskQueuesResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb2\x01\n" + + "\x17ListTaskQueuePartitions\x12F.temporal.server.api.matchingservice.v1.ListTaskQueuePartitionsRequest\x1aG.temporal.server.api.matchingservice.v1.ListTaskQueuePartitionsResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xcd\x01\n" + + " UpdateWorkerBuildIdCompatibility\x12O.temporal.server.api.matchingservice.v1.UpdateWorkerBuildIdCompatibilityRequest\x1aP.temporal.server.api.matchingservice.v1.UpdateWorkerBuildIdCompatibilityResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc4\x01\n" + + "\x1dGetWorkerBuildIdCompatibility\x12L.temporal.server.api.matchingservice.v1.GetWorkerBuildIdCompatibilityRequest\x1aM.temporal.server.api.matchingservice.v1.GetWorkerBuildIdCompatibilityResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa9\x01\n" + + "\x14GetTaskQueueUserData\x12C.temporal.server.api.matchingservice.v1.GetTaskQueueUserDataRequest\x1aD.temporal.server.api.matchingservice.v1.GetTaskQueueUserDataResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbe\x01\n" + + "\x1bUpdateWorkerVersioningRules\x12J.temporal.server.api.matchingservice.v1.UpdateWorkerVersioningRulesRequest\x1aK.temporal.server.api.matchingservice.v1.UpdateWorkerVersioningRulesResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb5\x01\n" + + "\x18GetWorkerVersioningRules\x12G.temporal.server.api.matchingservice.v1.GetWorkerVersioningRulesRequest\x1aH.temporal.server.api.matchingservice.v1.GetWorkerVersioningRulesResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xaf\x01\n" + + "\x16SyncDeploymentUserData\x12E.temporal.server.api.matchingservice.v1.SyncDeploymentUserDataRequest\x1aF.temporal.server.api.matchingservice.v1.SyncDeploymentUserDataResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xdf\x01\n" + + "&ApplyTaskQueueUserDataReplicationEvent\x12U.temporal.server.api.matchingservice.v1.ApplyTaskQueueUserDataReplicationEventRequest\x1aV.temporal.server.api.matchingservice.v1.ApplyTaskQueueUserDataReplicationEventResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbb\x01\n" + + "\x1aGetBuildIdTaskQueueMapping\x12I.temporal.server.api.matchingservice.v1.GetBuildIdTaskQueueMappingRequest\x1aJ.temporal.server.api.matchingservice.v1.GetBuildIdTaskQueueMappingResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbe\x01\n" + + "\x1bForceLoadTaskQueuePartition\x12J.temporal.server.api.matchingservice.v1.ForceLoadTaskQueuePartitionRequest\x1aK.temporal.server.api.matchingservice.v1.ForceLoadTaskQueuePartitionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa9\x01\n" + + "\x14ForceUnloadTaskQueue\x12C.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueueRequest\x1aD.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueueResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xc4\x01\n" + + "\x1dForceUnloadTaskQueuePartition\x12L.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueuePartitionRequest\x1aM.temporal.server.api.matchingservice.v1.ForceUnloadTaskQueuePartitionResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xb2\x01\n" + + "\x17UpdateTaskQueueUserData\x12F.temporal.server.api.matchingservice.v1.UpdateTaskQueueUserDataRequest\x1aG.temporal.server.api.matchingservice.v1.UpdateTaskQueueUserDataResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xbb\x01\n" + + "\x1aReplicateTaskQueueUserData\x12I.temporal.server.api.matchingservice.v1.ReplicateTaskQueueUserDataRequest\x1aJ.temporal.server.api.matchingservice.v1.ReplicateTaskQueueUserDataResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xd0\x01\n" + + "!CheckTaskQueueUserDataPropagation\x12P.temporal.server.api.matchingservice.v1.CheckTaskQueueUserDataPropagationRequest\x1aQ.temporal.server.api.matchingservice.v1.CheckTaskQueueUserDataPropagationResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa6\x01\n" + + "\x13CreateNexusEndpoint\x12B.temporal.server.api.matchingservice.v1.CreateNexusEndpointRequest\x1aC.temporal.server.api.matchingservice.v1.CreateNexusEndpointResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa6\x01\n" + + "\x13UpdateNexusEndpoint\x12B.temporal.server.api.matchingservice.v1.UpdateNexusEndpointRequest\x1aC.temporal.server.api.matchingservice.v1.UpdateNexusEndpointResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa6\x01\n" + + "\x13DeleteNexusEndpoint\x12B.temporal.server.api.matchingservice.v1.DeleteNexusEndpointRequest\x1aC.temporal.server.api.matchingservice.v1.DeleteNexusEndpointResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa3\x01\n" + + "\x12ListNexusEndpoints\x12A.temporal.server.api.matchingservice.v1.ListNexusEndpointsRequest\x1aB.temporal.server.api.matchingservice.v1.ListNexusEndpointsResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xac\x01\n" + + "\x15RecordWorkerHeartbeat\x12D.temporal.server.api.matchingservice.v1.RecordWorkerHeartbeatRequest\x1aE.temporal.server.api.matchingservice.v1.RecordWorkerHeartbeatResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x8e\x01\n" + + "\vListWorkers\x12:.temporal.server.api.matchingservice.v1.ListWorkersRequest\x1a;.temporal.server.api.matchingservice.v1.ListWorkersResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xac\x01\n" + + "\x15UpdateTaskQueueConfig\x12D.temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigRequest\x1aE.temporal.server.api.matchingservice.v1.UpdateTaskQueueConfigResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\x97\x01\n" + + "\x0eDescribeWorker\x12=.temporal.server.api.matchingservice.v1.DescribeWorkerRequest\x1a>.temporal.server.api.matchingservice.v1.DescribeWorkerResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xa6\x01\n" + + "\x13UpdateFairnessState\x12B.temporal.server.api.matchingservice.v1.UpdateFairnessStateRequest\x1aC.temporal.server.api.matchingservice.v1.UpdateFairnessStateResponse\"\x06\x8a\xb5\x18\x02\b\x01\x12\xca\x01\n" + + "\x1fCheckTaskQueueVersionMembership\x12N.temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipRequest\x1aO.temporal.server.api.matchingservice.v1.CheckTaskQueueVersionMembershipResponse\"\x06\x8a\xb5\x18\x02\b\x01B>Z google.protobuf.Timestamp - 27, // 1: temporal.server.api.persistence.v1.ShardInfo.replication_dlq_ack_level:type_name -> temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry - 28, // 2: temporal.server.api.persistence.v1.ShardInfo.queue_states:type_name -> temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry - 45, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration - 45, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration - 45, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration - 44, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp - 44, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp - 45, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration - 44, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp - 44, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp - 46, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType - 47, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason - 45, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 45, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 44, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp - 48, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints - 29, // 19: temporal.server.api.persistence.v1.WorkflowExecutionInfo.search_attributes:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry - 30, // 20: temporal.server.api.persistence.v1.WorkflowExecutionInfo.memo:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry - 49, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories - 2, // 22: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_stats:type_name -> temporal.server.api.persistence.v1.ExecutionStats - 44, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp - 44, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp - 50, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock - 44, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp - 51, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo - 52, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 31, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.update_infos:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry - 53, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 32, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machines_by_type:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry - 54, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup - 53, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 55, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch - 56, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo - 53, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 33, // 40: temporal.server.api.persistence.v1.WorkflowExecutionInfo.children_initialized_post_reset_point:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry - 57, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority - 26, // 42: temporal.server.api.persistence.v1.WorkflowExecutionInfo.pause_info:type_name -> temporal.server.api.persistence.v1.WorkflowPauseInfo - 58, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause - 59, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType - 60, // 45: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState - 61, // 46: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus - 53, // 47: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 44, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp - 34, // 49: temporal.server.api.persistence.v1.WorkflowExecutionState.request_ids:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry - 62, // 50: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType - 63, // 51: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 52: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 35, // 53: temporal.server.api.persistence.v1.TransferTaskInfo.close_execution_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails - 64, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 55: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 56: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 65, // 57: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority - 53, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 6, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo - 66, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 63, // 61: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 62: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 44, // 63: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp - 64, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 65: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 59, // 66: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType - 67, // 67: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType - 44, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 64, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 63, // 70: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 71: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 63, // 72: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType - 44, // 73: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp - 68, // 74: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo - 64, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo - 11, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.worker_commands_task:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask - 69, // 77: temporal.server.api.persistence.v1.WorkerCommandsTask.commands:type_name -> temporal.api.worker.v1.WorkerCommand - 44, // 78: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 79: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp - 45, // 80: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 81: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 45, // 82: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 45, // 83: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration - 45, // 84: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration - 45, // 85: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration - 44, // 86: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp - 70, // 87: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure - 71, // 88: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads - 44, // 89: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp - 72, // 90: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType - 36, // 91: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo - 52, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp - 53, // 93: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 44, // 94: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp - 44, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 73, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment - 74, // 97: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion - 57, // 98: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority - 37, // 99: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo - 44, // 100: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp - 53, // 101: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 75, // 102: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy - 50, // 103: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 53, // 104: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 57, // 105: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority - 53, // 106: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 53, // 107: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 76, // 108: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor - 39, // 109: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus - 40, // 110: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM - 77, // 111: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link - 78, // 112: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent - 20, // 113: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback - 43, // 114: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger - 44, // 115: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp - 79, // 116: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState - 44, // 117: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 70, // 118: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 119: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 45, // 120: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 121: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp - 80, // 122: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState - 44, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 70, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 45, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 45, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration - 44, // 128: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp - 44, // 129: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp - 81, // 130: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState - 44, // 131: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 70, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 44, // 133: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 44, // 134: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 82, // 135: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState - 83, // 136: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload - 83, // 137: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload - 84, // 138: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo - 85, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap - 25, // 140: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo - 4, // 141: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo - 44, // 142: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp - 38, // 143: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual - 41, // 144: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry - 86, // 145: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef - 42, // 146: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed - 147, // [147:147] is the sub-list for method output_type - 147, // [147:147] is the sub-list for method input_type - 147, // [147:147] is the sub-list for extension type_name - 147, // [147:147] is the sub-list for extension extendee - 0, // [0:147] is the sub-list for field type_name + 45, // 0: temporal.server.api.persistence.v1.ShardInfo.update_time:type_name -> google.protobuf.Timestamp + 28, // 1: temporal.server.api.persistence.v1.ShardInfo.replication_dlq_ack_level:type_name -> temporal.server.api.persistence.v1.ShardInfo.ReplicationDlqAckLevelEntry + 29, // 2: temporal.server.api.persistence.v1.ShardInfo.queue_states:type_name -> temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry + 46, // 3: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_timeout:type_name -> google.protobuf.Duration + 46, // 4: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_timeout:type_name -> google.protobuf.Duration + 46, // 5: temporal.server.api.persistence.v1.WorkflowExecutionInfo.default_workflow_task_timeout:type_name -> google.protobuf.Duration + 45, // 6: temporal.server.api.persistence.v1.WorkflowExecutionInfo.start_time:type_name -> google.protobuf.Timestamp + 45, // 7: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_update_time:type_name -> google.protobuf.Timestamp + 46, // 8: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_timeout:type_name -> google.protobuf.Duration + 45, // 9: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_started_time:type_name -> google.protobuf.Timestamp + 45, // 10: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_scheduled_time:type_name -> google.protobuf.Timestamp + 45, // 11: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_original_scheduled_time:type_name -> google.protobuf.Timestamp + 47, // 12: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_type:type_name -> temporal.server.api.enums.v1.WorkflowTaskType + 48, // 13: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_suggest_continue_as_new_reasons:type_name -> temporal.api.enums.v1.SuggestContinueAsNewReason + 46, // 14: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sticky_schedule_to_start_timeout:type_name -> google.protobuf.Duration + 46, // 15: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 46, // 16: temporal.server.api.persistence.v1.WorkflowExecutionInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 45, // 17: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_execution_expiration_time:type_name -> google.protobuf.Timestamp + 49, // 18: temporal.server.api.persistence.v1.WorkflowExecutionInfo.auto_reset_points:type_name -> temporal.api.workflow.v1.ResetPoints + 30, // 19: temporal.server.api.persistence.v1.WorkflowExecutionInfo.search_attributes:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry + 31, // 20: temporal.server.api.persistence.v1.WorkflowExecutionInfo.memo:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry + 50, // 21: temporal.server.api.persistence.v1.WorkflowExecutionInfo.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 3, // 22: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_stats:type_name -> temporal.server.api.persistence.v1.ExecutionStats + 45, // 23: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_run_expiration_time:type_name -> google.protobuf.Timestamp + 45, // 24: temporal.server.api.persistence.v1.WorkflowExecutionInfo.execution_time:type_name -> google.protobuf.Timestamp + 51, // 25: temporal.server.api.persistence.v1.WorkflowExecutionInfo.parent_clock:type_name -> temporal.server.api.clock.v1.VectorClock + 45, // 26: temporal.server.api.persistence.v1.WorkflowExecutionInfo.close_time:type_name -> google.protobuf.Timestamp + 52, // 27: temporal.server.api.persistence.v1.WorkflowExecutionInfo.base_execution_info:type_name -> temporal.server.api.workflow.v1.BaseExecutionInfo + 53, // 28: temporal.server.api.persistence.v1.WorkflowExecutionInfo.most_recent_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 32, // 29: temporal.server.api.persistence.v1.WorkflowExecutionInfo.update_infos:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry + 54, // 30: temporal.server.api.persistence.v1.WorkflowExecutionInfo.transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 33, // 31: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machines_by_type:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry + 55, // 32: temporal.server.api.persistence.v1.WorkflowExecutionInfo.state_machine_timers:type_name -> temporal.server.api.persistence.v1.StateMachineTimerGroup + 54, // 33: temporal.server.api.persistence.v1.WorkflowExecutionInfo.workflow_task_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 54, // 34: temporal.server.api.persistence.v1.WorkflowExecutionInfo.visibility_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 54, // 35: temporal.server.api.persistence.v1.WorkflowExecutionInfo.signal_request_ids_last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 56, // 36: temporal.server.api.persistence.v1.WorkflowExecutionInfo.sub_state_machine_tombstone_batches:type_name -> temporal.server.api.persistence.v1.StateMachineTombstoneBatch + 57, // 37: temporal.server.api.persistence.v1.WorkflowExecutionInfo.versioning_info:type_name -> temporal.api.workflow.v1.WorkflowExecutionVersioningInfo + 54, // 38: temporal.server.api.persistence.v1.WorkflowExecutionInfo.previous_transition_history:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 54, // 39: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_transition_history_break_point:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 34, // 40: temporal.server.api.persistence.v1.WorkflowExecutionInfo.children_initialized_post_reset_point:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry + 58, // 41: temporal.server.api.persistence.v1.WorkflowExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 27, // 42: temporal.server.api.persistence.v1.WorkflowExecutionInfo.pause_info:type_name -> temporal.server.api.persistence.v1.WorkflowPauseInfo + 59, // 43: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_failure_cause:type_name -> temporal.api.enums.v1.WorkflowTaskFailedCause + 60, // 44: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_workflow_task_timed_out_type:type_name -> temporal.api.enums.v1.TimeoutType + 2, // 45: temporal.server.api.persistence.v1.WorkflowExecutionInfo.last_notified_target_version:type_name -> temporal.server.api.persistence.v1.LastNotifiedTargetVersion + 61, // 46: temporal.server.api.persistence.v1.WorkflowExecutionInfo.declined_target_version_upgrade:type_name -> temporal.api.history.v1.DeclinedTargetVersionUpgrade + 62, // 47: temporal.server.api.persistence.v1.LastNotifiedTargetVersion.deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 63, // 48: temporal.server.api.persistence.v1.WorkflowExecutionState.state:type_name -> temporal.server.api.enums.v1.WorkflowExecutionState + 64, // 49: temporal.server.api.persistence.v1.WorkflowExecutionState.status:type_name -> temporal.api.enums.v1.WorkflowExecutionStatus + 54, // 50: temporal.server.api.persistence.v1.WorkflowExecutionState.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 45, // 51: temporal.server.api.persistence.v1.WorkflowExecutionState.start_time:type_name -> google.protobuf.Timestamp + 35, // 52: temporal.server.api.persistence.v1.WorkflowExecutionState.request_ids:type_name -> temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry + 65, // 53: temporal.server.api.persistence.v1.RequestIDInfo.event_type:type_name -> temporal.api.enums.v1.EventType + 66, // 54: temporal.server.api.persistence.v1.TransferTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 45, // 55: temporal.server.api.persistence.v1.TransferTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 36, // 56: temporal.server.api.persistence.v1.TransferTaskInfo.close_execution_task_details:type_name -> temporal.server.api.persistence.v1.TransferTaskInfo.CloseExecutionTaskDetails + 67, // 57: temporal.server.api.persistence.v1.TransferTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 66, // 58: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 45, // 59: temporal.server.api.persistence.v1.ReplicationTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 68, // 60: temporal.server.api.persistence.v1.ReplicationTaskInfo.priority:type_name -> temporal.server.api.enums.v1.TaskPriority + 54, // 61: temporal.server.api.persistence.v1.ReplicationTaskInfo.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 7, // 62: temporal.server.api.persistence.v1.ReplicationTaskInfo.task_equivalents:type_name -> temporal.server.api.persistence.v1.ReplicationTaskInfo + 69, // 63: temporal.server.api.persistence.v1.ReplicationTaskInfo.last_version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 66, // 64: temporal.server.api.persistence.v1.VisibilityTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 45, // 65: temporal.server.api.persistence.v1.VisibilityTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 45, // 66: temporal.server.api.persistence.v1.VisibilityTaskInfo.close_time:type_name -> google.protobuf.Timestamp + 67, // 67: temporal.server.api.persistence.v1.VisibilityTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 66, // 68: temporal.server.api.persistence.v1.TimerTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 60, // 69: temporal.server.api.persistence.v1.TimerTaskInfo.timeout_type:type_name -> temporal.api.enums.v1.TimeoutType + 70, // 70: temporal.server.api.persistence.v1.TimerTaskInfo.workflow_backoff_type:type_name -> temporal.server.api.enums.v1.WorkflowBackoffType + 45, // 71: temporal.server.api.persistence.v1.TimerTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 67, // 72: temporal.server.api.persistence.v1.TimerTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 66, // 73: temporal.server.api.persistence.v1.ArchivalTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 45, // 74: temporal.server.api.persistence.v1.ArchivalTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 66, // 75: temporal.server.api.persistence.v1.OutboundTaskInfo.task_type:type_name -> temporal.server.api.enums.v1.TaskType + 45, // 76: temporal.server.api.persistence.v1.OutboundTaskInfo.visibility_time:type_name -> google.protobuf.Timestamp + 71, // 77: temporal.server.api.persistence.v1.OutboundTaskInfo.state_machine_info:type_name -> temporal.server.api.persistence.v1.StateMachineTaskInfo + 67, // 78: temporal.server.api.persistence.v1.OutboundTaskInfo.chasm_task_info:type_name -> temporal.server.api.persistence.v1.ChasmTaskInfo + 12, // 79: temporal.server.api.persistence.v1.OutboundTaskInfo.worker_commands_task:type_name -> temporal.server.api.persistence.v1.WorkerCommandsTask + 72, // 80: temporal.server.api.persistence.v1.WorkerCommandsTask.commands:type_name -> temporal.api.worker.v1.WorkerCommand + 45, // 81: temporal.server.api.persistence.v1.ActivityInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 45, // 82: temporal.server.api.persistence.v1.ActivityInfo.started_time:type_name -> google.protobuf.Timestamp + 46, // 83: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 46, // 84: temporal.server.api.persistence.v1.ActivityInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 46, // 85: temporal.server.api.persistence.v1.ActivityInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 46, // 86: temporal.server.api.persistence.v1.ActivityInfo.heartbeat_timeout:type_name -> google.protobuf.Duration + 46, // 87: temporal.server.api.persistence.v1.ActivityInfo.retry_initial_interval:type_name -> google.protobuf.Duration + 46, // 88: temporal.server.api.persistence.v1.ActivityInfo.retry_maximum_interval:type_name -> google.protobuf.Duration + 45, // 89: temporal.server.api.persistence.v1.ActivityInfo.retry_expiration_time:type_name -> google.protobuf.Timestamp + 73, // 90: temporal.server.api.persistence.v1.ActivityInfo.retry_last_failure:type_name -> temporal.api.failure.v1.Failure + 74, // 91: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_details:type_name -> temporal.api.common.v1.Payloads + 45, // 92: temporal.server.api.persistence.v1.ActivityInfo.last_heartbeat_update_time:type_name -> google.protobuf.Timestamp + 75, // 93: temporal.server.api.persistence.v1.ActivityInfo.activity_type:type_name -> temporal.api.common.v1.ActivityType + 37, // 94: temporal.server.api.persistence.v1.ActivityInfo.use_workflow_build_id_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.UseWorkflowBuildIdInfo + 53, // 95: temporal.server.api.persistence.v1.ActivityInfo.last_worker_version_stamp:type_name -> temporal.api.common.v1.WorkerVersionStamp + 54, // 96: temporal.server.api.persistence.v1.ActivityInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 45, // 97: temporal.server.api.persistence.v1.ActivityInfo.first_scheduled_time:type_name -> google.protobuf.Timestamp + 45, // 98: temporal.server.api.persistence.v1.ActivityInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 76, // 99: temporal.server.api.persistence.v1.ActivityInfo.last_started_deployment:type_name -> temporal.api.deployment.v1.Deployment + 62, // 100: temporal.server.api.persistence.v1.ActivityInfo.last_deployment_version:type_name -> temporal.api.deployment.v1.WorkerDeploymentVersion + 58, // 101: temporal.server.api.persistence.v1.ActivityInfo.priority:type_name -> temporal.api.common.v1.Priority + 38, // 102: temporal.server.api.persistence.v1.ActivityInfo.pause_info:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo + 45, // 103: temporal.server.api.persistence.v1.TimerInfo.expiry_time:type_name -> google.protobuf.Timestamp + 54, // 104: temporal.server.api.persistence.v1.TimerInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 77, // 105: temporal.server.api.persistence.v1.ChildExecutionInfo.parent_close_policy:type_name -> temporal.api.enums.v1.ParentClosePolicy + 51, // 106: temporal.server.api.persistence.v1.ChildExecutionInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 54, // 107: temporal.server.api.persistence.v1.ChildExecutionInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 58, // 108: temporal.server.api.persistence.v1.ChildExecutionInfo.priority:type_name -> temporal.api.common.v1.Priority + 54, // 109: temporal.server.api.persistence.v1.RequestCancelInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 54, // 110: temporal.server.api.persistence.v1.SignalInfo.last_update_versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 78, // 111: temporal.server.api.persistence.v1.Checksum.flavor:type_name -> temporal.server.api.enums.v1.ChecksumFlavor + 40, // 112: temporal.server.api.persistence.v1.Callback.nexus:type_name -> temporal.server.api.persistence.v1.Callback.Nexus + 41, // 113: temporal.server.api.persistence.v1.Callback.hsm:type_name -> temporal.server.api.persistence.v1.Callback.HSM + 79, // 114: temporal.server.api.persistence.v1.Callback.links:type_name -> temporal.api.common.v1.Link + 80, // 115: temporal.server.api.persistence.v1.HSMCompletionCallbackArg.last_event:type_name -> temporal.api.history.v1.HistoryEvent + 21, // 116: temporal.server.api.persistence.v1.CallbackInfo.callback:type_name -> temporal.server.api.persistence.v1.Callback + 44, // 117: temporal.server.api.persistence.v1.CallbackInfo.trigger:type_name -> temporal.server.api.persistence.v1.CallbackInfo.Trigger + 45, // 118: temporal.server.api.persistence.v1.CallbackInfo.registration_time:type_name -> google.protobuf.Timestamp + 81, // 119: temporal.server.api.persistence.v1.CallbackInfo.state:type_name -> temporal.server.api.enums.v1.CallbackState + 45, // 120: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 73, // 121: temporal.server.api.persistence.v1.CallbackInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 45, // 122: temporal.server.api.persistence.v1.CallbackInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 46, // 123: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 124: temporal.server.api.persistence.v1.NexusOperationInfo.scheduled_time:type_name -> google.protobuf.Timestamp + 82, // 125: temporal.server.api.persistence.v1.NexusOperationInfo.state:type_name -> temporal.server.api.enums.v1.NexusOperationState + 45, // 126: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 73, // 127: temporal.server.api.persistence.v1.NexusOperationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 45, // 128: temporal.server.api.persistence.v1.NexusOperationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 46, // 129: temporal.server.api.persistence.v1.NexusOperationInfo.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 46, // 130: temporal.server.api.persistence.v1.NexusOperationInfo.start_to_close_timeout:type_name -> google.protobuf.Duration + 45, // 131: temporal.server.api.persistence.v1.NexusOperationInfo.started_time:type_name -> google.protobuf.Timestamp + 45, // 132: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.requested_time:type_name -> google.protobuf.Timestamp + 83, // 133: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.state:type_name -> temporal.api.enums.v1.NexusOperationCancellationState + 45, // 134: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 73, // 135: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 45, // 136: temporal.server.api.persistence.v1.NexusOperationCancellationInfo.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 45, // 137: temporal.server.api.persistence.v1.WorkflowPauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 84, // 138: temporal.server.api.persistence.v1.ShardInfo.QueueStatesEntry.value:type_name -> temporal.server.api.persistence.v1.QueueState + 85, // 139: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload + 85, // 140: temporal.server.api.persistence.v1.WorkflowExecutionInfo.MemoEntry.value:type_name -> temporal.api.common.v1.Payload + 86, // 141: temporal.server.api.persistence.v1.WorkflowExecutionInfo.UpdateInfosEntry.value:type_name -> temporal.server.api.persistence.v1.UpdateInfo + 87, // 142: temporal.server.api.persistence.v1.WorkflowExecutionInfo.SubStateMachinesByTypeEntry.value:type_name -> temporal.server.api.persistence.v1.StateMachineMap + 26, // 143: temporal.server.api.persistence.v1.WorkflowExecutionInfo.ChildrenInitializedPostResetPointEntry.value:type_name -> temporal.server.api.persistence.v1.ResetChildInfo + 5, // 144: temporal.server.api.persistence.v1.WorkflowExecutionState.RequestIdsEntry.value:type_name -> temporal.server.api.persistence.v1.RequestIDInfo + 45, // 145: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.pause_time:type_name -> google.protobuf.Timestamp + 39, // 146: temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.manual:type_name -> temporal.server.api.persistence.v1.ActivityInfo.PauseInfo.Manual + 42, // 147: temporal.server.api.persistence.v1.Callback.Nexus.header:type_name -> temporal.server.api.persistence.v1.Callback.Nexus.HeaderEntry + 88, // 148: temporal.server.api.persistence.v1.Callback.HSM.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef + 43, // 149: temporal.server.api.persistence.v1.CallbackInfo.Trigger.workflow_closed:type_name -> temporal.server.api.persistence.v1.CallbackInfo.WorkflowClosed + 150, // [150:150] is the sub-list for method output_type + 150, // [150:150] is the sub-list for method input_type + 150, // [150:150] is the sub-list for extension type_name + 150, // [150:150] is the sub-list for extension extendee + 0, // [0:150] is the sub-list for field type_name } func init() { file_temporal_server_api_persistence_v1_executions_proto_init() } @@ -5369,41 +5459,41 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { return } file_temporal_server_api_persistence_v1_chasm_proto_init() - file_temporal_server_api_persistence_v1_queues_proto_init() file_temporal_server_api_persistence_v1_hsm_proto_init() + file_temporal_server_api_persistence_v1_queues_proto_init() file_temporal_server_api_persistence_v1_update_proto_init() file_temporal_server_api_persistence_v1_executions_proto_msgTypes[1].OneofWrappers = []any{ (*WorkflowExecutionInfo_LastWorkflowTaskFailureCause)(nil), (*WorkflowExecutionInfo_LastWorkflowTaskTimedOutType)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[5].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[6].OneofWrappers = []any{ (*TransferTaskInfo_CloseExecutionTaskDetails_)(nil), (*TransferTaskInfo_ChasmTaskInfo)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[7].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[8].OneofWrappers = []any{ (*VisibilityTaskInfo_ChasmTaskInfo)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[8].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[9].OneofWrappers = []any{ (*TimerTaskInfo_ChasmTaskInfo)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[10].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[11].OneofWrappers = []any{ (*OutboundTaskInfo_StateMachineInfo)(nil), (*OutboundTaskInfo_ChasmTaskInfo)(nil), (*OutboundTaskInfo_WorkerCommandsTask)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[14].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[15].OneofWrappers = []any{ (*ActivityInfo_UseWorkflowBuildIdInfo_)(nil), (*ActivityInfo_LastIndependentlyAssignedBuildId)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[20].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[21].OneofWrappers = []any{ (*Callback_Nexus_)(nil), (*Callback_Hsm)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[37].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[38].OneofWrappers = []any{ (*ActivityInfo_PauseInfo_Manual_)(nil), (*ActivityInfo_PauseInfo_RuleId)(nil), } - file_temporal_server_api_persistence_v1_executions_proto_msgTypes[43].OneofWrappers = []any{ + file_temporal_server_api_persistence_v1_executions_proto_msgTypes[44].OneofWrappers = []any{ (*CallbackInfo_Trigger_WorkflowClosed)(nil), } type x struct{} @@ -5412,7 +5502,7 @@ func file_temporal_server_api_persistence_v1_executions_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_persistence_v1_executions_proto_rawDesc), len(file_temporal_server_api_persistence_v1_executions_proto_rawDesc)), NumEnums: 0, - NumMessages: 44, + NumMessages: 45, NumExtensions: 0, NumServices: 0, }, diff --git a/api/persistence/v1/tasks.go-helpers.pb.go b/api/persistence/v1/tasks.go-helpers.pb.go index cd888c65651..cee6ab32b50 100644 --- a/api/persistence/v1/tasks.go-helpers.pb.go +++ b/api/persistence/v1/tasks.go-helpers.pb.go @@ -153,6 +153,43 @@ func (this *SubqueueInfo) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type FairnessKeyCount to the protobuf v3 wire format +func (val *FairnessKeyCount) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type FairnessKeyCount from the protobuf v3 wire format +func (val *FairnessKeyCount) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *FairnessKeyCount) Size() int { + return proto.Size(val) +} + +// Equal returns whether two FairnessKeyCount values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *FairnessKeyCount) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *FairnessKeyCount + switch t := that.(type) { + case *FairnessKeyCount: + that1 = t + case FairnessKeyCount: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type SubqueueKey to the protobuf v3 wire format func (val *SubqueueKey) Marshal() ([]byte, error) { return proto.Marshal(val) diff --git a/api/persistence/v1/tasks.pb.go b/api/persistence/v1/tasks.pb.go index 593f1940bca..4a4bd498556 100644 --- a/api/persistence/v1/tasks.pb.go +++ b/api/persistence/v1/tasks.pb.go @@ -368,8 +368,11 @@ type SubqueueInfo struct { // Max read level keeps track of the highest task level ever written, but is only // maintained best-effort. Do not trust these values. FairMaxReadLevel *v11.FairLevel `protobuf:"bytes,5,opt,name=fair_max_read_level,json=fairMaxReadLevel,proto3" json:"fair_max_read_level,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // We can persist a limited number of fairness key counts in task queue + // metadata so they're not lost on migration. + TopKFairnessCounts []*FairnessKeyCount `protobuf:"bytes,6,rep,name=top_k_fairness_counts,json=topKFairnessCounts,proto3" json:"top_k_fairness_counts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SubqueueInfo) Reset() { @@ -437,6 +440,65 @@ func (x *SubqueueInfo) GetFairMaxReadLevel() *v11.FairLevel { return nil } +func (x *SubqueueInfo) GetTopKFairnessCounts() []*FairnessKeyCount { + if x != nil { + return x.TopKFairnessCounts + } + return nil +} + +type FairnessKeyCount struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Count int64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FairnessKeyCount) Reset() { + *x = FairnessKeyCount{} + mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FairnessKeyCount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FairnessKeyCount) ProtoMessage() {} + +func (x *FairnessKeyCount) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FairnessKeyCount.ProtoReflect.Descriptor instead. +func (*FairnessKeyCount) Descriptor() ([]byte, []int) { + return file_temporal_server_api_persistence_v1_tasks_proto_rawDescGZIP(), []int{4} +} + +func (x *FairnessKeyCount) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *FairnessKeyCount) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + type SubqueueKey struct { state protoimpl.MessageState `protogen:"open.v1"` // Each subqueue contains tasks from only one priority level. @@ -447,7 +509,7 @@ type SubqueueKey struct { func (x *SubqueueKey) Reset() { *x = SubqueueKey{} - mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[4] + mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -459,7 +521,7 @@ func (x *SubqueueKey) String() string { func (*SubqueueKey) ProtoMessage() {} func (x *SubqueueKey) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[4] + mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -472,7 +534,7 @@ func (x *SubqueueKey) ProtoReflect() protoreflect.Message { // Deprecated: Use SubqueueKey.ProtoReflect.Descriptor instead. func (*SubqueueKey) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_tasks_proto_rawDescGZIP(), []int{4} + return file_temporal_server_api_persistence_v1_tasks_proto_rawDescGZIP(), []int{5} } func (x *SubqueueKey) GetPriority() int32 { @@ -492,7 +554,7 @@ type TaskKey struct { func (x *TaskKey) Reset() { *x = TaskKey{} - mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[5] + mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -504,7 +566,7 @@ func (x *TaskKey) String() string { func (*TaskKey) ProtoMessage() {} func (x *TaskKey) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[5] + mi := &file_temporal_server_api_persistence_v1_tasks_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -517,7 +579,7 @@ func (x *TaskKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TaskKey.ProtoReflect.Descriptor instead. func (*TaskKey) Descriptor() ([]byte, []int) { - return file_temporal_server_api_persistence_v1_tasks_proto_rawDescGZIP(), []int{5} + return file_temporal_server_api_persistence_v1_tasks_proto_rawDescGZIP(), []int{6} } func (x *TaskKey) GetFireTime() *timestamppb.Timestamp { @@ -571,13 +633,17 @@ const file_temporal_server_api_persistence_v1_tasks_proto_rawDesc = "" + "\x19approximate_backlog_count\x18\b \x01(\x03R\x17approximateBacklogCount\x12N\n" + "\tsubqueues\x18\t \x03(\v20.temporal.server.api.persistence.v1.SubqueueInfoR\tsubqueues\x12&\n" + "\x0fother_has_tasks\x18\n" + - " \x01(\bR\rotherHasTasks\"\xd9\x02\n" + + " \x01(\bR\rotherHasTasks\"\xc2\x03\n" + "\fSubqueueInfo\x12A\n" + "\x03key\x18\x01 \x01(\v2/.temporal.server.api.persistence.v1.SubqueueKeyR\x03key\x12\x1b\n" + "\tack_level\x18\x02 \x01(\x03R\backLevel\x12Q\n" + "\x0efair_ack_level\x18\x04 \x01(\v2+.temporal.server.api.taskqueue.v1.FairLevelR\ffairAckLevel\x12:\n" + "\x19approximate_backlog_count\x18\x03 \x01(\x03R\x17approximateBacklogCount\x12Z\n" + - "\x13fair_max_read_level\x18\x05 \x01(\v2+.temporal.server.api.taskqueue.v1.FairLevelR\x10fairMaxReadLevel\")\n" + + "\x13fair_max_read_level\x18\x05 \x01(\v2+.temporal.server.api.taskqueue.v1.FairLevelR\x10fairMaxReadLevel\x12g\n" + + "\x15top_k_fairness_counts\x18\x06 \x03(\v24.temporal.server.api.persistence.v1.FairnessKeyCountR\x12topKFairnessCounts\":\n" + + "\x10FairnessKeyCount\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05count\x18\x02 \x01(\x03R\x05count\")\n" + "\vSubqueueKey\x12\x1a\n" + "\bpriority\x18\x01 \x01(\x05R\bpriority\"[\n" + "\aTaskKey\x127\n" + @@ -596,43 +662,45 @@ func file_temporal_server_api_persistence_v1_tasks_proto_rawDescGZIP() []byte { return file_temporal_server_api_persistence_v1_tasks_proto_rawDescData } -var file_temporal_server_api_persistence_v1_tasks_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_temporal_server_api_persistence_v1_tasks_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_temporal_server_api_persistence_v1_tasks_proto_goTypes = []any{ (*AllocatedTaskInfo)(nil), // 0: temporal.server.api.persistence.v1.AllocatedTaskInfo (*TaskInfo)(nil), // 1: temporal.server.api.persistence.v1.TaskInfo (*TaskQueueInfo)(nil), // 2: temporal.server.api.persistence.v1.TaskQueueInfo (*SubqueueInfo)(nil), // 3: temporal.server.api.persistence.v1.SubqueueInfo - (*SubqueueKey)(nil), // 4: temporal.server.api.persistence.v1.SubqueueKey - (*TaskKey)(nil), // 5: temporal.server.api.persistence.v1.TaskKey - (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp - (*v1.VectorClock)(nil), // 7: temporal.server.api.clock.v1.VectorClock - (*v11.TaskVersionDirective)(nil), // 8: temporal.server.api.taskqueue.v1.TaskVersionDirective - (*v12.Priority)(nil), // 9: temporal.api.common.v1.Priority - (v13.TaskQueueType)(0), // 10: temporal.api.enums.v1.TaskQueueType - (v13.TaskQueueKind)(0), // 11: temporal.api.enums.v1.TaskQueueKind - (*v11.FairLevel)(nil), // 12: temporal.server.api.taskqueue.v1.FairLevel + (*FairnessKeyCount)(nil), // 4: temporal.server.api.persistence.v1.FairnessKeyCount + (*SubqueueKey)(nil), // 5: temporal.server.api.persistence.v1.SubqueueKey + (*TaskKey)(nil), // 6: temporal.server.api.persistence.v1.TaskKey + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*v1.VectorClock)(nil), // 8: temporal.server.api.clock.v1.VectorClock + (*v11.TaskVersionDirective)(nil), // 9: temporal.server.api.taskqueue.v1.TaskVersionDirective + (*v12.Priority)(nil), // 10: temporal.api.common.v1.Priority + (v13.TaskQueueType)(0), // 11: temporal.api.enums.v1.TaskQueueType + (v13.TaskQueueKind)(0), // 12: temporal.api.enums.v1.TaskQueueKind + (*v11.FairLevel)(nil), // 13: temporal.server.api.taskqueue.v1.FairLevel } var file_temporal_server_api_persistence_v1_tasks_proto_depIdxs = []int32{ 1, // 0: temporal.server.api.persistence.v1.AllocatedTaskInfo.data:type_name -> temporal.server.api.persistence.v1.TaskInfo - 6, // 1: temporal.server.api.persistence.v1.TaskInfo.create_time:type_name -> google.protobuf.Timestamp - 6, // 2: temporal.server.api.persistence.v1.TaskInfo.expiry_time:type_name -> google.protobuf.Timestamp - 7, // 3: temporal.server.api.persistence.v1.TaskInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 8, // 4: temporal.server.api.persistence.v1.TaskInfo.version_directive:type_name -> temporal.server.api.taskqueue.v1.TaskVersionDirective - 9, // 5: temporal.server.api.persistence.v1.TaskInfo.priority:type_name -> temporal.api.common.v1.Priority - 10, // 6: temporal.server.api.persistence.v1.TaskQueueInfo.task_type:type_name -> temporal.api.enums.v1.TaskQueueType - 11, // 7: temporal.server.api.persistence.v1.TaskQueueInfo.kind:type_name -> temporal.api.enums.v1.TaskQueueKind - 6, // 8: temporal.server.api.persistence.v1.TaskQueueInfo.expiry_time:type_name -> google.protobuf.Timestamp - 6, // 9: temporal.server.api.persistence.v1.TaskQueueInfo.last_update_time:type_name -> google.protobuf.Timestamp + 7, // 1: temporal.server.api.persistence.v1.TaskInfo.create_time:type_name -> google.protobuf.Timestamp + 7, // 2: temporal.server.api.persistence.v1.TaskInfo.expiry_time:type_name -> google.protobuf.Timestamp + 8, // 3: temporal.server.api.persistence.v1.TaskInfo.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 9, // 4: temporal.server.api.persistence.v1.TaskInfo.version_directive:type_name -> temporal.server.api.taskqueue.v1.TaskVersionDirective + 10, // 5: temporal.server.api.persistence.v1.TaskInfo.priority:type_name -> temporal.api.common.v1.Priority + 11, // 6: temporal.server.api.persistence.v1.TaskQueueInfo.task_type:type_name -> temporal.api.enums.v1.TaskQueueType + 12, // 7: temporal.server.api.persistence.v1.TaskQueueInfo.kind:type_name -> temporal.api.enums.v1.TaskQueueKind + 7, // 8: temporal.server.api.persistence.v1.TaskQueueInfo.expiry_time:type_name -> google.protobuf.Timestamp + 7, // 9: temporal.server.api.persistence.v1.TaskQueueInfo.last_update_time:type_name -> google.protobuf.Timestamp 3, // 10: temporal.server.api.persistence.v1.TaskQueueInfo.subqueues:type_name -> temporal.server.api.persistence.v1.SubqueueInfo - 4, // 11: temporal.server.api.persistence.v1.SubqueueInfo.key:type_name -> temporal.server.api.persistence.v1.SubqueueKey - 12, // 12: temporal.server.api.persistence.v1.SubqueueInfo.fair_ack_level:type_name -> temporal.server.api.taskqueue.v1.FairLevel - 12, // 13: temporal.server.api.persistence.v1.SubqueueInfo.fair_max_read_level:type_name -> temporal.server.api.taskqueue.v1.FairLevel - 6, // 14: temporal.server.api.persistence.v1.TaskKey.fire_time:type_name -> google.protobuf.Timestamp - 15, // [15:15] is the sub-list for method output_type - 15, // [15:15] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 5, // 11: temporal.server.api.persistence.v1.SubqueueInfo.key:type_name -> temporal.server.api.persistence.v1.SubqueueKey + 13, // 12: temporal.server.api.persistence.v1.SubqueueInfo.fair_ack_level:type_name -> temporal.server.api.taskqueue.v1.FairLevel + 13, // 13: temporal.server.api.persistence.v1.SubqueueInfo.fair_max_read_level:type_name -> temporal.server.api.taskqueue.v1.FairLevel + 4, // 14: temporal.server.api.persistence.v1.SubqueueInfo.top_k_fairness_counts:type_name -> temporal.server.api.persistence.v1.FairnessKeyCount + 7, // 15: temporal.server.api.persistence.v1.TaskKey.fire_time:type_name -> google.protobuf.Timestamp + 16, // [16:16] is the sub-list for method output_type + 16, // [16:16] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_temporal_server_api_persistence_v1_tasks_proto_init() } @@ -646,7 +714,7 @@ func file_temporal_server_api_persistence_v1_tasks_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_persistence_v1_tasks_proto_rawDesc), len(file_temporal_server_api_persistence_v1_tasks_proto_rawDesc)), NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/api/persistence/v1/workflow_mutable_state.pb.go b/api/persistence/v1/workflow_mutable_state.pb.go index 85f96a97ceb..f099d8fb536 100644 --- a/api/persistence/v1/workflow_mutable_state.pb.go +++ b/api/persistence/v1/workflow_mutable_state.pb.go @@ -364,7 +364,7 @@ var File_temporal_server_api_persistence_v1_workflow_mutable_state_proto protore const file_temporal_server_api_persistence_v1_workflow_mutable_state_proto_rawDesc = "" + "\n" + - "?temporal/server/api/persistence/v1/workflow_mutable_state.proto\x12\"temporal.server.api.persistence.v1\x1a%temporal/api/history/v1/message.proto\x1a3temporal/server/api/persistence/v1/executions.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a.temporal/server/api/persistence/v1/chasm.proto\x1a/temporal/server/api/persistence/v1/update.proto\"\xd0\x0e\n" + + "?temporal/server/api/persistence/v1/workflow_mutable_state.proto\x12\"temporal.server.api.persistence.v1\x1a%temporal/api/history/v1/message.proto\x1a.temporal/server/api/persistence/v1/chasm.proto\x1a3temporal/server/api/persistence/v1/executions.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a/temporal/server/api/persistence/v1/update.proto\"\xd0\x0e\n" + "\x14WorkflowMutableState\x12r\n" + "\x0eactivity_infos\x18\x01 \x03(\v2K.temporal.server.api.persistence.v1.WorkflowMutableState.ActivityInfosEntryR\ractivityInfos\x12i\n" + "\vtimer_infos\x18\x02 \x03(\v2H.temporal.server.api.persistence.v1.WorkflowMutableState.TimerInfosEntryR\n" + @@ -536,9 +536,9 @@ func file_temporal_server_api_persistence_v1_workflow_mutable_state_proto_init() if File_temporal_server_api_persistence_v1_workflow_mutable_state_proto != nil { return } + file_temporal_server_api_persistence_v1_chasm_proto_init() file_temporal_server_api_persistence_v1_executions_proto_init() file_temporal_server_api_persistence_v1_hsm_proto_init() - file_temporal_server_api_persistence_v1_chasm_proto_init() file_temporal_server_api_persistence_v1_update_proto_init() type x struct{} out := protoimpl.TypeBuilder{ diff --git a/api/replication/v1/message.pb.go b/api/replication/v1/message.pb.go index a61557d751f..1fdcca398b9 100644 --- a/api/replication/v1/message.pb.go +++ b/api/replication/v1/message.pb.go @@ -2117,7 +2117,7 @@ var File_temporal_server_api_replication_v1_message_proto protoreflect.FileDescr const file_temporal_server_api_replication_v1_message_proto_rawDesc = "" + "\n" + - "0temporal/server/api/replication/v1/message.proto\x12\"temporal.server.api.replication.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a.temporal/server/api/enums/v1/replication.proto\x1a'temporal/server/api/enums/v1/task.proto\x1a,temporal/server/api/history/v1/message.proto\x1a3temporal/server/api/persistence/v1/executions.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a4temporal/server/api/persistence/v1/task_queues.proto\x1a?temporal/server/api/persistence/v1/workflow_mutable_state.proto\x1a$temporal/api/common/v1/message.proto\x1a'temporal/api/namespace/v1/message.proto\x1a)temporal/api/replication/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a-temporal/server/api/workflow/v1/message.proto\"\xa1\x0f\n" + + "0temporal/server/api/replication/v1/message.proto\x12\"temporal.server.api.replication.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a'temporal/api/namespace/v1/message.proto\x1a)temporal/api/replication/v1/message.proto\x1a.temporal/server/api/enums/v1/replication.proto\x1a'temporal/server/api/enums/v1/task.proto\x1a,temporal/server/api/history/v1/message.proto\x1a3temporal/server/api/persistence/v1/executions.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\x1a4temporal/server/api/persistence/v1/task_queues.proto\x1a?temporal/server/api/persistence/v1/workflow_mutable_state.proto\x1a-temporal/server/api/workflow/v1/message.proto\"\xa1\x0f\n" + "\x0fReplicationTask\x12N\n" + "\ttask_type\x18\x01 \x01(\x0e21.temporal.server.api.enums.v1.ReplicationTaskTypeR\btaskType\x12$\n" + "\x0esource_task_id\x18\x02 \x01(\x03R\fsourceTaskId\x12y\n" + diff --git a/api/schedule/v1/message.pb.go b/api/schedule/v1/message.pb.go index f1f890adbdc..513c6c1d61d 100644 --- a/api/schedule/v1/message.pb.go +++ b/api/schedule/v1/message.pb.go @@ -65,7 +65,11 @@ type BufferedStart struct { StartTime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` // Populated when the workflow execution completes. Presence indicates the // action is complete and retained for history. Only used by the CHASM scheduler. - Completed *CompletedResult `protobuf:"bytes,12,opt,name=completed,proto3" json:"completed,omitempty"` + Completed *CompletedResult `protobuf:"bytes,12,opt,name=completed,proto3" json:"completed,omitempty"` + // True when a running BufferedStart is known to have a Nexus callback + // attached. False when a BufferedStart originated from a migrated V1 + // workflow. Only used by CHASM scheduler, for migration from V1. + HasCallback bool `protobuf:"varint,13,opt,name=has_callback,json=hasCallback,proto3" json:"has_callback,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -184,6 +188,13 @@ func (x *BufferedStart) GetCompleted() *CompletedResult { return nil } +func (x *BufferedStart) GetHasCallback() bool { + if x != nil { + return x.HasCallback + } + return false +} + // Result when a workflow execution has completed. // Only used by the CHASM scheduler. type CompletedResult struct { @@ -252,10 +263,11 @@ type InternalState struct { LastCompletionResult *v12.Payloads `protobuf:"bytes,5,opt,name=last_completion_result,json=lastCompletionResult,proto3" json:"last_completion_result,omitempty"` ContinuedFailure *v13.Failure `protobuf:"bytes,6,opt,name=continued_failure,json=continuedFailure,proto3" json:"continued_failure,omitempty"` // conflict token is implemented as simple sequence number - ConflictToken int64 `protobuf:"varint,7,opt,name=conflict_token,json=conflictToken,proto3" json:"conflict_token,omitempty"` - NeedRefresh bool `protobuf:"varint,9,opt,name=need_refresh,json=needRefresh,proto3" json:"need_refresh,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ConflictToken int64 `protobuf:"varint,7,opt,name=conflict_token,json=conflictToken,proto3" json:"conflict_token,omitempty"` + NeedRefresh bool `protobuf:"varint,9,opt,name=need_refresh,json=needRefresh,proto3" json:"need_refresh,omitempty"` + PendingMigration bool `protobuf:"varint,11,opt,name=pending_migration,json=pendingMigration,proto3" json:"pending_migration,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *InternalState) Reset() { @@ -358,6 +370,13 @@ func (x *InternalState) GetNeedRefresh() bool { return false } +func (x *InternalState) GetPendingMigration() bool { + if x != nil { + return x.PendingMigration + } + return false +} + type StartScheduleArgs struct { state protoimpl.MessageState `protogen:"open.v1"` Schedule *v11.Schedule `protobuf:"bytes,1,opt,name=schedule,proto3" json:"schedule,omitempty"` @@ -1035,7 +1054,7 @@ var File_temporal_server_api_schedule_v1_message_proto protoreflect.FileDescript const file_temporal_server_api_schedule_v1_message_proto_rawDesc = "" + "\n" + - "-temporal/server/api/schedule/v1/message.proto\x12\x1ftemporal.server.api.schedule.v1\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/schedule.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a&temporal/api/schedule/v1/message.proto\x1a6temporal/api/workflowservice/v1/request_response.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf2\x04\n" + + "-temporal/server/api/schedule/v1/message.proto\x12\x1ftemporal.server.api.schedule.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/schedule.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a&temporal/api/schedule/v1/message.proto\x1a6temporal/api/workflowservice/v1/request_response.proto\"\x95\x05\n" + "\rBufferedStart\x12=\n" + "\fnominal_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\vnominalTime\x12;\n" + "\vactual_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + @@ -1053,11 +1072,12 @@ const file_temporal_server_api_schedule_v1_message_proto_rawDesc = "" + " \x01(\tR\x05runId\x129\n" + "\n" + "start_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tstartTime\x12N\n" + - "\tcompleted\x18\f \x01(\v20.temporal.server.api.schedule.v1.CompletedResultR\tcompleted\"\x94\x01\n" + + "\tcompleted\x18\f \x01(\v20.temporal.server.api.schedule.v1.CompletedResultR\tcompleted\x12!\n" + + "\fhas_callback\x18\r \x01(\bR\vhasCallback\"\x94\x01\n" + "\x0fCompletedResult\x12F\n" + "\x06status\x18\x01 \x01(\x0e2..temporal.api.enums.v1.WorkflowExecutionStatusR\x06status\x129\n" + "\n" + - "close_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcloseTime\"\xdf\x04\n" + + "close_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcloseTime\"\x8c\x05\n" + "\rInternalState\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12!\n" + "\fnamespace_id\x18\x02 \x01(\tR\vnamespaceId\x12\x1f\n" + @@ -1070,7 +1090,8 @@ const file_temporal_server_api_schedule_v1_message_proto_rawDesc = "" + "\x16last_completion_result\x18\x05 \x01(\v2 .temporal.api.common.v1.PayloadsR\x14lastCompletionResult\x12M\n" + "\x11continued_failure\x18\x06 \x01(\v2 .temporal.api.failure.v1.FailureR\x10continuedFailure\x12%\n" + "\x0econflict_token\x18\a \x01(\x03R\rconflictToken\x12!\n" + - "\fneed_refresh\x18\t \x01(\bR\vneedRefresh\"\xa3\x02\n" + + "\fneed_refresh\x18\t \x01(\bR\vneedRefresh\x12+\n" + + "\x11pending_migration\x18\v \x01(\bR\x10pendingMigration\"\xa3\x02\n" + "\x11StartScheduleArgs\x12>\n" + "\bschedule\x18\x01 \x01(\v2\".temporal.api.schedule.v1.ScheduleR\bschedule\x12:\n" + "\x04info\x18\x02 \x01(\v2&.temporal.api.schedule.v1.ScheduleInfoR\x04info\x12L\n" + diff --git a/api/taskqueue/v1/message.pb.go b/api/taskqueue/v1/message.pb.go index 3325246e5e8..384c9be1a09 100644 --- a/api/taskqueue/v1/message.pb.go +++ b/api/taskqueue/v1/message.pb.go @@ -19,6 +19,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) const ( @@ -223,10 +224,16 @@ type InternalTaskQueueStatus struct { ApproximateBacklogCount int64 `protobuf:"varint,5,opt,name=approximate_backlog_count,json=approximateBacklogCount,proto3" json:"approximate_backlog_count,omitempty"` MaxReadLevel int64 `protobuf:"varint,6,opt,name=max_read_level,json=maxReadLevel,proto3" json:"max_read_level,omitempty"` FairMaxReadLevel *FairLevel `protobuf:"bytes,9,opt,name=fair_max_read_level,json=fairMaxReadLevel,proto3" json:"fair_max_read_level,omitempty"` - // Draining means that this status is from a draining queue. - Draining bool `protobuf:"varint,10,opt,name=draining,proto3" json:"draining,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Draining means that this status is from a queue that is being drained to + // migrate from v1 to v2 tasks persistence (or backwards). + Draining bool `protobuf:"varint,10,opt,name=draining,proto3" json:"draining,omitempty"` + // BacklogDrained means this queue has an empty backlog at the time this status + // was generated. This is inherently racy — new tasks may arrive after this + // check. Consumers must use version-based validation (see scaleManager) to + // ensure correctness. + BacklogDrained bool `protobuf:"varint,11,opt,name=backlog_drained,json=backlogDrained,proto3" json:"backlog_drained,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *InternalTaskQueueStatus) Reset() { @@ -329,6 +336,13 @@ func (x *InternalTaskQueueStatus) GetDraining() bool { return false } +func (x *InternalTaskQueueStatus) GetBacklogDrained() bool { + if x != nil { + return x.BacklogDrained + } + return false +} + type TaskQueueVersionInfoInternal struct { state protoimpl.MessageState `protogen:"open.v1"` PhysicalTaskQueueInfo *PhysicalTaskQueueInfo `protobuf:"bytes,2,opt,name=physical_task_queue_info,json=physicalTaskQueueInfo,proto3" json:"physical_task_queue_info,omitempty"` @@ -603,6 +617,13 @@ type TaskForwardInfo struct { // In case of multiple hops, this is the source partition of the last hop. SourcePartition string `protobuf:"bytes,1,opt,name=source_partition,json=sourcePartition,proto3" json:"source_partition,omitempty"` TaskSource v14.TaskSource `protobuf:"varint,2,opt,name=task_source,json=taskSource,proto3,enum=temporal.server.api.enums.v1.TaskSource" json:"task_source,omitempty"` + // The partition where the task was initially forwarded from. + // Unlike source_partition which gets overwritten at each hop, origin_partition + // persists across all forwarding hops. + OriginPartition string `protobuf:"bytes,6,opt,name=origin_partition,json=originPartition,proto3" json:"origin_partition,omitempty"` + // For tasks that are forwarded, we should keep the original creation time that comes from the + // source partition. Used for dispatch latency metrics. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` // Redirect info is not present for Query and Nexus tasks. Versioning decisions for activity/workflow // tasks are made at the source partition and sent to the parent partition in this message so that parent partition // does not have to make versioning decision again. For Query/Nexus tasks, this works differently as the child's @@ -663,6 +684,20 @@ func (x *TaskForwardInfo) GetTaskSource() v14.TaskSource { return v14.TaskSource(0) } +func (x *TaskForwardInfo) GetOriginPartition() string { + if x != nil { + return x.OriginPartition + } + return "" +} + +func (x *TaskForwardInfo) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + func (x *TaskForwardInfo) GetRedirectInfo() *BuildIdRedirectInfo { if x != nil { return x.RedirectInfo @@ -896,7 +931,7 @@ var File_temporal_server_api_taskqueue_v1_message_proto protoreflect.FileDescrip const file_temporal_server_api_taskqueue_v1_message_proto_rawDesc = "" + "\n" + - ".temporal/server/api/taskqueue/v1/message.proto\x12 temporal.server.api.taskqueue.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a(temporal/api/deployment/v1/message.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a'temporal/api/taskqueue/v1/message.proto\x1a'temporal/server/api/enums/v1/task.proto\x1a/temporal/server/api/deployment/v1/message.proto\"\xbf\x03\n" + + ".temporal/server/api/taskqueue/v1/message.proto\x12 temporal.server.api.taskqueue.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a(temporal/api/deployment/v1/message.proto\x1a&temporal/api/enums/v1/task_queue.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a'temporal/api/taskqueue/v1/message.proto\x1a/temporal/server/api/deployment/v1/message.proto\x1a'temporal/server/api/enums/v1/task.proto\"\xbf\x03\n" + "\x14TaskVersionDirective\x12J\n" + "\x14use_assignment_rules\x18\x01 \x01(\v2\x16.google.protobuf.EmptyH\x00R\x12useAssignmentRules\x12,\n" + "\x11assigned_build_id\x18\x02 \x01(\tH\x00R\x0fassignedBuildId\x12E\n" + @@ -910,7 +945,7 @@ const file_temporal_server_api_taskqueue_v1_message_proto_rawDesc = "" + "\bbuild_id\"A\n" + "\tFairLevel\x12\x1b\n" + "\ttask_pass\x18\x01 \x01(\x03R\btaskPass\x12\x17\n" + - "\atask_id\x18\x02 \x01(\x03R\x06taskId\"\xc6\x04\n" + + "\atask_id\x18\x02 \x01(\x03R\x06taskId\"\xef\x04\n" + "\x17InternalTaskQueueStatus\x12\x1d\n" + "\n" + "read_level\x18\x01 \x01(\x03R\treadLevel\x12S\n" + @@ -923,7 +958,8 @@ const file_temporal_server_api_taskqueue_v1_message_proto_rawDesc = "" + "\x0emax_read_level\x18\x06 \x01(\x03R\fmaxReadLevel\x12Z\n" + "\x13fair_max_read_level\x18\t \x01(\v2+.temporal.server.api.taskqueue.v1.FairLevelR\x10fairMaxReadLevel\x12\x1a\n" + "\bdraining\x18\n" + - " \x01(\bR\bdraining\"\x90\x01\n" + + " \x01(\bR\bdraining\x12'\n" + + "\x0fbacklog_drained\x18\v \x01(\bR\x0ebacklogDrained\"\x90\x01\n" + "\x1cTaskQueueVersionInfoInternal\x12p\n" + "\x18physical_task_queue_info\x18\x02 \x01(\v27.temporal.server.api.taskqueue.v1.PhysicalTaskQueueInfoR\x15physicalTaskQueueInfo\"\xc2\x04\n" + "\x15PhysicalTaskQueueInfo\x12?\n" + @@ -943,11 +979,14 @@ const file_temporal_server_api_taskqueue_v1_message_proto_rawDesc = "" + "stickyNameB\x0e\n" + "\fpartition_id\"A\n" + "\x13BuildIdRedirectInfo\x12*\n" + - "\x11assigned_build_id\x18\x01 \x01(\tR\x0fassignedBuildId\"\xc1\x02\n" + + "\x11assigned_build_id\x18\x01 \x01(\tR\x0fassignedBuildId\"\xa9\x03\n" + "\x0fTaskForwardInfo\x12)\n" + "\x10source_partition\x18\x01 \x01(\tR\x0fsourcePartition\x12I\n" + "\vtask_source\x18\x02 \x01(\x0e2(.temporal.server.api.enums.v1.TaskSourceR\n" + - "taskSource\x12Z\n" + + "taskSource\x12)\n" + + "\x10origin_partition\x18\x06 \x01(\tR\x0foriginPartition\x12;\n" + + "\vcreate_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "createTime\x12Z\n" + "\rredirect_info\x18\x03 \x01(\v25.temporal.server.api.taskqueue.v1.BuildIdRedirectInfoR\fredirectInfo\x12*\n" + "\x11dispatch_build_id\x18\x04 \x01(\tR\x0fdispatchBuildId\x120\n" + "\x14dispatch_version_set\x18\x05 \x01(\tR\x12dispatchVersionSet\"\x89\x03\n" + @@ -999,6 +1038,7 @@ var file_temporal_server_api_taskqueue_v1_message_proto_goTypes = []any{ (*v13.TaskQueueStats)(nil), // 19: temporal.api.taskqueue.v1.TaskQueueStats (v1.TaskQueueType)(0), // 20: temporal.api.enums.v1.TaskQueueType (v14.TaskSource)(0), // 21: temporal.server.api.enums.v1.TaskSource + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp } var file_temporal_server_api_taskqueue_v1_message_proto_depIdxs = []int32{ 13, // 0: temporal.server.api.taskqueue.v1.TaskVersionDirective.use_assignment_rules:type_name -> google.protobuf.Empty @@ -1016,17 +1056,18 @@ var file_temporal_server_api_taskqueue_v1_message_proto_depIdxs = []int32{ 10, // 12: temporal.server.api.taskqueue.v1.PhysicalTaskQueueInfo.task_queue_stats_by_priority_key:type_name -> temporal.server.api.taskqueue.v1.PhysicalTaskQueueInfo.TaskQueueStatsByPriorityKeyEntry 20, // 13: temporal.server.api.taskqueue.v1.TaskQueuePartition.task_queue_type:type_name -> temporal.api.enums.v1.TaskQueueType 21, // 14: temporal.server.api.taskqueue.v1.TaskForwardInfo.task_source:type_name -> temporal.server.api.enums.v1.TaskSource - 6, // 15: temporal.server.api.taskqueue.v1.TaskForwardInfo.redirect_info:type_name -> temporal.server.api.taskqueue.v1.BuildIdRedirectInfo - 12, // 16: temporal.server.api.taskqueue.v1.EphemeralData.partition:type_name -> temporal.server.api.taskqueue.v1.EphemeralData.ByPartition - 8, // 17: temporal.server.api.taskqueue.v1.VersionedEphemeralData.data:type_name -> temporal.server.api.taskqueue.v1.EphemeralData - 19, // 18: temporal.server.api.taskqueue.v1.PhysicalTaskQueueInfo.TaskQueueStatsByPriorityKeyEntry.value:type_name -> temporal.api.taskqueue.v1.TaskQueueStats - 16, // 19: temporal.server.api.taskqueue.v1.EphemeralData.ByVersion.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion - 11, // 20: temporal.server.api.taskqueue.v1.EphemeralData.ByPartition.version:type_name -> temporal.server.api.taskqueue.v1.EphemeralData.ByVersion - 21, // [21:21] is the sub-list for method output_type - 21, // [21:21] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 22, // 15: temporal.server.api.taskqueue.v1.TaskForwardInfo.create_time:type_name -> google.protobuf.Timestamp + 6, // 16: temporal.server.api.taskqueue.v1.TaskForwardInfo.redirect_info:type_name -> temporal.server.api.taskqueue.v1.BuildIdRedirectInfo + 12, // 17: temporal.server.api.taskqueue.v1.EphemeralData.partition:type_name -> temporal.server.api.taskqueue.v1.EphemeralData.ByPartition + 8, // 18: temporal.server.api.taskqueue.v1.VersionedEphemeralData.data:type_name -> temporal.server.api.taskqueue.v1.EphemeralData + 19, // 19: temporal.server.api.taskqueue.v1.PhysicalTaskQueueInfo.TaskQueueStatsByPriorityKeyEntry.value:type_name -> temporal.api.taskqueue.v1.TaskQueueStats + 16, // 20: temporal.server.api.taskqueue.v1.EphemeralData.ByVersion.version:type_name -> temporal.server.api.deployment.v1.WorkerDeploymentVersion + 11, // 21: temporal.server.api.taskqueue.v1.EphemeralData.ByPartition.version:type_name -> temporal.server.api.taskqueue.v1.EphemeralData.ByVersion + 22, // [22:22] is the sub-list for method output_type + 22, // [22:22] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_temporal_server_api_taskqueue_v1_message_proto_init() } diff --git a/api/token/v1/message.pb.go b/api/token/v1/message.pb.go index 4751af251bc..0dbc4627b5b 100644 --- a/api/token/v1/message.pb.go +++ b/api/token/v1/message.pb.go @@ -27,18 +27,17 @@ const ( ) type HistoryContinuation struct { - state protoimpl.MessageState `protogen:"open.v1"` - RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` - FirstEventId int64 `protobuf:"varint,2,opt,name=first_event_id,json=firstEventId,proto3" json:"first_event_id,omitempty"` - NextEventId int64 `protobuf:"varint,3,opt,name=next_event_id,json=nextEventId,proto3" json:"next_event_id,omitempty"` - IsWorkflowRunning bool `protobuf:"varint,5,opt,name=is_workflow_running,json=isWorkflowRunning,proto3" json:"is_workflow_running,omitempty"` - PersistenceToken []byte `protobuf:"bytes,6,opt,name=persistence_token,json=persistenceToken,proto3" json:"persistence_token,omitempty"` - TransientWorkflowTask *v1.TransientWorkflowTaskInfo `protobuf:"bytes,7,opt,name=transient_workflow_task,json=transientWorkflowTask,proto3" json:"transient_workflow_task,omitempty"` - BranchToken []byte `protobuf:"bytes,8,opt,name=branch_token,json=branchToken,proto3" json:"branch_token,omitempty"` - VersionHistoryItem *v1.VersionHistoryItem `protobuf:"bytes,10,opt,name=version_history_item,json=versionHistoryItem,proto3" json:"version_history_item,omitempty"` - VersionedTransition *v11.VersionedTransition `protobuf:"bytes,11,opt,name=versioned_transition,json=versionedTransition,proto3" json:"versioned_transition,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + FirstEventId int64 `protobuf:"varint,2,opt,name=first_event_id,json=firstEventId,proto3" json:"first_event_id,omitempty"` + NextEventId int64 `protobuf:"varint,3,opt,name=next_event_id,json=nextEventId,proto3" json:"next_event_id,omitempty"` + IsWorkflowRunning bool `protobuf:"varint,5,opt,name=is_workflow_running,json=isWorkflowRunning,proto3" json:"is_workflow_running,omitempty"` + PersistenceToken []byte `protobuf:"bytes,6,opt,name=persistence_token,json=persistenceToken,proto3" json:"persistence_token,omitempty"` + BranchToken []byte `protobuf:"bytes,8,opt,name=branch_token,json=branchToken,proto3" json:"branch_token,omitempty"` + VersionHistoryItem *v1.VersionHistoryItem `protobuf:"bytes,10,opt,name=version_history_item,json=versionHistoryItem,proto3" json:"version_history_item,omitempty"` + VersionedTransition *v11.VersionedTransition `protobuf:"bytes,11,opt,name=versioned_transition,json=versionedTransition,proto3" json:"versioned_transition,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HistoryContinuation) Reset() { @@ -106,13 +105,6 @@ func (x *HistoryContinuation) GetPersistenceToken() []byte { return nil } -func (x *HistoryContinuation) GetTransientWorkflowTask() *v1.TransientWorkflowTaskInfo { - if x != nil { - return x.TransientWorkflowTask - } - return nil -} - func (x *HistoryContinuation) GetBranchToken() []byte { if x != nil { return x.BranchToken @@ -663,18 +655,17 @@ var File_temporal_server_api_token_v1_message_proto protoreflect.FileDescriptor const file_temporal_server_api_token_v1_message_proto_rawDesc = "" + "\n" + - "*temporal/server/api/token/v1/message.proto\x12\x1ctemporal.server.api.token.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a*temporal/server/api/clock/v1/message.proto\x1a,temporal/server/api/history/v1/message.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\"\xc1\x04\n" + + "*temporal/server/api/token/v1/message.proto\x12\x1ctemporal.server.api.token.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a*temporal/server/api/clock/v1/message.proto\x1a,temporal/server/api/history/v1/message.proto\x1a,temporal/server/api/persistence/v1/hsm.proto\"\xd4\x03\n" + "\x13HistoryContinuation\x12\x15\n" + "\x06run_id\x18\x01 \x01(\tR\x05runId\x12$\n" + "\x0efirst_event_id\x18\x02 \x01(\x03R\ffirstEventId\x12\"\n" + "\rnext_event_id\x18\x03 \x01(\x03R\vnextEventId\x12.\n" + "\x13is_workflow_running\x18\x05 \x01(\bR\x11isWorkflowRunning\x12+\n" + - "\x11persistence_token\x18\x06 \x01(\fR\x10persistenceToken\x12q\n" + - "\x17transient_workflow_task\x18\a \x01(\v29.temporal.server.api.history.v1.TransientWorkflowTaskInfoR\x15transientWorkflowTask\x12!\n" + + "\x11persistence_token\x18\x06 \x01(\fR\x10persistenceToken\x12!\n" + "\fbranch_token\x18\b \x01(\fR\vbranchToken\x12d\n" + "\x14version_history_item\x18\n" + " \x01(\v22.temporal.server.api.history.v1.VersionHistoryItemR\x12versionHistoryItem\x12j\n" + - "\x14versioned_transition\x18\v \x01(\v27.temporal.server.api.persistence.v1.VersionedTransitionR\x13versionedTransitionJ\x04\b\t\x10\n" + + "\x14versioned_transition\x18\v \x01(\v27.temporal.server.api.persistence.v1.VersionedTransitionR\x13versionedTransitionJ\x04\b\a\x10\bJ\x04\b\t\x10\n" + "\"\xa9\x03\n" + "\x16RawHistoryContinuation\x12!\n" + "\fnamespace_id\x18\n" + @@ -744,34 +735,32 @@ func file_temporal_server_api_token_v1_message_proto_rawDescGZIP() []byte { var file_temporal_server_api_token_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_temporal_server_api_token_v1_message_proto_goTypes = []any{ - (*HistoryContinuation)(nil), // 0: temporal.server.api.token.v1.HistoryContinuation - (*RawHistoryContinuation)(nil), // 1: temporal.server.api.token.v1.RawHistoryContinuation - (*Task)(nil), // 2: temporal.server.api.token.v1.Task - (*QueryTask)(nil), // 3: temporal.server.api.token.v1.QueryTask - (*NexusTask)(nil), // 4: temporal.server.api.token.v1.NexusTask - (*HistoryEventRef)(nil), // 5: temporal.server.api.token.v1.HistoryEventRef - (*NexusOperationCompletion)(nil), // 6: temporal.server.api.token.v1.NexusOperationCompletion - (*v1.TransientWorkflowTaskInfo)(nil), // 7: temporal.server.api.history.v1.TransientWorkflowTaskInfo - (*v1.VersionHistoryItem)(nil), // 8: temporal.server.api.history.v1.VersionHistoryItem - (*v11.VersionedTransition)(nil), // 9: temporal.server.api.persistence.v1.VersionedTransition - (*v1.VersionHistories)(nil), // 10: temporal.server.api.history.v1.VersionHistories - (*v12.VectorClock)(nil), // 11: temporal.server.api.clock.v1.VectorClock - (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp - (*v11.StateMachineRef)(nil), // 13: temporal.server.api.persistence.v1.StateMachineRef + (*HistoryContinuation)(nil), // 0: temporal.server.api.token.v1.HistoryContinuation + (*RawHistoryContinuation)(nil), // 1: temporal.server.api.token.v1.RawHistoryContinuation + (*Task)(nil), // 2: temporal.server.api.token.v1.Task + (*QueryTask)(nil), // 3: temporal.server.api.token.v1.QueryTask + (*NexusTask)(nil), // 4: temporal.server.api.token.v1.NexusTask + (*HistoryEventRef)(nil), // 5: temporal.server.api.token.v1.HistoryEventRef + (*NexusOperationCompletion)(nil), // 6: temporal.server.api.token.v1.NexusOperationCompletion + (*v1.VersionHistoryItem)(nil), // 7: temporal.server.api.history.v1.VersionHistoryItem + (*v11.VersionedTransition)(nil), // 8: temporal.server.api.persistence.v1.VersionedTransition + (*v1.VersionHistories)(nil), // 9: temporal.server.api.history.v1.VersionHistories + (*v12.VectorClock)(nil), // 10: temporal.server.api.clock.v1.VectorClock + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*v11.StateMachineRef)(nil), // 12: temporal.server.api.persistence.v1.StateMachineRef } var file_temporal_server_api_token_v1_message_proto_depIdxs = []int32{ - 7, // 0: temporal.server.api.token.v1.HistoryContinuation.transient_workflow_task:type_name -> temporal.server.api.history.v1.TransientWorkflowTaskInfo - 8, // 1: temporal.server.api.token.v1.HistoryContinuation.version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem - 9, // 2: temporal.server.api.token.v1.HistoryContinuation.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition - 10, // 3: temporal.server.api.token.v1.RawHistoryContinuation.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories - 11, // 4: temporal.server.api.token.v1.Task.clock:type_name -> temporal.server.api.clock.v1.VectorClock - 12, // 5: temporal.server.api.token.v1.Task.started_time:type_name -> google.protobuf.Timestamp - 13, // 6: temporal.server.api.token.v1.NexusOperationCompletion.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 7, // 0: temporal.server.api.token.v1.HistoryContinuation.version_history_item:type_name -> temporal.server.api.history.v1.VersionHistoryItem + 8, // 1: temporal.server.api.token.v1.HistoryContinuation.versioned_transition:type_name -> temporal.server.api.persistence.v1.VersionedTransition + 9, // 2: temporal.server.api.token.v1.RawHistoryContinuation.version_histories:type_name -> temporal.server.api.history.v1.VersionHistories + 10, // 3: temporal.server.api.token.v1.Task.clock:type_name -> temporal.server.api.clock.v1.VectorClock + 11, // 4: temporal.server.api.token.v1.Task.started_time:type_name -> google.protobuf.Timestamp + 12, // 5: temporal.server.api.token.v1.NexusOperationCompletion.ref:type_name -> temporal.server.api.persistence.v1.StateMachineRef + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_temporal_server_api_token_v1_message_proto_init() } diff --git a/api/visibilityservice/v1/request_response.go-helpers.pb.go b/api/visibilityservice/v1/request_response.go-helpers.pb.go new file mode 100644 index 00000000000..3373ed06574 --- /dev/null +++ b/api/visibilityservice/v1/request_response.go-helpers.pb.go @@ -0,0 +1,154 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package visibilityservice + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type ListChasmExecutionsRequest to the protobuf v3 wire format +func (val *ListChasmExecutionsRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ListChasmExecutionsRequest from the protobuf v3 wire format +func (val *ListChasmExecutionsRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ListChasmExecutionsRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ListChasmExecutionsRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ListChasmExecutionsRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ListChasmExecutionsRequest + switch t := that.(type) { + case *ListChasmExecutionsRequest: + that1 = t + case ListChasmExecutionsRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ListChasmExecutionsResponse to the protobuf v3 wire format +func (val *ListChasmExecutionsResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ListChasmExecutionsResponse from the protobuf v3 wire format +func (val *ListChasmExecutionsResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ListChasmExecutionsResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ListChasmExecutionsResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ListChasmExecutionsResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ListChasmExecutionsResponse + switch t := that.(type) { + case *ListChasmExecutionsResponse: + that1 = t + case ListChasmExecutionsResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CountChasmExecutionsRequest to the protobuf v3 wire format +func (val *CountChasmExecutionsRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CountChasmExecutionsRequest from the protobuf v3 wire format +func (val *CountChasmExecutionsRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CountChasmExecutionsRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CountChasmExecutionsRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CountChasmExecutionsRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CountChasmExecutionsRequest + switch t := that.(type) { + case *CountChasmExecutionsRequest: + that1 = t + case CountChasmExecutionsRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CountChasmExecutionsResponse to the protobuf v3 wire format +func (val *CountChasmExecutionsResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CountChasmExecutionsResponse from the protobuf v3 wire format +func (val *CountChasmExecutionsResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CountChasmExecutionsResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CountChasmExecutionsResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CountChasmExecutionsResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CountChasmExecutionsResponse + switch t := that.(type) { + case *CountChasmExecutionsResponse: + that1 = t + case CountChasmExecutionsResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/api/visibilityservice/v1/request_response.pb.go b/api/visibilityservice/v1/request_response.pb.go new file mode 100644 index 00000000000..550dbd50ca0 --- /dev/null +++ b/api/visibilityservice/v1/request_response.pb.go @@ -0,0 +1,426 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/api/visibilityservice/v1/request_response.proto + +package visibilityservice + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + v11 "go.temporal.io/api/common/v1" + v1 "go.temporal.io/server/api/chasm/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ListChasmExecutionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // (-- api-linter: core::0141::forbidden-types=disabled --) + ArchetypeId uint32 `protobuf:"varint,1,opt,name=archetype_id,json=archetypeId,proto3" json:"archetype_id,omitempty"` + NamespaceId string `protobuf:"bytes,2,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` + Query string `protobuf:"bytes,4,opt,name=query,proto3" json:"query,omitempty"` + // Maximum number of executions per page + PageSize int32 `protobuf:"varint,5,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token to continue reading next page of executions. + // Pass in empty slice for first page. + NextPageToken []byte `protobuf:"bytes,6,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChasmExecutionsRequest) Reset() { + *x = ListChasmExecutionsRequest{} + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChasmExecutionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChasmExecutionsRequest) ProtoMessage() {} + +func (x *ListChasmExecutionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListChasmExecutionsRequest.ProtoReflect.Descriptor instead. +func (*ListChasmExecutionsRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescGZIP(), []int{0} +} + +func (x *ListChasmExecutionsRequest) GetArchetypeId() uint32 { + if x != nil { + return x.ArchetypeId + } + return 0 +} + +func (x *ListChasmExecutionsRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *ListChasmExecutionsRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ListChasmExecutionsRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *ListChasmExecutionsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListChasmExecutionsRequest) GetNextPageToken() []byte { + if x != nil { + return x.NextPageToken + } + return nil +} + +type ListChasmExecutionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Executions []*v1.VisibilityExecutionInfo `protobuf:"bytes,1,rep,name=executions,proto3" json:"executions,omitempty"` + NextPageToken []byte `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChasmExecutionsResponse) Reset() { + *x = ListChasmExecutionsResponse{} + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChasmExecutionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChasmExecutionsResponse) ProtoMessage() {} + +func (x *ListChasmExecutionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListChasmExecutionsResponse.ProtoReflect.Descriptor instead. +func (*ListChasmExecutionsResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescGZIP(), []int{1} +} + +func (x *ListChasmExecutionsResponse) GetExecutions() []*v1.VisibilityExecutionInfo { + if x != nil { + return x.Executions + } + return nil +} + +func (x *ListChasmExecutionsResponse) GetNextPageToken() []byte { + if x != nil { + return x.NextPageToken + } + return nil +} + +type CountChasmExecutionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // (-- api-linter: core::0141::forbidden-types=disabled --) + ArchetypeId uint32 `protobuf:"varint,1,opt,name=archetype_id,json=archetypeId,proto3" json:"archetype_id,omitempty"` + NamespaceId string `protobuf:"bytes,2,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` + Query string `protobuf:"bytes,4,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CountChasmExecutionsRequest) Reset() { + *x = CountChasmExecutionsRequest{} + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CountChasmExecutionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CountChasmExecutionsRequest) ProtoMessage() {} + +func (x *CountChasmExecutionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CountChasmExecutionsRequest.ProtoReflect.Descriptor instead. +func (*CountChasmExecutionsRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescGZIP(), []int{2} +} + +func (x *CountChasmExecutionsRequest) GetArchetypeId() uint32 { + if x != nil { + return x.ArchetypeId + } + return 0 +} + +func (x *CountChasmExecutionsRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CountChasmExecutionsRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *CountChasmExecutionsRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +type CountChasmExecutionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + // `groups` contains the groups if the request is grouping by a field. + // The list might not be complete, and the counts of each group are approximations. + Groups []*CountChasmExecutionsResponse_AggregationGroup `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CountChasmExecutionsResponse) Reset() { + *x = CountChasmExecutionsResponse{} + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CountChasmExecutionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CountChasmExecutionsResponse) ProtoMessage() {} + +func (x *CountChasmExecutionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CountChasmExecutionsResponse.ProtoReflect.Descriptor instead. +func (*CountChasmExecutionsResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescGZIP(), []int{3} +} + +func (x *CountChasmExecutionsResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *CountChasmExecutionsResponse) GetGroups() []*CountChasmExecutionsResponse_AggregationGroup { + if x != nil { + return x.Groups + } + return nil +} + +type CountChasmExecutionsResponse_AggregationGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupValues []*v11.Payload `protobuf:"bytes,1,rep,name=group_values,json=groupValues,proto3" json:"group_values,omitempty"` + Count int64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CountChasmExecutionsResponse_AggregationGroup) Reset() { + *x = CountChasmExecutionsResponse_AggregationGroup{} + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CountChasmExecutionsResponse_AggregationGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CountChasmExecutionsResponse_AggregationGroup) ProtoMessage() {} + +func (x *CountChasmExecutionsResponse_AggregationGroup) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CountChasmExecutionsResponse_AggregationGroup.ProtoReflect.Descriptor instead. +func (*CountChasmExecutionsResponse_AggregationGroup) Descriptor() ([]byte, []int) { + return file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *CountChasmExecutionsResponse_AggregationGroup) GetGroupValues() []*v11.Payload { + if x != nil { + return x.GroupValues + } + return nil +} + +func (x *CountChasmExecutionsResponse_AggregationGroup) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +var File_temporal_server_api_visibilityservice_v1_request_response_proto protoreflect.FileDescriptor + +const file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDesc = "" + + "\n" + + "?temporal/server/api/visibilityservice/v1/request_response.proto\x12(temporal.server.api.visibilityservice.v1\x1a$temporal/api/common/v1/message.proto\x1a*temporal/server/api/chasm/v1/message.proto\"\xdb\x01\n" + + "\x1aListChasmExecutionsRequest\x12!\n" + + "\farchetype_id\x18\x01 \x01(\rR\varchetypeId\x12!\n" + + "\fnamespace_id\x18\x02 \x01(\tR\vnamespaceId\x12\x1c\n" + + "\tnamespace\x18\x03 \x01(\tR\tnamespace\x12\x14\n" + + "\x05query\x18\x04 \x01(\tR\x05query\x12\x1b\n" + + "\tpage_size\x18\x05 \x01(\x05R\bpageSize\x12&\n" + + "\x0fnext_page_token\x18\x06 \x01(\fR\rnextPageToken\"\x9c\x01\n" + + "\x1bListChasmExecutionsResponse\x12U\n" + + "\n" + + "executions\x18\x01 \x03(\v25.temporal.server.api.chasm.v1.VisibilityExecutionInfoR\n" + + "executions\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\fR\rnextPageToken\"\x97\x01\n" + + "\x1bCountChasmExecutionsRequest\x12!\n" + + "\farchetype_id\x18\x01 \x01(\rR\varchetypeId\x12!\n" + + "\fnamespace_id\x18\x02 \x01(\tR\vnamespaceId\x12\x1c\n" + + "\tnamespace\x18\x03 \x01(\tR\tnamespace\x12\x14\n" + + "\x05query\x18\x04 \x01(\tR\x05query\"\x93\x02\n" + + "\x1cCountChasmExecutionsResponse\x12\x14\n" + + "\x05count\x18\x01 \x01(\x03R\x05count\x12o\n" + + "\x06groups\x18\x02 \x03(\v2W.temporal.server.api.visibilityservice.v1.CountChasmExecutionsResponse.AggregationGroupR\x06groups\x1al\n" + + "\x10AggregationGroup\x12B\n" + + "\fgroup_values\x18\x01 \x03(\v2\x1f.temporal.api.common.v1.PayloadR\vgroupValues\x12\x14\n" + + "\x05count\x18\x02 \x01(\x03R\x05countBBZ@go.temporal.io/server/api/visibilityservice/v1;visibilityserviceb\x06proto3" + +var ( + file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescOnce sync.Once + file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescData []byte +) + +func file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescGZIP() []byte { + file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescOnce.Do(func() { + file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDesc), len(file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDesc))) + }) + return file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDescData +} + +var file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_temporal_server_api_visibilityservice_v1_request_response_proto_goTypes = []any{ + (*ListChasmExecutionsRequest)(nil), // 0: temporal.server.api.visibilityservice.v1.ListChasmExecutionsRequest + (*ListChasmExecutionsResponse)(nil), // 1: temporal.server.api.visibilityservice.v1.ListChasmExecutionsResponse + (*CountChasmExecutionsRequest)(nil), // 2: temporal.server.api.visibilityservice.v1.CountChasmExecutionsRequest + (*CountChasmExecutionsResponse)(nil), // 3: temporal.server.api.visibilityservice.v1.CountChasmExecutionsResponse + (*CountChasmExecutionsResponse_AggregationGroup)(nil), // 4: temporal.server.api.visibilityservice.v1.CountChasmExecutionsResponse.AggregationGroup + (*v1.VisibilityExecutionInfo)(nil), // 5: temporal.server.api.chasm.v1.VisibilityExecutionInfo + (*v11.Payload)(nil), // 6: temporal.api.common.v1.Payload +} +var file_temporal_server_api_visibilityservice_v1_request_response_proto_depIdxs = []int32{ + 5, // 0: temporal.server.api.visibilityservice.v1.ListChasmExecutionsResponse.executions:type_name -> temporal.server.api.chasm.v1.VisibilityExecutionInfo + 4, // 1: temporal.server.api.visibilityservice.v1.CountChasmExecutionsResponse.groups:type_name -> temporal.server.api.visibilityservice.v1.CountChasmExecutionsResponse.AggregationGroup + 6, // 2: temporal.server.api.visibilityservice.v1.CountChasmExecutionsResponse.AggregationGroup.group_values:type_name -> temporal.api.common.v1.Payload + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_temporal_server_api_visibilityservice_v1_request_response_proto_init() } +func file_temporal_server_api_visibilityservice_v1_request_response_proto_init() { + if File_temporal_server_api_visibilityservice_v1_request_response_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDesc), len(file_temporal_server_api_visibilityservice_v1_request_response_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_api_visibilityservice_v1_request_response_proto_goTypes, + DependencyIndexes: file_temporal_server_api_visibilityservice_v1_request_response_proto_depIdxs, + MessageInfos: file_temporal_server_api_visibilityservice_v1_request_response_proto_msgTypes, + }.Build() + File_temporal_server_api_visibilityservice_v1_request_response_proto = out.File + file_temporal_server_api_visibilityservice_v1_request_response_proto_goTypes = nil + file_temporal_server_api_visibilityservice_v1_request_response_proto_depIdxs = nil +} diff --git a/chasm/component.go b/chasm/component.go index 2ceded4a1dc..9506f09a8e8 100644 --- a/chasm/component.go +++ b/chasm/component.go @@ -13,27 +13,45 @@ import ( type Component interface { LifecycleState(Context) LifecycleState - Terminate(MutableContext, TerminateComponentRequest) (TerminateComponentResponse, error) - // we may not need this in the beginning mustEmbedUnimplementedComponent() } +type TerminableComponent interface { + Component + + // Terminate method is invoked by the chasm framework on an execution's root component when the execution + // needs to be forcefully terminated. + // Some examples include: + // - Execution state becomes too large. + // - Two running executions with the same businessID when namespace performs a force failover. + Terminate(MutableContext, TerminateComponentRequest) (TerminateComponentResponse, error) +} + type TerminateComponentRequest struct { - Identity string - Reason string - Details *commonpb.Payloads + Identity string + Reason string + Details *commonpb.Payloads + RequestID string } type TerminateComponentResponse struct{} -// Embed UnimplementedComponent to get forward compatibility -type UnimplementedComponent struct{} +// RootComponent is the interface that must be implemented by the top level component of a chasm execution. +// When the RootComponent's LifecycleState transitions to a closed state, the entire execution is considered closed, +// and will be cleaned up by the chasm framework after namespace's retention period. The BusinessID is also available for reuse. +// +// TODO: (not yet true) Visibility record will no longer be updated after RootComponent is closed. +type RootComponent interface { + TerminableComponent -func (UnimplementedComponent) Terminate(MutableContext, TerminateComponentRequest) (TerminateComponentResponse, error) { - return TerminateComponentResponse{}, nil + // ContextMetadata returns execution metadata to propagate to the request context. + ContextMetadata(Context) map[string]string } +// Embed UnimplementedComponent to get forward compatibility +type UnimplementedComponent struct{} + func (UnimplementedComponent) mustEmbedUnimplementedComponent() {} var UnimplementedComponentT = reflect.TypeFor[UnimplementedComponent]() diff --git a/chasm/component_mock.go b/chasm/component_mock.go index 886ee693e58..3d65d7c4cb6 100644 --- a/chasm/component_mock.go +++ b/chasm/component_mock.go @@ -53,8 +53,58 @@ func (mr *MockComponentMockRecorder) LifecycleState(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LifecycleState", reflect.TypeOf((*MockComponent)(nil).LifecycleState), arg0) } +// mustEmbedUnimplementedComponent mocks base method. +func (m *MockComponent) mustEmbedUnimplementedComponent() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedComponent") +} + +// mustEmbedUnimplementedComponent indicates an expected call of mustEmbedUnimplementedComponent. +func (mr *MockComponentMockRecorder) mustEmbedUnimplementedComponent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedComponent", reflect.TypeOf((*MockComponent)(nil).mustEmbedUnimplementedComponent)) +} + +// MockTerminableComponent is a mock of TerminableComponent interface. +type MockTerminableComponent struct { + ctrl *gomock.Controller + recorder *MockTerminableComponentMockRecorder + isgomock struct{} +} + +// MockTerminableComponentMockRecorder is the mock recorder for MockTerminableComponent. +type MockTerminableComponentMockRecorder struct { + mock *MockTerminableComponent +} + +// NewMockTerminableComponent creates a new mock instance. +func NewMockTerminableComponent(ctrl *gomock.Controller) *MockTerminableComponent { + mock := &MockTerminableComponent{ctrl: ctrl} + mock.recorder = &MockTerminableComponentMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTerminableComponent) EXPECT() *MockTerminableComponentMockRecorder { + return m.recorder +} + +// LifecycleState mocks base method. +func (m *MockTerminableComponent) LifecycleState(arg0 Context) LifecycleState { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LifecycleState", arg0) + ret0, _ := ret[0].(LifecycleState) + return ret0 +} + +// LifecycleState indicates an expected call of LifecycleState. +func (mr *MockTerminableComponentMockRecorder) LifecycleState(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LifecycleState", reflect.TypeOf((*MockTerminableComponent)(nil).LifecycleState), arg0) +} + // Terminate mocks base method. -func (m *MockComponent) Terminate(arg0 MutableContext, arg1 TerminateComponentRequest) (TerminateComponentResponse, error) { +func (m *MockTerminableComponent) Terminate(arg0 MutableContext, arg1 TerminateComponentRequest) (TerminateComponentResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Terminate", arg0, arg1) ret0, _ := ret[0].(TerminateComponentResponse) @@ -63,19 +113,98 @@ func (m *MockComponent) Terminate(arg0 MutableContext, arg1 TerminateComponentRe } // Terminate indicates an expected call of Terminate. -func (mr *MockComponentMockRecorder) Terminate(arg0, arg1 any) *gomock.Call { +func (mr *MockTerminableComponentMockRecorder) Terminate(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Terminate", reflect.TypeOf((*MockComponent)(nil).Terminate), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Terminate", reflect.TypeOf((*MockTerminableComponent)(nil).Terminate), arg0, arg1) } // mustEmbedUnimplementedComponent mocks base method. -func (m *MockComponent) mustEmbedUnimplementedComponent() { +func (m *MockTerminableComponent) mustEmbedUnimplementedComponent() { m.ctrl.T.Helper() m.ctrl.Call(m, "mustEmbedUnimplementedComponent") } // mustEmbedUnimplementedComponent indicates an expected call of mustEmbedUnimplementedComponent. -func (mr *MockComponentMockRecorder) mustEmbedUnimplementedComponent() *gomock.Call { +func (mr *MockTerminableComponentMockRecorder) mustEmbedUnimplementedComponent() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedComponent", reflect.TypeOf((*MockComponent)(nil).mustEmbedUnimplementedComponent)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedComponent", reflect.TypeOf((*MockTerminableComponent)(nil).mustEmbedUnimplementedComponent)) +} + +// MockRootComponent is a mock of RootComponent interface. +type MockRootComponent struct { + ctrl *gomock.Controller + recorder *MockRootComponentMockRecorder + isgomock struct{} +} + +// MockRootComponentMockRecorder is the mock recorder for MockRootComponent. +type MockRootComponentMockRecorder struct { + mock *MockRootComponent +} + +// NewMockRootComponent creates a new mock instance. +func NewMockRootComponent(ctrl *gomock.Controller) *MockRootComponent { + mock := &MockRootComponent{ctrl: ctrl} + mock.recorder = &MockRootComponentMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRootComponent) EXPECT() *MockRootComponentMockRecorder { + return m.recorder +} + +// ContextMetadata mocks base method. +func (m *MockRootComponent) ContextMetadata(arg0 Context) map[string]string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ContextMetadata", arg0) + ret0, _ := ret[0].(map[string]string) + return ret0 +} + +// ContextMetadata indicates an expected call of ContextMetadata. +func (mr *MockRootComponentMockRecorder) ContextMetadata(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextMetadata", reflect.TypeOf((*MockRootComponent)(nil).ContextMetadata), arg0) +} + +// LifecycleState mocks base method. +func (m *MockRootComponent) LifecycleState(arg0 Context) LifecycleState { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LifecycleState", arg0) + ret0, _ := ret[0].(LifecycleState) + return ret0 +} + +// LifecycleState indicates an expected call of LifecycleState. +func (mr *MockRootComponentMockRecorder) LifecycleState(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LifecycleState", reflect.TypeOf((*MockRootComponent)(nil).LifecycleState), arg0) +} + +// Terminate mocks base method. +func (m *MockRootComponent) Terminate(arg0 MutableContext, arg1 TerminateComponentRequest) (TerminateComponentResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Terminate", arg0, arg1) + ret0, _ := ret[0].(TerminateComponentResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Terminate indicates an expected call of Terminate. +func (mr *MockRootComponentMockRecorder) Terminate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Terminate", reflect.TypeOf((*MockRootComponent)(nil).Terminate), arg0, arg1) +} + +// mustEmbedUnimplementedComponent mocks base method. +func (m *MockRootComponent) mustEmbedUnimplementedComponent() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedComponent") +} + +// mustEmbedUnimplementedComponent indicates an expected call of mustEmbedUnimplementedComponent. +func (mr *MockRootComponentMockRecorder) mustEmbedUnimplementedComponent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedComponent", reflect.TypeOf((*MockRootComponent)(nil).mustEmbedUnimplementedComponent)) } diff --git a/chasm/context.go b/chasm/context.go index 85760d3815a..82755112c22 100644 --- a/chasm/context.go +++ b/chasm/context.go @@ -5,6 +5,7 @@ import ( "time" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" ) type Context interface { @@ -19,26 +20,45 @@ type Context interface { Now(Component) time.Time // ExecutionKey returns the execution key for the execution the context is operating on. ExecutionKey() ExecutionKey - // StateTransitionCount returns the number of create/update transactions in the history of this execution. - StateTransitionCount() int64 - // ExecutionCloseTime returns the time when the execution was closed. An execution is closed when its root component reaches a terminal - // state in its lifecycle. If the component is still running (not yet closed), it returns a zero time.Time value. - ExecutionCloseTime() time.Time + // ExecutionInfo returns metadata information about the execution. + ExecutionInfo() ExecutionInfo // Logger returns a logger tagged with execution key and other chasm framework internal information. Logger() log.Logger + // MetricsHandler returns a metrics handler with namespace tag. + MetricsHandler() metrics.Handler + // Value returns the value associated with this context for key. The behavior is the same as context.Context.Value(). + // Use WithContextValues RegistrableComponentOption to set key values pair for a component upon registration. + // Registered key-value pairs will automatically be added to the Context whenever framework accesses the component. + // Alternatively, use ContextWithValue() to manually set values on Context which will take precedence over registered ones. + Value(key any) any // Intent() OperationIntent // ComponentOptions(Component) []ComponentOption + // withValue should only be used by ContextWithValue() function, do NOT call it directly. + // For structs implementing this method, although the returned value has type Context, + // the concrete type MUST be the same concrete type as the receiver. + withValue(key any, value any) Context structuredRef(Component) (ComponentRef, error) - getContext() context.Context + goContext() context.Context +} + +type ExecutionInfo struct { + // StateTransitionCount is the number of create/update transactions in the history of this execution. + StateTransitionCount int64 + // ApproximateStateSize is the approximate size in bytes of the persisted execution state of this execution. + ApproximateStateSize int + // CloseTime is the time when the execution was closed. + // An execution is closed when its root component reaches a terminal state in its lifecycle. + // If the component is still running (not yet closed), it returns a zero time.Time value. + CloseTime time.Time } type MutableContext interface { Context // AddTask adds a task to be emitted as part of the current transaction. - // The task is associated with the given component and will be invoked via the registered executor for the given task + // The task is associated with the given component and will be invoked via the registered handler for the given task // referencing the component. AddTask(Component, TaskAttributes, any) @@ -112,27 +132,51 @@ func (c *immutableCtx) ExecutionKey() ExecutionKey { return c.executionKey } -func (c *immutableCtx) StateTransitionCount() int64 { - return c.root.backend.GetExecutionInfo().GetStateTransitionCount() -} +func (c *immutableCtx) ExecutionInfo() ExecutionInfo { + executionInfo := c.root.backend.GetExecutionInfo() -func (c *immutableCtx) ExecutionCloseTime() time.Time { - closeTime := c.root.backend.GetExecutionInfo().GetCloseTime() - if closeTime == nil { - return time.Time{} + var closeTime time.Time + closeTimestamp := executionInfo.GetCloseTime() + if closeTimestamp != nil { + closeTime = closeTimestamp.AsTime() + } + + return ExecutionInfo{ + StateTransitionCount: executionInfo.GetStateTransitionCount(), + ApproximateStateSize: c.root.backend.GetApproximatePersistedSize(), + CloseTime: closeTime, } - return closeTime.AsTime() } func (c *immutableCtx) Logger() log.Logger { return c.root.logger } +func (c *immutableCtx) MetricsHandler() metrics.Handler { + return c.root.metricsHandler +} + +func (c *immutableCtx) Value(key any) any { + if v := c.goContext().Value(key); v != nil { + return v + } + + return c.root.registry.componentContextValue(key) +} + +func (c *immutableCtx) withValue(key any, value any) Context { + return &immutableCtx{ + ctx: context.WithValue(c.goContext(), key, value), + root: c.root, + executionKey: c.executionKey, + } +} + func (c *immutableCtx) structuredRef(component Component) (ComponentRef, error) { return c.root.structuredRef(component) } -func (c *immutableCtx) getContext() context.Context { +func (c *immutableCtx) goContext() context.Context { return c.ctx } @@ -142,10 +186,10 @@ func (c *immutableCtx) getContext() context.Context { // [UpdateWithStartExecution], or [StartExecution] APIs. func NewMutableContext( ctx context.Context, - root *Node, + node *Node, ) MutableContext { return &mutableCtx{ - immutableCtx: newContext(ctx, root), + immutableCtx: newContext(ctx, node), } } @@ -156,3 +200,17 @@ func (c *mutableCtx) AddTask( ) { c.root.AddTask(component, attributes, payload) } + +func (c *mutableCtx) withValue(key any, value any) Context { + return &mutableCtx{ + immutableCtx: ContextWithValue(c.immutableCtx, key, value), + } +} + +// ContextWithValue returns a new Context with the given key-value pair added. +// Added key-value pairs will be accessible via the Value() method on the returned Context, +// and the behavior of the key-value pair is the same as context.Context.WithValue(). +func ContextWithValue[C Context](c C, key any, value any) C { + //nolint:revive // unchecked-type-assertion + return any(c.withValue(key, value)).(C) +} diff --git a/chasm/context_mock.go b/chasm/context_mock.go index c9032cc09b8..b5e5abb587d 100644 --- a/chasm/context_mock.go +++ b/chasm/context_mock.go @@ -2,24 +2,56 @@ package chasm import ( "context" + "fmt" + "slices" "sync" "time" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/metrics" ) +var _ Context = (*MockContext)(nil) +var _ MutableContext = (*MockMutableContext)(nil) + // MockContext is a mock implementation of [Context]. type MockContext struct { - HandleExecutionKey func() ExecutionKey - HandleNow func(component Component) time.Time - HandleRef func(component Component) ([]byte, error) - HandleExecutionCloseTime func() time.Time - HandleStateTransitionCount func() int64 + HandleExecutionKey func() ExecutionKey + HandleNow func(component Component) time.Time + HandleRef func(component Component) ([]byte, error) + HandleExecutionInfo func() ExecutionInfo + HandleMetricsHandler func() metrics.Handler + + // GoCtx is the underlying context.Context used for context value lookups. + // Any values set on it will be available via the CHASM mock context's Value method, + // and take precedence over any registered context values. + // Defaults to context.Background() if nil. + GoCtx context.Context + + registeredContextValues map[any]any } -func (c *MockContext) getContext() context.Context { - return nil +func (c *MockContext) RegisterComponentContextValues( + keyValues map[any]any, +) { + if c.registeredContextValues == nil { + c.registeredContextValues = make(map[any]any) + } + for k, v := range keyValues { + if _, exists := c.registeredContextValues[k]; exists { + // nolint:forbidigo + panic(fmt.Sprintf("context value key already registered: %v", k)) + } + c.registeredContextValues[k] = v + } +} + +func (c *MockContext) goContext() context.Context { + if c.GoCtx == nil { + c.GoCtx = context.Background() + } + return c.GoCtx } func (c *MockContext) Now(cmp Component) time.Time { @@ -47,18 +79,11 @@ func (c *MockContext) ExecutionKey() ExecutionKey { return ExecutionKey{} } -func (c *MockContext) ExecutionCloseTime() time.Time { - if c.HandleExecutionCloseTime != nil { - return c.HandleExecutionCloseTime() - } - return time.Time{} -} - -func (c *MockContext) StateTransitionCount() int64 { - if c.HandleStateTransitionCount != nil { - return c.HandleStateTransitionCount() +func (c *MockContext) ExecutionInfo() ExecutionInfo { + if c.HandleExecutionInfo != nil { + return c.HandleExecutionInfo() } - return 0 + return ExecutionInfo{} } func (c *MockContext) Logger() log.Logger { @@ -70,6 +95,28 @@ func (c *MockContext) Logger() log.Logger { ) } +func (c *MockContext) MetricsHandler() metrics.Handler { + if c.HandleMetricsHandler != nil { + return c.HandleMetricsHandler() + } + return metrics.NoopMetricsHandler +} + +func (c *MockContext) Value(key any) any { + return c.goContext().Value(key) +} + +func (c *MockContext) withValue(key any, value any) Context { + return &MockContext{ + HandleExecutionKey: c.HandleExecutionKey, + HandleNow: c.HandleNow, + HandleRef: c.HandleRef, + HandleExecutionInfo: c.HandleExecutionInfo, + HandleMetricsHandler: c.HandleMetricsHandler, + GoCtx: context.WithValue(c.goContext(), key, value), + } +} + // MockMutableContext is a mock implementation of [MutableContext] that records added tasks for inspection in // tests. type MockMutableContext struct { @@ -85,6 +132,13 @@ func (c *MockMutableContext) AddTask(component Component, attributes TaskAttribu c.Tasks = append(c.Tasks, MockTask{component, attributes, payload}) } +func (c *MockMutableContext) withValue(key any, value any) Context { + return &MockMutableContext{ + MockContext: *ContextWithValue(&c.MockContext, key, value), + Tasks: slices.Clone(c.Tasks), + } +} + type MockTask struct { Component Component Attributes TaskAttributes diff --git a/chasm/engine.go b/chasm/engine.go index 222c01307af..ea78d9a42f1 100644 --- a/chasm/engine.go +++ b/chasm/engine.go @@ -17,13 +17,13 @@ type Engine interface { StartExecution( context.Context, ComponentRef, - func(MutableContext) (Component, error), + func(MutableContext) (RootComponent, error), ...TransitionOption, ) (StartExecutionResult, error) UpdateWithStartExecution( context.Context, ComponentRef, - func(MutableContext) (Component, error), + func(MutableContext) (RootComponent, error), func(MutableContext, Component) error, ...TransitionOption, ) (EngineUpdateWithStartExecutionResult, error) @@ -48,10 +48,23 @@ type Engine interface { ...TransitionOption, ) ([]byte, error) + DeleteExecution( + context.Context, + ComponentRef, + DeleteExecutionRequest, + ) error + // NotifyExecution notifies any PollComponent callers waiting on the execution. NotifyExecution(ExecutionKey) } +// DeleteExecutionRequest is the request for [DeleteExecution]. TerminateComponentRequest will only be +// used if the execution is still running. The actual deletion of the execution is async, and will return +// after creating the DeleteExecutionTask. +type DeleteExecutionRequest struct { + TerminateComponentRequest +} + type BusinessIDReusePolicy int const ( @@ -172,7 +185,7 @@ func WithRequestID( // the lifecycle of creating and persisting a new component within an execution context. // // Type Parameters: -// - C: The component type to create, must implement [Component] +// - C: The component type to create, must implement [RootComponent] // - I: The input type passed to the factory function // - O: The output type returned by the factory function // @@ -191,7 +204,7 @@ func WithRequestID( // - O: The output value produced by startFn // - [NewExecutionResult]: Contains the execution key, serialized ref, and whether a new execution was created // - error: Non-nil if creation failed or policy constraints were violated -func StartExecution[C Component, I any]( +func StartExecution[C RootComponent, I any]( ctx context.Context, key ExecutionKey, startFn func(MutableContext, I) (C, error), @@ -201,12 +214,12 @@ func StartExecution[C Component, I any]( result, err := engineFromContext(ctx).StartExecution( ctx, NewComponentRef[C](key), - func(ctx MutableContext) (_ Component, retErr error) { - defer log.CapturePanic(ctx.Logger(), &retErr) + func(mutableContext MutableContext) (_ RootComponent, retErr error) { + defer log.CapturePanic(mutableContext.Logger(), &retErr) var c C var err error - c, err = startFn(ctx, input) + c, err = startFn(mutableContext, input) return c, err }, opts..., @@ -222,7 +235,7 @@ func StartExecution[C Component, I any]( }, nil } -func UpdateWithStartExecution[C Component, I any, O any]( +func UpdateWithStartExecution[C RootComponent, I any, O any]( ctx context.Context, key ExecutionKey, startFn func(MutableContext, I) (C, error), @@ -234,19 +247,23 @@ func UpdateWithStartExecution[C Component, I any, O any]( result, err := engineFromContext(ctx).UpdateWithStartExecution( ctx, NewComponentRef[C](key), - func(ctx MutableContext) (_ Component, retErr error) { - defer log.CapturePanic(ctx.Logger(), &retErr) + func(mutableContext MutableContext) (_ RootComponent, retErr error) { + defer log.CapturePanic(mutableContext.Logger(), &retErr) var c C var err error - c, err = startFn(ctx, input) + c, err = startFn(mutableContext, input) return c, err }, - func(ctx MutableContext, c Component) (retErr error) { - defer log.CapturePanic(ctx.Logger(), &retErr) + func(mutableContext MutableContext, c Component) (retErr error) { + defer log.CapturePanic(mutableContext.Logger(), &retErr) var err error - output, err = updateFn(c.(C), ctx, input) + output, err = updateFn( + c.(C), + mutableContext, + input, + ) return err }, opts..., @@ -289,11 +306,15 @@ func UpdateComponent[C any, R []byte | ComponentRef, I any, O any]( newSerializedRef, err := engineFromContext(ctx).UpdateComponent( ctx, ref, - func(ctx MutableContext, c Component) (retErr error) { - defer log.CapturePanic(ctx.Logger(), &retErr) + func(mutableContext MutableContext, c Component) (retErr error) { + defer log.CapturePanic(mutableContext.Logger(), &retErr) var err error - output, err = updateFn(c.(C), ctx, input) + output, err = updateFn( + c.(C), + mutableContext, + input, + ) return err }, opts..., @@ -324,11 +345,15 @@ func ReadComponent[C any, R []byte | ComponentRef, I any, O any]( err = engineFromContext(ctx).ReadComponent( ctx, ref, - func(ctx Context, c Component) (retErr error) { - defer log.CapturePanic(ctx.Logger(), &retErr) + func(chasmContext Context, c Component) (retErr error) { + defer log.CapturePanic(chasmContext.Logger(), &retErr) var err error - output, err = readFn(c.(C), ctx, input) + output, err = readFn( + c.(C), + chasmContext, + input, + ) return err }, opts..., @@ -361,10 +386,14 @@ func PollComponent[C any, R []byte | ComponentRef, I any, O any]( newSerializedRef, err := engineFromContext(ctx).PollComponent( ctx, ref, - func(ctx Context, c Component) (_ bool, retErr error) { - defer log.CapturePanic(ctx.Logger(), &retErr) - - out, satisfied, err := monotonicPredicate(c.(C), ctx, input) + func(chasmContext Context, c Component) (_ bool, retErr error) { + defer log.CapturePanic(chasmContext.Logger(), &retErr) + + out, satisfied, err := monotonicPredicate( + c.(C), + chasmContext, + input, + ) if satisfied { output = out } @@ -378,6 +407,21 @@ func PollComponent[C any, R []byte | ComponentRef, I any, O any]( return output, newSerializedRef, err } +// DeleteExecution deletes the execution identified by the supplied execution key. +// If the execution is still running, it is terminated first. A DeleteExecutionTask is +// then queued to remove all execution data from persistence. +func DeleteExecution[C RootComponent]( + ctx context.Context, + key ExecutionKey, + request DeleteExecutionRequest, +) error { + return engineFromContext(ctx).DeleteExecution( + ctx, + NewComponentRef[C](key), + request, + ) +} + func convertComponentRef[R []byte | ComponentRef]( r R, ) (ComponentRef, error) { diff --git a/chasm/engine_mock.go b/chasm/engine_mock.go index a3ba111ea1a..a3d86b11ba9 100644 --- a/chasm/engine_mock.go +++ b/chasm/engine_mock.go @@ -40,6 +40,20 @@ func (m *MockEngine) EXPECT() *MockEngineMockRecorder { return m.recorder } +// DeleteExecution mocks base method. +func (m *MockEngine) DeleteExecution(arg0 context.Context, arg1 ComponentRef, arg2 DeleteExecutionRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteExecution", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteExecution indicates an expected call of DeleteExecution. +func (mr *MockEngineMockRecorder) DeleteExecution(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExecution", reflect.TypeOf((*MockEngine)(nil).DeleteExecution), arg0, arg1, arg2) +} + // NotifyExecution mocks base method. func (m *MockEngine) NotifyExecution(arg0 ExecutionKey) { m.ctrl.T.Helper() @@ -92,7 +106,7 @@ func (mr *MockEngineMockRecorder) ReadComponent(arg0, arg1, arg2 any, arg3 ...an } // StartExecution mocks base method. -func (m *MockEngine) StartExecution(arg0 context.Context, arg1 ComponentRef, arg2 func(MutableContext) (Component, error), arg3 ...TransitionOption) (StartExecutionResult, error) { +func (m *MockEngine) StartExecution(arg0 context.Context, arg1 ComponentRef, arg2 func(MutableContext) (RootComponent, error), arg3 ...TransitionOption) (StartExecutionResult, error) { m.ctrl.T.Helper() varargs := []any{arg0, arg1, arg2} for _, a := range arg3 { @@ -132,7 +146,7 @@ func (mr *MockEngineMockRecorder) UpdateComponent(arg0, arg1, arg2 any, arg3 ... } // UpdateWithStartExecution mocks base method. -func (m *MockEngine) UpdateWithStartExecution(arg0 context.Context, arg1 ComponentRef, arg2 func(MutableContext) (Component, error), arg3 func(MutableContext, Component) error, arg4 ...TransitionOption) (EngineUpdateWithStartExecutionResult, error) { +func (m *MockEngine) UpdateWithStartExecution(arg0 context.Context, arg1 ComponentRef, arg2 func(MutableContext) (RootComponent, error), arg3 func(MutableContext, Component) error, arg4 ...TransitionOption) (EngineUpdateWithStartExecutionResult, error) { m.ctrl.T.Helper() varargs := []any{arg0, arg1, arg2, arg3} for _, a := range arg4 { diff --git a/chasm/field_test.go b/chasm/field_test.go index 97130d80186..de65423882a 100644 --- a/chasm/field_test.go +++ b/chasm/field_test.go @@ -11,6 +11,7 @@ import ( "go.temporal.io/server/common/clock" "go.temporal.io/server/common/definition" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/testing/protorequire" "go.temporal.io/server/common/testing/testlogger" @@ -29,6 +30,7 @@ type fieldSuite struct { timeSource *clock.EventTimeSource nodePathEncoder NodePathEncoder logger log.Logger + metricsHandler metrics.Handler } func TestFieldSuite(t *testing.T) { @@ -41,6 +43,7 @@ func (s *fieldSuite) SetupTest() { s.nodeBackend = &MockNodeBackend{} s.logger = testlogger.NewTestLogger(s.T(), testlogger.FailOnAnyUnexpectedError) + s.metricsHandler = metrics.NoopMetricsHandler s.registry = NewRegistry(s.logger) err := s.registry.Register(newTestLibrary(s.controller)) s.NoError(err) @@ -145,6 +148,7 @@ func (s *fieldSuite) newTestTree( s.nodeBackend, s.nodePathEncoder, s.logger, + s.metricsHandler, ), nil } return NewTreeFromDB( @@ -154,6 +158,7 @@ func (s *fieldSuite) newTestTree( s.nodeBackend, s.nodePathEncoder, s.logger, + s.metricsHandler, ) } @@ -165,6 +170,7 @@ func (s *fieldSuite) setupComponentWithTree(rootComponent *TestComponent) (*Node s.nodeBackend, s.nodePathEncoder, s.logger, + s.metricsHandler, ) if err := rootNode.SetRootComponent(rootComponent); err != nil { return nil, nil, err diff --git a/chasm/interceptors.go b/chasm/interceptors.go index 64d2e85e080..484eb5f0c48 100644 --- a/chasm/interceptors.go +++ b/chasm/interceptors.go @@ -52,10 +52,10 @@ type ChasmVisibilityInterceptor struct { func (i *ChasmVisibilityInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (resp interface{}, retError error) { +) (resp any, retError error) { ctx = NewVisibilityManagerContext(ctx, i.visibilityMgr) return handler(ctx, req) } diff --git a/chasm/lib/activity/activity.go b/chasm/lib/activity/activity.go index 24e6afda62f..4c08637314b 100644 --- a/chasm/lib/activity/activity.go +++ b/chasm/lib/activity/activity.go @@ -20,8 +20,8 @@ import ( "go.temporal.io/server/chasm/lib/activity/gen/activitypb/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/backoff" - "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/payload" serviceerrors "go.temporal.io/server/common/serviceerror" "go.temporal.io/server/common/tqid" @@ -73,46 +73,25 @@ type WithToken[R any] struct { Request R } -// MetricsHandlerBuilderParams contains parameters for building/enriching a metrics handler for activity operations -type MetricsHandlerBuilderParams struct { - Handler metrics.Handler - NamespaceName string - BreakdownMetricsByTaskQueue dynamicconfig.TypedPropertyFnWithTaskQueueFilter[bool] -} - // RespondCompletedEvent wraps the RespondActivityTaskCompletedRequest with context-specific data. type RespondCompletedEvent struct { - Request *historyservice.RespondActivityTaskCompletedRequest - Token *tokenspb.Task - MetricsHandlerBuilderParams MetricsHandlerBuilderParams + Request *historyservice.RespondActivityTaskCompletedRequest + Token *tokenspb.Task } // RespondFailedEvent wraps the RespondActivityTaskFailedRequest with context-specific data. type RespondFailedEvent struct { - Request *historyservice.RespondActivityTaskFailedRequest - Token *tokenspb.Task - MetricsHandlerBuilderParams MetricsHandlerBuilderParams + Request *historyservice.RespondActivityTaskFailedRequest + Token *tokenspb.Task } // RespondCancelledEvent wraps the RespondActivityTaskCanceledRequest with context-specific data. type RespondCancelledEvent struct { - Request *historyservice.RespondActivityTaskCanceledRequest - Token *tokenspb.Task - MetricsHandlerBuilderParams MetricsHandlerBuilderParams -} - -// requestCancelEvent wraps the RequestCancelActivityExecutionRequest with context-specific data. -type requestCancelEvent struct { - request *activitypb.RequestCancelActivityExecutionRequest - MetricsHandlerBuilderParams MetricsHandlerBuilderParams -} - -// terminateEvent wraps the TerminateActivityExecutionRequest with context-specific data. -type terminateEvent struct { - request *activitypb.TerminateActivityExecutionRequest - MetricsHandlerBuilderParams MetricsHandlerBuilderParams + Request *historyservice.RespondActivityTaskCanceledRequest + Token *tokenspb.Task } +// LifecycleState implements the chasm.Component interface. func (a *Activity) LifecycleState(_ chasm.Context) chasm.LifecycleState { switch a.Status { case activitypb.ACTIVITY_EXECUTION_STATUS_COMPLETED: @@ -127,6 +106,11 @@ func (a *Activity) LifecycleState(_ chasm.Context) chasm.LifecycleState { } } +func (a *Activity) ContextMetadata(_ chasm.Context) map[string]string { + // TODO: Export standalone activity context metadata. + return nil +} + // NewStandaloneActivity creates a new activity component and adds associated tasks to start execution. func NewStandaloneActivity( ctx chasm.MutableContext, @@ -220,14 +204,16 @@ func (a *Activity) GenerateRecordActivityTaskStartedResponse( lastHeartbeat, _ := a.LastHeartbeat.TryGet(ctx) requestData := a.RequestData.Get(ctx) attempt := a.LastAttempt.Get(ctx) + return &historyservice.RecordActivityTaskStartedResponse{ - StartedTime: attempt.GetStartedTime(), - Attempt: attempt.GetCount(), - Priority: a.GetPriority(), - RetryPolicy: a.GetRetryPolicy(), - ActivityRunId: key.RunID, - WorkflowNamespace: namespace, - HeartbeatDetails: lastHeartbeat.GetDetails(), + StartedTime: attempt.GetStartedTime(), + Attempt: attempt.GetCount(), + Priority: a.GetPriority(), + RetryPolicy: a.GetRetryPolicy(), + ActivityRunId: key.RunID, + WorkflowNamespace: namespace, + HeartbeatDetails: lastHeartbeat.GetDetails(), + CurrentAttemptScheduledTime: a.attemptScheduleTime(attempt), ScheduledEvent: &historypb.HistoryEvent{ EventType: enumspb.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED, EventTime: a.GetScheduleTime(), @@ -248,6 +234,27 @@ func (a *Activity) GenerateRecordActivityTaskStartedResponse( }, nil } +// attemptScheduleTime returns when the given attempt was scheduled to run: +// the activity's original schedule time for the first attempt, or +// calculated from attemptScheduleTimeForRetry on retries. +func (a *Activity) attemptScheduleTime(attempt *activitypb.ActivityAttemptState) *timestamppb.Timestamp { + if attempt.GetCount() == 1 { + return a.GetScheduleTime() + } + return attemptScheduleTimeForRetry(attempt) +} + +// attemptScheduleTimeForRetry computes the time a retried attempt is scheduled to start, +// as complete_time + retry_interval. Returns nil if either field is missing or zero. +func attemptScheduleTimeForRetry(attempt *activitypb.ActivityAttemptState) *timestamppb.Timestamp { + retryInterval := attempt.GetCurrentRetryInterval() + completeTime := attempt.GetCompleteTime() + if retryInterval != nil && retryInterval.AsDuration() > 0 && completeTime != nil { + return timestamppb.New(completeTime.AsTime().Add(retryInterval.AsDuration())) + } + return nil +} + // RecordCompleted applies the provided function to record activity completion. func (a *Activity) RecordCompleted(ctx chasm.MutableContext, applyFn func(ctx chasm.MutableContext) error) error { return applyFn(ctx) @@ -262,12 +269,10 @@ func (a *Activity) HandleCompleted( return nil, err } - metricsHandler := enrichMetricsHandler( - a, - event.MetricsHandlerBuilderParams.Handler, - event.MetricsHandlerBuilderParams.NamespaceName, - metrics.HistoryRespondActivityTaskCompletedScope, - event.MetricsHandlerBuilderParams.BreakdownMetricsByTaskQueue) + metricsHandler, err := a.enrichMetricsHandler(ctx, metrics.HistoryRespondActivityTaskCompletedScope) + if err != nil { + return nil, err + } if err := TransitionCompleted.Apply(a, ctx, completeEvent{ req: event.Request, @@ -289,13 +294,10 @@ func (a *Activity) HandleFailed( return nil, err } - metricsHandler := enrichMetricsHandler( - a, - event.MetricsHandlerBuilderParams.Handler, - event.MetricsHandlerBuilderParams.NamespaceName, - metrics.HistoryRespondActivityTaskFailedScope, - event.MetricsHandlerBuilderParams.BreakdownMetricsByTaskQueue) - + metricsHandler, err := a.enrichMetricsHandler(ctx, metrics.HistoryRespondActivityTaskFailedScope) + if err != nil { + return nil, err + } failure := event.Request.GetFailedRequest().GetFailure() appFailure := failure.GetApplicationFailureInfo() @@ -334,12 +336,10 @@ func (a *Activity) HandleCanceled( return nil, err } - metricsHandler := enrichMetricsHandler( - a, - event.MetricsHandlerBuilderParams.Handler, - event.MetricsHandlerBuilderParams.NamespaceName, - metrics.HistoryRespondActivityTaskCanceledScope, - event.MetricsHandlerBuilderParams.BreakdownMetricsByTaskQueue) + metricsHandler, err := a.enrichMetricsHandler(ctx, metrics.HistoryRespondActivityTaskCanceledScope) + if err != nil { + return nil, err + } if err := TransitionCanceled.Apply(a, ctx, cancelEvent{ details: event.Request.GetCancelRequest().GetDetails(), @@ -352,29 +352,33 @@ func (a *Activity) HandleCanceled( return &historyservice.RespondActivityTaskCanceledResponse{}, nil } -func (a *Activity) handleTerminated(ctx chasm.MutableContext, req terminateEvent) ( - *activitypb.TerminateActivityExecutionResponse, error, -) { - frontendReq := req.request.GetFrontendRequest() - +// Terminate implements the chasm.RootComponent interface. +func (a *Activity) Terminate( + ctx chasm.MutableContext, + req chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { // If already in terminated state, fail if request ID is different, else no-op if a.GetStatus() == activitypb.ACTIVITY_EXECUTION_STATUS_TERMINATED { - newReqID := frontendReq.GetRequestId() + newReqID := req.RequestID existingReqID := a.GetTerminateState().GetRequestId() if existingReqID != newReqID { - return nil, serviceerror.NewFailedPrecondition( - fmt.Sprintf("already terminated with request ID %s", existingReqID)) + return chasm.TerminateComponentResponse{}, serviceerror.NewFailedPreconditionf( + "already terminated with request ID %s", existingReqID) } - return &activitypb.TerminateActivityExecutionResponse{}, nil + return chasm.TerminateComponentResponse{}, nil } - if err := TransitionTerminated.Apply(a, ctx, req); err != nil { - return nil, err + metricsHandler, err := a.enrichMetricsHandler(ctx, metrics.ActivityTerminatedScope) + if err != nil { + return chasm.TerminateComponentResponse{}, err } - - return &activitypb.TerminateActivityExecutionResponse{}, nil + return chasm.TerminateComponentResponse{}, TransitionTerminated.Apply(a, ctx, terminateEvent{ + request: req, + metricsHandler: metricsHandler, + fromStatus: a.GetStatus(), + }) } // getOrCreateLastHeartbeat retrieves the last heartbeat state, initializing it if not present. The heartbeat is lazily created @@ -388,10 +392,10 @@ func (a *Activity) getOrCreateLastHeartbeat(ctx chasm.MutableContext) *activityp return heartbeat } -func (a *Activity) handleCancellationRequested(ctx chasm.MutableContext, event requestCancelEvent) ( +func (a *Activity) handleCancellationRequested(ctx chasm.MutableContext, request *activitypb.RequestCancelActivityExecutionRequest) ( *activitypb.RequestCancelActivityExecutionResponse, error, ) { - req := event.request.GetFrontendRequest() + req := request.GetFrontendRequest() newReqID := req.GetRequestId() existingReqID := a.GetCancelState().GetRequestId() @@ -419,14 +423,11 @@ func (a *Activity) handleCancellationRequested(ctx chasm.MutableContext, event r }, } - metricsHandler := enrichMetricsHandler( - a, - event.MetricsHandlerBuilderParams.Handler, - event.MetricsHandlerBuilderParams.NamespaceName, - metrics.HistoryRespondActivityTaskCanceledScope, - event.MetricsHandlerBuilderParams.BreakdownMetricsByTaskQueue) - - err := TransitionCanceled.Apply(a, ctx, cancelEvent{ + metricsHandler, err := a.enrichMetricsHandler(ctx, metrics.HistoryRespondActivityTaskCanceledScope) + if err != nil { + return nil, err + } + err = TransitionCanceled.Apply(a, ctx, cancelEvent{ details: details, handler: metricsHandler, fromStatus: activitypb.ACTIVITY_EXECUTION_STATUS_SCHEDULED, // if we're here the original status was scheduled @@ -572,15 +573,17 @@ func (a *Activity) RecordHeartbeat( RecordedTime: timestamppb.New(ctx.Now(a)), Details: input.Request.GetHeartbeatRequest().GetDetails(), }) - ctx.AddTask( - a, - chasm.TaskAttributes{ - ScheduledTime: ctx.Now(a).Add(a.GetHeartbeatTimeout().AsDuration()), - }, - &activitypb.HeartbeatTimeoutTask{ - Stamp: a.LastAttempt.Get(ctx).GetStamp(), - }, - ) + if heartbeatTimeout := a.GetHeartbeatTimeout().AsDuration(); heartbeatTimeout > 0 { + ctx.AddTask( + a, + chasm.TaskAttributes{ + ScheduledTime: ctx.Now(a).Add(heartbeatTimeout), + }, + &activitypb.HeartbeatTimeoutTask{ + Stamp: a.LastAttempt.Get(ctx).GetStamp(), + }, + ) + } return &historyservice.RecordActivityTaskHeartbeatResponse{ CancelRequested: a.Status == activitypb.ACTIVITY_EXECUTION_STATUS_CANCEL_REQUESTED, // TODO(saa-preview): ActivityPaused, ActivityReset @@ -640,20 +643,13 @@ func (a *Activity) buildActivityExecutionInfo(ctx chasm.Context) *apiactivitypb. attempt := a.LastAttempt.Get(ctx) heartbeat, _ := a.LastHeartbeat.TryGet(ctx) key := ctx.ExecutionKey() - - // TODO(saa-preview): debating if we should persist next attempt schedule time for stronger consistency - var nextAttemptScheduleTime *timestamppb.Timestamp - interval := attempt.GetCurrentRetryInterval() - completeTime := attempt.GetCompleteTime() - if interval != nil && interval.AsDuration() > 0 && completeTime != nil { - nextAttemptScheduleTime = timestamppb.New(completeTime.AsTime().Add(interval.AsDuration())) - } + executionInfo := ctx.ExecutionInfo() var closeTime *timestamppb.Timestamp var executionDuration *durationpb.Duration if a.LifecycleState(ctx) != chasm.LifecycleStateRunning { - executionDuration = durationpb.New(ctx.ExecutionCloseTime().Sub(a.GetScheduleTime().AsTime())) - closeTime = timestamppb.New(ctx.ExecutionCloseTime()) + executionDuration = durationpb.New(executionInfo.CloseTime.Sub(a.GetScheduleTime().AsTime())) + closeTime = timestamppb.New(executionInfo.CloseTime) } var expirationTime *timestamppb.Timestamp @@ -682,7 +678,7 @@ func (a *Activity) buildActivityExecutionInfo(ctx chasm.Context) *apiactivitypb. LastHeartbeatTime: heartbeat.GetRecordedTime(), LastStartedTime: attempt.GetStartedTime(), LastWorkerIdentity: attempt.GetLastWorkerIdentity(), - NextAttemptScheduleTime: nextAttemptScheduleTime, + NextAttemptScheduleTime: attemptScheduleTimeForRetry(attempt), Priority: a.GetPriority(), RetryPolicy: a.GetRetryPolicy(), RunId: key.RunID, @@ -691,7 +687,7 @@ func (a *Activity) buildActivityExecutionInfo(ctx chasm.Context) *apiactivitypb. ScheduleToCloseTimeout: a.GetScheduleToCloseTimeout(), ScheduleToStartTimeout: a.GetScheduleToStartTimeout(), StartToCloseTimeout: a.GetStartToCloseTimeout(), - StateTransitionCount: ctx.StateTransitionCount(), + StateTransitionCount: executionInfo.StateTransitionCount, // TODO(saa-preview): StateSizeBytes? SearchAttributes: sa, Status: status, @@ -811,24 +807,26 @@ func (a *Activity) validateActivityTaskToken( return nil } -func enrichMetricsHandler( - a *Activity, - handler metrics.Handler, - namespaceName string, - operationTag string, - breakdownMetricsByTaskQueue dynamicconfig.TypedPropertyFnWithTaskQueueFilter[bool], -) metrics.Handler { +func (a *Activity) enrichMetricsHandler(ctx chasm.Context, operationTag string) (metrics.Handler, error) { + // activityContextFromChasm panics if the context value is missing; this is intentional and + // indicates a library registration bug rather than a runtime error. + actCtx := activityContextFromChasm(ctx) + namespaceName, err := actCtx.namespaceRegistry.GetNamespaceName(namespace.ID(ctx.ExecutionKey().NamespaceID)) + if err != nil { + return nil, err + } + breakdownMetricsByTaskQueue := actCtx.config.BreakdownMetricsByTaskQueue taskQueueFamily := a.GetTaskQueue().GetName() return metrics.GetPerTaskQueueFamilyScope( - handler, - namespaceName, - tqid.UnsafeTaskQueueFamily(namespaceName, taskQueueFamily), - breakdownMetricsByTaskQueue(namespaceName, taskQueueFamily, enumspb.TASK_QUEUE_TYPE_ACTIVITY), + ctx.MetricsHandler(), + namespaceName.String(), + tqid.UnsafeTaskQueueFamily(namespaceName.String(), taskQueueFamily), + breakdownMetricsByTaskQueue(namespaceName.String(), taskQueueFamily, enumspb.TASK_QUEUE_TYPE_ACTIVITY), metrics.OperationTag(operationTag), metrics.ActivityTypeTag(a.GetActivityType().GetName()), metrics.VersioningBehaviorTag(enumspb.VERSIONING_BEHAVIOR_UNSPECIFIED), metrics.WorkflowTypeTag(WorkflowTypeTag), - ) + ), nil } func (a *Activity) emitOnAttemptTimedOutMetrics(ctx chasm.Context, handler metrics.Handler, timeoutType enumspb.TimeoutType) { @@ -879,6 +877,14 @@ func (a *Activity) emitOnFailedMetrics(ctx chasm.Context, handler metrics.Handle metrics.ActivityFail.With(handler).Record(1) } +func (a *Activity) emitOnTerminatedMetrics( + handler metrics.Handler, +) { + // Terminated activities do not count as properly finished activities so we do not + // record any of the latency metrics. + metrics.ActivityTerminate.With(handler).Record(1) +} + func (a *Activity) emitOnCanceledMetrics( ctx chasm.Context, handler metrics.Handler, diff --git a/chasm/lib/activity/activity_tasks.go b/chasm/lib/activity/activity_tasks.go index e425e65d39b..60d7a61007e 100644 --- a/chasm/lib/activity/activity_tasks.go +++ b/chasm/lib/activity/activity_tasks.go @@ -7,29 +7,29 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity/gen/activitypb/v1" "go.temporal.io/server/common/metrics" - "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/resource" "go.temporal.io/server/common/util" "go.uber.org/fx" ) -type activityDispatchTaskExecutorOptions struct { +type activityDispatchTaskHandlerOptions struct { fx.In MatchingClient resource.MatchingClient } -type activityDispatchTaskExecutor struct { - opts activityDispatchTaskExecutorOptions +type activityDispatchTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*activitypb.ActivityDispatchTask] + opts activityDispatchTaskHandlerOptions } -func newActivityDispatchTaskExecutor(opts activityDispatchTaskExecutorOptions) *activityDispatchTaskExecutor { - return &activityDispatchTaskExecutor{ - opts, +func newActivityDispatchTaskHandler(opts activityDispatchTaskHandlerOptions) *activityDispatchTaskHandler { + return &activityDispatchTaskHandler{ + opts: opts, } } -func (e *activityDispatchTaskExecutor) Validate( +func (h *activityDispatchTaskHandler) Validate( ctx chasm.Context, activity *Activity, _ chasm.TaskAttributes, @@ -40,11 +40,29 @@ func (e *activityDispatchTaskExecutor) Validate( task.Stamp == activity.LastAttempt.Get(ctx).GetStamp()), nil } -func (e *activityDispatchTaskExecutor) Execute( +func (h *activityDispatchTaskHandler) Execute( ctx context.Context, activityRef chasm.ComponentRef, _ chasm.TaskAttributes, _ *activitypb.ActivityDispatchTask, +) error { + return h.pushToMatching(ctx, activityRef) +} + +// Discard spills the task to matching instead of silently discarding it on standby clusters when the activity +// dispatch task has been pending past the discard delay. +func (h *activityDispatchTaskHandler) Discard( + ctx context.Context, + activityRef chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *activitypb.ActivityDispatchTask, +) error { + return h.pushToMatching(ctx, activityRef) +} + +func (h *activityDispatchTaskHandler) pushToMatching( + ctx context.Context, + activityRef chasm.ComponentRef, ) error { request, err := chasm.ReadComponent( ctx, @@ -56,30 +74,20 @@ func (e *activityDispatchTaskExecutor) Execute( return err } - _, err = e.opts.MatchingClient.AddActivityTask(ctx, request) + _, err = h.opts.MatchingClient.AddActivityTask(ctx, request) return err } -type timeoutTaskExecutorOptions struct { - fx.In - - Config *Config - MetricsHandler metrics.Handler - NamespaceRegistry namespace.Registry -} - -type scheduleToStartTimeoutTaskExecutor struct { - opts timeoutTaskExecutorOptions +type scheduleToStartTimeoutTaskHandler struct { + chasm.PureTaskHandlerBase } -func newScheduleToStartTimeoutTaskExecutor(opts timeoutTaskExecutorOptions) *scheduleToStartTimeoutTaskExecutor { - return &scheduleToStartTimeoutTaskExecutor{ - opts, - } +func newScheduleToStartTimeoutTaskHandler() *scheduleToStartTimeoutTaskHandler { + return &scheduleToStartTimeoutTaskHandler{} } -func (e *scheduleToStartTimeoutTaskExecutor) Validate( +func (h *scheduleToStartTimeoutTaskHandler) Validate( ctx chasm.Context, activity *Activity, _ chasm.TaskAttributes, @@ -89,25 +97,17 @@ func (e *scheduleToStartTimeoutTaskExecutor) Validate( task.Stamp == activity.LastAttempt.Get(ctx).GetStamp()), nil } -func (e *scheduleToStartTimeoutTaskExecutor) Execute( +func (h *scheduleToStartTimeoutTaskHandler) Execute( ctx chasm.MutableContext, activity *Activity, _ chasm.TaskAttributes, _ *activitypb.ScheduleToStartTimeoutTask, ) error { - nsID := namespace.ID(ctx.ExecutionKey().NamespaceID) - namespaceName, err := e.opts.NamespaceRegistry.GetNamespaceName(nsID) + metricsHandler, err := activity.enrichMetricsHandler(ctx, metrics.TimerActiveTaskActivityTimeoutScope) if err != nil { return err } - metricsHandler := enrichMetricsHandler( - activity, - e.opts.MetricsHandler, - namespaceName.String(), - metrics.TimerActiveTaskActivityTimeoutScope, - e.opts.Config.BreakdownMetricsByTaskQueue) - event := timeoutEvent{ timeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_START, metricsHandler: metricsHandler, @@ -117,17 +117,13 @@ func (e *scheduleToStartTimeoutTaskExecutor) Execute( return TransitionTimedOut.Apply(activity, ctx, event) } -type scheduleToCloseTimeoutTaskExecutor struct { - opts timeoutTaskExecutorOptions -} +type scheduleToCloseTimeoutTaskHandler struct{ chasm.PureTaskHandlerBase } -func newScheduleToCloseTimeoutTaskExecutor(opts timeoutTaskExecutorOptions) *scheduleToCloseTimeoutTaskExecutor { - return &scheduleToCloseTimeoutTaskExecutor{ - opts, - } +func newScheduleToCloseTimeoutTaskHandler() *scheduleToCloseTimeoutTaskHandler { + return &scheduleToCloseTimeoutTaskHandler{} } -func (e *scheduleToCloseTimeoutTaskExecutor) Validate( +func (h *scheduleToCloseTimeoutTaskHandler) Validate( _ chasm.Context, activity *Activity, _ chasm.TaskAttributes, @@ -136,25 +132,16 @@ func (e *scheduleToCloseTimeoutTaskExecutor) Validate( return TransitionTimedOut.Possible(activity), nil } -func (e *scheduleToCloseTimeoutTaskExecutor) Execute( +func (h *scheduleToCloseTimeoutTaskHandler) Execute( ctx chasm.MutableContext, activity *Activity, _ chasm.TaskAttributes, _ *activitypb.ScheduleToCloseTimeoutTask, ) error { - nsID := namespace.ID(ctx.ExecutionKey().NamespaceID) - namespaceName, err := e.opts.NamespaceRegistry.GetNamespaceName(nsID) + metricsHandler, err := activity.enrichMetricsHandler(ctx, metrics.TimerActiveTaskActivityTimeoutScope) if err != nil { return err } - - metricsHandler := enrichMetricsHandler( - activity, - e.opts.MetricsHandler, - namespaceName.String(), - metrics.TimerActiveTaskActivityTimeoutScope, - e.opts.Config.BreakdownMetricsByTaskQueue) - event := timeoutEvent{ timeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, metricsHandler: metricsHandler, @@ -164,17 +151,13 @@ func (e *scheduleToCloseTimeoutTaskExecutor) Execute( return TransitionTimedOut.Apply(activity, ctx, event) } -type startToCloseTimeoutTaskExecutor struct { - opts timeoutTaskExecutorOptions -} +type startToCloseTimeoutTaskHandler struct{ chasm.PureTaskHandlerBase } -func newStartToCloseTimeoutTaskExecutor(opts timeoutTaskExecutorOptions) *startToCloseTimeoutTaskExecutor { - return &startToCloseTimeoutTaskExecutor{ - opts, - } +func newStartToCloseTimeoutTaskHandler() *startToCloseTimeoutTaskHandler { + return &startToCloseTimeoutTaskHandler{} } -func (e *startToCloseTimeoutTaskExecutor) Validate( +func (h *startToCloseTimeoutTaskHandler) Validate( ctx chasm.Context, activity *Activity, _ chasm.TaskAttributes, @@ -187,7 +170,7 @@ func (e *startToCloseTimeoutTaskExecutor) Validate( // Execute executes a StartToCloseTimeoutTask. It fails the attempt, leading to retry or activity // failure. -func (e *startToCloseTimeoutTaskExecutor) Execute( +func (h *startToCloseTimeoutTaskHandler) Execute( ctx chasm.MutableContext, activity *Activity, _ chasm.TaskAttributes, @@ -198,19 +181,11 @@ func (e *startToCloseTimeoutTaskExecutor) Execute( return err } - nsID := namespace.ID(ctx.ExecutionKey().NamespaceID) - namespaceName, err := e.opts.NamespaceRegistry.GetNamespaceName(nsID) + metricsHandler, err := activity.enrichMetricsHandler(ctx, metrics.TimerActiveTaskActivityTimeoutScope) if err != nil { return err } - metricsHandler := enrichMetricsHandler( - activity, - e.opts.MetricsHandler, - namespaceName.String(), - metrics.TimerActiveTaskActivityTimeoutScope, - e.opts.Config.BreakdownMetricsByTaskQueue) - if rescheduled { activity.emitOnAttemptTimedOutMetrics(ctx, metricsHandler, enumspb.TIMEOUT_TYPE_START_TO_CLOSE) @@ -225,18 +200,14 @@ func (e *startToCloseTimeoutTaskExecutor) Execute( } // HeartbeatTimeoutTask is a pure task that enforces heartbeat timeouts. -type heartbeatTimeoutTaskExecutor struct { - opts timeoutTaskExecutorOptions -} +type heartbeatTimeoutTaskHandler struct{ chasm.PureTaskHandlerBase } -func newHeartbeatTimeoutTaskExecutor(opts timeoutTaskExecutorOptions) *heartbeatTimeoutTaskExecutor { - return &heartbeatTimeoutTaskExecutor{ - opts, - } +func newHeartbeatTimeoutTaskHandler() *heartbeatTimeoutTaskHandler { + return &heartbeatTimeoutTaskHandler{} } // Validate validates a HeartbeatTimeoutTask. -func (e *heartbeatTimeoutTaskExecutor) Validate( +func (h *heartbeatTimeoutTaskHandler) Validate( ctx chasm.Context, activity *Activity, taskAttrs chasm.TaskAttributes, @@ -278,7 +249,7 @@ func (e *heartbeatTimeoutTaskExecutor) Validate( // Execute executes a HeartbeatTimeoutTask. It fails the attempt, leading to retry or activity // failure. -func (e *heartbeatTimeoutTaskExecutor) Execute( +func (h *heartbeatTimeoutTaskHandler) Execute( ctx chasm.MutableContext, activity *Activity, _ chasm.TaskAttributes, @@ -289,19 +260,11 @@ func (e *heartbeatTimeoutTaskExecutor) Execute( return err } - nsID := namespace.ID(ctx.ExecutionKey().NamespaceID) - namespaceName, err := e.opts.NamespaceRegistry.GetNamespaceName(nsID) + metricsHandler, err := activity.enrichMetricsHandler(ctx, metrics.TimerActiveTaskActivityTimeoutScope) if err != nil { return err } - metricsHandler := enrichMetricsHandler( - activity, - e.opts.MetricsHandler, - namespaceName.String(), - metrics.TimerActiveTaskActivityTimeoutScope, - e.opts.Config.BreakdownMetricsByTaskQueue) - if rescheduled { activity.emitOnAttemptTimedOutMetrics(ctx, metricsHandler, enumspb.TIMEOUT_TYPE_HEARTBEAT) return nil diff --git a/chasm/lib/activity/activity_test.go b/chasm/lib/activity/activity_test.go index c50a86d19c1..6dd31f3def3 100644 --- a/chasm/lib/activity/activity_test.go +++ b/chasm/lib/activity/activity_test.go @@ -1,6 +1,7 @@ package activity import ( + "context" "testing" "time" @@ -10,7 +11,10 @@ import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity/gen/activitypb/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/namespace" serviceerrors "go.temporal.io/server/common/serviceerror" + "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -154,3 +158,94 @@ func TestHandleStarted(t *testing.T) { }) } } + +func TestActivityTerminate(t *testing.T) { + testCases := []struct { + name string + activityStatus activitypb.ActivityExecutionStatus + expectErr string + }{ + { + name: "terminate scheduled activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_SCHEDULED, + }, + { + name: "terminate started activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_STARTED, + }, + { + name: "terminate cancel-requested activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_CANCEL_REQUESTED, + }, + { + name: "error on completed activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_COMPLETED, + expectErr: "invalid transition from Completed", + }, + { + name: "no-op on already terminated activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_TERMINATED, + }, + { + name: "error on failed activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_FAILED, + expectErr: "invalid transition from Failed", + }, + { + name: "error on timed out activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_TIMED_OUT, + expectErr: "invalid transition from TimedOut", + }, + { + name: "error on canceled activity", + activityStatus: activitypb.ACTIVITY_EXECUTION_STATUS_CANCELED, + expectErr: "invalid transition from Canceled", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + nsRegistry := namespace.NewMockRegistry(ctrl) + nsRegistry.EXPECT().GetNamespaceName(gomock.Any()).Return(namespace.Name("test-namespace"), nil).AnyTimes() + + ctx := &chasm.MockMutableContext{ + MockContext: chasm.MockContext{ + HandleNow: func(chasm.Component) time.Time { return defaultTime }, + GoCtx: context.WithValue(context.Background(), ctxKeyActivityContext, &activityContext{ + config: &Config{ + BreakdownMetricsByTaskQueue: dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(true), + }, + namespaceRegistry: nsRegistry, + }), + }, + } + + activity := &Activity{ + ActivityState: &activitypb.ActivityState{ + ActivityType: &commonpb.ActivityType{Name: "test-activity-type"}, + Status: tc.activityStatus, + TaskQueue: &taskqueuepb.TaskQueue{Name: "test-task-queue"}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Minute), + ScheduleToStartTimeout: durationpb.New(2 * time.Minute), + StartToCloseTimeout: durationpb.New(3 * time.Minute), + }, + LastAttempt: chasm.NewDataField(ctx, &activitypb.ActivityAttemptState{Count: 1}), + Outcome: chasm.NewDataField(ctx, &activitypb.ActivityOutcome{}), + } + + _, err := activity.Terminate(ctx, chasm.TerminateComponentRequest{ + Reason: "Delete activity execution", + }) + + if tc.expectErr != "" { + require.EqualError(t, err, tc.expectErr) + require.Equal(t, tc.activityStatus, activity.Status, "expected no state change on error") + } else { + require.NoError(t, err) + require.Equal(t, activitypb.ACTIVITY_EXECUTION_STATUS_TERMINATED, activity.Status) + } + }) + } +} diff --git a/chasm/lib/activity/frontend.go b/chasm/lib/activity/frontend.go index 30fcf325b98..f2633672037 100644 --- a/chasm/lib/activity/frontend.go +++ b/chasm/lib/activity/frontend.go @@ -175,13 +175,7 @@ func (h *frontendHandler) ListActivityExecutions( pageSize = maxPageSize } - namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) - if err != nil { - return nil, err - } - resp, err := chasm.ListExecutions[*Activity, *emptypb.Empty](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: namespaceID.String(), NamespaceName: req.GetNamespace(), PageSize: int(pageSize), NextPageToken: req.GetNextPageToken(), @@ -233,13 +227,7 @@ func (h *frontendHandler) CountActivityExecutions( return nil, ErrStandaloneActivityDisabled } - namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) - if err != nil { - return nil, err - } - resp, err := chasm.CountExecutions[*Activity](ctx, &chasm.CountExecutionsRequest{ - NamespaceID: namespaceID.String(), NamespaceName: req.GetNamespace(), Query: req.GetQuery(), }) @@ -261,6 +249,35 @@ func (h *frontendHandler) CountActivityExecutions( }, nil } +// DeleteActivityExecution terminates and schedules a standalone activity execution for deletion. +func (h *frontendHandler) DeleteActivityExecution( + ctx context.Context, + req *workflowservice.DeleteActivityExecutionRequest, +) (*workflowservice.DeleteActivityExecutionResponse, error) { + if !h.config.Enabled(req.GetNamespace()) { + return nil, ErrStandaloneActivityDisabled + } + + if err := validateAndNormalizeDeleteRequest(req, h.config.MaxIDLengthLimit()); err != nil { + return nil, err + } + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) + if err != nil { + return nil, err + } + + _, err = h.client.DeleteActivityExecution(ctx, &activitypb.DeleteActivityExecutionRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: req, + }) + if err != nil { + return nil, err + } + + return &workflowservice.DeleteActivityExecutionResponse{}, nil +} + // TerminateActivityExecution terminates a standalone activity execution func (h *frontendHandler) TerminateActivityExecution( ctx context.Context, @@ -276,13 +293,7 @@ func (h *frontendHandler) TerminateActivityExecution( return nil, err } - if req.GetRequestId() == "" { - // Since this mutates the request, we clone it first so that any retries use the original request. - req = common.CloneProto(req) - req.RequestId = uuid.NewString() - } - - if err := validateTerminateActivityExecutionRequest( + if err := validateAndNormalizeTerminateRequest( req, h.config.MaxIDLengthLimit(), h.config.BlobSizeLimitError, @@ -315,13 +326,7 @@ func (h *frontendHandler) RequestCancelActivityExecution( return nil, err } - if req.GetRequestId() == "" { - // Since this mutates the request, we clone it first so that any retries use the original request. - req = common.CloneProto(req) - req.RequestId = uuid.NewString() - } - - if err := validateRequestCancelActivityExecutionRequest( + if err := validateAndNormalizeCancelRequest( req, h.config.MaxIDLengthLimit(), h.config.BlobSizeLimitError, @@ -345,7 +350,12 @@ func (h *frontendHandler) validateAndPopulateStartRequest( req *workflowservice.StartActivityExecutionRequest, namespaceID namespace.ID, ) (*workflowservice.StartActivityExecutionRequest, error) { - // Since validation includes mutation of the request, we clone it first so that any retries use the original request. + // Since validation mutates the request, clone it first so that retries use the original + // request. However if the client did not set a request ID then set that before cloning so that + // retries use the same request ID. + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } req = common.CloneProto(req) activityType := req.ActivityType.GetName() @@ -354,7 +364,7 @@ func (h *frontendHandler) validateAndPopulateStartRequest( } opts := activityOptionsFromStartRequest(req) - err := ValidateAndNormalizeActivityAttributes( + err := ValidateAndNormalizeStandaloneActivity( req.ActivityId, activityType, h.config.DefaultActivityRetryPolicy, @@ -369,61 +379,20 @@ func (h *frontendHandler) validateAndPopulateStartRequest( } applyActivityOptionsToStartRequest(opts, req) - err = h.validateAndNormalizeStartActivityExecutionRequest(req) - if err != nil { - return nil, err - } - - return req, nil -} - -// validateAndNormalizeStartActivityExecutionRequest validates and normalizes the standalone -// activity specific attributes. Note that this method mutates the input params; the caller must -// clone the request if necessary (e.g. if it may be retried). -func (h *frontendHandler) validateAndNormalizeStartActivityExecutionRequest( - req *workflowservice.StartActivityExecutionRequest, -) error { - if req.GetRequestId() == "" { - req.RequestId = uuid.NewString() - } - - maxIDLengthLimit := h.config.MaxIDLengthLimit() - - if len(req.GetRequestId()) > maxIDLengthLimit { - return serviceerror.NewInvalidArgumentf("request ID exceeds length limit. Length=%d Limit=%d", - len(req.GetRequestId()), maxIDLengthLimit) - } - - if len(req.GetIdentity()) > maxIDLengthLimit { - return serviceerror.NewInvalidArgumentf("identity exceeds length limit. Length=%d Limit=%d", - len(req.GetIdentity()), maxIDLengthLimit) - } - - if err := normalizeAndValidateIDPolicy(req); err != nil { - return err - } - - if err := validateBlobSize( - req.GetActivityId(), - "StartActivityExecution", + err = validateAndNormalizeStartRequest( + req, + h.config.MaxIDLengthLimit(), h.config.BlobSizeLimitError, h.config.BlobSizeLimitWarn, - req.Input.Size(), h.logger, - req.GetNamespace()); err != nil { - return serviceerror.NewInvalidArgument("input exceeds length limit") - } - - if req.GetSearchAttributes() != nil { - if err := validateAndNormalizeSearchAttributes( - req, - h.saMapperProvider, - h.saValidator); err != nil { - return err - } + h.saMapperProvider, + h.saValidator, + ) + if err != nil { + return nil, err } - return nil + return req, nil } // activityOptionsFromStartRequest builds an ActivityOptions from the inlined fields diff --git a/chasm/lib/activity/frontend_test.go b/chasm/lib/activity/frontend_test.go new file mode 100644 index 00000000000..9dc13cda79c --- /dev/null +++ b/chasm/lib/activity/frontend_test.go @@ -0,0 +1,101 @@ +package activity + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + "google.golang.org/protobuf/types/known/durationpb" +) + +type hasRequestID interface { + GetRequestId() string +} + +// TestRequestIdStableAcrossRetries verifies that a request ID is re-used +// across retries, even if server-generated. +func TestRequestIdStableAcrossRetries(t *testing.T) { + h := &frontendHandler{ + config: &Config{ + BlobSizeLimitError: defaultBlobSizeLimitError, + BlobSizeLimitWarn: defaultBlobSizeLimitWarn, + MaxIDLengthLimit: func() int { return defaultMaxIDLengthLimit }, + DefaultActivityRetryPolicy: getDefaultRetrySettings, + }, + logger: log.NewNoopLogger(), + } + nsID := namespace.ID("test-namespace-id") + + newReq := func(requestId string) *workflowservice.StartActivityExecutionRequest { + return &workflowservice.StartActivityExecutionRequest{ + Namespace: "test-namespace", + ActivityId: "test-activity", + ActivityType: &commonpb.ActivityType{ + Name: "test-type", + }, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: "test-queue", + }, + StartToCloseTimeout: durationpb.New(time.Minute), + RequestId: requestId, + } + } + + // Simulate two RetryableInterceptor attempts: both call + // validateAndPopulateStartRequest with the same request pointer. + validateTwoAttempts := func(t *testing.T, req *workflowservice.StartActivityExecutionRequest) { + t.Helper() + clone1, err := h.validateAndPopulateStartRequest(req, nsID) + require.NoError(t, err) + require.NotEmpty(t, clone1.RequestId) + + clone2, err := h.validateAndPopulateStartRequest(req, nsID) + require.NoError(t, err) + require.Equal(t, clone1.RequestId, clone2.RequestId) + } + + // validateTwice calls validate twice and asserts the request ID is stable. + validateTwice := func(t *testing.T, req hasRequestID, validate func() error) { + t.Helper() + require.NoError(t, validate()) + require.NotEmpty(t, req.GetRequestId()) + firstID := req.GetRequestId() + require.NoError(t, validate()) + require.Equal(t, firstID, req.GetRequestId()) + } + + t.Run("start/server-generated", func(t *testing.T) { + validateTwoAttempts(t, newReq("")) + }) + + t.Run("start/client-provided", func(t *testing.T) { + validateTwoAttempts(t, newReq("my-request-id")) + }) + + t.Run("terminate/server-generated", func(t *testing.T) { + req := &workflowservice.TerminateActivityExecutionRequest{ + Namespace: "test-namespace", + ActivityId: "test-activity", + } + validateTwice(t, req, func() error { + return validateAndNormalizeTerminateRequest( + req, defaultMaxIDLengthLimit, defaultBlobSizeLimitError, defaultBlobSizeLimitWarn, log.NewNoopLogger()) + }) + }) + + t.Run("cancel/server-generated", func(t *testing.T) { + req := &workflowservice.RequestCancelActivityExecutionRequest{ + Namespace: "test-namespace", + ActivityId: "test-activity", + } + validateTwice(t, req, func() error { + return validateAndNormalizeCancelRequest( + req, defaultMaxIDLengthLimit, defaultBlobSizeLimitError, defaultBlobSizeLimitWarn, log.NewNoopLogger()) + }) + }) +} diff --git a/chasm/lib/activity/fx.go b/chasm/lib/activity/fx.go index 60deaec41d2..905042382c2 100644 --- a/chasm/lib/activity/fx.go +++ b/chasm/lib/activity/fx.go @@ -11,11 +11,11 @@ var HistoryModule = fx.Module( "activity-history", fx.Provide( ConfigProvider, - newActivityDispatchTaskExecutor, - newScheduleToStartTimeoutTaskExecutor, - newScheduleToCloseTimeoutTaskExecutor, - newStartToCloseTimeoutTaskExecutor, - newHeartbeatTimeoutTaskExecutor, + newActivityDispatchTaskHandler, + newScheduleToStartTimeoutTaskHandler, + newScheduleToCloseTimeoutTaskHandler, + newStartToCloseTimeoutTaskHandler, + newHeartbeatTimeoutTaskHandler, newHandler, newLibrary, ), @@ -30,9 +30,10 @@ var FrontendModule = fx.Module( fx.Provide(activitypb.NewActivityServiceLayeredClient), fx.Provide(NewFrontendHandler), fx.Provide(resource.SearchAttributeValidatorProvider), - fx.Invoke(func(registry *chasm.Registry) error { + fx.Provide(newComponentOnlyLibrary), + fx.Invoke(func(l *componentOnlyLibrary, registry *chasm.Registry) error { // Frontend needs to register the component in order to serialize ComponentRefs, but doesn't - // need task executors. - return registry.Register(newComponentOnlyLibrary()) + // need task handlers. + return registry.Register(l) }), ) diff --git a/chasm/lib/activity/gen/activitypb/v1/request_response.go-helpers.pb.go b/chasm/lib/activity/gen/activitypb/v1/request_response.go-helpers.pb.go index 63311278fa6..517287dc3e2 100644 --- a/chasm/lib/activity/gen/activitypb/v1/request_response.go-helpers.pb.go +++ b/chasm/lib/activity/gen/activitypb/v1/request_response.go-helpers.pb.go @@ -374,3 +374,77 @@ func (this *RequestCancelActivityExecutionResponse) Equal(that interface{}) bool return proto.Equal(this, that1) } + +// Marshal an object of type DeleteActivityExecutionRequest to the protobuf v3 wire format +func (val *DeleteActivityExecutionRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteActivityExecutionRequest from the protobuf v3 wire format +func (val *DeleteActivityExecutionRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteActivityExecutionRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteActivityExecutionRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeleteActivityExecutionRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteActivityExecutionRequest + switch t := that.(type) { + case *DeleteActivityExecutionRequest: + that1 = t + case DeleteActivityExecutionRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteActivityExecutionResponse to the protobuf v3 wire format +func (val *DeleteActivityExecutionResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteActivityExecutionResponse from the protobuf v3 wire format +func (val *DeleteActivityExecutionResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteActivityExecutionResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteActivityExecutionResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeleteActivityExecutionResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteActivityExecutionResponse + switch t := that.(type) { + case *DeleteActivityExecutionResponse: + that1 = t + case DeleteActivityExecutionResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/activity/gen/activitypb/v1/request_response.pb.go b/chasm/lib/activity/gen/activitypb/v1/request_response.pb.go index 44d947db73a..0407199486e 100644 --- a/chasm/lib/activity/gen/activitypb/v1/request_response.pb.go +++ b/chasm/lib/activity/gen/activitypb/v1/request_response.pb.go @@ -487,6 +487,94 @@ func (*RequestCancelActivityExecutionResponse) Descriptor() ([]byte, []int) { return file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDescGZIP(), []int{9} } +type DeleteActivityExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.DeleteActivityExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteActivityExecutionRequest) Reset() { + *x = DeleteActivityExecutionRequest{} + mi := &file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteActivityExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteActivityExecutionRequest) ProtoMessage() {} + +func (x *DeleteActivityExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteActivityExecutionRequest.ProtoReflect.Descriptor instead. +func (*DeleteActivityExecutionRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDescGZIP(), []int{10} +} + +func (x *DeleteActivityExecutionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DeleteActivityExecutionRequest) GetFrontendRequest() *v1.DeleteActivityExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DeleteActivityExecutionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteActivityExecutionResponse) Reset() { + *x = DeleteActivityExecutionResponse{} + mi := &file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteActivityExecutionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteActivityExecutionResponse) ProtoMessage() {} + +func (x *DeleteActivityExecutionResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteActivityExecutionResponse.ProtoReflect.Descriptor instead. +func (*DeleteActivityExecutionResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDescGZIP(), []int{11} +} + var File_temporal_server_chasm_lib_activity_proto_v1_request_response_proto protoreflect.FileDescriptor const file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDesc = "" + @@ -514,7 +602,11 @@ const file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_ra "%RequestCancelActivityExecutionRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12q\n" + "\x10frontend_request\x18\x02 \x01(\v2F.temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequestR\x0ffrontendRequest\"(\n" + - "&RequestCancelActivityExecutionResponseBDZBgo.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypbb\x06proto3" + "&RequestCancelActivityExecutionResponse\"\xaf\x01\n" + + "\x1eDeleteActivityExecutionRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12j\n" + + "\x10frontend_request\x18\x02 \x01(\v2?.temporal.api.workflowservice.v1.DeleteActivityExecutionRequestR\x0ffrontendRequest\"!\n" + + "\x1fDeleteActivityExecutionResponseBDZBgo.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypbb\x06proto3" var ( file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDescOnce sync.Once @@ -528,7 +620,7 @@ func file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_raw return file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDescData } -var file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_goTypes = []any{ (*StartActivityExecutionRequest)(nil), // 0: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest (*StartActivityExecutionResponse)(nil), // 1: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse @@ -540,29 +632,33 @@ var file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_goTy (*TerminateActivityExecutionResponse)(nil), // 7: temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionResponse (*RequestCancelActivityExecutionRequest)(nil), // 8: temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest (*RequestCancelActivityExecutionResponse)(nil), // 9: temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionResponse - (*v1.StartActivityExecutionRequest)(nil), // 10: temporal.api.workflowservice.v1.StartActivityExecutionRequest - (*v1.StartActivityExecutionResponse)(nil), // 11: temporal.api.workflowservice.v1.StartActivityExecutionResponse - (*v1.DescribeActivityExecutionRequest)(nil), // 12: temporal.api.workflowservice.v1.DescribeActivityExecutionRequest - (*v1.DescribeActivityExecutionResponse)(nil), // 13: temporal.api.workflowservice.v1.DescribeActivityExecutionResponse - (*v1.PollActivityExecutionRequest)(nil), // 14: temporal.api.workflowservice.v1.PollActivityExecutionRequest - (*v1.PollActivityExecutionResponse)(nil), // 15: temporal.api.workflowservice.v1.PollActivityExecutionResponse - (*v1.TerminateActivityExecutionRequest)(nil), // 16: temporal.api.workflowservice.v1.TerminateActivityExecutionRequest - (*v1.RequestCancelActivityExecutionRequest)(nil), // 17: temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest + (*DeleteActivityExecutionRequest)(nil), // 10: temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionRequest + (*DeleteActivityExecutionResponse)(nil), // 11: temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionResponse + (*v1.StartActivityExecutionRequest)(nil), // 12: temporal.api.workflowservice.v1.StartActivityExecutionRequest + (*v1.StartActivityExecutionResponse)(nil), // 13: temporal.api.workflowservice.v1.StartActivityExecutionResponse + (*v1.DescribeActivityExecutionRequest)(nil), // 14: temporal.api.workflowservice.v1.DescribeActivityExecutionRequest + (*v1.DescribeActivityExecutionResponse)(nil), // 15: temporal.api.workflowservice.v1.DescribeActivityExecutionResponse + (*v1.PollActivityExecutionRequest)(nil), // 16: temporal.api.workflowservice.v1.PollActivityExecutionRequest + (*v1.PollActivityExecutionResponse)(nil), // 17: temporal.api.workflowservice.v1.PollActivityExecutionResponse + (*v1.TerminateActivityExecutionRequest)(nil), // 18: temporal.api.workflowservice.v1.TerminateActivityExecutionRequest + (*v1.RequestCancelActivityExecutionRequest)(nil), // 19: temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest + (*v1.DeleteActivityExecutionRequest)(nil), // 20: temporal.api.workflowservice.v1.DeleteActivityExecutionRequest } var file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_depIdxs = []int32{ - 10, // 0: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.StartActivityExecutionRequest - 11, // 1: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.StartActivityExecutionResponse - 12, // 2: temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DescribeActivityExecutionRequest - 13, // 3: temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DescribeActivityExecutionResponse - 14, // 4: temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PollActivityExecutionRequest - 15, // 5: temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.PollActivityExecutionResponse - 16, // 6: temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.TerminateActivityExecutionRequest - 17, // 7: temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 12, // 0: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.StartActivityExecutionRequest + 13, // 1: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.StartActivityExecutionResponse + 14, // 2: temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DescribeActivityExecutionRequest + 15, // 3: temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DescribeActivityExecutionResponse + 16, // 4: temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PollActivityExecutionRequest + 17, // 5: temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.PollActivityExecutionResponse + 18, // 6: temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.TerminateActivityExecutionRequest + 19, // 7: temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest + 20, // 8: temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DeleteActivityExecutionRequest + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_init() } @@ -576,7 +672,7 @@ func file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_ini GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_activity_proto_v1_request_response_proto_rawDesc)), NumEnums: 0, - NumMessages: 10, + NumMessages: 12, NumExtensions: 0, NumServices: 0, }, diff --git a/chasm/lib/activity/gen/activitypb/v1/service.pb.go b/chasm/lib/activity/gen/activitypb/v1/service.pb.go index 2b58d3ec649..c33425c6393 100644 --- a/chasm/lib/activity/gen/activitypb/v1/service.pb.go +++ b/chasm/lib/activity/gen/activitypb/v1/service.pb.go @@ -10,6 +10,7 @@ import ( reflect "reflect" unsafe "unsafe" + _ "go.temporal.io/server/api/common/v1" _ "go.temporal.io/server/api/routing/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -26,13 +27,15 @@ var File_temporal_server_chasm_lib_activity_proto_v1_service_proto protoreflect. const file_temporal_server_chasm_lib_activity_proto_v1_service_proto_rawDesc = "" + "\n" + - "9temporal/server/chasm/lib/activity/proto/v1/service.proto\x12+temporal.server.chasm.lib.activity.proto.v1\x1aBtemporal/server/chasm/lib/activity/proto/v1/request_response.proto\x1a.temporal/server/api/routing/v1/extension.proto2\xf3\b\n" + - "\x0fActivityService\x12\xd5\x01\n" + - "\x16StartActivityExecution\x12J.temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest\x1aK.temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x12\xde\x01\n" + - "\x19DescribeActivityExecution\x12M.temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionRequest\x1aN.temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x12\xd2\x01\n" + - "\x15PollActivityExecution\x12I.temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionRequest\x1aJ.temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x12\xe1\x01\n" + - "\x1aTerminateActivityExecution\x12N.temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionRequest\x1aO.temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x12\xed\x01\n" + - "\x1eRequestCancelActivityExecution\x12R.temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest\x1aS.temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_idBDZBgo.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypbb\x06proto3" + "9temporal/server/chasm/lib/activity/proto/v1/service.proto\x12+temporal.server.chasm.lib.activity.proto.v1\x1aBtemporal/server/chasm/lib/activity/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\xf2\n" + + "\n" + + "\x0fActivityService\x12\xdb\x01\n" + + "\x16StartActivityExecution\x12J.temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest\x1aK.temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x8a\xb5\x18\x02\b\x01\x12\xe4\x01\n" + + "\x19DescribeActivityExecution\x12M.temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionRequest\x1aN.temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x8a\xb5\x18\x02\b\x01\x12\xd8\x01\n" + + "\x15PollActivityExecution\x12I.temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionRequest\x1aJ.temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x8a\xb5\x18\x02\b\x02\x12\xe7\x01\n" + + "\x1aTerminateActivityExecution\x12N.temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionRequest\x1aO.temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x8a\xb5\x18\x02\b\x01\x12\xf3\x01\n" + + "\x1eRequestCancelActivityExecution\x12R.temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest\x1aS.temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x8a\xb5\x18\x02\b\x01\x12\xde\x01\n" + + "\x17DeleteActivityExecution\x12K.temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionRequest\x1aL.temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.activity_id\x8a\xb5\x18\x02\b\x01BDZBgo.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypbb\x06proto3" var file_temporal_server_chasm_lib_activity_proto_v1_service_proto_goTypes = []any{ (*StartActivityExecutionRequest)(nil), // 0: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest @@ -40,28 +43,32 @@ var file_temporal_server_chasm_lib_activity_proto_v1_service_proto_goTypes = []a (*PollActivityExecutionRequest)(nil), // 2: temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionRequest (*TerminateActivityExecutionRequest)(nil), // 3: temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionRequest (*RequestCancelActivityExecutionRequest)(nil), // 4: temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest - (*StartActivityExecutionResponse)(nil), // 5: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse - (*DescribeActivityExecutionResponse)(nil), // 6: temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse - (*PollActivityExecutionResponse)(nil), // 7: temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse - (*TerminateActivityExecutionResponse)(nil), // 8: temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionResponse - (*RequestCancelActivityExecutionResponse)(nil), // 9: temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionResponse + (*DeleteActivityExecutionRequest)(nil), // 5: temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionRequest + (*StartActivityExecutionResponse)(nil), // 6: temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse + (*DescribeActivityExecutionResponse)(nil), // 7: temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse + (*PollActivityExecutionResponse)(nil), // 8: temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse + (*TerminateActivityExecutionResponse)(nil), // 9: temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionResponse + (*RequestCancelActivityExecutionResponse)(nil), // 10: temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionResponse + (*DeleteActivityExecutionResponse)(nil), // 11: temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionResponse } var file_temporal_server_chasm_lib_activity_proto_v1_service_proto_depIdxs = []int32{ - 0, // 0: temporal.server.chasm.lib.activity.proto.v1.ActivityService.StartActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest - 1, // 1: temporal.server.chasm.lib.activity.proto.v1.ActivityService.DescribeActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionRequest - 2, // 2: temporal.server.chasm.lib.activity.proto.v1.ActivityService.PollActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionRequest - 3, // 3: temporal.server.chasm.lib.activity.proto.v1.ActivityService.TerminateActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionRequest - 4, // 4: temporal.server.chasm.lib.activity.proto.v1.ActivityService.RequestCancelActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest - 5, // 5: temporal.server.chasm.lib.activity.proto.v1.ActivityService.StartActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse - 6, // 6: temporal.server.chasm.lib.activity.proto.v1.ActivityService.DescribeActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse - 7, // 7: temporal.server.chasm.lib.activity.proto.v1.ActivityService.PollActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse - 8, // 8: temporal.server.chasm.lib.activity.proto.v1.ActivityService.TerminateActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionResponse - 9, // 9: temporal.server.chasm.lib.activity.proto.v1.ActivityService.RequestCancelActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionResponse - 5, // [5:10] is the sub-list for method output_type - 0, // [0:5] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 0, // 0: temporal.server.chasm.lib.activity.proto.v1.ActivityService.StartActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionRequest + 1, // 1: temporal.server.chasm.lib.activity.proto.v1.ActivityService.DescribeActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionRequest + 2, // 2: temporal.server.chasm.lib.activity.proto.v1.ActivityService.PollActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionRequest + 3, // 3: temporal.server.chasm.lib.activity.proto.v1.ActivityService.TerminateActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionRequest + 4, // 4: temporal.server.chasm.lib.activity.proto.v1.ActivityService.RequestCancelActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionRequest + 5, // 5: temporal.server.chasm.lib.activity.proto.v1.ActivityService.DeleteActivityExecution:input_type -> temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionRequest + 6, // 6: temporal.server.chasm.lib.activity.proto.v1.ActivityService.StartActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.StartActivityExecutionResponse + 7, // 7: temporal.server.chasm.lib.activity.proto.v1.ActivityService.DescribeActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.DescribeActivityExecutionResponse + 8, // 8: temporal.server.chasm.lib.activity.proto.v1.ActivityService.PollActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.PollActivityExecutionResponse + 9, // 9: temporal.server.chasm.lib.activity.proto.v1.ActivityService.TerminateActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.TerminateActivityExecutionResponse + 10, // 10: temporal.server.chasm.lib.activity.proto.v1.ActivityService.RequestCancelActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.RequestCancelActivityExecutionResponse + 11, // 11: temporal.server.chasm.lib.activity.proto.v1.ActivityService.DeleteActivityExecution:output_type -> temporal.server.chasm.lib.activity.proto.v1.DeleteActivityExecutionResponse + 6, // [6:12] is the sub-list for method output_type + 0, // [0:6] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_activity_proto_v1_service_proto_init() } diff --git a/chasm/lib/activity/gen/activitypb/v1/service_client.pb.go b/chasm/lib/activity/gen/activitypb/v1/service_client.pb.go index cef8a2b47ed..b1d80f018f1 100644 --- a/chasm/lib/activity/gen/activitypb/v1/service_client.pb.go +++ b/chasm/lib/activity/gen/activitypb/v1/service_client.pb.go @@ -273,3 +273,46 @@ func (c *ActivityServiceLayeredClient) RequestCancelActivityExecution( } return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) } +func (c *ActivityServiceLayeredClient) callDeleteActivityExecutionNoRetry( + ctx context.Context, + request *DeleteActivityExecutionRequest, + opts ...grpc.CallOption, +) (*DeleteActivityExecutionResponse, error) { + var response *DeleteActivityExecutionResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("ActivityService.DeleteActivityExecution"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetActivityId(), c.numShards) + op := func(ctx context.Context, client ActivityServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DeleteActivityExecution(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ActivityServiceLayeredClient) DeleteActivityExecution( + ctx context.Context, + request *DeleteActivityExecutionRequest, + opts ...grpc.CallOption, +) (*DeleteActivityExecutionResponse, error) { + call := func(ctx context.Context) (*DeleteActivityExecutionResponse, error) { + return c.callDeleteActivityExecutionNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} diff --git a/chasm/lib/activity/gen/activitypb/v1/service_grpc.pb.go b/chasm/lib/activity/gen/activitypb/v1/service_grpc.pb.go index bc90af38727..f02184fbd40 100644 --- a/chasm/lib/activity/gen/activitypb/v1/service_grpc.pb.go +++ b/chasm/lib/activity/gen/activitypb/v1/service_grpc.pb.go @@ -25,6 +25,7 @@ const ( ActivityService_PollActivityExecution_FullMethodName = "/temporal.server.chasm.lib.activity.proto.v1.ActivityService/PollActivityExecution" ActivityService_TerminateActivityExecution_FullMethodName = "/temporal.server.chasm.lib.activity.proto.v1.ActivityService/TerminateActivityExecution" ActivityService_RequestCancelActivityExecution_FullMethodName = "/temporal.server.chasm.lib.activity.proto.v1.ActivityService/RequestCancelActivityExecution" + ActivityService_DeleteActivityExecution_FullMethodName = "/temporal.server.chasm.lib.activity.proto.v1.ActivityService/DeleteActivityExecution" ) // ActivityServiceClient is the client API for ActivityService service. @@ -36,6 +37,7 @@ type ActivityServiceClient interface { PollActivityExecution(ctx context.Context, in *PollActivityExecutionRequest, opts ...grpc.CallOption) (*PollActivityExecutionResponse, error) TerminateActivityExecution(ctx context.Context, in *TerminateActivityExecutionRequest, opts ...grpc.CallOption) (*TerminateActivityExecutionResponse, error) RequestCancelActivityExecution(ctx context.Context, in *RequestCancelActivityExecutionRequest, opts ...grpc.CallOption) (*RequestCancelActivityExecutionResponse, error) + DeleteActivityExecution(ctx context.Context, in *DeleteActivityExecutionRequest, opts ...grpc.CallOption) (*DeleteActivityExecutionResponse, error) } type activityServiceClient struct { @@ -91,6 +93,15 @@ func (c *activityServiceClient) RequestCancelActivityExecution(ctx context.Conte return out, nil } +func (c *activityServiceClient) DeleteActivityExecution(ctx context.Context, in *DeleteActivityExecutionRequest, opts ...grpc.CallOption) (*DeleteActivityExecutionResponse, error) { + out := new(DeleteActivityExecutionResponse) + err := c.cc.Invoke(ctx, ActivityService_DeleteActivityExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ActivityServiceServer is the server API for ActivityService service. // All implementations must embed UnimplementedActivityServiceServer // for forward compatibility @@ -100,6 +111,7 @@ type ActivityServiceServer interface { PollActivityExecution(context.Context, *PollActivityExecutionRequest) (*PollActivityExecutionResponse, error) TerminateActivityExecution(context.Context, *TerminateActivityExecutionRequest) (*TerminateActivityExecutionResponse, error) RequestCancelActivityExecution(context.Context, *RequestCancelActivityExecutionRequest) (*RequestCancelActivityExecutionResponse, error) + DeleteActivityExecution(context.Context, *DeleteActivityExecutionRequest) (*DeleteActivityExecutionResponse, error) mustEmbedUnimplementedActivityServiceServer() } @@ -122,6 +134,9 @@ func (UnimplementedActivityServiceServer) TerminateActivityExecution(context.Con func (UnimplementedActivityServiceServer) RequestCancelActivityExecution(context.Context, *RequestCancelActivityExecutionRequest) (*RequestCancelActivityExecutionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RequestCancelActivityExecution not implemented") } +func (UnimplementedActivityServiceServer) DeleteActivityExecution(context.Context, *DeleteActivityExecutionRequest) (*DeleteActivityExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteActivityExecution not implemented") +} func (UnimplementedActivityServiceServer) mustEmbedUnimplementedActivityServiceServer() {} // UnsafeActivityServiceServer may be embedded to opt out of forward compatibility for this service. @@ -225,6 +240,24 @@ func _ActivityService_RequestCancelActivityExecution_Handler(srv interface{}, ct return interceptor(ctx, in, info, handler) } +func _ActivityService_DeleteActivityExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteActivityExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ActivityServiceServer).DeleteActivityExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ActivityService_DeleteActivityExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ActivityServiceServer).DeleteActivityExecution(ctx, req.(*DeleteActivityExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ActivityService_ServiceDesc is the grpc.ServiceDesc for ActivityService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -252,6 +285,10 @@ var ActivityService_ServiceDesc = grpc.ServiceDesc{ MethodName: "RequestCancelActivityExecution", Handler: _ActivityService_RequestCancelActivityExecution_Handler, }, + { + MethodName: "DeleteActivityExecution", + Handler: _ActivityService_DeleteActivityExecution_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "temporal/server/chasm/lib/activity/proto/v1/service.proto", diff --git a/chasm/lib/activity/handler.go b/chasm/lib/activity/handler.go index 9935c26687f..f97a0f1a9a0 100644 --- a/chasm/lib/activity/handler.go +++ b/chasm/lib/activity/handler.go @@ -119,6 +119,11 @@ func (h *handler) DescribeActivityExecution( RunID: req.GetFrontendRequest().GetRunId(), }) + token := req.GetFrontendRequest().GetLongPollToken() + if len(token) == 0 { + return chasm.ReadComponent(ctx, ref, (*Activity).buildDescribeActivityExecutionResponse, req) + } + // Below, we send an empty non-error response on context deadline expiry. Here we compute a // deadline that causes us to send that response before the caller's own deadline (see // chasm.activity.longPollBuffer). We also cap the caller's deadline at @@ -131,10 +136,6 @@ func (h *handler) DescribeActivityExecution( ) defer cancel() - token := req.GetFrontendRequest().GetLongPollToken() - if len(token) == 0 { - return chasm.ReadComponent(ctx, ref, (*Activity).buildDescribeActivityExecutionResponse, req, nil) - } response, _, err = chasm.PollComponent(ctx, ref, func( a *Activity, ctx chasm.Context, @@ -214,11 +215,35 @@ func (h *handler) PollActivityExecution( return response, err } +// DeleteActivityExecution terminates the activity if running, then schedules it for deletion. +func (h *handler) DeleteActivityExecution( + ctx context.Context, + req *activitypb.DeleteActivityExecutionRequest, +) (*activitypb.DeleteActivityExecutionResponse, error) { + frontendReq := req.GetFrontendRequest() + + key := chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: frontendReq.GetActivityId(), + RunID: frontendReq.GetRunId(), + } + + if err := chasm.DeleteExecution[*Activity](ctx, key, chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: "Delete activity execution", + }, + }); err != nil { + return nil, err + } + + return &activitypb.DeleteActivityExecutionResponse{}, nil +} + // TerminateActivityExecution terminates an activity execution. func (h *handler) TerminateActivityExecution( ctx context.Context, req *activitypb.TerminateActivityExecutionRequest, -) (response *activitypb.TerminateActivityExecutionResponse, err error) { +) (*activitypb.TerminateActivityExecutionResponse, error) { frontendReq := req.GetFrontendRequest() ref := chasm.NewComponentRef[*Activity](chasm.ExecutionKey{ @@ -227,22 +252,14 @@ func (h *handler) TerminateActivityExecution( RunID: frontendReq.GetRunId(), }) - namespaceName, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(req.GetNamespaceId())) - if err != nil { - return nil, err - } - - response, _, err = chasm.UpdateComponent( + _, _, err := chasm.UpdateComponent( ctx, ref, - (*Activity).handleTerminated, - terminateEvent{ - request: req, - MetricsHandlerBuilderParams: MetricsHandlerBuilderParams{ - Handler: h.metricsHandler, - NamespaceName: namespaceName.String(), - BreakdownMetricsByTaskQueue: h.config.BreakdownMetricsByTaskQueue, - }, + (*Activity).Terminate, + chasm.TerminateComponentRequest{ + Reason: frontendReq.GetReason(), + Identity: frontendReq.GetIdentity(), + RequestID: frontendReq.GetRequestId(), }, ) @@ -250,7 +267,7 @@ func (h *handler) TerminateActivityExecution( return nil, err } - return response, nil + return &activitypb.TerminateActivityExecutionResponse{}, nil } // RequestCancelActivityExecution requests cancellation of an activity execution. @@ -266,23 +283,11 @@ func (h *handler) RequestCancelActivityExecution( RunID: frontendReq.GetRunId(), }) - namespaceName, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(req.GetNamespaceId())) - if err != nil { - return nil, err - } - response, _, err = chasm.UpdateComponent( ctx, ref, (*Activity).handleCancellationRequested, - requestCancelEvent{ - request: req, - MetricsHandlerBuilderParams: MetricsHandlerBuilderParams{ - Handler: h.metricsHandler, - NamespaceName: namespaceName.String(), - BreakdownMetricsByTaskQueue: h.config.BreakdownMetricsByTaskQueue, - }, - }, + req, ) if err != nil { return nil, err diff --git a/chasm/lib/activity/library.go b/chasm/lib/activity/library.go index a84df335988..83e3d9067af 100644 --- a/chasm/lib/activity/library.go +++ b/chasm/lib/activity/library.go @@ -3,11 +3,25 @@ package activity import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity/gen/activitypb/v1" + "go.temporal.io/server/common/namespace" "google.golang.org/grpc" ) -type componentOnlyLibrary struct { - chasm.UnimplementedLibrary +type ctxKeyActivityContextType struct{} + +var ctxKeyActivityContext = ctxKeyActivityContextType{} + +// activityContext holds dependencies injected into the chasm.Context for use by Activity methods. +type activityContext struct { + config *Config + namespaceRegistry namespace.Registry +} + +// activityContextFromChasm extracts the activityContext from a chasm.Context. +// Panics if the context value is missing, which indicates a library registration bug. +func activityContextFromChasm(ctx chasm.Context) *activityContext { + //nolint:revive // unchecked-type-assertion: intentional panic on missing context value + return ctx.Value(ctxKeyActivityContext).(*activityContext) } const ( @@ -20,8 +34,20 @@ var ( ArchetypeID = chasm.GenerateTypeID(Archetype) ) -func newComponentOnlyLibrary() *componentOnlyLibrary { - return &componentOnlyLibrary{} +type componentOnlyLibrary struct { + chasm.UnimplementedLibrary + config *Config + namespaceRegistry namespace.Registry +} + +func newComponentOnlyLibrary( + config *Config, + namespaceRegistry namespace.Registry, +) *componentOnlyLibrary { + return &componentOnlyLibrary{ + config: config, + namespaceRegistry: namespaceRegistry, + } } func (l *componentOnlyLibrary) Name() string { @@ -38,6 +64,12 @@ func (l *componentOnlyLibrary) Components() []*chasm.RegistrableComponent { chasm.SearchAttributeTaskQueue, ), chasm.WithBusinessIDAlias("ActivityId"), + chasm.WithContextValues(map[any]any{ + ctxKeyActivityContext: &activityContext{ + config: l.config, + namespaceRegistry: l.namespaceRegistry, + }, + }), ), } } @@ -45,29 +77,32 @@ func (l *componentOnlyLibrary) Components() []*chasm.RegistrableComponent { type library struct { componentOnlyLibrary - handler *handler - activityDispatchTaskExecutor *activityDispatchTaskExecutor - scheduleToStartTimeoutTaskExecutor *scheduleToStartTimeoutTaskExecutor - scheduleToCloseTimeoutTaskExecutor *scheduleToCloseTimeoutTaskExecutor - startToCloseTimeoutTaskExecutor *startToCloseTimeoutTaskExecutor - heartbeatTimeoutTaskExecutor *heartbeatTimeoutTaskExecutor + handler *handler + activityDispatchTaskHandler *activityDispatchTaskHandler + scheduleToStartTimeoutTaskHandler *scheduleToStartTimeoutTaskHandler + scheduleToCloseTimeoutTaskHandler *scheduleToCloseTimeoutTaskHandler + startToCloseTimeoutTaskHandler *startToCloseTimeoutTaskHandler + heartbeatTimeoutTaskHandler *heartbeatTimeoutTaskHandler } func newLibrary( handler *handler, - activityDispatchTaskExecutor *activityDispatchTaskExecutor, - scheduleToStartTimeoutTaskExecutor *scheduleToStartTimeoutTaskExecutor, - scheduleToCloseTimeoutTaskExecutor *scheduleToCloseTimeoutTaskExecutor, - startToCloseTimeoutTaskExecutor *startToCloseTimeoutTaskExecutor, - heartbeatTimeoutTaskExecutor *heartbeatTimeoutTaskExecutor, + activityDispatchTaskHandler *activityDispatchTaskHandler, + scheduleToStartTimeoutTaskHandler *scheduleToStartTimeoutTaskHandler, + scheduleToCloseTimeoutTaskHandler *scheduleToCloseTimeoutTaskHandler, + startToCloseTimeoutTaskHandler *startToCloseTimeoutTaskHandler, + heartbeatTimeoutTaskHandler *heartbeatTimeoutTaskHandler, + config *Config, + namespaceRegistry namespace.Registry, ) *library { return &library{ - handler: handler, - activityDispatchTaskExecutor: activityDispatchTaskExecutor, - scheduleToStartTimeoutTaskExecutor: scheduleToStartTimeoutTaskExecutor, - scheduleToCloseTimeoutTaskExecutor: scheduleToCloseTimeoutTaskExecutor, - startToCloseTimeoutTaskExecutor: startToCloseTimeoutTaskExecutor, - heartbeatTimeoutTaskExecutor: heartbeatTimeoutTaskExecutor, + componentOnlyLibrary: *newComponentOnlyLibrary(config, namespaceRegistry), + handler: handler, + activityDispatchTaskHandler: activityDispatchTaskHandler, + scheduleToStartTimeoutTaskHandler: scheduleToStartTimeoutTaskHandler, + scheduleToCloseTimeoutTaskHandler: scheduleToCloseTimeoutTaskHandler, + startToCloseTimeoutTaskHandler: startToCloseTimeoutTaskHandler, + heartbeatTimeoutTaskHandler: heartbeatTimeoutTaskHandler, } } @@ -79,28 +114,23 @@ func (l *library) Tasks() []*chasm.RegistrableTask { return []*chasm.RegistrableTask{ chasm.NewRegistrableSideEffectTask( "dispatch", - l.activityDispatchTaskExecutor, - l.activityDispatchTaskExecutor, + l.activityDispatchTaskHandler, ), chasm.NewRegistrablePureTask( "scheduleToStartTimer", - l.scheduleToStartTimeoutTaskExecutor, - l.scheduleToStartTimeoutTaskExecutor, + l.scheduleToStartTimeoutTaskHandler, ), chasm.NewRegistrablePureTask( "scheduleToCloseTimer", - l.scheduleToCloseTimeoutTaskExecutor, - l.scheduleToCloseTimeoutTaskExecutor, + l.scheduleToCloseTimeoutTaskHandler, ), chasm.NewRegistrablePureTask( "startToCloseTimer", - l.startToCloseTimeoutTaskExecutor, - l.startToCloseTimeoutTaskExecutor, + l.startToCloseTimeoutTaskHandler, ), chasm.NewRegistrablePureTask( "heartbeatTimer", - l.heartbeatTimeoutTaskExecutor, - l.heartbeatTimeoutTaskExecutor, + l.heartbeatTimeoutTaskHandler, ), } } diff --git a/chasm/lib/activity/proto/v1/activity_state.proto b/chasm/lib/activity/proto/v1/activity_state.proto index 85809f59b57..00ebee8b9e7 100644 --- a/chasm/lib/activity/proto/v1/activity_state.proto +++ b/chasm/lib/activity/proto/v1/activity_state.proto @@ -2,8 +2,6 @@ syntax = "proto3"; package temporal.server.chasm.lib.activity.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypb"; - import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; @@ -12,91 +10,93 @@ import "temporal/api/failure/v1/message.proto"; import "temporal/api/sdk/v1/user_metadata.proto"; import "temporal/api/taskqueue/v1/message.proto"; +option go_package = "go.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypb"; + enum ActivityExecutionStatus { - ACTIVITY_EXECUTION_STATUS_UNSPECIFIED = 0; - // The activity has been scheduled, but a worker has not accepted the task for the current - // attempt. The activity may be backing off between attempts or waiting for a worker to pick it - // up. - ACTIVITY_EXECUTION_STATUS_SCHEDULED = 1; - // A worker has accepted a task for the current attempt. - ACTIVITY_EXECUTION_STATUS_STARTED = 2; - // A caller has requested cancellation of the activity. - ACTIVITY_EXECUTION_STATUS_CANCEL_REQUESTED = 3; - // The activity completed successfully. - ACTIVITY_EXECUTION_STATUS_COMPLETED = 4; - // The activity completed with failure. - ACTIVITY_EXECUTION_STATUS_FAILED = 5; - // The activity completed as canceled. - // Requesting to cancel an activity does not automatically transition the activity to canceled status. If the worker - // responds to cancel the activity after requesting cancellation, the status will transition to cancelled. If the - // activity completes, fails, times out or terminates after cancel is requested and before the worker responds with - // cancelled. The activity will be stay in the terminal non-cancelled status. - ACTIVITY_EXECUTION_STATUS_CANCELED = 6; - // The activity was terminated. Termination does not reach the worker and the activity code cannot react to it. - // A terminated activity may have a running attempt and will be requested to be canceled by the server when it - // heartbeats. - ACTIVITY_EXECUTION_STATUS_TERMINATED = 7; - // The activity has timed out by reaching the specified schedule-to-start or schedule-to-close timeouts. - // Additionally, after all retries are exhausted for start-to-close or heartbeat timeouts, the activity will also - // transition to timed out status. - ACTIVITY_EXECUTION_STATUS_TIMED_OUT = 8; + ACTIVITY_EXECUTION_STATUS_UNSPECIFIED = 0; + // The activity has been scheduled, but a worker has not accepted the task for the current + // attempt. The activity may be backing off between attempts or waiting for a worker to pick it + // up. + ACTIVITY_EXECUTION_STATUS_SCHEDULED = 1; + // A worker has accepted a task for the current attempt. + ACTIVITY_EXECUTION_STATUS_STARTED = 2; + // A caller has requested cancellation of the activity. + ACTIVITY_EXECUTION_STATUS_CANCEL_REQUESTED = 3; + // The activity completed successfully. + ACTIVITY_EXECUTION_STATUS_COMPLETED = 4; + // The activity completed with failure. + ACTIVITY_EXECUTION_STATUS_FAILED = 5; + // The activity completed as canceled. + // Requesting to cancel an activity does not automatically transition the activity to canceled status. If the worker + // responds to cancel the activity after requesting cancellation, the status will transition to cancelled. If the + // activity completes, fails, times out or terminates after cancel is requested and before the worker responds with + // cancelled. The activity will be stay in the terminal non-cancelled status. + ACTIVITY_EXECUTION_STATUS_CANCELED = 6; + // The activity was terminated. Termination does not reach the worker and the activity code cannot react to it. + // A terminated activity may have a running attempt and will be requested to be canceled by the server when it + // heartbeats. + ACTIVITY_EXECUTION_STATUS_TERMINATED = 7; + // The activity has timed out by reaching the specified schedule-to-start or schedule-to-close timeouts. + // Additionally, after all retries are exhausted for start-to-close or heartbeat timeouts, the activity will also + // transition to timed out status. + ACTIVITY_EXECUTION_STATUS_TIMED_OUT = 8; } message ActivityState { - // The type of the activity, a string that maps to a registered activity on a worker. - temporal.api.common.v1.ActivityType activity_type = 1; - - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; - - // Indicates how long the caller is willing to wait for an activity completion. Limits how long - // retries will be attempted. Either this or `start_to_close_timeout` must be specified. - // - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_close_timeout = 3; - // Limits time an activity task can stay in a task queue before a worker picks it up. This - // timeout is always non retryable, as all a retry would achieve is to put it back into the same - // queue. Defaults to `schedule_to_close_timeout` or workflow execution timeout if not - // specified. - // - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_start_timeout = 4; - // Maximum time an activity is allowed to execute after being picked up by a worker. This - // timeout is always retryable. Either this or `schedule_to_close_timeout` must be - // specified. - // - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration start_to_close_timeout = 5; - // Maximum permitted time between successful worker heartbeats. - google.protobuf.Duration heartbeat_timeout = 6; - // The retry policy for the activity. Will never exceed `schedule_to_close_timeout`. - temporal.api.common.v1.RetryPolicy retry_policy = 7; - - // All of the possible activity statuses (covers both the public ActivityExecutionStatus and PendingActivityState). - // TODO: consider moving this into ActivityAttemptState and renaming that message. This could save mutating two - // components on each attempt transition. - ActivityExecutionStatus status = 8; - - // Time the activity was originally scheduled via a StartActivityExecution request. - google.protobuf.Timestamp schedule_time = 9; - - // Priority metadata. - temporal.api.common.v1.Priority priority = 10; - - // Set if activity cancellation was requested. - ActivityCancelState cancel_state = 11; - - // Set if the activity was terminated - ActivityTerminateState terminate_state = 12; + // The type of the activity, a string that maps to a registered activity on a worker. + temporal.api.common.v1.ActivityType activity_type = 1; + + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + + // Indicates how long the caller is willing to wait for an activity completion. Limits how long + // retries will be attempted. Either this or `start_to_close_timeout` must be specified. + // + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_close_timeout = 3; + // Limits time an activity task can stay in a task queue before a worker picks it up. This + // timeout is always non retryable, as all a retry would achieve is to put it back into the same + // queue. Defaults to `schedule_to_close_timeout` or workflow execution timeout if not + // specified. + // + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_start_timeout = 4; + // Maximum time an activity is allowed to execute after being picked up by a worker. This + // timeout is always retryable. Either this or `schedule_to_close_timeout` must be + // specified. + // + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration start_to_close_timeout = 5; + // Maximum permitted time between successful worker heartbeats. + google.protobuf.Duration heartbeat_timeout = 6; + // The retry policy for the activity. Will never exceed `schedule_to_close_timeout`. + temporal.api.common.v1.RetryPolicy retry_policy = 7; + + // All of the possible activity statuses (covers both the public ActivityExecutionStatus and PendingActivityState). + // TODO: consider moving this into ActivityAttemptState and renaming that message. This could save mutating two + // components on each attempt transition. + ActivityExecutionStatus status = 8; + + // Time the activity was originally scheduled via a StartActivityExecution request. + google.protobuf.Timestamp schedule_time = 9; + + // Priority metadata. + temporal.api.common.v1.Priority priority = 10; + + // Set if activity cancellation was requested. + ActivityCancelState cancel_state = 11; + + // Set if the activity was terminated + ActivityTerminateState terminate_state = 12; } message ActivityCancelState { - string request_id = 1; - google.protobuf.Timestamp request_time = 2; - string identity = 3; - string reason = 4; + string request_id = 1; + google.protobuf.Timestamp request_time = 2; + string identity = 3; + string reason = 4; } message ActivityTerminateState { @@ -104,84 +104,84 @@ message ActivityTerminateState { } message ActivityAttemptState { - // The attempt this activity is currently on. - // Incremented each time a new attempt is scheduled. A newly created activity will immediately be scheduled, and - // the count is set to 1. - int32 count = 1; - - // Time from the last attempt failure to the next activity retry. - // If the activity is currently running, this represents the next retry interval in case the attempt fails. - // If activity is currently backing off between attempt, this represents the current retry interval. - // If there is no next retry allowed, this field will be null. - // This interval is typically calculated from the specified retry policy, but may be modified if an activity fails - // with a retryable application failure specifying a retry delay. - google.protobuf.Duration current_retry_interval = 2; - - // Time the last attempt was started. - google.protobuf.Timestamp started_time = 3; - - // The time when the last activity attempt completed. If activity has not been completed yet, it will be null. - google.protobuf.Timestamp complete_time = 4; - - message LastFailureDetails { - // The last time the activity attempt failed. - google.protobuf.Timestamp time = 1; - - // Failure details from the last failed attempt. - temporal.api.failure.v1.Failure failure = 2; - } - - // Details about the last failure. This will only be updated when an activity attempt fails, - // including start-to-close timeout. Activity success, termination, schedule-to-start and schedule-to-close timeouts - // will not reset it. - LastFailureDetails last_failure_details = 5; - - // An incremental version number used to validate tasks. - // Initially this only verifies that a task belong to the current attempt. - // Later on this stamp will be used to also invalidate tasks when the activity is paused, reset, or has its options - // updated. - int32 stamp = 6; - - string last_worker_identity = 7; - - // The Worker Deployment Version this activity was dispatched to most recently. - // If nil, the activity has not yet been dispatched or was last dispatched to an unversioned worker. - temporal.api.deployment.v1.WorkerDeploymentVersion last_deployment_version = 8; - - // The request ID that came from matching's RecordActivityTaskStarted API call. Used to make this API idempotent in - // case of implicit retries. - string start_request_id = 9; + // The attempt this activity is currently on. + // Incremented each time a new attempt is scheduled. A newly created activity will immediately be scheduled, and + // the count is set to 1. + int32 count = 1; + + // Time from the last attempt failure to the next activity retry. + // If the activity is currently running, this represents the next retry interval in case the attempt fails. + // If activity is currently backing off between attempt, this represents the current retry interval. + // If there is no next retry allowed, this field will be null. + // This interval is typically calculated from the specified retry policy, but may be modified if an activity fails + // with a retryable application failure specifying a retry delay. + google.protobuf.Duration current_retry_interval = 2; + + // Time the last attempt was started. + google.protobuf.Timestamp started_time = 3; + + // The time when the last activity attempt completed. If activity has not been completed yet, it will be null. + google.protobuf.Timestamp complete_time = 4; + + message LastFailureDetails { + // The last time the activity attempt failed. + google.protobuf.Timestamp time = 1; + + // Failure details from the last failed attempt. + temporal.api.failure.v1.Failure failure = 2; + } + + // Details about the last failure. This will only be updated when an activity attempt fails, + // including start-to-close timeout. Activity success, termination, schedule-to-start and schedule-to-close timeouts + // will not reset it. + LastFailureDetails last_failure_details = 5; + + // An incremental version number used to validate tasks. + // Initially this only verifies that a task belong to the current attempt. + // Later on this stamp will be used to also invalidate tasks when the activity is paused, reset, or has its options + // updated. + int32 stamp = 6; + + string last_worker_identity = 7; + + // The Worker Deployment Version this activity was dispatched to most recently. + // If nil, the activity has not yet been dispatched or was last dispatched to an unversioned worker. + temporal.api.deployment.v1.WorkerDeploymentVersion last_deployment_version = 8; + + // The request ID that came from matching's RecordActivityTaskStarted API call. Used to make this API idempotent in + // case of implicit retries. + string start_request_id = 9; } message ActivityHeartbeatState { - // Details provided in the last recorded activity heartbeat. - temporal.api.common.v1.Payloads details = 1; - // Time the last heartbeat was recorded. - google.protobuf.Timestamp recorded_time = 2; + // Details provided in the last recorded activity heartbeat. + temporal.api.common.v1.Payloads details = 1; + // Time the last heartbeat was recorded. + google.protobuf.Timestamp recorded_time = 2; } message ActivityRequestData { - // Serialized activity input, passed as arguments to the activity function. - temporal.api.common.v1.Payloads input = 1; - temporal.api.common.v1.Header header = 2; + // Serialized activity input, passed as arguments to the activity function. + temporal.api.common.v1.Payloads input = 1; + temporal.api.common.v1.Header header = 2; - // Metadata for use by user interfaces to display the fixed as-of-start summary and details of the activity. - temporal.api.sdk.v1.UserMetadata user_metadata = 3; + // Metadata for use by user interfaces to display the fixed as-of-start summary and details of the activity. + temporal.api.sdk.v1.UserMetadata user_metadata = 3; } message ActivityOutcome { - message Successful { - temporal.api.common.v1.Payloads output = 1; - } - - message Failed { - // Only filled on schedule-to-start timeouts, schedule-to-close timeouts or terminations. All other attempt - // failures will be recorded in ActivityAttemptState.last_failure_details. - temporal.api.failure.v1.Failure failure = 1; - } - - oneof variant { - Successful successful = 1; - Failed failed = 2; - } + message Successful { + temporal.api.common.v1.Payloads output = 1; + } + + message Failed { + // Only filled on schedule-to-start timeouts, schedule-to-close timeouts or terminations. All other attempt + // failures will be recorded in ActivityAttemptState.last_failure_details. + temporal.api.failure.v1.Failure failure = 1; + } + + oneof variant { + Successful successful = 1; + Failed failed = 2; + } } diff --git a/chasm/lib/activity/proto/v1/request_response.proto b/chasm/lib/activity/proto/v1/request_response.proto index 835071815bd..918c6f4de31 100644 --- a/chasm/lib/activity/proto/v1/request_response.proto +++ b/chasm/lib/activity/proto/v1/request_response.proto @@ -2,54 +2,60 @@ syntax = "proto3"; package temporal.server.chasm.lib.activity.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypb"; - import "temporal/api/workflowservice/v1/request_response.proto"; +option go_package = "go.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypb"; + message StartActivityExecutionRequest { - string namespace_id = 1; + string namespace_id = 1; - temporal.api.workflowservice.v1.StartActivityExecutionRequest frontend_request = 2; + temporal.api.workflowservice.v1.StartActivityExecutionRequest frontend_request = 2; } message StartActivityExecutionResponse { - temporal.api.workflowservice.v1.StartActivityExecutionResponse frontend_response = 1; + temporal.api.workflowservice.v1.StartActivityExecutionResponse frontend_response = 1; } message DescribeActivityExecutionRequest { - string namespace_id = 1; + string namespace_id = 1; - temporal.api.workflowservice.v1.DescribeActivityExecutionRequest frontend_request = 2; + temporal.api.workflowservice.v1.DescribeActivityExecutionRequest frontend_request = 2; } message DescribeActivityExecutionResponse { - temporal.api.workflowservice.v1.DescribeActivityExecutionResponse frontend_response = 1; + temporal.api.workflowservice.v1.DescribeActivityExecutionResponse frontend_response = 1; } message PollActivityExecutionRequest { - string namespace_id = 1; + string namespace_id = 1; - temporal.api.workflowservice.v1.PollActivityExecutionRequest frontend_request = 2; + temporal.api.workflowservice.v1.PollActivityExecutionRequest frontend_request = 2; } message PollActivityExecutionResponse { - temporal.api.workflowservice.v1.PollActivityExecutionResponse frontend_response = 1; + temporal.api.workflowservice.v1.PollActivityExecutionResponse frontend_response = 1; } message TerminateActivityExecutionRequest { - string namespace_id = 1; + string namespace_id = 1; - temporal.api.workflowservice.v1.TerminateActivityExecutionRequest frontend_request = 2; + temporal.api.workflowservice.v1.TerminateActivityExecutionRequest frontend_request = 2; } -message TerminateActivityExecutionResponse { -} +message TerminateActivityExecutionResponse {} message RequestCancelActivityExecutionRequest { - string namespace_id = 1; + string namespace_id = 1; + + temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest frontend_request = 2; +} + +message RequestCancelActivityExecutionResponse {} + +message DeleteActivityExecutionRequest { + string namespace_id = 1; - temporal.api.workflowservice.v1.RequestCancelActivityExecutionRequest frontend_request = 2; + temporal.api.workflowservice.v1.DeleteActivityExecutionRequest frontend_request = 2; } -message RequestCancelActivityExecutionResponse { -} \ No newline at end of file +message DeleteActivityExecutionResponse {} diff --git a/chasm/lib/activity/proto/v1/service.proto b/chasm/lib/activity/proto/v1/service.proto index f669b065de6..69810bee55c 100644 --- a/chasm/lib/activity/proto/v1/service.proto +++ b/chasm/lib/activity/proto/v1/service.proto @@ -2,29 +2,40 @@ syntax = "proto3"; package temporal.server.chasm.lib.activity.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypb"; - import "chasm/lib/activity/proto/v1/request_response.proto"; +import "temporal/server/api/common/v1/api_category.proto"; import "temporal/server/api/routing/v1/extension.proto"; -service ActivityService { - rpc StartActivityExecution(StartActivityExecutionRequest) returns (StartActivityExecutionResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; - } - - rpc DescribeActivityExecution(DescribeActivityExecutionRequest) returns (DescribeActivityExecutionResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; - } - - rpc PollActivityExecution(PollActivityExecutionRequest) returns (PollActivityExecutionResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; - } - - rpc TerminateActivityExecution(TerminateActivityExecutionRequest) returns (TerminateActivityExecutionResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; - } +option go_package = "go.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypb"; - rpc RequestCancelActivityExecution(RequestCancelActivityExecutionRequest) returns (RequestCancelActivityExecutionResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; - } +service ActivityService { + rpc StartActivityExecution(StartActivityExecutionRequest) returns (StartActivityExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DescribeActivityExecution(DescribeActivityExecutionRequest) returns (DescribeActivityExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc PollActivityExecution(PollActivityExecutionRequest) returns (PollActivityExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + rpc TerminateActivityExecution(TerminateActivityExecutionRequest) returns (TerminateActivityExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc RequestCancelActivityExecution(RequestCancelActivityExecutionRequest) returns (RequestCancelActivityExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DeleteActivityExecution(DeleteActivityExecutionRequest) returns (DeleteActivityExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.activity_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } } diff --git a/chasm/lib/activity/proto/v1/tasks.proto b/chasm/lib/activity/proto/v1/tasks.proto index 59d4bd2d901..9a1996e3dd2 100644 --- a/chasm/lib/activity/proto/v1/tasks.proto +++ b/chasm/lib/activity/proto/v1/tasks.proto @@ -5,25 +5,24 @@ package temporal.server.chasm.lib.activity.proto.v1; option go_package = "go.temporal.io/server/chasm/lib/activity/gen/activitypb;activitypb"; message ActivityDispatchTask { - // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. - int32 stamp = 1; + // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. + int32 stamp = 1; } message ScheduleToStartTimeoutTask { - // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. - int32 stamp = 1; + // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. + int32 stamp = 1; } -message ScheduleToCloseTimeoutTask { -} +message ScheduleToCloseTimeoutTask {} message StartToCloseTimeoutTask { - // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. - int32 stamp = 1; + // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. + int32 stamp = 1; } // HeartbeatTimeoutTask is a pure task that enforces heartbeat timeouts. message HeartbeatTimeoutTask { - // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. - int32 stamp = 1; + // The current stamp for this activity execution. Used for task validation. See also [ActivityAttemptState]. + int32 stamp = 1; } diff --git a/chasm/lib/activity/statemachine.go b/chasm/lib/activity/statemachine.go index 23e6fd4050f..1b47772e5fb 100644 --- a/chasm/lib/activity/statemachine.go +++ b/chasm/lib/activity/statemachine.go @@ -100,11 +100,13 @@ var TransitionRescheduled = chasm.NewTransition( return err } + retryScheduledTime := attemptScheduleTimeForRetry(attempt).AsTime() + if timeout := a.GetScheduleToStartTimeout().AsDuration(); timeout > 0 { ctx.AddTask( a, chasm.TaskAttributes{ - ScheduledTime: currentTime.Add(timeout).Add(event.retryInterval), + ScheduledTime: retryScheduledTime.Add(timeout), }, &activitypb.ScheduleToStartTimeoutTask{ Stamp: attempt.GetStamp(), @@ -114,7 +116,7 @@ var TransitionRescheduled = chasm.NewTransition( ctx.AddTask( a, chasm.TaskAttributes{ - ScheduledTime: currentTime.Add(event.retryInterval), + ScheduledTime: retryScheduledTime, }, &activitypb.ActivityDispatchTask{ Stamp: attempt.GetStamp(), @@ -234,6 +236,12 @@ var TransitionFailed = chasm.NewTransition( }, ) +type terminateEvent struct { + request chasm.TerminateComponentRequest + metricsHandler metrics.Handler + fromStatus activitypb.ActivityExecutionStatus +} + // TransitionTerminated transitions to Terminated status. var TransitionTerminated = chasm.NewTransition( []activitypb.ActivityExecutionStatus{ @@ -244,16 +252,17 @@ var TransitionTerminated = chasm.NewTransition( activitypb.ACTIVITY_EXECUTION_STATUS_TERMINATED, func(a *Activity, ctx chasm.MutableContext, event terminateEvent) error { return a.StoreOrSelf(ctx).RecordCompleted(ctx, func(ctx chasm.MutableContext) error { - req := event.request.GetFrontendRequest() - a.TerminateState = &activitypb.ActivityTerminateState{ - RequestId: req.GetRequestId(), + RequestId: event.request.RequestID, } outcome := a.Outcome.Get(ctx) failure := &failurepb.Failure{ - // TODO(saa-preview): if the reason isn't provided, perhaps set a default reason. Also see if we should prefix with "Activity terminated: " - Message: req.GetReason(), - FailureInfo: &failurepb.Failure_TerminatedFailureInfo{}, + Message: event.request.Reason, + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{ + TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{ + Identity: event.request.Identity, + }, + }, } outcome.Variant = &activitypb.ActivityOutcome_Failed_{ Failed: &activitypb.ActivityOutcome_Failed{ @@ -261,14 +270,7 @@ var TransitionTerminated = chasm.NewTransition( }, } - metricsHandler := enrichMetricsHandler( - a, - event.MetricsHandlerBuilderParams.Handler, - event.MetricsHandlerBuilderParams.NamespaceName, - metrics.ActivityTerminatedScope, - event.MetricsHandlerBuilderParams.BreakdownMetricsByTaskQueue) - - metrics.ActivityTerminate.With(metricsHandler).Record(1) + a.emitOnTerminatedMetrics(event.metricsHandler) return nil }) @@ -314,7 +316,8 @@ var TransitionCanceled = chasm.NewTransition( Message: "Activity canceled", FailureInfo: &failurepb.Failure_CanceledFailureInfo{ CanceledFailureInfo: &failurepb.CanceledFailureInfo{ - Details: event.details, + Details: event.details, + Identity: a.GetCancelState().GetIdentity(), }, }, } diff --git a/chasm/lib/activity/statemachine_test.go b/chasm/lib/activity/statemachine_test.go index 68d12191822..8e412b09bd4 100644 --- a/chasm/lib/activity/statemachine_test.go +++ b/chasm/lib/activity/statemachine_test.go @@ -590,39 +590,22 @@ func TestTransitionTerminated(t *testing.T) { controller := gomock.NewController(t) metricsHandler := metrics.NewMockHandler(controller) - enrichedMetricsHandler := metrics.NewMockHandler(controller) - - tags := []metrics.Tag{ - metrics.OperationTag(metrics.ActivityTerminatedScope), - metrics.ActivityTypeTag("test-activity-type"), - metrics.VersioningBehaviorTag(enumspb.VERSIONING_BEHAVIOR_UNSPECIFIED), - metrics.WorkflowTypeTag(WorkflowTypeTag), - metrics.NamespaceTag("test-namespace"), - metrics.UnsafeTaskQueueTag("test-task-queue"), - } - metricsHandler.EXPECT().WithTags(tags).Return(enrichedMetricsHandler) counterTerminate := metrics.NewMockCounterIface(controller) counterTerminate.EXPECT().Record(int64(1)).Times(1) - enrichedMetricsHandler.EXPECT().Counter(metrics.ActivityTerminate.Name()).Return(counterTerminate) + metricsHandler.EXPECT().Counter(metrics.ActivityTerminate.Name()).Return(counterTerminate) - req := &activitypb.TerminateActivityExecutionRequest{ - FrontendRequest: &workflowservice.TerminateActivityExecutionRequest{ - Reason: "Test Termination", - Identity: "terminator", - RequestId: "test-request-id", - }, + identity := "terminator" + req := chasm.TerminateComponentRequest{ + Reason: "Test Termination", + Identity: identity, + RequestID: "test-request-id", } err := TransitionTerminated.Apply(activity, ctx, terminateEvent{ - request: req, - MetricsHandlerBuilderParams: MetricsHandlerBuilderParams{ - Handler: metricsHandler, - NamespaceName: "test-namespace", - BreakdownMetricsByTaskQueue: func(namespace string, taskQueue string, taskQueueType enumspb.TaskQueueType) bool { - return namespace == "test-namespace" && taskQueue == "test-task-queue" && taskQueueType == enumspb.TASK_QUEUE_TYPE_ACTIVITY - }, - }, + request: req, + metricsHandler: metricsHandler, + fromStatus: activitypb.ACTIVITY_EXECUTION_STATUS_STARTED, }) require.NoError(t, err) require.Equal(t, activitypb.ACTIVITY_EXECUTION_STATUS_TERMINATED, activity.Status) @@ -631,8 +614,12 @@ func TestTransitionTerminated(t *testing.T) { require.Equal(t, "test-request-id", activity.GetTerminateState().RequestId) expectedFailure := &failurepb.Failure{ - Message: "Test Termination", - FailureInfo: &failurepb.Failure_TerminatedFailureInfo{}, + Message: "Test Termination", + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{ + TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{ + Identity: identity, + }, + }, } protorequire.ProtoEqual(t, expectedFailure, outcome.GetFailed().GetFailure()) } @@ -674,6 +661,7 @@ func TestTransitionCanceled(t *testing.T) { ctx.HandleNow = func(chasm.Component) time.Time { return defaultTime } attemptState := &activitypb.ActivityAttemptState{Count: 1} outcome := &activitypb.ActivityOutcome{} + identity := "canceler" activity := &Activity{ ActivityState: &activitypb.ActivityState{ @@ -684,6 +672,9 @@ func TestTransitionCanceled(t *testing.T) { StartToCloseTimeout: durationpb.New(defaultStartToCloseTimeout), Status: activitypb.ACTIVITY_EXECUTION_STATUS_CANCEL_REQUESTED, TaskQueue: &taskqueuepb.TaskQueue{Name: "test-task-queue"}, + CancelState: &activitypb.ActivityCancelState{ + Identity: identity, + }, }, LastAttempt: chasm.NewDataField(ctx, attemptState), Outcome: chasm.NewDataField(ctx, outcome), @@ -717,7 +708,8 @@ func TestTransitionCanceled(t *testing.T) { Message: "Activity canceled", FailureInfo: &failurepb.Failure_CanceledFailureInfo{ CanceledFailureInfo: &failurepb.CanceledFailureInfo{ - Details: payloads.EncodeString("Details"), + Details: payloads.EncodeString("Details"), + Identity: identity, }, }, } diff --git a/chasm/lib/activity/validator.go b/chasm/lib/activity/validator.go index 1b1d88b6b99..5b67f076a19 100644 --- a/chasm/lib/activity/validator.go +++ b/chasm/lib/activity/validator.go @@ -20,6 +20,60 @@ import ( "google.golang.org/protobuf/types/known/durationpb" ) +// ValidateAndNormalizeStandaloneActivity validates and normalizes the attributes for a standalone activity. +func ValidateAndNormalizeStandaloneActivity( + activityID string, + activityType string, + getDefaultActivityRetrySettings dynamicconfig.TypedPropertyFnWithNamespaceFilter[retrypolicy.DefaultRetrySettings], + maxIDLengthLimit int, + namespaceID namespace.ID, + options *activitypb.ActivityOptions, + priority *commonpb.Priority, + runTimeout *durationpb.Duration, +) error { + // Standalone activities always use user defined task queues, so we can enforce user defined task queue validation + if err := tqid.NormalizeAndValidateUserDefined(options.TaskQueue, "", "", maxIDLengthLimit); err != nil { + return err + } + + return validateAndNormalizeActivityAttributes( + activityID, + activityType, + getDefaultActivityRetrySettings, + maxIDLengthLimit, + namespaceID, + options, + priority, + runTimeout) +} + +// ValidateAndNormalizeEmbeddedActivity validates and normalizes the attributes for an embedded activity. +func ValidateAndNormalizeEmbeddedActivity( + activityID string, + activityType string, + getDefaultActivityRetrySettings dynamicconfig.TypedPropertyFnWithNamespaceFilter[retrypolicy.DefaultRetrySettings], + maxIDLengthLimit int, + namespaceID namespace.ID, + options *activitypb.ActivityOptions, + priority *commonpb.Priority, + runTimeout *durationpb.Duration, + workflowTaskQueueName string, +) error { + if err := tqid.NormalizeAndValidateUserDefined(options.TaskQueue, "", workflowTaskQueueName, maxIDLengthLimit); err != nil { + return err + } + + return validateAndNormalizeActivityAttributes( + activityID, + activityType, + getDefaultActivityRetrySettings, + maxIDLengthLimit, + namespaceID, + options, + priority, + runTimeout) +} + // ValidateAndNormalizeActivityAttributes validates and normalizes the common activity request attributes. // This validation is shared by both standalone and embedded activities. // IMPORTANT: this method mutates the input params; in cases where it's critical to maintain immutability @@ -31,7 +85,7 @@ import ( // 3. If neither ScheduleToClose nor StartToClose is set, return error // 4. Ensure all timeouts do not exceed runTimeout if runTimeout is set (>0) // 5. Ensure HeartbeatTimeout does not exceed StartToClose -func ValidateAndNormalizeActivityAttributes( +func validateAndNormalizeActivityAttributes( activityID string, activityType string, getDefaultActivityRetrySettings dynamicconfig.TypedPropertyFnWithNamespaceFilter[retrypolicy.DefaultRetrySettings], @@ -41,15 +95,11 @@ func ValidateAndNormalizeActivityAttributes( priority *commonpb.Priority, runTimeout *durationpb.Duration, ) error { - if err := tqid.NormalizeAndValidate(options.TaskQueue, "", maxIDLengthLimit); err != nil { - return err - } - if activityID == "" { - return serviceerror.NewInvalidArgumentf("ActivityId is not set. ActivityType=%s", activityType) + return serviceerror.NewInvalidArgument("activityId is not set") } if activityType == "" { - return serviceerror.NewInvalidArgumentf("ActivityType is not set. ActivityID=%s", activityID) + return serviceerror.NewInvalidArgument("activityType is not set") } if err := validateActivityRetryPolicy(namespaceID, options.RetryPolicy, getDefaultActivityRetrySettings); err != nil { @@ -57,20 +107,19 @@ func ValidateAndNormalizeActivityAttributes( } if len(activityID) > maxIDLengthLimit { - return serviceerror.NewInvalidArgumentf("ActivityId exceeds length limit. ActivityId=%s ActivityType=%s Length=%d Limit=%d", - activityID, activityType, len(activityID), maxIDLengthLimit) + return serviceerror.NewInvalidArgumentf("activityId exceeds length limit. Length=%d Limit=%d", + len(activityID), maxIDLengthLimit) } if len(activityType) > maxIDLengthLimit { - return serviceerror.NewInvalidArgumentf("ActivityType exceeds length limit. ActivityId=%s ActivityType=%s Length=%d Limit=%d", - activityID, activityType, len(activityType), maxIDLengthLimit) + return serviceerror.NewInvalidArgumentf("activityType exceeds length limit. Length=%d Limit=%d", + len(activityType), maxIDLengthLimit) } if err := priorities.Validate(priority); err != nil { - return serviceerror.NewInvalidArgumentf("Invalid Priorities: %v ActivityId=%s ActivityType=%s", - err, activityID, activityType) + return serviceerror.NewInvalidArgumentf("invalid priorities: %v", err) } - return normalizeAndValidateTimeouts(activityID, + return validateAndNormalizeTimeouts(activityID, activityType, runTimeout, options) @@ -90,7 +139,7 @@ func validateActivityRetryPolicy( return retrypolicy.Validate(retryPolicy) } -func normalizeAndValidateTimeouts( +func validateAndNormalizeTimeouts( activityID string, activityType string, runTimeout *durationpb.Duration, @@ -98,20 +147,16 @@ func normalizeAndValidateTimeouts( ) error { // Only attempt to deduce and fill in unspecified timeouts only when all timeouts are non-negative. if err := timestamp.ValidateAndCapProtoDuration(options.GetScheduleToCloseTimeout()); err != nil { - return serviceerror.NewInvalidArgumentf("Invalid ScheduleToCloseTimeout: %v ActivityId=%s ActivityType=%s", - err, activityID, activityType) + return serviceerror.NewInvalidArgumentf("invalid ScheduleToCloseTimeout: %v", err) } if err := timestamp.ValidateAndCapProtoDuration(options.GetScheduleToStartTimeout()); err != nil { - return serviceerror.NewInvalidArgumentf("Invalid ScheduleToStartTimeout: %v ActivityId=%s ActivityType=%s", - err, activityID, activityType) + return serviceerror.NewInvalidArgumentf("invalid ScheduleToStartTimeout: %v", err) } if err := timestamp.ValidateAndCapProtoDuration(options.GetStartToCloseTimeout()); err != nil { - return serviceerror.NewInvalidArgumentf("Invalid StartToCloseTimeout: %v ActivityId=%s ActivityType=%s", - err, activityID, activityType) + return serviceerror.NewInvalidArgumentf("invalid StartToCloseTimeout: %v", err) } if err := timestamp.ValidateAndCapProtoDuration(options.GetHeartbeatTimeout()); err != nil { - return serviceerror.NewInvalidArgumentf("Invalid HeartbeatTimeout: %v ActivityId=%s ActivityType=%s", - err, activityID, activityType) + return serviceerror.NewInvalidArgumentf("invalid HeartbeatTimeout: %v", err) } scheduleToCloseSet := options.GetScheduleToCloseTimeout().AsDuration() > 0 @@ -137,7 +182,7 @@ func normalizeAndValidateTimeouts( } } else { // Deduction failed as there's not enough information to fill in missing timeouts. - return serviceerror.NewInvalidArgumentf("A valid StartToClose or ScheduleToCloseTimeout is not set on ScheduleActivityTaskCommand. ActivityId=%s ActivityType=%s", + return serviceerror.NewInvalidArgumentf("a valid StartToClose or ScheduleToCloseTimeout is not set on ScheduleActivityTaskCommand. ActivityId=%s ActivityType=%s", activityID, activityType) } // ensure activity timeout never larger than workflow timeout @@ -162,7 +207,7 @@ func normalizeAndValidateTimeouts( return nil } -func normalizeAndValidateIDPolicy(req *workflowservice.StartActivityExecutionRequest) error { +func validateAndNormalizeIDPolicy(req *workflowservice.StartActivityExecutionRequest) error { if req.GetIdReusePolicy() == enumspb.ACTIVITY_ID_REUSE_POLICY_UNSPECIFIED { req.IdReusePolicy = enumspb.ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE } @@ -271,7 +316,55 @@ func validatePollActivityExecutionRequest( return nil } -func validateRequestCancelActivityExecutionRequest( +func validateAndNormalizeStartRequest( + req *workflowservice.StartActivityExecutionRequest, + maxIDLengthLimit int, + blobSizeLimitError dynamicconfig.IntPropertyFnWithNamespaceFilter, + blobSizeLimitWarn dynamicconfig.IntPropertyFnWithNamespaceFilter, + logger log.Logger, + saMapperProvider searchattribute.MapperProvider, + saValidator *searchattribute.Validator, +) error { + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } else if len(req.GetRequestId()) > maxIDLengthLimit { + return serviceerror.NewInvalidArgumentf("request ID exceeds length limit. Length=%d Limit=%d", + len(req.GetRequestId()), maxIDLengthLimit) + } + + if len(req.GetIdentity()) > maxIDLengthLimit { + return serviceerror.NewInvalidArgumentf("identity exceeds length limit. Length=%d Limit=%d", + len(req.GetIdentity()), maxIDLengthLimit) + } + + if err := validateAndNormalizeIDPolicy(req); err != nil { + return err + } + + if err := validateBlobSize( + req.GetActivityId(), + "StartActivityExecution", + blobSizeLimitError, + blobSizeLimitWarn, + req.Input.Size(), + logger, + req.GetNamespace()); err != nil { + return serviceerror.NewInvalidArgument("input exceeds length limit") + } + + if req.GetSearchAttributes() != nil { + if err := validateAndNormalizeSearchAttributes( + req, + saMapperProvider, + saValidator); err != nil { + return err + } + } + + return nil +} + +func validateAndNormalizeCancelRequest( req *workflowservice.RequestCancelActivityExecutionRequest, maxIDLengthLimit int, blobSizeLimitError dynamicconfig.IntPropertyFnWithNamespaceFilter, @@ -287,7 +380,9 @@ func validateRequestCancelActivityExecutionRequest( len(req.GetActivityId()), maxIDLengthLimit) } - if len(req.GetRequestId()) > maxIDLengthLimit { + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } else if len(req.GetRequestId()) > maxIDLengthLimit { return serviceerror.NewInvalidArgumentf("request ID exceeds length limit. Length=%d Limit=%d", len(req.GetRequestId()), maxIDLengthLimit) } @@ -319,7 +414,30 @@ func validateRequestCancelActivityExecutionRequest( return nil } -func validateTerminateActivityExecutionRequest( +func validateAndNormalizeDeleteRequest( + req *workflowservice.DeleteActivityExecutionRequest, + maxIDLengthLimit int, +) error { + if req.GetActivityId() == "" { + return serviceerror.NewInvalidArgument("activity ID is required") + } + + if len(req.GetActivityId()) > maxIDLengthLimit { + return serviceerror.NewInvalidArgumentf("activity ID exceeds length limit. Length=%d Limit=%d", + len(req.GetActivityId()), maxIDLengthLimit) + } + + if runID := req.GetRunId(); runID != "" { + _, err := uuid.Parse(runID) + if err != nil { + return serviceerror.NewInvalidArgument("invalid run id: must be a valid UUID") + } + } + + return nil +} + +func validateAndNormalizeTerminateRequest( req *workflowservice.TerminateActivityExecutionRequest, maxIDLengthLimit int, blobSizeLimitError dynamicconfig.IntPropertyFnWithNamespaceFilter, @@ -335,7 +453,9 @@ func validateTerminateActivityExecutionRequest( len(req.GetActivityId()), maxIDLengthLimit) } - if len(req.GetRequestId()) > maxIDLengthLimit { + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } else if len(req.GetRequestId()) > maxIDLengthLimit { return serviceerror.NewInvalidArgumentf("request ID exceeds length limit. Length=%d Limit=%d", len(req.GetRequestId()), maxIDLengthLimit) } diff --git a/chasm/lib/activity/validator_test.go b/chasm/lib/activity/validator_test.go index 4da016a3690..a142b1a140b 100644 --- a/chasm/lib/activity/validator_test.go +++ b/chasm/lib/activity/validator_test.go @@ -1,6 +1,7 @@ package activity import ( + "fmt" "testing" "time" @@ -15,6 +16,7 @@ import ( "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/retrypolicy" "google.golang.org/protobuf/types/known/durationpb" ) @@ -47,19 +49,35 @@ var ( ) func TestValidateSuccess(t *testing.T) { - err := ValidateAndNormalizeActivityAttributes( - defaultActivityID, - defaultActivityType, - getDefaultRetrySettings, - defaultMaxIDLengthLimit, - defaultNamespaceID, - &defaultActivityOptions, - &defaultPriority, - durationpb.New(0)) - require.NoError(t, err) + t.Run("StandaloneActivitySuccess", func(t *testing.T) { + err := ValidateAndNormalizeStandaloneActivity( + defaultActivityID, + defaultActivityType, + getDefaultRetrySettings, + defaultMaxIDLengthLimit, + defaultNamespaceID, + &defaultActivityOptions, + &defaultPriority, + durationpb.New(0)) + require.NoError(t, err) + }) + + t.Run("EmbeddedActivitySuccess", func(t *testing.T) { + err := ValidateAndNormalizeEmbeddedActivity( + defaultActivityID, + defaultActivityType, + getDefaultRetrySettings, + defaultMaxIDLengthLimit, + defaultNamespaceID, + &defaultActivityOptions, + &defaultPriority, + durationpb.New(0), + defaultTaskQueue) + require.NoError(t, err) + }) } -func TestValidateFailures(t *testing.T) { +func TestValidateAllActivityFailures(t *testing.T) { cases := []struct { name string activityID string @@ -70,6 +88,7 @@ func TestValidateFailures(t *testing.T) { options *activitypb.ActivityOptions priority *commonpb.Priority runTimeout *durationpb.Duration + expectedErrMessage string }{ { name: "Empty ActivityId", @@ -81,6 +100,7 @@ func TestValidateFailures(t *testing.T) { options: &defaultActivityOptions, priority: &defaultPriority, runTimeout: nil, + expectedErrMessage: "activityId is not set", }, { name: "Empty ActivityType", @@ -92,6 +112,7 @@ func TestValidateFailures(t *testing.T) { options: &defaultActivityOptions, priority: &defaultPriority, runTimeout: nil, + expectedErrMessage: "activityType is not set", }, { name: "ActivityId exceeds length limit", @@ -103,6 +124,7 @@ func TestValidateFailures(t *testing.T) { options: &defaultActivityOptions, priority: &defaultPriority, runTimeout: nil, + expectedErrMessage: fmt.Sprintf("activityId exceeds length limit. Length=%d Limit=%d", 1001, defaultMaxIDLengthLimit), }, { name: "ActivityType exceeds length limit", @@ -114,20 +136,7 @@ func TestValidateFailures(t *testing.T) { options: &defaultActivityOptions, priority: &defaultPriority, runTimeout: nil, - }, - { - name: "Invalid TaskQueue", - activityID: defaultActivityID, - activityType: defaultActivityType, - getDefaultActivityRetrySettings: getDefaultRetrySettings, - maxIDLengthLimit: defaultMaxIDLengthLimit, - namespaceID: defaultNamespaceID, - options: &activitypb.ActivityOptions{ - TaskQueue: &taskqueuepb.TaskQueue{Name: ""}, - ScheduleToCloseTimeout: durationpb.New(10 * time.Second), - }, - priority: &defaultPriority, - runTimeout: nil, + expectedErrMessage: fmt.Sprintf("activityType exceeds length limit. Length=%d Limit=%d", 1001, defaultMaxIDLengthLimit), }, { name: "Negative ScheduleToCloseTimeout", @@ -140,8 +149,9 @@ func TestValidateFailures(t *testing.T) { TaskQueue: &taskqueuepb.TaskQueue{Name: defaultTaskQueue}, ScheduleToCloseTimeout: durationpb.New(-1 * time.Second), }, - priority: &defaultPriority, - runTimeout: nil, + priority: &defaultPriority, + runTimeout: nil, + expectedErrMessage: "invalid ScheduleToCloseTimeout", }, { name: "Negative ScheduleToStartTimeout", @@ -155,8 +165,9 @@ func TestValidateFailures(t *testing.T) { ScheduleToCloseTimeout: durationpb.New(10 * time.Second), ScheduleToStartTimeout: durationpb.New(-1 * time.Second), }, - priority: &defaultPriority, - runTimeout: nil, + priority: &defaultPriority, + runTimeout: nil, + expectedErrMessage: "invalid ScheduleToStartTimeout", }, { name: "Negative StartToCloseTimeout", @@ -169,8 +180,9 @@ func TestValidateFailures(t *testing.T) { TaskQueue: &taskqueuepb.TaskQueue{Name: defaultTaskQueue}, StartToCloseTimeout: durationpb.New(-1 * time.Second), }, - priority: &defaultPriority, - runTimeout: nil, + priority: &defaultPriority, + runTimeout: nil, + expectedErrMessage: "invalid StartToCloseTimeout", }, { name: "Negative HeartbeatTimeout", @@ -184,8 +196,9 @@ func TestValidateFailures(t *testing.T) { ScheduleToCloseTimeout: durationpb.New(10 * time.Second), HeartbeatTimeout: durationpb.New(-1 * time.Second), }, - priority: &defaultPriority, - runTimeout: nil, + priority: &defaultPriority, + runTimeout: nil, + expectedErrMessage: "invalid HeartbeatTimeout", }, { name: "Invalid Priority", @@ -197,12 +210,13 @@ func TestValidateFailures(t *testing.T) { options: &defaultActivityOptions, priority: &commonpb.Priority{FairnessKey: string(make([]byte, 1001))}, runTimeout: nil, + expectedErrMessage: "invalid priorities", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := ValidateAndNormalizeActivityAttributes( + err := validateAndNormalizeActivityAttributes( tc.activityID, tc.activityType, tc.getDefaultActivityRetrySettings, @@ -213,10 +227,178 @@ func TestValidateFailures(t *testing.T) { durationpb.New(0)) var invalidArgErr *serviceerror.InvalidArgument require.ErrorAs(t, err, &invalidArgErr) + if tc.expectedErrMessage != "" { + require.Contains(t, invalidArgErr.Error(), tc.expectedErrMessage) + } }) } } +func TestStandaloneActivityTaskQueueValidations(t *testing.T) { + cases := []struct { + name string + activityID string + activityType string + getDefaultActivityRetrySettings dynamicconfig.TypedPropertyFnWithNamespaceFilter[retrypolicy.DefaultRetrySettings] + maxIDLengthLimit int + namespaceID namespace.ID + options *activitypb.ActivityOptions + priority *commonpb.Priority + runTimeout *durationpb.Duration + expectedErrMessage string + }{ + { + name: "Disallow PerNSWorkerTaskQueue TaskQueue", + activityID: defaultActivityID, + activityType: defaultActivityType, + getDefaultActivityRetrySettings: getDefaultRetrySettings, + maxIDLengthLimit: defaultMaxIDLengthLimit, + namespaceID: defaultNamespaceID, + options: &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + }, + priority: &defaultPriority, + runTimeout: nil, + expectedErrMessage: fmt.Sprintf("cannot use internal per-namespace task queue:%s", primitives.PerNSWorkerTaskQueue), + }, + { + name: "Disallow Internal TaskQueue Prefix", + activityID: defaultActivityID, + activityType: defaultActivityType, + getDefaultActivityRetrySettings: getDefaultRetrySettings, + maxIDLengthLimit: defaultMaxIDLengthLimit, + namespaceID: defaultNamespaceID, + options: &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: "/_sys/my-task-queue"}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + }, + priority: &defaultPriority, + runTimeout: nil, + expectedErrMessage: "task queue name cannot start with reserved prefix /_sys/", + }, + { + name: "Disallow Empty TaskQueue", + activityID: defaultActivityID, + activityType: defaultActivityType, + getDefaultActivityRetrySettings: getDefaultRetrySettings, + maxIDLengthLimit: defaultMaxIDLengthLimit, + namespaceID: defaultNamespaceID, + options: &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: ""}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + }, + priority: &defaultPriority, + runTimeout: nil, + expectedErrMessage: "missing task queue name", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateAndNormalizeStandaloneActivity( + tc.activityID, + tc.activityType, + tc.getDefaultActivityRetrySettings, + tc.maxIDLengthLimit, + tc.namespaceID, + tc.options, + tc.priority, + durationpb.New(0)) + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, invalidArgErr.Error(), tc.expectedErrMessage) + }) + } +} + +func TestEmbeddedActivityTaskQueueValidations(t *testing.T) { + t.Run("Allow PerNSWorkerTaskQueue TaskQueue", func(t *testing.T) { + options := &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + } + + err := ValidateAndNormalizeEmbeddedActivity( + defaultActivityID, + defaultActivityType, + getDefaultRetrySettings, + defaultMaxIDLengthLimit, + defaultNamespaceID, + options, + &defaultPriority, + durationpb.New(0), + primitives.PerNSWorkerTaskQueue) + require.NoError(t, err) + }) + + t.Run("Disallow PerNSWorkerTaskQueue TaskQueue", func(t *testing.T) { + options := &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + } + + err := ValidateAndNormalizeEmbeddedActivity( + defaultActivityID, + defaultActivityType, + getDefaultRetrySettings, + defaultMaxIDLengthLimit, + defaultNamespaceID, + options, + &defaultPriority, + durationpb.New(0), + defaultTaskQueue) + + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, invalidArgErr.Error(), "cannot use internal per-namespace task queue") + }) + + t.Run("Disallow Internal TaskQueue Prefix", func(t *testing.T) { + options := &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: "/_sys/my-task-queue"}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + } + + err := ValidateAndNormalizeEmbeddedActivity( + defaultActivityID, + defaultActivityType, + getDefaultRetrySettings, + defaultMaxIDLengthLimit, + defaultNamespaceID, + options, + &defaultPriority, + durationpb.New(0), + defaultTaskQueue) + + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, invalidArgErr.Error(), "task queue name cannot start with reserved prefix /_sys/") + }) + + t.Run("Disallow Empty TaskQueue", func(t *testing.T) { + options := &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: ""}, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + } + + err := ValidateAndNormalizeEmbeddedActivity( + defaultActivityID, + defaultActivityType, + getDefaultRetrySettings, + defaultMaxIDLengthLimit, + defaultNamespaceID, + options, + &defaultPriority, + durationpb.New(0), + defaultTaskQueue) + + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, invalidArgErr.Error(), "missing task queue name") + }) +} + func newTestFrontendHandler( blobSizeLimitError func(string) int, blobSizeLimitWarn func(string) int, @@ -247,7 +429,7 @@ func TestValidateStandAloneRequestIDTooLong(t *testing.T) { } h := newTestFrontendHandler(defaultBlobSizeLimitError, defaultBlobSizeLimitWarn, defaultMaxIDLengthLimit) - err := h.validateAndNormalizeStartActivityExecutionRequest(req) + err := validateAndNormalizeStartRequest(req, h.config.MaxIDLengthLimit(), h.config.BlobSizeLimitError, h.config.BlobSizeLimitWarn, h.logger, h.saMapperProvider, h.saValidator) var invalidArgErr *serviceerror.InvalidArgument require.ErrorAs(t, err, &invalidArgErr) } @@ -267,7 +449,7 @@ func TestValidateStandAloneInputTooLarge(t *testing.T) { } h := newTestFrontendHandler(defaultBlobSizeLimitError, defaultBlobSizeLimitWarn, defaultMaxIDLengthLimit) - err := h.validateAndNormalizeStartActivityExecutionRequest(req) + err := validateAndNormalizeStartRequest(req, h.config.MaxIDLengthLimit(), h.config.BlobSizeLimitError, h.config.BlobSizeLimitWarn, h.logger, h.saMapperProvider, h.saValidator) var invalidArgErr *serviceerror.InvalidArgument require.ErrorAs(t, err, &invalidArgErr) } @@ -294,7 +476,7 @@ func TestValidateStandAloneInputWarningSizeShouldSucceed(t *testing.T) { func(ns string) int { return payloadSize }, defaultMaxIDLengthLimit, ) - err := h.validateAndNormalizeStartActivityExecutionRequest(req) + err := validateAndNormalizeStartRequest(req, h.config.MaxIDLengthLimit(), h.config.BlobSizeLimitError, h.config.BlobSizeLimitWarn, h.logger, h.saMapperProvider, h.saValidator) require.NoError(t, err) } @@ -312,7 +494,7 @@ func TestValidateStandAlone_IDPolicyShouldDefault(t *testing.T) { } h := newTestFrontendHandler(defaultBlobSizeLimitError, defaultBlobSizeLimitWarn, defaultMaxIDLengthLimit) - err := h.validateAndNormalizeStartActivityExecutionRequest(req) + err := validateAndNormalizeStartRequest(req, h.config.MaxIDLengthLimit(), h.config.BlobSizeLimitError, h.config.BlobSizeLimitWarn, h.logger, h.saMapperProvider, h.saValidator) require.NoError(t, err) require.Equal(t, enumspb.ACTIVITY_ID_REUSE_POLICY_ALLOW_DUPLICATE, req.IdReusePolicy) @@ -437,7 +619,7 @@ func TestModifiedActivityTimeouts(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := ValidateAndNormalizeActivityAttributes( + err := validateAndNormalizeActivityAttributes( defaultActivityID, defaultActivityType, getDefaultRetrySettings, @@ -459,6 +641,53 @@ func TestModifiedActivityTimeouts(t *testing.T) { } } +func TestValidateDeleteActivityExecutionRequest(t *testing.T) { + t.Run("Success", func(t *testing.T) { + req := &workflowservice.DeleteActivityExecutionRequest{ + ActivityId: defaultActivityID, + } + err := validateAndNormalizeDeleteRequest(req, defaultMaxIDLengthLimit) + require.NoError(t, err) + }) + + t.Run("SuccessWithRunID", func(t *testing.T) { + req := &workflowservice.DeleteActivityExecutionRequest{ + ActivityId: defaultActivityID, + RunId: "f47ac10b-58cc-4372-a567-0e02b2c3d479", + } + err := validateAndNormalizeDeleteRequest(req, defaultMaxIDLengthLimit) + require.NoError(t, err) + }) + + t.Run("EmptyActivityID", func(t *testing.T) { + req := &workflowservice.DeleteActivityExecutionRequest{ + ActivityId: "", + } + err := validateAndNormalizeDeleteRequest(req, defaultMaxIDLengthLimit) + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + }) + + t.Run("ActivityIDTooLong", func(t *testing.T) { + req := &workflowservice.DeleteActivityExecutionRequest{ + ActivityId: string(make([]byte, defaultMaxIDLengthLimit+1)), + } + err := validateAndNormalizeDeleteRequest(req, defaultMaxIDLengthLimit) + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + }) + + t.Run("InvalidRunID", func(t *testing.T) { + req := &workflowservice.DeleteActivityExecutionRequest{ + ActivityId: defaultActivityID, + RunId: "not-a-valid-uuid", + } + err := validateAndNormalizeDeleteRequest(req, defaultMaxIDLengthLimit) + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + }) +} + func getDefaultRetrySettings(_ string) retrypolicy.DefaultRetrySettings { return retrypolicy.DefaultRetrySettings{ InitialInterval: time.Second, diff --git a/chasm/lib/callback/component.go b/chasm/lib/callback/component.go index c288ff76c23..0f64150f687 100644 --- a/chasm/lib/callback/component.go +++ b/chasm/lib/callback/component.go @@ -76,7 +76,7 @@ func (c *Callback) recordAttempt(ts time.Time) { func (c *Callback) loadInvocationArgs( ctx chasm.Context, _ chasm.NoValue, -) (callbackInvokable, error) { +) (invocable, error) { target := c.CompletionSource.Get(ctx) completion, err := target.GetNexusCompletion(ctx, c.RequestId) @@ -84,23 +84,23 @@ func (c *Callback) loadInvocationArgs( return nil, err } - variant := c.GetCallback().GetNexus() - if variant == nil { + callback := c.GetCallback().GetNexus() + if callback == nil { return nil, queueserrors.NewUnprocessableTaskError( - fmt.Sprintf("unprocessable callback variant: %v", variant), + fmt.Sprintf("unprocessable callback variant: %v", callback), ) } - if variant.Url == chasm.NexusCompletionHandlerURL { - return chasmInvocation{ - nexus: variant, + if callback.Url == chasm.NexusCompletionHandlerURL { + return invocableInternal{ + callback: callback, attempt: c.Attempt, completion: completion, requestID: c.RequestId, }, nil } - return nexusInvocation{ - nexus: variant, + return invocableOutbound{ + callback: callback, completion: completion, workflowID: ctx.ExecutionKey().BusinessID, runID: ctx.ExecutionKey().RunID, diff --git a/chasm/lib/callback/fx.go b/chasm/lib/callback/fx.go index 00e003b57c0..1da518d8368 100644 --- a/chasm/lib/callback/fx.go +++ b/chasm/lib/callback/fx.go @@ -9,6 +9,8 @@ import ( "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/collection" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" queuescommon "go.temporal.io/server/service/history/queues/common" "go.uber.org/fx" ) @@ -23,6 +25,7 @@ func register( // httpCallerProviderProvider provides an HTTPCallerProvider for CHASM callbacks. func httpCallerProviderProvider( clusterMetadata cluster.Metadata, + namespaceRegistry namespace.Registry, rpcFactory common.RPCFactory, httpClientCache *cluster.FrontendHTTPClientCache, logger log.Logger, @@ -32,12 +35,15 @@ func httpCallerProviderProvider( return nil, fmt.Errorf("cannot create local frontend HTTP client: %w", err) } defaultClient := &http.Client{} + callbackTokenGenerator := commonnexus.NewCallbackTokenGenerator() m := collection.NewOnceMap(func(queuescommon.NamespaceIDAndDestination) HTTPCaller { return func(r *http.Request) (*http.Response, error) { return routeRequest(r, clusterMetadata, + namespaceRegistry, httpClientCache, + callbackTokenGenerator, defaultClient, localClient, logger, @@ -51,8 +57,8 @@ var Module = fx.Module( "chasm.lib.callback", fx.Provide(configProvider), fx.Provide(httpCallerProviderProvider), - fx.Provide(NewInvocationTaskExecutor), - fx.Provide(NewBackoffTaskExecutor), + fx.Provide(newInvocationTaskHandler), + fx.Provide(newBackoffTaskHandler), fx.Provide(newLibrary), fx.Invoke(register), ) diff --git a/chasm/lib/callback/chasm_invocation.go b/chasm/lib/callback/invocable_internal.go similarity index 85% rename from chasm/lib/callback/chasm_invocation.go rename to chasm/lib/callback/invocable_internal.go index 668985e2163..273c0a38634 100644 --- a/chasm/lib/callback/chasm_invocation.go +++ b/chasm/lib/callback/invocable_internal.go @@ -24,14 +24,25 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -type chasmInvocation struct { - nexus *callbackspb.Callback_Nexus +// logInternalError emits a log statement for internalMsg, tagged with both +// internalErr and a reference-id. An opaque error containing the reference-id is +// returned. Intended to be used to hide internal errors from end users. +func logInternalError(logger log.Logger, internalMsg string, internalErr error) error { + referenceID := uuid.NewString() + logger.Error(internalMsg, tag.Error(internalErr), tag.String("reference-id", referenceID)) + return fmt.Errorf("internal error, reference-id: %v", referenceID) +} + +// invocableInternal is an invocable that delivers the Nexus operation completion data to History for cross-shard +// callbacks. +type invocableInternal struct { + callback *callbackspb.Callback_Nexus attempt int32 completion nexusrpc.CompleteOperationOptions requestID string } -func (c chasmInvocation) WrapError(result invocationResult, err error) error { +func (c invocableInternal) WrapError(result invocationResult, err error) error { // Return the invocation result error if present if resultErr := result.error(); resultErr != nil { return resultErr @@ -40,23 +51,14 @@ func (c chasmInvocation) WrapError(result invocationResult, err error) error { return err } -// logInternalError emits a log statement for internalMsg, tagged with both -// internalErr and a reference-id. An opaque error containing the reference-id is -// returned. Intended to be used to hide internal errors from end users. -func logInternalError(logger log.Logger, internalMsg string, internalErr error) error { - referenceID := uuid.NewString() - logger.Error(internalMsg, tag.Error(internalErr), tag.String("reference-id", referenceID)) - return fmt.Errorf("internal error, reference-id: %v", referenceID) -} - -func (c chasmInvocation) Invoke( +func (c invocableInternal) Invoke( ctx context.Context, ns *namespace.Namespace, - e InvocationTaskExecutor, + h *invocationTaskHandler, task *callbackspb.InvocationTask, taskAttr chasm.TaskAttributes, ) invocationResult { - header := nexus.Header(c.nexus.GetHeader()) + header := nexus.Header(c.callback.GetHeader()) if header == nil { header = nexus.Header{} } @@ -64,30 +66,30 @@ func (c chasmInvocation) Invoke( // Get back the base64-encoded ComponentRef from the header. encodedRef := header.Get(commonnexus.CallbackTokenHeader) if encodedRef == "" { - return invocationResultFail{logInternalError(e.logger, "callback missing token", nil)} + return invocationResultFail{logInternalError(h.logger, "callback missing token", nil)} } decodedRef, err := base64.RawURLEncoding.DecodeString(encodedRef) if err != nil { - return invocationResultFail{logInternalError(e.logger, "failed to decode CHASM ComponentRef", err)} + return invocationResultFail{logInternalError(h.logger, "failed to decode CHASM ComponentRef", err)} } // Validate that the bytes are a valid ChasmComponentRef ref := &persistencespb.ChasmComponentRef{} err = proto.Unmarshal(decodedRef, ref) if err != nil { - return invocationResultFail{logInternalError(e.logger, "failed to unmarshal CHASM ComponentRef", err)} + return invocationResultFail{logInternalError(h.logger, "failed to unmarshal CHASM ComponentRef", err)} } request, err := c.getHistoryRequest(decodedRef) if err != nil { - return invocationResultFail{logInternalError(e.logger, "failed to build history request", err)} + return invocationResultFail{logInternalError(h.logger, "failed to build history request", err)} } // RPC to History for cross-shard completion delivery. - _, err = e.historyClient.CompleteNexusOperationChasm(ctx, request) + _, err = h.historyClient.CompleteNexusOperationChasm(ctx, request) if err != nil { - msg := logInternalError(e.logger, "failed to complete Nexus operation", err) + msg := logInternalError(h.logger, "failed to complete Nexus operation", err) if isRetryableRPCResponse(err) { return invocationResultRetry{err: msg} } @@ -124,7 +126,7 @@ func isRetryableRPCResponse(err error) bool { } } -func (c chasmInvocation) getHistoryRequest( +func (c invocableInternal) getHistoryRequest( refBytes []byte, ) (*historyservice.CompleteNexusOperationChasmRequest, error) { var req *historyservice.CompleteNexusOperationChasmRequest diff --git a/chasm/lib/callback/nexus_invocation.go b/chasm/lib/callback/invocable_outbound.go similarity index 74% rename from chasm/lib/callback/nexus_invocation.go rename to chasm/lib/callback/invocable_outbound.go index ec29fabbaca..6ed0a65d6dc 100644 --- a/chasm/lib/callback/nexus_invocation.go +++ b/chasm/lib/callback/invocable_outbound.go @@ -3,7 +3,6 @@ package callback import ( "context" "errors" - "net/http" "net/http/httptrace" "time" @@ -20,34 +19,31 @@ import ( queueserrors "go.temporal.io/server/service/history/queues/errors" ) -var retryable4xxErrorTypes = []int{ - http.StatusRequestTimeout, - http.StatusTooManyRequests, -} - -type nexusInvocation struct { - nexus *callbackspb.Callback_Nexus +// invocableOutbound is an invocable that delivers the Nexus operation completion data to an external destination for +// cross-namespace or cross-cell callbacks. +type invocableOutbound struct { + callback *callbackspb.Callback_Nexus completion nexusrpc.CompleteOperationOptions workflowID, runID string attempt int32 } -func (n nexusInvocation) WrapError(result invocationResult, err error) error { +func (n invocableOutbound) WrapError(result invocationResult, err error) error { if retry, ok := result.(invocationResultRetry); ok { return queueserrors.NewDestinationDownError(retry.err.Error(), err) } return err } -func (n nexusInvocation) Invoke( +func (n invocableOutbound) Invoke( ctx context.Context, ns *namespace.Namespace, - e InvocationTaskExecutor, + h *invocationTaskHandler, task *callbackspb.InvocationTask, taskAttr chasm.TaskAttributes, ) invocationResult { - if e.httpTraceProvider != nil { - traceLogger := log.With(e.logger, + if h.httpTraceProvider != nil { + traceLogger := log.With(h.logger, tag.WorkflowNamespace(ns.Name().String()), tag.Operation("CompleteNexusOperation"), tag.String("destination", taskAttr.Destination), @@ -56,13 +52,13 @@ func (n nexusInvocation) Invoke( tag.AttemptStart(time.Now().UTC()), tag.Attempt(n.attempt), ) - if trace := e.httpTraceProvider.NewTrace(n.attempt, traceLogger); trace != nil { + if trace := h.httpTraceProvider.NewTrace(n.attempt, traceLogger); trace != nil { ctx = httptrace.WithClientTrace(ctx, trace) } } client := nexusrpc.NewCompletionHTTPClient(nexusrpc.CompletionHTTPClientOptions{ - HTTPCaller: e.httpCallerProvider(queuescommon.NamespaceIDAndDestination{ + HTTPCaller: h.httpCallerProvider(queuescommon.NamespaceIDAndDestination{ NamespaceID: ns.ID().String(), Destination: taskAttr.Destination, }), @@ -71,18 +67,18 @@ func (n nexusInvocation) Invoke( // Make the call and record metrics. startTime := time.Now() - n.completion.Header = n.nexus.Header - err := client.CompleteOperation(ctx, n.nexus.Url, n.completion) + n.completion.Header = n.callback.Header + err := client.CompleteOperation(ctx, n.callback.Url, n.completion) namespaceTag := metrics.NamespaceTag(ns.Name().String()) destTag := metrics.DestinationTag(taskAttr.Destination) outcomeTag := metrics.OutcomeTag(outcomeTag(ctx, err)) - e.metricsHandler.Counter(RequestCounter.Name()).Record(1, namespaceTag, destTag, outcomeTag) - e.metricsHandler.Timer(RequestLatencyHistogram.Name()).Record(time.Since(startTime), namespaceTag, destTag, outcomeTag) + h.metricsHandler.Counter(RequestCounter.Name()).Record(1, namespaceTag, destTag, outcomeTag) + h.metricsHandler.Timer(RequestLatencyHistogram.Name()).Record(time.Since(startTime), namespaceTag, destTag, outcomeTag) if err != nil { retryable := isRetryableCallError(err) - e.logger.Error("Callback request failed", tag.Error(err), tag.Bool("retryable", retryable)) + h.logger.Error("Callback request failed", tag.Error(err), tag.Bool("retryable", retryable)) if retryable { return invocationResultRetry{err} } diff --git a/chasm/lib/callback/library.go b/chasm/lib/callback/library.go index 12e8754deaf..838a88e1608 100644 --- a/chasm/lib/callback/library.go +++ b/chasm/lib/callback/library.go @@ -9,18 +9,18 @@ type ( Library struct { chasm.UnimplementedLibrary - InvocationTaskExecutor *InvocationTaskExecutor - BackoffTaskExecutor *BackoffTaskExecutor + InvocationTaskHandler *invocationTaskHandler + BackoffTaskHandler *backoffTaskHandler } ) func newLibrary( - InvocationTaskExecutor *InvocationTaskExecutor, - BackoffTaskExecutor *BackoffTaskExecutor, + InvocationTaskHandler *invocationTaskHandler, + BackoffTaskHandler *backoffTaskHandler, ) *Library { return &Library{ - InvocationTaskExecutor: InvocationTaskExecutor, - BackoffTaskExecutor: BackoffTaskExecutor, + InvocationTaskHandler: InvocationTaskHandler, + BackoffTaskHandler: BackoffTaskHandler, } } @@ -41,13 +41,11 @@ func (l *Library) Tasks() []*chasm.RegistrableTask { return []*chasm.RegistrableTask{ chasm.NewRegistrableSideEffectTask( "invoke", - l.InvocationTaskExecutor, - l.InvocationTaskExecutor, + l.InvocationTaskHandler, ), chasm.NewRegistrablePureTask( "backoff", - l.BackoffTaskExecutor, - l.BackoffTaskExecutor, + l.BackoffTaskHandler, ), } } diff --git a/chasm/lib/callback/proto/v1/message.proto b/chasm/lib/callback/proto/v1/message.proto index 0b0f102e805..057e5c470e0 100644 --- a/chasm/lib/callback/proto/v1/message.proto +++ b/chasm/lib/callback/proto/v1/message.proto @@ -2,70 +2,69 @@ syntax = "proto3"; package temporal.server.chasm.lib.callbacks.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; - import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/failure/v1/message.proto"; +option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; + message CallbackState { - // Trigger for when the workflow is closed. - message WorkflowClosed {} + // Trigger for when the workflow is closed. + message WorkflowClosed {} - // Information on how this callback should be invoked (e.g. its URL and type). - Callback callback = 1; - // The time when the callback was registered. - google.protobuf.Timestamp registration_time = 3; + // Information on how this callback should be invoked (e.g. its URL and type). + Callback callback = 1; + // The time when the callback was registered. + google.protobuf.Timestamp registration_time = 3; - CallbackStatus status = 4; - // The number of attempts made to deliver the callback. - // This number represents a minimum bound since the attempt is incremented after the callback request completes. - int32 attempt = 5; + CallbackStatus status = 4; + // The number of attempts made to deliver the callback. + // This number represents a minimum bound since the attempt is incremented after the callback request completes. + int32 attempt = 5; - // The time when the last attempt completed. - google.protobuf.Timestamp last_attempt_complete_time = 6; - // The last attempt's failure, if any. - temporal.api.failure.v1.Failure last_attempt_failure = 7; - // The time when the next attempt is scheduled. - // NOTE (seankane): this field might go away in the future, discussion: - // https://github.com/temporalio/temporal/pull/8473#discussion_r2427348436 - google.protobuf.Timestamp next_attempt_schedule_time = 8; + // The time when the last attempt completed. + google.protobuf.Timestamp last_attempt_complete_time = 6; + // The last attempt's failure, if any. + temporal.api.failure.v1.Failure last_attempt_failure = 7; + // The time when the next attempt is scheduled. + // NOTE (seankane): this field might go away in the future, discussion: + // https://github.com/temporalio/temporal/pull/8473#discussion_r2427348436 + google.protobuf.Timestamp next_attempt_schedule_time = 8; - // Request ID that added the callback. - string request_id = 9; + // Request ID that added the callback. + string request_id = 9; } - // Status of a callback. enum CallbackStatus { - // Default value, unspecified state. - CALLBACK_STATUS_UNSPECIFIED = 0; - // Callback is standing by, waiting to be triggered. - CALLBACK_STATUS_STANDBY = 1; - // Callback is in the queue waiting to be executed or is currently executing. - CALLBACK_STATUS_SCHEDULED = 2; - // Callback has failed with a retryable error and is backing off before the next attempt. - CALLBACK_STATUS_BACKING_OFF = 3; - // Callback has failed. - CALLBACK_STATUS_FAILED = 4; - // Callback has succeeded. - CALLBACK_STATUS_SUCCEEDED = 5; + // Default value, unspecified state. + CALLBACK_STATUS_UNSPECIFIED = 0; + // Callback is standing by, waiting to be triggered. + CALLBACK_STATUS_STANDBY = 1; + // Callback is in the queue waiting to be executed or is currently executing. + CALLBACK_STATUS_SCHEDULED = 2; + // Callback has failed with a retryable error and is backing off before the next attempt. + CALLBACK_STATUS_BACKING_OFF = 3; + // Callback has failed. + CALLBACK_STATUS_FAILED = 4; + // Callback has succeeded. + CALLBACK_STATUS_SUCCEEDED = 5; } message Callback { - message Nexus { - // Callback URL. - // (-- api-linter: core::0140::uri=disabled - // aip.dev/not-precedent: Not respecting aip here. --) - string url = 1; - // Header to attach to callback request. - map header = 2; - } + message Nexus { + // Callback URL. + // (-- api-linter: core::0140::uri=disabled + // aip.dev/not-precedent: Not respecting aip here. --) + string url = 1; + // Header to attach to callback request. + map header = 2; + } - reserved 1; // For a generic callback mechanism to be added later. - oneof variant { - Nexus nexus = 2; - } + reserved 1; // For a generic callback mechanism to be added later. + oneof variant { + Nexus nexus = 2; + } - repeated temporal.api.common.v1.Link links = 100; + repeated temporal.api.common.v1.Link links = 100; } diff --git a/chasm/lib/callback/proto/v1/tasks.proto b/chasm/lib/callback/proto/v1/tasks.proto index b4a9bf68e33..f4cb65faa6a 100644 --- a/chasm/lib/callback/proto/v1/tasks.proto +++ b/chasm/lib/callback/proto/v1/tasks.proto @@ -5,11 +5,11 @@ package temporal.server.chasm.lib.callbacks.proto.v1; option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; message InvocationTask { - // The attempt number for this invocation. - int32 attempt = 1; + // The attempt number for this invocation. + int32 attempt = 1; } message BackoffTask { - // The attempt number for this invocation. - int32 attempt = 1; -} \ No newline at end of file + // The attempt number for this invocation. + int32 attempt = 1; +} diff --git a/chasm/lib/callback/request.go b/chasm/lib/callback/request.go index 4e7e61aaf0a..3ae5c653856 100644 --- a/chasm/lib/callback/request.go +++ b/chasm/lib/callback/request.go @@ -1,29 +1,98 @@ package callback import ( + "errors" "net/http" + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/api/serviceerror" "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" - "go.temporal.io/server/common/nexus" + "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" ) // Header key used to identify callbacks that originate from and target the same cluster. // Note: this is the nexusoperations.NexusCallbackSourceHeader stripped of Nexus-Callback- const callbackSourceHeader = "source" +// routeSystemCallbackRequest routes a system callback request to the appropriate frontend client +// based on the callback token's namespace and active cluster. +func routeSystemCallbackRequest( + r *http.Request, + clusterMetadata cluster.Metadata, + namespaceRegistry namespace.Registry, + httpClientCache *cluster.FrontendHTTPClientCache, + callbackTokenGenerator *commonnexus.CallbackTokenGenerator, + localClient *common.FrontendHTTPClient, + logger log.Logger, +) (*http.Response, error) { + var frontendClient *common.FrontendHTTPClient + if r.Header != nil { + token, err := commonnexus.DecodeCallbackToken(r.Header.Get(commonnexus.CallbackTokenHeader)) + if err != nil { + logger.Error("failed to decode callback token", tag.Error(err)) + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid callback token") + } + + completion, err := callbackTokenGenerator.DecodeCompletion(token) + if err != nil { + logger.Error("failed to decode completion from token", tag.Error(err)) + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid callback token") + } + ns, err := namespaceRegistry.GetNamespaceByID(namespace.ID(completion.NamespaceId)) + if err != nil { + logger.Error("failed to get namespace for nexus completion request", tag.WorkflowNamespaceID(completion.NamespaceId), tag.Error(err)) + var nfe *serviceerror.NamespaceNotFound + if errors.As(err, &nfe) { + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "namespace %q not found", completion.NamespaceId) + } + return nil, commonnexus.ConvertGRPCError(err, false) + } + clusterName := ns.ActiveClusterName(completion.GetWorkflowId()) + if clusterMetadata.GetCurrentClusterName() == clusterName { + frontendClient = localClient + } else { + fec, err := httpClientCache.Get(clusterName) + if err != nil { + logger.Warn( + "HTTPCallerProvider unable to get FrontendHTTPClient for callback target cluster. Using local HTTP Client.", + tag.SourceCluster(clusterMetadata.GetCurrentClusterName()), + tag.TargetCluster(clusterName), + tag.Error(err), + ) + frontendClient = localClient + } else { + frontendClient = fec + } + } + } else { + frontendClient = localClient + } + r.URL.Path = commonnexus.PathCompletionCallbackNoIdentifier + r.URL.Scheme = frontendClient.Scheme + r.URL.Host = frontendClient.Address + r.Host = frontendClient.Address + return frontendClient.Do(r) +} + func routeRequest( r *http.Request, clusterMetadata cluster.Metadata, + namespaceRegistry namespace.Registry, httpClientCache *cluster.FrontendHTTPClientCache, + callbackTokenGenerator *commonnexus.CallbackTokenGenerator, defaultClient *http.Client, localClient *common.FrontendHTTPClient, logger log.Logger, ) (*http.Response, error) { - // This source header is populated in nexusoperations/executors (via the ClientProvider) for worker targets - // if this header is not populated then we assume it's and external target. + if r.URL.String() == commonnexus.SystemCallbackURL { + return routeSystemCallbackRequest(r, clusterMetadata, namespaceRegistry, httpClientCache, callbackTokenGenerator, localClient, logger) + } + // This source header is populated in nexusoperations/tasks (via the ClientProvider) for worker targets + // if this header is not populated then we assume it's an external target. if r.Header == nil || r.Header.Get(callbackSourceHeader) == "" { return defaultClient.Do(r) } @@ -61,9 +130,6 @@ func routeRequest( frontendClient = localClient } - if r.URL.String() == nexus.SystemCallbackURL { - r.URL.Path = nexus.PathCompletionCallbackNoIdentifier - } r.URL.Scheme = frontendClient.Scheme r.URL.Host = frontendClient.Address r.Host = frontendClient.Address diff --git a/chasm/lib/callback/request_test.go b/chasm/lib/callback/request_test.go new file mode 100644 index 00000000000..0ffeb03b466 --- /dev/null +++ b/chasm/lib/callback/request_test.go @@ -0,0 +1,324 @@ +package callback + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.temporal.io/api/serviceerror" + persistencespb "go.temporal.io/server/api/persistence/v1" + tokenspb "go.temporal.io/server/api/token/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/cluster" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" + "go.uber.org/mock/gomock" +) + +func newTestFrontendHTTPClient(ts *httptest.Server) *common.FrontendHTTPClient { + u, _ := url.Parse(ts.URL) + return &common.FrontendHTTPClient{ + Client: *ts.Client(), + Address: u.Host, + Scheme: u.Scheme, + } +} + +func TestRouteRequest_ExternalTarget(t *testing.T) { + // When no source header is set, the request should be sent via the default client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + + r, err := http.NewRequest(http.MethodPost, ts.URL+"/some/path", nil) + require.NoError(t, err) + + resp, err := routeRequest( + r, + clusterMeta, + nil, // namespaceRegistry not needed for external targets + nil, // httpClientCache not needed for external targets + nil, // callbackTokenGenerator not needed for external targets + ts.Client(), + nil, // localClient not needed for external targets + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRouteRequest_SourceHeaderLocal(t *testing.T) { + // When the source header matches the local cluster, the request should be routed to the local client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + clusterMeta.EXPECT().GetAllClusterInfo().Return(map[string]cluster.ClusterInformation{ + "cluster-A": {ClusterID: "cluster-id-A"}, + }) + clusterMeta.EXPECT().GetCurrentClusterName().Return("cluster-A") + + localClient := newTestFrontendHTTPClient(ts) + + r, err := http.NewRequest(http.MethodPost, "http://original-host/some/path", nil) + require.NoError(t, err) + r.Header.Set(callbackSourceHeader, "cluster-id-A") + + resp, err := routeRequest( + r, + clusterMeta, + nil, // namespaceRegistry + nil, // httpClientCache - not used since it's the local cluster + nil, // callbackTokenGenerator + &http.Client{}, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusAccepted, resp.StatusCode) +} + +func TestRouteRequest_SourceHeaderUnknownCluster(t *testing.T) { + // When the source header doesn't match any known cluster, falls back to local client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + clusterMeta.EXPECT().GetAllClusterInfo().Return(map[string]cluster.ClusterInformation{ + "cluster-A": {ClusterID: "cluster-id-A"}, + }) + clusterMeta.EXPECT().GetCurrentClusterName().Return("cluster-A") + + localClient := newTestFrontendHTTPClient(ts) + + r, err := http.NewRequest(http.MethodPost, "http://original-host/some/path", nil) + require.NoError(t, err) + r.Header.Set(callbackSourceHeader, "unknown-cluster-id") + + resp, err := routeRequest( + r, + clusterMeta, + nil, + nil, + nil, + &http.Client{}, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusAccepted, resp.StatusCode) +} + +func TestRouteSystemCallbackRequest_NilHeaders(t *testing.T) { + // When the request has nil headers, it should fall back to the local client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, commonnexus.PathCompletionCallbackNoIdentifier, r.URL.Path) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + localClient := newTestFrontendHTTPClient(ts) + + r := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{Path: "/"}, + Header: nil, + } + + resp, err := routeSystemCallbackRequest( + r, + nil, // clusterMetadata - not needed for nil headers path + nil, // namespaceRegistry + nil, // httpClientCache + nil, // callbackTokenGenerator + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRouteSystemCallbackRequest_InvalidToken(t *testing.T) { + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, "not-valid-json") + + _, err = routeSystemCallbackRequest( + r, + nil, + nil, + nil, + nil, + nil, + log.NewNoopLogger(), + ) + require.Error(t, err) + var handlerErr *nexus.HandlerError + require.ErrorAs(t, err, &handlerErr) + require.Equal(t, nexus.HandlerErrorTypeBadRequest, handlerErr.Type) + require.Contains(t, handlerErr.Error(), "invalid callback token") +} + +func TestRouteSystemCallbackRequest_InvalidTokenData(t *testing.T) { + // Valid token structure but invalid data field. + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, `{"v":1,"d":"!!!invalid-base64"}`) + + tokenGen := commonnexus.NewCallbackTokenGenerator() + + _, err = routeSystemCallbackRequest( + r, + nil, + nil, + nil, + tokenGen, + nil, + log.NewNoopLogger(), + ) + require.Error(t, err) + var handlerErr *nexus.HandlerError + require.ErrorAs(t, err, &handlerErr) + require.Equal(t, nexus.HandlerErrorTypeBadRequest, handlerErr.Type) +} + +func TestRouteSystemCallbackRequest_NamespaceNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + nsRegistry := namespace.NewMockRegistry(ctrl) + + tokenGen := commonnexus.NewCallbackTokenGenerator() + tokenStr, err := tokenGen.Tokenize(&tokenspb.NexusOperationCompletion{ + NamespaceId: "ns-id-1", + WorkflowId: "wf-1", + }) + require.NoError(t, err) + + nsRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id-1")).Return( + nil, serviceerror.NewNamespaceNotFound("ns-id-1"), + ) + + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, tokenStr) + + _, err = routeSystemCallbackRequest( + r, + nil, + nsRegistry, + nil, + tokenGen, + nil, + log.NewNoopLogger(), + ) + require.Error(t, err) + var handlerErr *nexus.HandlerError + require.ErrorAs(t, err, &handlerErr) + require.Equal(t, nexus.HandlerErrorTypeNotFound, handlerErr.Type) +} + +func TestRouteSystemCallbackRequest_Success(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, commonnexus.PathCompletionCallbackNoIdentifier, r.URL.Path) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + nsRegistry := namespace.NewMockRegistry(ctrl) + + tokenGen := commonnexus.NewCallbackTokenGenerator() + tokenStr, err := tokenGen.Tokenize(&tokenspb.NexusOperationCompletion{ + NamespaceId: "ns-id-1", + WorkflowId: "wf-1", + }) + require.NoError(t, err) + + testNS := namespace.NewLocalNamespaceForTest( + &persistencespb.NamespaceInfo{Id: "ns-id-1", Name: "test-ns"}, + nil, + "cluster-A", + ) + nsRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id-1")).Return(testNS, nil) + + // httpClientCache.Get will fail for "cluster-A", so it falls back to localClient. + clusterMeta.EXPECT().GetCurrentClusterName().Return("cluster-A").AnyTimes() + clusterMeta.EXPECT().GetAllClusterInfo().Return(map[string]cluster.ClusterInformation{}).AnyTimes() + clusterMeta.EXPECT().RegisterMetadataChangeCallback(gomock.Any(), gomock.Any()) + + localClient := newTestFrontendHTTPClient(ts) + + // Create a cache that will fail for the requested cluster since we don't set up metadata fully. + httpClientCache := cluster.NewFrontendHTTPClientCache(clusterMeta, nil) + + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, tokenStr) + + resp, err := routeSystemCallbackRequest( + r, + clusterMeta, + nsRegistry, + httpClientCache, + tokenGen, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRouteRequest_SystemCallback(t *testing.T) { + // Verify that routeRequest delegates to routeSystemCallbackRequest for system callback URLs. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, commonnexus.PathCompletionCallbackNoIdentifier, r.URL.Path) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + localClient := newTestFrontendHTTPClient(ts) + + // Use nil headers to take the simplest path through routeSystemCallbackRequest. + r := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "temporal", + Host: "system", + }, + Header: nil, + } + + resp, err := routeRequest( + r, + nil, + nil, + nil, + nil, + &http.Client{}, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/chasm/lib/callback/executors.go b/chasm/lib/callback/tasks.go similarity index 68% rename from chasm/lib/callback/executors.go rename to chasm/lib/callback/tasks.go index 9ab9e199e51..b53efafea19 100644 --- a/chasm/lib/callback/executors.go +++ b/chasm/lib/callback/tasks.go @@ -18,51 +18,11 @@ import ( // HTTPCaller is a method that can be used to invoke HTTP requests. type HTTPCaller func(*http.Request) (*http.Response, error) -type HTTPCallerProvider func(common.NamespaceIDAndDestination) HTTPCaller - -func NewInvocationTaskExecutor(opts InvocationTaskExecutorOptions) *InvocationTaskExecutor { - return &InvocationTaskExecutor{ - config: opts.Config, - namespaceRegistry: opts.NamespaceRegistry, - metricsHandler: opts.MetricsHandler, - logger: opts.Logger, - httpCallerProvider: opts.HTTPCallerProvider, - httpTraceProvider: opts.HTTPTraceProvider, - historyClient: opts.HistoryClient, - } -} - -type InvocationTaskExecutorOptions struct { - fx.In - - Config *Config - NamespaceRegistry namespace.Registry - MetricsHandler metrics.Handler - Logger log.Logger - HTTPCallerProvider HTTPCallerProvider - HTTPTraceProvider commonnexus.HTTPClientTraceProvider - HistoryClient resource.HistoryClient -} - -type InvocationTaskExecutor struct { - config *Config - namespaceRegistry namespace.Registry - metricsHandler metrics.Handler - logger log.Logger - httpCallerProvider HTTPCallerProvider - httpTraceProvider commonnexus.HTTPClientTraceProvider - historyClient resource.HistoryClient -} -func (e InvocationTaskExecutor) Execute(ctx context.Context, ref chasm.ComponentRef, attrs chasm.TaskAttributes, task *callbackspb.InvocationTask) error { - return e.Invoke(ctx, ref, attrs, task) -} - -func (e InvocationTaskExecutor) Validate(ctx chasm.Context, cb *Callback, attrs chasm.TaskAttributes, task *callbackspb.InvocationTask) (bool, error) { - return cb.Attempt == task.Attempt && cb.Status == callbackspb.CALLBACK_STATUS_SCHEDULED, nil -} +// HTTPCallerProvider is a method that can be used to retrieve an HTTPCaller for a given namespace and destination. +type HTTPCallerProvider func(common.NamespaceIDAndDestination) HTTPCaller -// invocationResult is a marker for the callbackInvokable.Invoke result to indicate to the executor how to handle the +// invocationResult is a marker for the callbackInvokable.Invoke result to indicate to the handler how to handle the // invocation outcome. type invocationResult interface { // A marker for all possible implementations. @@ -70,7 +30,7 @@ type invocationResult interface { error() error } -// invocationResultFail marks an invocation as successful. +// invocationResultOK marks an invocation as successful. type invocationResultOK struct{} func (invocationResultOK) mustImplementInvocationResult() {} @@ -101,21 +61,60 @@ func (r invocationResultRetry) error() error { return r.err } -type callbackInvokable interface { +type invocable interface { // Invoke executes the callback logic and returns the invocation result. - Invoke(ctx context.Context, ns *namespace.Namespace, e InvocationTaskExecutor, task *callbackspb.InvocationTask, taskAttr chasm.TaskAttributes) invocationResult - // WrapError provides each variant the opportunity to wrap the error returned by the task executor for, e.g. to + Invoke(ctx context.Context, ns *namespace.Namespace, h *invocationTaskHandler, task *callbackspb.InvocationTask, taskAttr chasm.TaskAttributes) invocationResult + // WrapError provides each variant the opportunity to wrap the error returned by the task handler for, e.g. to // trigger the circuit breaker. WrapError(result invocationResult, err error) error } -func (e InvocationTaskExecutor) Invoke( +type invocationTaskHandlerOptions struct { + fx.In + + Config *Config + NamespaceRegistry namespace.Registry + MetricsHandler metrics.Handler + Logger log.Logger + HTTPCallerProvider HTTPCallerProvider + HTTPTraceProvider commonnexus.HTTPClientTraceProvider + HistoryClient resource.HistoryClient +} + +type invocationTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*callbackspb.InvocationTask] + config *Config + namespaceRegistry namespace.Registry + metricsHandler metrics.Handler + logger log.Logger + httpCallerProvider HTTPCallerProvider + httpTraceProvider commonnexus.HTTPClientTraceProvider + historyClient resource.HistoryClient +} + +func newInvocationTaskHandler(opts invocationTaskHandlerOptions) *invocationTaskHandler { + return &invocationTaskHandler{ + config: opts.Config, + namespaceRegistry: opts.NamespaceRegistry, + metricsHandler: opts.MetricsHandler, + logger: opts.Logger, + httpCallerProvider: opts.HTTPCallerProvider, + httpTraceProvider: opts.HTTPTraceProvider, + historyClient: opts.HistoryClient, + } +} + +func (h *invocationTaskHandler) Validate(ctx chasm.Context, cb *Callback, attrs chasm.TaskAttributes, task *callbackspb.InvocationTask) (bool, error) { + return cb.Attempt == task.Attempt && cb.Status == callbackspb.CALLBACK_STATUS_SCHEDULED, nil +} + +func (h *invocationTaskHandler) Execute( ctx context.Context, ref chasm.ComponentRef, taskAttr chasm.TaskAttributes, task *callbackspb.InvocationTask, ) error { - ns, err := e.namespaceRegistry.GetNamespaceByID(namespace.ID(ref.NamespaceID)) + ns, err := h.namespaceRegistry.GetNamespaceByID(namespace.ID(ref.NamespaceID)) if err != nil { return fmt.Errorf("failed to get namespace by ID: %w", err) } @@ -132,48 +131,37 @@ func (e InvocationTaskExecutor) Invoke( callCtx, cancel := context.WithTimeout( ctx, - e.config.RequestTimeout(ns.Name().String(), taskAttr.Destination), + h.config.RequestTimeout(ns.Name().String(), taskAttr.Destination), ) defer cancel() - result := invokable.Invoke(callCtx, ns, e, task, taskAttr) + result := invokable.Invoke(callCtx, ns, h, task, taskAttr) _, _, saveErr := chasm.UpdateComponent( ctx, ref, (*Callback).saveResult, saveResultInput{ result: result, - retryPolicy: e.config.RetryPolicy(), + retryPolicy: h.config.RetryPolicy(), }, ) return invokable.WrapError(result, saveErr) } -type BackoffTaskExecutor struct { - config *Config - metricsHandler metrics.Handler - logger log.Logger +type backoffTaskHandler struct { + chasm.PureTaskHandlerBase } -type BackoffTaskExecutorOptions struct { +type backoffTaskHandlerOptions struct { fx.In - - Config *Config - MetricsHandler metrics.Handler - Logger log.Logger } -func NewBackoffTaskExecutor(opts BackoffTaskExecutorOptions) *BackoffTaskExecutor { - return &BackoffTaskExecutor{ - config: opts.Config, - metricsHandler: opts.MetricsHandler, - logger: opts.Logger, - } +func newBackoffTaskHandler(opts backoffTaskHandlerOptions) *backoffTaskHandler { + return &backoffTaskHandler{} } -// Execute transitions the callback from BACKING_OFF to SCHEDULED state -// and generates an InvocationTask for the next attempt. -func (e *BackoffTaskExecutor) Execute( +// Execute toggles the callback status from BACKING_OFF to SCHEDULED to trigger a new invocation attempt. +func (h *backoffTaskHandler) Execute( ctx chasm.MutableContext, callback *Callback, taskAttrs chasm.TaskAttributes, @@ -182,12 +170,13 @@ func (e *BackoffTaskExecutor) Execute( return TransitionRescheduled.Apply(callback, ctx, EventRescheduled{}) } -func (e *BackoffTaskExecutor) Validate( +// Validate validates that the callback is in BACKING_OFF state and that the attempt number matches before allowing the +// backoff task to execute. +func (h *backoffTaskHandler) Validate( ctx chasm.Context, callback *Callback, taskAttr chasm.TaskAttributes, task *callbackspb.BackoffTask, ) (bool, error) { - // Validate that the callback is in BACKING_OFF state return callback.Status == callbackspb.CALLBACK_STATUS_BACKING_OFF && callback.Attempt == task.Attempt, nil } diff --git a/chasm/lib/callback/executors_test.go b/chasm/lib/callback/tasks_test.go similarity index 94% rename from chasm/lib/callback/executors_test.go rename to chasm/lib/callback/tasks_test.go index b20cdf6c89c..7804d084edc 100644 --- a/chasm/lib/callback/executors_test.go +++ b/chasm/lib/callback/tasks_test.go @@ -54,6 +54,17 @@ func (m *mockNexusCompletionGetterComponent) LifecycleState(_ chasm.Context) cha return chasm.LifecycleStateRunning } +func (m *mockNexusCompletionGetterComponent) ContextMetadata(_ chasm.Context) map[string]string { + return nil +} + +func (m *mockNexusCompletionGetterComponent) Terminate( + _ chasm.MutableContext, + _ chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + return chasm.TerminateComponentResponse{}, nil +} + type mockNexusCompletionGetterLibrary struct { chasm.UnimplementedLibrary } @@ -68,7 +79,7 @@ func (l *mockNexusCompletionGetterLibrary) Components() []*chasm.RegistrableComp } } -// Test the full executeInvocationTask flow with direct executor calls +// Test the full executeInvocationTask flow with direct handler calls func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { cases := []struct { name string @@ -143,6 +154,7 @@ func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { // Setup metrics expectations metricsHandler := metrics.NewMockHandler(ctrl) + metricsHandler.EXPECT().WithTags(gomock.Any()).Return(metricsHandler).AnyTimes() counter := metrics.NewMockCounterIface(ctrl) timer := metrics.NewMockTimerIface(ctrl) metricsHandler.EXPECT().Counter(RequestCounter.Name()).Return(counter) @@ -161,13 +173,13 @@ func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { timeSource := clock.NewEventTimeSource() timeSource.Update(time.Now()) - // Create task executor with mock namespace registry + // Create task handler with mock namespace registry nsRegistry := namespace.NewMockRegistry(ctrl) nsRegistry.EXPECT().GetNamespaceByID(gomock.Any()).Return(ns, nil) // Create mock engine mockEngine := chasm.NewMockEngine(ctrl) - executor := &InvocationTaskExecutor{ + handler := &invocationTaskHandler{ config: &Config{ RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Second), RetryPolicy: func() backoff.RetryPolicy { @@ -184,14 +196,14 @@ func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { chasmRegistry := chasm.NewRegistry(logger) err = chasmRegistry.Register(&Library{ - InvocationTaskExecutor: executor, + InvocationTaskHandler: handler, }) require.NoError(t, err) err = chasmRegistry.Register(&mockNexusCompletionGetterLibrary{}) require.NoError(t, err) nodeBackend := &chasm.MockNodeBackend{} - root := chasm.NewEmptyTree(chasmRegistry, timeSource, nodeBackend, chasm.DefaultPathEncoder, logger) + root := chasm.NewEmptyTree(chasmRegistry, timeSource, nodeBackend, chasm.DefaultPathEncoder, logger, metricsHandler) callback := &Callback{ CallbackState: &callbackspb.CallbackState{ @@ -224,7 +236,7 @@ func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { _, err = root.CloseTransaction() require.NoError(t, err) - // Setup engine expectations to directly call executor logic with MockMutableContext + // Setup engine expectations to directly call handler logic with MockMutableContext mockEngine.EXPECT().ReadComponent( gomock.Any(), gomock.Any(), @@ -269,7 +281,7 @@ func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { // Execute with engine context engineCtx := chasm.NewEngineContext(context.Background(), mockEngine) - err = executor.Invoke( + err = handler.Execute( engineCtx, ref, chasm.TaskAttributes{Destination: "http://localhost"}, @@ -288,7 +300,6 @@ func TestProcessBackoffTask(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - logger := log.NewTestLogger() timeSource := clock.NewEventTimeSource() timeSource.Update(time.Now()) @@ -321,20 +332,12 @@ func TestProcessBackoffTask(t *testing.T) { }, } - executor := BackoffTaskExecutor{ - config: &Config{ - RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Second), - RetryPolicy: func() backoff.RetryPolicy { - return backoff.NewExponentialRetryPolicy(time.Second) - }, - }, - logger: logger, - } + handler := backoffTaskHandler{} // Execute the backoff task task := &callbackspb.BackoffTask{Attempt: 1} attrs := chasm.TaskAttributes{Destination: "http://localhost"} - err := executor.Execute(mockCtx, callback, attrs, task) + err := handler.Execute(mockCtx, callback, attrs, task) // Verify no error require.NoError(t, err) @@ -537,8 +540,9 @@ func TestExecuteInvocationTaskChasm_Outcomes(t *testing.T) { // Setup history client historyClient := tc.setupHistoryClient(t, ctrl) - // Setup logger and time source + // Setup logger, metricsHandler, and time source logger := log.NewTestLogger() + metricsHandler := metrics.NoopMetricsHandler timeSource := clock.NewEventTimeSource() timeSource.Update(time.Now()) @@ -548,7 +552,7 @@ func TestExecuteInvocationTaskChasm_Outcomes(t *testing.T) { // Create mock engine and setup expectations mockEngine := chasm.NewMockEngine(ctrl) - executor := &InvocationTaskExecutor{ + handler := &invocationTaskHandler{ config: &Config{ RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Second), RetryPolicy: func() backoff.RetryPolicy { @@ -556,21 +560,21 @@ func TestExecuteInvocationTaskChasm_Outcomes(t *testing.T) { }, }, namespaceRegistry: nsRegistry, - metricsHandler: metrics.NoopMetricsHandler, + metricsHandler: metricsHandler, logger: logger, historyClient: historyClient, } chasmRegistry := chasm.NewRegistry(logger) err = chasmRegistry.Register(&Library{ - InvocationTaskExecutor: executor, + InvocationTaskHandler: handler, }) require.NoError(t, err) err = chasmRegistry.Register(&mockNexusCompletionGetterLibrary{}) require.NoError(t, err) nodeBackend := &chasm.MockNodeBackend{} - root := chasm.NewEmptyTree(chasmRegistry, timeSource, nodeBackend, chasm.DefaultPathEncoder, logger) + root := chasm.NewEmptyTree(chasmRegistry, timeSource, nodeBackend, chasm.DefaultPathEncoder, logger, metricsHandler) // Create headers headers := nexus.Header{} @@ -668,7 +672,7 @@ func TestExecuteInvocationTaskChasm_Outcomes(t *testing.T) { // Execute the invocation task task := &callbackspb.InvocationTask{Attempt: 1} - err = executor.Invoke( + err = handler.Execute( ctx, ref, chasm.TaskAttributes{}, diff --git a/chasm/lib/nexusoperation/cancellation_executors.go b/chasm/lib/nexusoperation/cancellation_tasks.go similarity index 69% rename from chasm/lib/nexusoperation/cancellation_executors.go rename to chasm/lib/nexusoperation/cancellation_tasks.go index 72fdeb1566f..06368782161 100644 --- a/chasm/lib/nexusoperation/cancellation_executors.go +++ b/chasm/lib/nexusoperation/cancellation_tasks.go @@ -11,7 +11,7 @@ import ( "go.uber.org/fx" ) -type CancellationTaskExecutorOptions struct { +type CancellationTaskHandlerOptions struct { fx.In Config *Config @@ -20,22 +20,23 @@ type CancellationTaskExecutorOptions struct { Logger log.Logger } -type CancellationTaskExecutor struct { +type CancellationTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*nexusoperationpb.CancellationTask] config *Config metricsHandler metrics.Handler logger log.Logger } -func NewCancellationTaskExecutor(opts CancellationTaskExecutorOptions) *CancellationTaskExecutor { - return &CancellationTaskExecutor{ +func NewCancellationTaskHandler(opts CancellationTaskHandlerOptions) *CancellationTaskHandler { + return &CancellationTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, logger: opts.Logger, } } -func (e *CancellationTaskExecutor) Validate( +func (h *CancellationTaskHandler) Validate( ctx chasm.Context, cancellation *Cancellation, attrs chasm.TaskAttributes, @@ -44,7 +45,7 @@ func (e *CancellationTaskExecutor) Validate( return false, serviceerror.NewUnimplemented("unimplemented") } -func (e *CancellationTaskExecutor) Execute( +func (h *CancellationTaskHandler) Execute( ctx context.Context, cancelRef chasm.ComponentRef, attrs chasm.TaskAttributes, @@ -53,22 +54,23 @@ func (e *CancellationTaskExecutor) Execute( return serviceerror.NewUnimplemented("unimplemented") } -type CancellationBackoffTaskExecutor struct { +type CancellationBackoffTaskHandler struct { + chasm.PureTaskHandlerBase config *Config metricsHandler metrics.Handler logger log.Logger } -func NewCancellationBackoffTaskExecutor(opts CancellationTaskExecutorOptions) *CancellationBackoffTaskExecutor { - return &CancellationBackoffTaskExecutor{ +func NewCancellationBackoffTaskHandler(opts CancellationTaskHandlerOptions) *CancellationBackoffTaskHandler { + return &CancellationBackoffTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, logger: opts.Logger, } } -func (e *CancellationBackoffTaskExecutor) Validate( +func (h *CancellationBackoffTaskHandler) Validate( ctx chasm.Context, cancellation *Cancellation, attrs chasm.TaskAttributes, @@ -77,7 +79,7 @@ func (e *CancellationBackoffTaskExecutor) Validate( return false, serviceerror.NewUnimplemented("unimplemented") } -func (e *CancellationBackoffTaskExecutor) Execute( +func (h *CancellationBackoffTaskHandler) Execute( ctx chasm.MutableContext, cancellation *Cancellation, attrs chasm.TaskAttributes, diff --git a/chasm/lib/nexusoperation/config.go b/chasm/lib/nexusoperation/config.go index ce0fcbc9008..1e98f39da37 100644 --- a/chasm/lib/nexusoperation/config.go +++ b/chasm/lib/nexusoperation/config.go @@ -149,7 +149,6 @@ Added for safety. Defaults to true. Likely to be removed in future server versio ) type Config struct { - Enabled dynamicconfig.BoolPropertyFn ChasmEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter ChasmNexusEnabled dynamicconfig.BoolPropertyFn RequestTimeout dynamicconfig.DurationPropertyFnWithDestinationFilter @@ -170,7 +169,6 @@ type Config struct { func configProvider(dc *dynamicconfig.Collection) *Config { return &Config{ - Enabled: dynamicconfig.EnableNexus.Get(dc), ChasmEnabled: dynamicconfig.EnableChasm.Get(dc), ChasmNexusEnabled: ChasmNexusEnabled.Get(dc), RequestTimeout: RequestTimeout.Get(dc), diff --git a/chasm/lib/nexusoperation/fx.go b/chasm/lib/nexusoperation/fx.go index ccb1699b843..e5186c3d33b 100644 --- a/chasm/lib/nexusoperation/fx.go +++ b/chasm/lib/nexusoperation/fx.go @@ -8,11 +8,11 @@ import ( var Module = fx.Module( "chasm.lib.nexusoperations", fx.Provide(configProvider), - fx.Provide(NewOperationInvocationTaskExecutor), - fx.Provide(NewOperationBackoffTaskExecutor), - fx.Provide(NewOperationTimeoutTaskExecutor), - fx.Provide(NewCancellationTaskExecutor), - fx.Provide(NewCancellationBackoffTaskExecutor), + fx.Provide(NewOperationInvocationTaskHandler), + fx.Provide(NewOperationBackoffTaskHandler), + fx.Provide(NewOperationTimeoutTaskHandler), + fx.Provide(NewCancellationTaskHandler), + fx.Provide(NewCancellationBackoffTaskHandler), fx.Provide(newLibrary), fx.Invoke(register), ) diff --git a/chasm/lib/nexusoperation/library.go b/chasm/lib/nexusoperation/library.go index 180f06076af..31a1d18f16d 100644 --- a/chasm/lib/nexusoperation/library.go +++ b/chasm/lib/nexusoperation/library.go @@ -2,19 +2,18 @@ package nexusoperation import ( "go.temporal.io/server/chasm" - "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "google.golang.org/grpc" ) type Library struct { chasm.UnimplementedLibrary - OperationInvocationTaskExecutor *OperationInvocationTaskExecutor - OperationBackoffTaskExecutor *OperationBackoffTaskExecutor - OperationTimeoutTaskExecutor *OperationTimeoutTaskExecutor + OperationInvocationTaskHandler *OperationInvocationTaskHandler + OperationBackoffTaskHandler *OperationBackoffTaskHandler + OperationTimeoutTaskHandler *OperationTimeoutTaskHandler - CancellationTaskExecutor *CancellationTaskExecutor - CancellationBackoffTaskExecutor *CancellationBackoffTaskExecutor + CancellationTaskHandler *CancellationTaskHandler + CancellationBackoffTaskHandler *CancellationBackoffTaskHandler } func newLibrary() *Library { @@ -34,11 +33,11 @@ func (l *Library) Components() []*chasm.RegistrableComponent { func (l *Library) Tasks() []*chasm.RegistrableTask { return []*chasm.RegistrableTask{ - chasm.NewRegistrableSideEffectTask[*Operation, *nexusoperationpb.InvocationTask]("invocation", l.OperationInvocationTaskExecutor, l.OperationInvocationTaskExecutor), - chasm.NewRegistrablePureTask[*Operation, *nexusoperationpb.InvocationBackoffTask]("invocationBackoff", l.OperationBackoffTaskExecutor, l.OperationBackoffTaskExecutor), - chasm.NewRegistrablePureTask[*Operation, *nexusoperationpb.InvocationTimeoutTask]("scheduleToCloseTimeout", l.OperationTimeoutTaskExecutor, l.OperationTimeoutTaskExecutor), - chasm.NewRegistrableSideEffectTask[*Cancellation, *nexusoperationpb.CancellationTask]("cancellation", l.CancellationTaskExecutor, l.CancellationTaskExecutor), - chasm.NewRegistrablePureTask[*Cancellation, *nexusoperationpb.CancellationBackoffTask]("cancellationBackoff", l.CancellationBackoffTaskExecutor, l.CancellationBackoffTaskExecutor), + chasm.NewRegistrableSideEffectTask("invocation", l.OperationInvocationTaskHandler), + chasm.NewRegistrablePureTask("invocationBackoff", l.OperationBackoffTaskHandler), + chasm.NewRegistrablePureTask("scheduleToCloseTimeout", l.OperationTimeoutTaskHandler), + chasm.NewRegistrableSideEffectTask("cancellation", l.CancellationTaskHandler), + chasm.NewRegistrablePureTask("cancellationBackoff", l.CancellationBackoffTaskHandler), } } diff --git a/chasm/lib/nexusoperation/operation_executors.go b/chasm/lib/nexusoperation/operation_tasks.go similarity index 64% rename from chasm/lib/nexusoperation/operation_executors.go rename to chasm/lib/nexusoperation/operation_tasks.go index 67fb320b79e..396bf03e0e5 100644 --- a/chasm/lib/nexusoperation/operation_executors.go +++ b/chasm/lib/nexusoperation/operation_tasks.go @@ -11,8 +11,8 @@ import ( "go.uber.org/fx" ) -// OperationTaskExecutorOptions is the fx parameter object for common options supplied to all operation task executors. -type OperationTaskExecutorOptions struct { +// OperationTaskHandlerOptions is the fx parameter object for common options supplied to all operation task handlers. +type OperationTaskHandlerOptions struct { fx.In Config *Config @@ -21,22 +21,23 @@ type OperationTaskExecutorOptions struct { Logger log.Logger } -type OperationInvocationTaskExecutor struct { +type OperationInvocationTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*nexusoperationpb.InvocationTask] config *Config metricsHandler metrics.Handler logger log.Logger } -func NewOperationInvocationTaskExecutor(opts OperationTaskExecutorOptions) *OperationInvocationTaskExecutor { - return &OperationInvocationTaskExecutor{ +func NewOperationInvocationTaskHandler(opts OperationTaskHandlerOptions) *OperationInvocationTaskHandler { + return &OperationInvocationTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, logger: opts.Logger, } } -func (e *OperationInvocationTaskExecutor) Validate( +func (h *OperationInvocationTaskHandler) Validate( ctx chasm.Context, op *Operation, attrs chasm.TaskAttributes, @@ -45,7 +46,7 @@ func (e *OperationInvocationTaskExecutor) Validate( return false, serviceerror.NewUnimplemented("unimplemented") } -func (e *OperationInvocationTaskExecutor) Execute( +func (h *OperationInvocationTaskHandler) Execute( ctx context.Context, opRef chasm.ComponentRef, attrs chasm.TaskAttributes, @@ -54,22 +55,23 @@ func (e *OperationInvocationTaskExecutor) Execute( return serviceerror.NewUnimplemented("unimplemented") } -type OperationBackoffTaskExecutor struct { +type OperationBackoffTaskHandler struct { + chasm.PureTaskHandlerBase config *Config metricsHandler metrics.Handler logger log.Logger } -func NewOperationBackoffTaskExecutor(opts OperationTaskExecutorOptions) *OperationBackoffTaskExecutor { - return &OperationBackoffTaskExecutor{ +func NewOperationBackoffTaskHandler(opts OperationTaskHandlerOptions) *OperationBackoffTaskHandler { + return &OperationBackoffTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, logger: opts.Logger, } } -func (e *OperationBackoffTaskExecutor) Validate( +func (h *OperationBackoffTaskHandler) Validate( ctx chasm.Context, op *Operation, attrs chasm.TaskAttributes, @@ -78,7 +80,7 @@ func (e *OperationBackoffTaskExecutor) Validate( return false, serviceerror.NewUnimplemented("unimplemented") } -func (e *OperationBackoffTaskExecutor) Execute( +func (h *OperationBackoffTaskHandler) Execute( ctx chasm.MutableContext, op *Operation, attrs chasm.TaskAttributes, @@ -87,22 +89,23 @@ func (e *OperationBackoffTaskExecutor) Execute( return serviceerror.NewUnimplemented("unimplemented") } -type OperationTimeoutTaskExecutor struct { +type OperationTimeoutTaskHandler struct { + chasm.PureTaskHandlerBase config *Config metricsHandler metrics.Handler logger log.Logger } -func NewOperationTimeoutTaskExecutor(opts OperationTaskExecutorOptions) *OperationTimeoutTaskExecutor { - return &OperationTimeoutTaskExecutor{ +func NewOperationTimeoutTaskHandler(opts OperationTaskHandlerOptions) *OperationTimeoutTaskHandler { + return &OperationTimeoutTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, logger: opts.Logger, } } -func (e *OperationTimeoutTaskExecutor) Validate( +func (h *OperationTimeoutTaskHandler) Validate( ctx chasm.Context, op *Operation, attrs chasm.TaskAttributes, @@ -111,7 +114,7 @@ func (e *OperationTimeoutTaskExecutor) Validate( return false, serviceerror.NewUnimplemented("unimplemented") } -func (e *OperationTimeoutTaskExecutor) Execute( +func (h *OperationTimeoutTaskHandler) Execute( ctx chasm.MutableContext, op *Operation, attrs chasm.TaskAttributes, diff --git a/chasm/lib/nexusoperation/proto/v1/operation.proto b/chasm/lib/nexusoperation/proto/v1/operation.proto index b8203d8203f..b5eabf30015 100644 --- a/chasm/lib/nexusoperation/proto/v1/operation.proto +++ b/chasm/lib/nexusoperation/proto/v1/operation.proto @@ -10,24 +10,24 @@ message OperationState { enum OperationStatus { // Default value, unspecified status. - OPERATION_STATUS_UNSPECIFIED = 0; + OPERATION_STATUS_UNSPECIFIED = 0; // Operation is in the queue waiting to be executed or is currently executing. - OPERATION_STATUS_SCHEDULED = 1; + OPERATION_STATUS_SCHEDULED = 1; // Operation has failed with a retryable error and is backing off before the next attempt. - OPERATION_STATUS_BACKING_OFF = 2; + OPERATION_STATUS_BACKING_OFF = 2; // Operation was started and will complete asynchronously. - OPERATION_STATUS_STARTED = 3; + OPERATION_STATUS_STARTED = 3; // Operation succeeded. // This may happen either as a response to a start request or as reported via callback. - OPERATION_STATUS_SUCCEEDED = 4; + OPERATION_STATUS_SUCCEEDED = 4; // Operation failed either when a start request encounters a non-retryable error or as reported via callback. - OPERATION_STATUS_FAILED = 5; + OPERATION_STATUS_FAILED = 5; // Operation completed as canceled (may have not ever been delivered). // This may happen either as a response to a start request or as reported via callback. - OPERATION_STATUS_CANCELED = 6; + OPERATION_STATUS_CANCELED = 6; // Operation timed out - exceeded the user supplied schedule-to-close timeout. // Any attempts to complete the operation in this status will be ignored. - OPERATION_STATUS_TIMED_OUT = 7; + OPERATION_STATUS_TIMED_OUT = 7; } message CancellationState { @@ -36,17 +36,17 @@ message CancellationState { enum CancellationStatus { // Default value, unspecified status. - CANCELLATION_STATUS_UNSPECIFIED = 0; + CANCELLATION_STATUS_UNSPECIFIED = 0; // Cancellation request is in the queue waiting to be executed or is currently executing. - CANCELLATION_STATUS_SCHEDULED = 1; + CANCELLATION_STATUS_SCHEDULED = 1; // Cancellation request has failed with a retryable error and is backing off before the next attempt. - CANCELLATION_STATUS_BACKING_OFF = 2; + CANCELLATION_STATUS_BACKING_OFF = 2; // Cancellation request succeeded. - CANCELLATION_STATUS_SUCCEEDED = 3; + CANCELLATION_STATUS_SUCCEEDED = 3; // Cancellation request failed with a non-retryable error. - CANCELLATION_STATUS_FAILED = 4; + CANCELLATION_STATUS_FAILED = 4; // The associated operation timed out - exceeded the user supplied schedule-to-close timeout. - CANCELLATION_STATUS_TIMED_OUT = 5; + CANCELLATION_STATUS_TIMED_OUT = 5; // Cancellation request is blocked (eg: by circuit breaker). - CANCELLATION_STATUS_BLOCKED = 6; + CANCELLATION_STATUS_BLOCKED = 6; } diff --git a/chasm/lib/nexusoperation/proto/v1/tasks.proto b/chasm/lib/nexusoperation/proto/v1/tasks.proto index a144c988824..a5ccddce060 100644 --- a/chasm/lib/nexusoperation/proto/v1/tasks.proto +++ b/chasm/lib/nexusoperation/proto/v1/tasks.proto @@ -22,4 +22,4 @@ message CancellationTask { message CancellationBackoffTask { int32 attempt = 1; -} \ No newline at end of file +} diff --git a/chasm/lib/scheduler/backfiller.go b/chasm/lib/scheduler/backfiller.go index 1f7d3f8e51a..2b8a28a7a21 100644 --- a/chasm/lib/scheduler/backfiller.go +++ b/chasm/lib/scheduler/backfiller.go @@ -34,18 +34,23 @@ func addBackfiller( scheduler *Scheduler, ) *Backfiller { id := schedulescommon.GenerateBackfillerID() - backfiller := &Backfiller{ - BackfillerState: &schedulerpb.BackfillerState{ - BackfillId: id, - LastProcessedTime: timestamppb.New(ctx.Now(scheduler)), - }, - } + backfiller := newBackfillerWithState(ctx, &schedulerpb.BackfillerState{ + BackfillId: id, + LastProcessedTime: timestamppb.New(ctx.Now(scheduler)), + }) if scheduler.Backfillers == nil { scheduler.Backfillers = make(chasm.Map[string, *Backfiller]) } scheduler.Backfillers[id] = chasm.NewComponentField(ctx, backfiller) + return backfiller +} + +func newBackfillerWithState(ctx chasm.MutableContext, state *schedulerpb.BackfillerState) *Backfiller { + backfiller := &Backfiller{ + BackfillerState: state, + } backfiller.scheduleTask(ctx, chasm.TaskScheduledTimeImmediate) return backfiller } diff --git a/chasm/lib/scheduler/backfiller_tasks.go b/chasm/lib/scheduler/backfiller_tasks.go index e5e1e265155..fa5380b1fa3 100644 --- a/chasm/lib/scheduler/backfiller_tasks.go +++ b/chasm/lib/scheduler/backfiller_tasks.go @@ -17,7 +17,7 @@ import ( ) type ( - BackfillerTaskExecutorOptions struct { + BackfillerTaskHandlerOptions struct { fx.In Config *Config @@ -26,7 +26,8 @@ type ( SpecProcessor SpecProcessor } - BackfillerTaskExecutor struct { + BackfillerTaskHandler struct { + chasm.PureTaskHandlerBase config *Config metricsHandler metrics.Handler baseLogger log.Logger @@ -34,8 +35,8 @@ type ( } ) -func NewBackfillerTaskExecutor(opts BackfillerTaskExecutorOptions) *BackfillerTaskExecutor { - return &BackfillerTaskExecutor{ +func NewBackfillerTaskHandler(opts BackfillerTaskHandlerOptions) *BackfillerTaskHandler { + return &BackfillerTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, baseLogger: opts.BaseLogger, @@ -43,7 +44,7 @@ func NewBackfillerTaskExecutor(opts BackfillerTaskExecutorOptions) *BackfillerTa } } -func (b *BackfillerTaskExecutor) Validate( +func (b *BackfillerTaskHandler) Validate( ctx chasm.Context, backfiller *Backfiller, attrs chasm.TaskAttributes, @@ -55,7 +56,7 @@ func (b *BackfillerTaskExecutor) Validate( ) } -func (b *BackfillerTaskExecutor) Execute( +func (b *BackfillerTaskHandler) Execute( ctx chasm.MutableContext, backfiller *Backfiller, _ chasm.TaskAttributes, @@ -119,13 +120,13 @@ func (b *BackfillerTaskExecutor) Execute( return nil } -func (b *BackfillerTaskExecutor) rescheduleBackfill(ctx chasm.MutableContext, backfiller *Backfiller) { +func (b *BackfillerTaskHandler) rescheduleBackfill(ctx chasm.MutableContext, backfiller *Backfiller) { backoffTime := ctx.Now(backfiller).Add(b.backoffDelay(backfiller)) backfiller.scheduleTask(ctx, backoffTime) } // processBackfill processes a Backfiller's BackfillRequest. -func (b *BackfillerTaskExecutor) processBackfill( +func (b *BackfillerTaskHandler) processBackfill( _ chasm.MutableContext, scheduler *Scheduler, backfiller *Backfiller, @@ -172,14 +173,14 @@ func (b *BackfillerTaskExecutor) processBackfill( } // backoffDelay returns the amount of delay that should be added when retrying. -func (b *BackfillerTaskExecutor) backoffDelay(backfiller *Backfiller) time.Duration { +func (b *BackfillerTaskHandler) backoffDelay(backfiller *Backfiller) time.Duration { // Increment GetAttempt here early, to avoid needing to increment // backfiller.Attempt wherever backoffDelay's result is needed. return b.config.RetryPolicy().ComputeNextDelay(0, int(backfiller.GetAttempt()+1), nil) } // processTrigger processes a Backfiller's TriggerImmediatelyRequest. -func (b *BackfillerTaskExecutor) processTrigger( +func (b *BackfillerTaskHandler) processTrigger( _ chasm.MutableContext, scheduler *Scheduler, backfiller *Backfiller, @@ -214,7 +215,7 @@ func (b *BackfillerTaskExecutor) processTrigger( // allowedBufferedStarts returns the number of BufferedStarts that the Backfiller should // buffer, taking into account buffer limits and concurrent backfills. -func (b *BackfillerTaskExecutor) allowedBufferedStarts( +func (b *BackfillerTaskHandler) allowedBufferedStarts( ctx chasm.Context, scheduler *Scheduler, invoker *Invoker, diff --git a/chasm/lib/scheduler/backfiller_tasks_test.go b/chasm/lib/scheduler/backfiller_tasks_test.go index 15d23df0976..10963a6dc50 100644 --- a/chasm/lib/scheduler/backfiller_tasks_test.go +++ b/chasm/lib/scheduler/backfiller_tasks_test.go @@ -1,152 +1,120 @@ package scheduler_test import ( - "context" "testing" "time" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" enumspb "go.temporal.io/api/enums/v1" schedulepb "go.temporal.io/api/schedule/v1" - persistencespb "go.temporal.io/server/api/persistence/v1" schedulespb "go.temporal.io/server/api/schedule/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" - "go.temporal.io/server/common/clock" - "go.temporal.io/server/common/log" "go.temporal.io/server/common/metrics" - "go.temporal.io/server/common/testing/protorequire" - "go.temporal.io/server/common/testing/testlogger" - "go.temporal.io/server/common/testing/testvars" - legacyscheduler "go.temporal.io/server/service/worker/scheduler" - "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" ) -type backfillerTasksSuite struct { - suite.Suite - *require.Assertions - protorequire.ProtoAssertions - - controller *gomock.Controller - nodeBackend *chasm.MockNodeBackend - node *chasm.Node - scheduler *scheduler.Scheduler - registry *chasm.Registry - timeSource *clock.EventTimeSource - logger log.Logger - specProcessor scheduler.SpecProcessor -} +type backfillTestCase struct { + InitialTriggerRequest *schedulepb.TriggerImmediatelyRequest + InitialBackfillRequest *schedulepb.BackfillRequest + ExpectedBufferedStarts int + ExpectedComplete bool // asserts the Backfiller is deleted + ExpectedLastProcessedTime time.Time + ExpectedAttempt int -func TestBackfillerTasksSuite(t *testing.T) { - suite.Run(t, &backfillerTasksSuite{}) + ValidateInvoker func(t *testing.T, invoker *scheduler.Invoker) + ValidateBackfiller func(t *testing.T, backfiller *scheduler.Backfiller) } -func (s *backfillerTasksSuite) SetupTest() { - s.Assertions = require.New(s.T()) - s.ProtoAssertions = protorequire.New(s.T()) - - s.controller = gomock.NewController(s.T()) - s.logger = testlogger.NewTestLogger(s.T(), testlogger.FailOnExpectedErrorOnly) - - // Use real spec processor for backfiller tests. - mockMetrics := metrics.NewMockHandler(s.controller) - mockMetrics.EXPECT().Counter(gomock.Any()).Return(metrics.NoopCounterMetricFunc).AnyTimes() - mockMetrics.EXPECT().WithTags(gomock.Any()).Return(mockMetrics).AnyTimes() - mockMetrics.EXPECT().Timer(gomock.Any()).Return(metrics.NoopTimerMetricFunc).AnyTimes() - s.specProcessor = scheduler.NewSpecProcessor( - defaultConfig(), - mockMetrics, - s.logger, - legacyscheduler.NewSpecBuilder(), - ) - - s.registry = chasm.NewRegistry(s.logger) - err := s.registry.Register(&chasm.CoreLibrary{}) - s.NoError(err) - err = s.registry.Register(newTestLibrary(s.logger, s.specProcessor)) - s.NoError(err) - - s.timeSource = clock.NewEventTimeSource() - now := time.Now() - s.timeSource.Update(now) - - tv := testvars.New(s.T()) - s.nodeBackend = &chasm.MockNodeBackend{ - HandleNextTransitionCount: func() int64 { return 2 }, - HandleGetCurrentVersion: func() int64 { return 1 }, - HandleGetWorkflowKey: tv.Any().WorkflowKey, - HandleIsWorkflow: func() bool { return false }, - HandleCurrentVersionedTransition: func() *persistencespb.VersionedTransition { - return &persistencespb.VersionedTransition{ - NamespaceFailoverVersion: 1, - TransitionCount: 1, - } - }, +func runBackfillTestCase(t *testing.T, env *testEnv, c *backfillTestCase) { + ctx := env.MutableContext() + schedComponent, err := env.Node.Component(ctx, chasm.ComponentRef{}) + require.NoError(t, err) + sched := schedComponent.(*scheduler.Scheduler) + invoker := sched.Invoker.Get(ctx) + + // Exactly one type of request can be set per Backfiller. + require.False(t, c.InitialBackfillRequest != nil && c.InitialTriggerRequest != nil) + require.False(t, c.InitialBackfillRequest == nil && c.InitialTriggerRequest == nil) + + // Spawn backfiller. + var backfiller *scheduler.Backfiller + if c.InitialTriggerRequest != nil { + backfiller = sched.NewImmediateBackfiller(ctx, c.InitialTriggerRequest) + } else { + backfiller = sched.NewRangeBackfiller(ctx, c.InitialBackfillRequest) } - s.node = chasm.NewEmptyTree(s.registry, s.timeSource, s.nodeBackend, chasm.DefaultPathEncoder, s.logger) - ctx := s.newMutableContext() - s.scheduler, err = scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) - s.node.SetRootComponent(s.scheduler) + // Either type of request will spawn a Backfiller and schedule an immediate pure task. + // The immediate task executes automatically during CloseTransaction(). + require.NoError(t, env.CloseTransaction()) - // Advance Generator's high water mark to 'now'. - generator := s.scheduler.Generator.Get(ctx) - generator.LastProcessedTime = timestamppb.New(now) + // Validate completion or partial progress. + if c.ExpectedComplete { + // Backfiller should no longer be present in the backfiller map. + _, ok := sched.Backfillers[backfiller.BackfillId].TryGet(ctx) + require.False(t, ok) + } else { + // TODO - check that a pure task to continue driving backfill exists here. Because + // a pure task in the tree already has the physically-created status, closing the + // transaction won't call our backend mock for AddTasks twice. Fix this when CHASM + // offers unit testing hooks for task generation. - _, err = s.node.CloseTransaction() - s.NoError(err) -} + require.Equal(t, int64(c.ExpectedAttempt), backfiller.GetAttempt()) + require.Equal(t, c.ExpectedLastProcessedTime.UTC(), backfiller.GetLastProcessedTime().AsTime()) + } -func (s *backfillerTasksSuite) newMutableContext() chasm.MutableContext { - return chasm.NewMutableContext(context.Background(), s.node) -} + // Validate BufferedStarts. More detailed validation must be done in the callbacks. + require.Len(t, invoker.GetBufferedStarts(), c.ExpectedBufferedStarts) -type backfillTestCase struct { - InitialTriggerRequest *schedulepb.TriggerImmediatelyRequest - InitialBackfillRequest *schedulepb.BackfillRequest - ExpectedBufferedStarts int - ExpectedComplete bool // asserts the Backfiller is deleted - ExpectedLastProcessedTime time.Time - ExpectedAttempt int + // Validate RequestId -> WorkflowId mapping. + for _, start := range invoker.GetBufferedStarts() { + require.Equal(t, start.WorkflowId, invoker.RunningWorkflowID(start.RequestId)) + } - ValidateInvoker func(invoker *scheduler.Invoker) - ValidateBackfiller func(backfiller *scheduler.Backfiller) + // Callbacks. + if c.ValidateInvoker != nil { + c.ValidateInvoker(t, invoker) + } + if c.ValidateBackfiller != nil { + c.ValidateBackfiller(t, backfiller) + } } // An immediately-triggered run should result in the machine being deleted after // completion. -func (s *backfillerTasksSuite) TestBackfillTask_TriggerImmediate() { +func TestBackfillTask_TriggerImmediate(t *testing.T) { + env := newTestEnv(t) request := &schedulepb.TriggerImmediatelyRequest{ OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, } - s.runTestCase(&backfillTestCase{ + runBackfillTestCase(t, env, &backfillTestCase{ InitialTriggerRequest: request, ExpectedBufferedStarts: 1, ExpectedComplete: true, - ValidateInvoker: func(invoker *scheduler.Invoker) { + ValidateInvoker: func(t *testing.T, invoker *scheduler.Invoker) { start := invoker.GetBufferedStarts()[0] - s.Equal(request.OverlapPolicy, start.OverlapPolicy) - s.True(start.Manual) + require.Equal(t, request.OverlapPolicy, start.OverlapPolicy) + require.True(t, start.Manual) }, }) } // An immediately-triggered run will back off and retry if the buffer is full. -func (s *backfillerTasksSuite) TestBackfillTask_TriggerImmediateFullBuffer() { +func TestBackfillTask_TriggerImmediateFullBuffer(t *testing.T) { + env := newTestEnv(t) + // Backfillers get half of the max buffer size, so fill (half the buffer - // expected starts). - ctx := s.newMutableContext() - invoker := s.scheduler.Invoker.Get(ctx) + ctx := env.MutableContext() + invoker := env.Scheduler.Invoker.Get(ctx) for range scheduler.DefaultTweakables.MaxBufferSize { invoker.BufferedStarts = append(invoker.BufferedStarts, &schedulespb.BufferedStart{}) } - now := s.timeSource.Now() - s.runTestCase(&backfillTestCase{ + now := env.TimeSource.Now() + runBackfillTestCase(t, env, &backfillTestCase{ InitialTriggerRequest: &schedulepb.TriggerImmediatelyRequest{}, ExpectedBufferedStarts: 1000, ExpectedComplete: false, @@ -157,25 +125,26 @@ func (s *backfillerTasksSuite) TestBackfillTask_TriggerImmediateFullBuffer() { // A backfill request completes entirely should result in the machine being // deleted after completion. -func (s *backfillerTasksSuite) TestBackfillTask_CompleteFill() { - startTime := s.timeSource.Now() +func TestBackfillTask_CompleteFill(t *testing.T) { + env := newTestEnv(t) + startTime := env.TimeSource.Now() endTime := startTime.Add(5 * defaultInterval) request := &schedulepb.BackfillRequest{ StartTime: timestamppb.New(startTime), EndTime: timestamppb.New(endTime), OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, } - s.runTestCase(&backfillTestCase{ + runBackfillTestCase(t, env, &backfillTestCase{ InitialBackfillRequest: request, ExpectedBufferedStarts: 5, ExpectedComplete: true, - ValidateInvoker: func(invoker *scheduler.Invoker) { + ValidateInvoker: func(t *testing.T, invoker *scheduler.Invoker) { for _, start := range invoker.GetBufferedStarts() { - s.Equal(request.OverlapPolicy, start.OverlapPolicy) + require.Equal(t, request.OverlapPolicy, start.OverlapPolicy) startAt := start.GetActualTime().AsTime() - s.True(startAt.After(startTime)) - s.True(startAt.Before(endTime)) - s.True(start.Manual) + require.True(t, startAt.After(startTime)) + require.True(t, startAt.Before(endTime)) + require.True(t, start.Manual) } }, }) @@ -184,22 +153,24 @@ func (s *backfillerTasksSuite) TestBackfillTask_CompleteFill() { // Backfill start and end times are inclusive, so a backfill scheduled for an // instant that exactly matches a time in the calendar spec's sequence should result // in a start. -func (s *backfillerTasksSuite) TestBackfillTask_InclusiveStartEnd() { +func TestBackfillTask_InclusiveStartEnd(t *testing.T) { + env := newTestEnv(t) + // Set an identical start and end time, landing on the calendar spec's interval. - backfillTime := s.timeSource.Now().Truncate(defaultInterval) + backfillTime := env.TimeSource.Now().Truncate(defaultInterval) request := &schedulepb.BackfillRequest{ StartTime: timestamppb.New(backfillTime), EndTime: timestamppb.New(backfillTime), } - s.runTestCase(&backfillTestCase{ + runBackfillTestCase(t, env, &backfillTestCase{ InitialBackfillRequest: request, ExpectedBufferedStarts: 1, ExpectedComplete: true, }) // Clear the Invoker's buffered starts. - ctx := s.newMutableContext() - invoker := s.scheduler.Invoker.Get(ctx) + ctx := env.MutableContext() + invoker := env.Scheduler.Invoker.Get(ctx) invoker.BufferedStarts = nil // A hair off and the action won't fire. @@ -208,7 +179,7 @@ func (s *backfillerTasksSuite) TestBackfillTask_InclusiveStartEnd() { StartTime: timestamppb.New(backfillTime), EndTime: timestamppb.New(backfillTime), } - s.runTestCase(&backfillTestCase{ + runBackfillTestCase(t, env, &backfillTestCase{ InitialBackfillRequest: request, ExpectedBufferedStarts: 0, ExpectedComplete: true, @@ -217,21 +188,23 @@ func (s *backfillerTasksSuite) TestBackfillTask_InclusiveStartEnd() { // When the buffer's completely full, the high watermark shouldn't advance and no // starts should be buffered. -func (s *backfillerTasksSuite) TestBackfillTask_BufferCompletelyFull() { +func TestBackfillTask_BufferCompletelyFull(t *testing.T) { + env := newTestEnv(t) + // Fill buffer past max. - ctx := s.newMutableContext() - invoker := s.scheduler.Invoker.Get(ctx) + ctx := env.MutableContext() + invoker := env.Scheduler.Invoker.Get(ctx) for range scheduler.DefaultTweakables.MaxBufferSize { invoker.BufferedStarts = append(invoker.BufferedStarts, &schedulespb.BufferedStart{}) } - startTime := s.timeSource.Now() + startTime := env.TimeSource.Now() endTime := startTime.Add(5 * defaultInterval) request := &schedulepb.BackfillRequest{ StartTime: timestamppb.New(startTime), EndTime: timestamppb.New(endTime), } - s.runTestCase(&backfillTestCase{ + runBackfillTestCase(t, env, &backfillTestCase{ InitialBackfillRequest: request, ExpectedBufferedStarts: 1000, ExpectedComplete: false, @@ -242,10 +215,12 @@ func (s *backfillerTasksSuite) TestBackfillTask_BufferCompletelyFull() { // When the backfill range exceeds buffer capacity, partial filling should occur // with the remainder left for a retry. -func (s *backfillerTasksSuite) TestBackfillTask_PartialFill() { +func TestBackfillTask_PartialFill(t *testing.T) { + env := newTestEnv(t) + // Use a large backfill range (1000 intervals) that exceeds the backfiller's // buffer limit (MaxBufferSize/2 = 500). - startTime := s.timeSource.Now() + startTime := env.TimeSource.Now() endTime := startTime.Add(1000 * defaultInterval) request := &schedulepb.BackfillRequest{ StartTime: timestamppb.New(startTime), @@ -253,97 +228,39 @@ func (s *backfillerTasksSuite) TestBackfillTask_PartialFill() { OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, } - ctx := s.newMutableContext() - schedComponent, err := s.node.Component(ctx, chasm.ComponentRef{}) - s.NoError(err) + ctx := env.MutableContext() + schedComponent, err := env.Node.Component(ctx, chasm.ComponentRef{}) + require.NoError(t, err) sched := schedComponent.(*scheduler.Scheduler) backfiller := sched.NewRangeBackfiller(ctx, request) - _, err = s.node.CloseTransaction() - s.NoError(err) + require.NoError(t, env.CloseTransaction()) // Backfiller should have processed up to its limit (500), not the full 1000. - s.False(backfiller.GetLastProcessedTime().AsTime().IsZero()) - s.Equal(int64(1), backfiller.GetAttempt()) + require.False(t, backfiller.GetLastProcessedTime().AsTime().IsZero()) + require.Equal(t, int64(1), backfiller.GetAttempt()) // Backfiller should still exist (not complete). - ctx = s.newMutableContext() - schedComponent, err = s.node.Component(ctx, chasm.ComponentRef{}) - s.NoError(err) + ctx = env.MutableContext() + schedComponent, err = env.Node.Component(ctx, chasm.ComponentRef{}) + require.NoError(t, err) sched = schedComponent.(*scheduler.Scheduler) _, ok := sched.Backfillers[backfiller.BackfillId].TryGet(ctx) - s.True(ok) + require.True(t, ok) // Manually execute the second iteration since the scheduled continuation // task is in the future (after backoff delay). invoker := sched.Invoker.Get(ctx) invoker.BufferedStarts = nil // Clear to make room for next batch - executor := scheduler.NewBackfillerTaskExecutor(scheduler.BackfillerTaskExecutorOptions{ + handler := scheduler.NewBackfillerTaskHandler(scheduler.BackfillerTaskHandlerOptions{ Config: defaultConfig(), MetricsHandler: metrics.NoopMetricsHandler, - BaseLogger: s.logger, - SpecProcessor: s.specProcessor, + BaseLogger: env.Logger, + SpecProcessor: env.SpecProcessor, }) - err = executor.Execute(ctx, backfiller, chasm.TaskAttributes{}, &schedulerpb.BackfillerTask{}) - s.NoError(err) - _, err = s.node.CloseTransaction() - s.NoError(err) + err = handler.Execute(ctx, backfiller, chasm.TaskAttributes{}, &schedulerpb.BackfillerTask{}) + require.NoError(t, err) + require.NoError(t, env.CloseTransaction()) // After second iteration, should have processed another batch. - s.Equal(int64(2), backfiller.GetAttempt()) -} - -func (s *backfillerTasksSuite) runTestCase(c *backfillTestCase) { - ctx := s.newMutableContext() - schedComponent, err := s.node.Component(ctx, chasm.ComponentRef{}) - s.NoError(err) - sched := schedComponent.(*scheduler.Scheduler) - invoker := sched.Invoker.Get(ctx) - - // Exactly one type of request can be set per Backfiller. - s.False(c.InitialBackfillRequest != nil && c.InitialTriggerRequest != nil) - s.False(c.InitialBackfillRequest == nil && c.InitialTriggerRequest == nil) - - // Spawn backfiller. - var backfiller *scheduler.Backfiller - if c.InitialTriggerRequest != nil { - backfiller = sched.NewImmediateBackfiller(ctx, c.InitialTriggerRequest) - } else { - backfiller = sched.NewRangeBackfiller(ctx, c.InitialBackfillRequest) - } - - // Either type of request will spawn a Backfiller and schedule an immediate pure task. - // The immediate task executes automatically during CloseTransaction(). - _, err = s.node.CloseTransaction() - s.NoError(err) - - // Validate completion or partial progress. - if c.ExpectedComplete { - // Backfiller should no longer be present in the backfiller map. - _, ok := sched.Backfillers[backfiller.BackfillId].TryGet(ctx) - s.False(ok) - } else { - // TODO - check that a pure task to continue driving backfill exists here. Because - // a pure task in the tree already has the physically-created status, closing the - // transaction won't call our backend mock for AddTasks twice. Fix this when CHASM - // offers unit testing hooks for task generation. - - s.Equal(int64(c.ExpectedAttempt), backfiller.GetAttempt()) - s.Equal(c.ExpectedLastProcessedTime.UTC(), backfiller.GetLastProcessedTime().AsTime()) - } - - // Validate BufferedStarts. More detailed validation must be done in the callbacks. - s.Equal(c.ExpectedBufferedStarts, len(invoker.GetBufferedStarts())) - - // Validate RequestId -> WorkflowId mapping - for _, start := range invoker.GetBufferedStarts() { - s.Equal(start.WorkflowId, invoker.RunningWorkflowID(start.RequestId)) - } - - // Callbacks. - if c.ValidateInvoker != nil { - c.ValidateInvoker(invoker) - } - if c.ValidateBackfiller != nil { - c.ValidateBackfiller(backfiller) - } + require.Equal(t, int64(2), backfiller.GetAttempt()) } diff --git a/chasm/lib/scheduler/config.go b/chasm/lib/scheduler/config.go index b15b60baad4..a687464739f 100644 --- a/chasm/lib/scheduler/config.go +++ b/chasm/lib/scheduler/config.go @@ -50,6 +50,11 @@ var ( `The upper bound on how long a service call can take before being timed out.`, ) + // SentinelIdleTime is how long a CHASM sentinel reserves the schedule ID + // before auto-closing via the idle task mechanism. Matches the dummy + // workflow's duration. + SentinelIdleTime = 15 * time.Minute + DefaultTweakables = Tweakables{ DefaultCatchupWindow: 365 * 24 * time.Hour, MinCatchupWindow: 10 * time.Second, diff --git a/chasm/lib/scheduler/export_test.go b/chasm/lib/scheduler/export_test.go index b6268c104a5..0995ae1a056 100644 --- a/chasm/lib/scheduler/export_test.go +++ b/chasm/lib/scheduler/export_test.go @@ -1,14 +1,30 @@ package scheduler import ( + "context" "time" schedulespb "go.temporal.io/server/api/schedule/v1" "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + "go.temporal.io/server/common/log" + legacyscheduler "go.temporal.io/server/service/worker/scheduler" ) // Export unexported methods for testing. +func NewTestHandler(logger log.Logger) *handler { + return newHandler(logger, legacyscheduler.NewSpecBuilder()) +} + +func (h *handler) TestCreateFromMigrationState(ctx context.Context, req *schedulerpb.CreateFromMigrationStateRequest) (*schedulerpb.CreateFromMigrationStateResponse, error) { + return h.CreateFromMigrationState(ctx, req) +} + +func (h *handler) TestMigrateToWorkflow(ctx context.Context, req *schedulerpb.MigrateToWorkflowRequest) (*schedulerpb.MigrateToWorkflowResponse, error) { + return h.MigrateToWorkflow(ctx, req) +} + func (s *Scheduler) RecordCompletedAction( ctx chasm.MutableContext, completed *schedulespb.CompletedResult, diff --git a/chasm/lib/scheduler/fx.go b/chasm/lib/scheduler/fx.go index e54d487dea2..15dfb763ed8 100644 --- a/chasm/lib/scheduler/fx.go +++ b/chasm/lib/scheduler/fx.go @@ -20,11 +20,13 @@ var Module = fx.Module( fx.Provide(NewSpecProcessor), fx.Provide(func(impl *SpecProcessorImpl) SpecProcessor { return impl }), fx.Provide(newHandler), - fx.Provide(NewSchedulerIdleTaskExecutor), - fx.Provide(NewGeneratorTaskExecutor), - fx.Provide(NewInvokerExecuteTaskExecutor), - fx.Provide(NewInvokerProcessBufferTaskExecutor), - fx.Provide(NewBackfillerTaskExecutor), + fx.Provide(NewSchedulerIdleTaskHandler), + fx.Provide(NewSchedulerCallbacksTaskHandler), + fx.Provide(NewGeneratorTaskHandler), + fx.Provide(NewInvokerExecuteTaskHandler), + fx.Provide(NewInvokerProcessBufferTaskHandler), + fx.Provide(NewBackfillerTaskHandler), + fx.Provide(NewSchedulerMigrateToWorkflowTaskHandler), fx.Provide(NewLibrary), fx.Invoke(Register), ) diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/message.go-helpers.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/message.go-helpers.pb.go index 7798cf997a6..202513f2988 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/message.go-helpers.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/message.go-helpers.pb.go @@ -42,6 +42,43 @@ func (this *SchedulerState) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type WorkflowMigrationState to the protobuf v3 wire format +func (val *WorkflowMigrationState) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type WorkflowMigrationState from the protobuf v3 wire format +func (val *WorkflowMigrationState) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *WorkflowMigrationState) Size() int { + return proto.Size(val) +} + +// Equal returns whether two WorkflowMigrationState values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *WorkflowMigrationState) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *WorkflowMigrationState + switch t := that.(type) { + case *WorkflowMigrationState: + that1 = t + case WorkflowMigrationState: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type GeneratorState to the protobuf v3 wire format func (val *GeneratorState) Marshal() ([]byte, error) { return proto.Marshal(val) diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/message.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/message.pb.go index 8af1bfc176c..16e69c85590 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/message.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/message.pb.go @@ -45,9 +45,12 @@ type SchedulerState struct { Closed bool `protobuf:"varint,9,opt,name=closed,proto3" json:"closed,omitempty"` // When true, this scheduler is a sentinel that exists only to reserve the // schedule ID. All API operations return NotFound. - Sentinel bool `protobuf:"varint,10,opt,name=sentinel,proto3" json:"sentinel,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Sentinel bool `protobuf:"varint,10,opt,name=sentinel,proto3" json:"sentinel,omitempty"` + // Set when a migration to workflow-backed scheduler (V1) is pending. + // Unpause operations are blocked while this is set. + WorkflowMigration *WorkflowMigrationState `protobuf:"bytes,11,opt,name=workflow_migration,json=workflowMigration,proto3" json:"workflow_migration,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SchedulerState) Reset() { @@ -136,6 +139,69 @@ func (x *SchedulerState) GetSentinel() bool { return false } +func (x *SchedulerState) GetWorkflowMigration() *WorkflowMigrationState { + if x != nil { + return x.WorkflowMigration + } + return nil +} + +// WorkflowMigrationState tracks the state of an in-progress V2-to-V1 migration. +type WorkflowMigrationState struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The schedule's paused state before migration was initiated. Used to + // restore the correct paused state when passing state to the V1 workflow. + PreMigrationPaused bool `protobuf:"varint,1,opt,name=pre_migration_paused,json=preMigrationPaused,proto3" json:"pre_migration_paused,omitempty"` + // The schedule's notes before migration was initiated. + PreMigrationNotes string `protobuf:"bytes,2,opt,name=pre_migration_notes,json=preMigrationNotes,proto3" json:"pre_migration_notes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkflowMigrationState) Reset() { + *x = WorkflowMigrationState{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkflowMigrationState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkflowMigrationState) ProtoMessage() {} + +func (x *WorkflowMigrationState) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkflowMigrationState.ProtoReflect.Descriptor instead. +func (*WorkflowMigrationState) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{1} +} + +func (x *WorkflowMigrationState) GetPreMigrationPaused() bool { + if x != nil { + return x.PreMigrationPaused + } + return false +} + +func (x *WorkflowMigrationState) GetPreMigrationNotes() string { + if x != nil { + return x.PreMigrationNotes + } + return "" +} + // CHASM scheduler's Generator internal state. type GeneratorState struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -149,7 +215,7 @@ type GeneratorState struct { func (x *GeneratorState) Reset() { *x = GeneratorState{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -161,7 +227,7 @@ func (x *GeneratorState) String() string { func (*GeneratorState) ProtoMessage() {} func (x *GeneratorState) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -174,7 +240,7 @@ func (x *GeneratorState) ProtoReflect() protoreflect.Message { // Deprecated: Use GeneratorState.ProtoReflect.Descriptor instead. func (*GeneratorState) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{1} + return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{2} } func (x *GeneratorState) GetLastProcessedTime() *timestamppb.Timestamp { @@ -212,7 +278,7 @@ type InvokerState struct { func (x *InvokerState) Reset() { *x = InvokerState{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[2] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -224,7 +290,7 @@ func (x *InvokerState) String() string { func (*InvokerState) ProtoMessage() {} func (x *InvokerState) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[2] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -237,7 +303,7 @@ func (x *InvokerState) ProtoReflect() protoreflect.Message { // Deprecated: Use InvokerState.ProtoReflect.Descriptor instead. func (*InvokerState) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{2} + return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{3} } func (x *InvokerState) GetBufferedStarts() []*v11.BufferedStart { @@ -291,7 +357,7 @@ type BackfillerState struct { func (x *BackfillerState) Reset() { *x = BackfillerState{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[3] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -303,7 +369,7 @@ func (x *BackfillerState) String() string { func (*BackfillerState) ProtoMessage() {} func (x *BackfillerState) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[3] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -316,7 +382,7 @@ func (x *BackfillerState) ProtoReflect() protoreflect.Message { // Deprecated: Use BackfillerState.ProtoReflect.Descriptor instead. func (*BackfillerState) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{3} + return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{4} } func (x *BackfillerState) GetRequest() isBackfillerState_Request { @@ -394,7 +460,7 @@ type LastCompletionResult struct { func (x *LastCompletionResult) Reset() { *x = LastCompletionResult{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[4] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -406,7 +472,7 @@ func (x *LastCompletionResult) String() string { func (*LastCompletionResult) ProtoMessage() {} func (x *LastCompletionResult) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[4] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -419,7 +485,7 @@ func (x *LastCompletionResult) ProtoReflect() protoreflect.Message { // Deprecated: Use LastCompletionResult.ProtoReflect.Descriptor instead. func (*LastCompletionResult) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{4} + return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{5} } func (x *LastCompletionResult) GetSuccess() *v12.Payload { @@ -454,7 +520,7 @@ type SchedulerMigrationState struct { func (x *SchedulerMigrationState) Reset() { *x = SchedulerMigrationState{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[5] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -466,7 +532,7 @@ func (x *SchedulerMigrationState) String() string { func (*SchedulerMigrationState) ProtoMessage() {} func (x *SchedulerMigrationState) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[5] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -479,7 +545,7 @@ func (x *SchedulerMigrationState) ProtoReflect() protoreflect.Message { // Deprecated: Use SchedulerMigrationState.ProtoReflect.Descriptor instead. func (*SchedulerMigrationState) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{5} + return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP(), []int{6} } func (x *SchedulerMigrationState) GetSchedulerState() *SchedulerState { @@ -535,7 +601,7 @@ var File_temporal_server_chasm_lib_scheduler_proto_v1_message_proto protoreflect const file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDesc = "" + "\n" + - ":temporal/server/chasm/lib/scheduler/proto/v1/message.proto\x12,temporal.server.chasm.lib.scheduler.proto.v1\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a&temporal/api/schedule/v1/message.proto\x1a-temporal/server/api/schedule/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc9\x02\n" + + ":temporal/server/chasm/lib/scheduler/proto/v1/message.proto\x12,temporal.server.chasm.lib.scheduler.proto.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a&temporal/api/schedule/v1/message.proto\x1a-temporal/server/api/schedule/v1/message.proto\"\xbe\x03\n" + "\x0eSchedulerState\x12>\n" + "\bschedule\x18\x02 \x01(\v2\".temporal.api.schedule.v1.ScheduleR\bschedule\x12:\n" + "\x04info\x18\x03 \x01(\v2&.temporal.api.schedule.v1.ScheduleInfoR\x04info\x12\x1c\n" + @@ -546,7 +612,11 @@ const file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDesc = "\x0econflict_token\x18\b \x01(\x03R\rconflictToken\x12\x16\n" + "\x06closed\x18\t \x01(\bR\x06closed\x12\x1a\n" + "\bsentinel\x18\n" + - " \x01(\bR\bsentinel\"\xa8\x01\n" + + " \x01(\bR\bsentinel\x12s\n" + + "\x12workflow_migration\x18\v \x01(\v2D.temporal.server.chasm.lib.scheduler.proto.v1.WorkflowMigrationStateR\x11workflowMigration\"z\n" + + "\x16WorkflowMigrationState\x120\n" + + "\x14pre_migration_paused\x18\x01 \x01(\bR\x12preMigrationPaused\x12.\n" + + "\x13pre_migration_notes\x18\x02 \x01(\tR\x11preMigrationNotes\"\xa8\x01\n" + "\x0eGeneratorState\x12J\n" + "\x13last_processed_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x11lastProcessedTime\x12J\n" + "\x13future_action_times\x18\x04 \x03(\v2\x1a.google.protobuf.TimestampR\x11futureActionTimes\"\xeb\x02\n" + @@ -596,56 +666,58 @@ func file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescGZIP return file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDescData } -var file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_goTypes = []any{ (*SchedulerState)(nil), // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState - (*GeneratorState)(nil), // 1: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState - (*InvokerState)(nil), // 2: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState - (*BackfillerState)(nil), // 3: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState - (*LastCompletionResult)(nil), // 4: temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult - (*SchedulerMigrationState)(nil), // 5: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState - nil, // 6: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.BackfillersEntry - nil, // 7: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.SearchAttributesEntry - nil, // 8: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.MemoEntry - (*v1.Schedule)(nil), // 9: temporal.api.schedule.v1.Schedule - (*v1.ScheduleInfo)(nil), // 10: temporal.api.schedule.v1.ScheduleInfo - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp - (*v11.BufferedStart)(nil), // 12: temporal.server.api.schedule.v1.BufferedStart - (*v12.WorkflowExecution)(nil), // 13: temporal.api.common.v1.WorkflowExecution - (*v1.BackfillRequest)(nil), // 14: temporal.api.schedule.v1.BackfillRequest - (*v1.TriggerImmediatelyRequest)(nil), // 15: temporal.api.schedule.v1.TriggerImmediatelyRequest - (*v12.Payload)(nil), // 16: temporal.api.common.v1.Payload - (*v13.Failure)(nil), // 17: temporal.api.failure.v1.Failure + (*WorkflowMigrationState)(nil), // 1: temporal.server.chasm.lib.scheduler.proto.v1.WorkflowMigrationState + (*GeneratorState)(nil), // 2: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState + (*InvokerState)(nil), // 3: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState + (*BackfillerState)(nil), // 4: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState + (*LastCompletionResult)(nil), // 5: temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult + (*SchedulerMigrationState)(nil), // 6: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState + nil, // 7: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.BackfillersEntry + nil, // 8: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.SearchAttributesEntry + nil, // 9: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.MemoEntry + (*v1.Schedule)(nil), // 10: temporal.api.schedule.v1.Schedule + (*v1.ScheduleInfo)(nil), // 11: temporal.api.schedule.v1.ScheduleInfo + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp + (*v11.BufferedStart)(nil), // 13: temporal.server.api.schedule.v1.BufferedStart + (*v12.WorkflowExecution)(nil), // 14: temporal.api.common.v1.WorkflowExecution + (*v1.BackfillRequest)(nil), // 15: temporal.api.schedule.v1.BackfillRequest + (*v1.TriggerImmediatelyRequest)(nil), // 16: temporal.api.schedule.v1.TriggerImmediatelyRequest + (*v12.Payload)(nil), // 17: temporal.api.common.v1.Payload + (*v13.Failure)(nil), // 18: temporal.api.failure.v1.Failure } var file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_depIdxs = []int32{ - 9, // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState.schedule:type_name -> temporal.api.schedule.v1.Schedule - 10, // 1: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState.info:type_name -> temporal.api.schedule.v1.ScheduleInfo - 11, // 2: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState.last_processed_time:type_name -> google.protobuf.Timestamp - 11, // 3: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState.future_action_times:type_name -> google.protobuf.Timestamp - 12, // 4: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.buffered_starts:type_name -> temporal.server.api.schedule.v1.BufferedStart - 13, // 5: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.cancel_workflows:type_name -> temporal.api.common.v1.WorkflowExecution - 13, // 6: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.terminate_workflows:type_name -> temporal.api.common.v1.WorkflowExecution - 11, // 7: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.last_processed_time:type_name -> google.protobuf.Timestamp - 14, // 8: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState.backfill_request:type_name -> temporal.api.schedule.v1.BackfillRequest - 15, // 9: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState.trigger_request:type_name -> temporal.api.schedule.v1.TriggerImmediatelyRequest - 11, // 10: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState.last_processed_time:type_name -> google.protobuf.Timestamp - 16, // 11: temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult.success:type_name -> temporal.api.common.v1.Payload - 17, // 12: temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult.failure:type_name -> temporal.api.failure.v1.Failure - 0, // 13: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.scheduler_state:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState - 1, // 14: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.generator_state:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState - 2, // 15: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.invoker_state:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.InvokerState - 6, // 16: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.backfillers:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.BackfillersEntry - 4, // 17: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.last_completion_result:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult - 7, // 18: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.search_attributes:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.SearchAttributesEntry - 8, // 19: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.memo:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.MemoEntry - 3, // 20: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.BackfillersEntry.value:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState - 16, // 21: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload - 16, // 22: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.MemoEntry.value:type_name -> temporal.api.common.v1.Payload - 23, // [23:23] is the sub-list for method output_type - 23, // [23:23] is the sub-list for method input_type - 23, // [23:23] is the sub-list for extension type_name - 23, // [23:23] is the sub-list for extension extendee - 0, // [0:23] is the sub-list for field type_name + 10, // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState.schedule:type_name -> temporal.api.schedule.v1.Schedule + 11, // 1: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState.info:type_name -> temporal.api.schedule.v1.ScheduleInfo + 1, // 2: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState.workflow_migration:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.WorkflowMigrationState + 12, // 3: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState.last_processed_time:type_name -> google.protobuf.Timestamp + 12, // 4: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState.future_action_times:type_name -> google.protobuf.Timestamp + 13, // 5: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.buffered_starts:type_name -> temporal.server.api.schedule.v1.BufferedStart + 14, // 6: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.cancel_workflows:type_name -> temporal.api.common.v1.WorkflowExecution + 14, // 7: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.terminate_workflows:type_name -> temporal.api.common.v1.WorkflowExecution + 12, // 8: temporal.server.chasm.lib.scheduler.proto.v1.InvokerState.last_processed_time:type_name -> google.protobuf.Timestamp + 15, // 9: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState.backfill_request:type_name -> temporal.api.schedule.v1.BackfillRequest + 16, // 10: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState.trigger_request:type_name -> temporal.api.schedule.v1.TriggerImmediatelyRequest + 12, // 11: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState.last_processed_time:type_name -> google.protobuf.Timestamp + 17, // 12: temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult.success:type_name -> temporal.api.common.v1.Payload + 18, // 13: temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult.failure:type_name -> temporal.api.failure.v1.Failure + 0, // 14: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.scheduler_state:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerState + 2, // 15: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.generator_state:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.GeneratorState + 3, // 16: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.invoker_state:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.InvokerState + 7, // 17: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.backfillers:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.BackfillersEntry + 5, // 18: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.last_completion_result:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.LastCompletionResult + 8, // 19: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.search_attributes:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.SearchAttributesEntry + 9, // 20: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.memo:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.MemoEntry + 4, // 21: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.BackfillersEntry.value:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.BackfillerState + 17, // 22: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.SearchAttributesEntry.value:type_name -> temporal.api.common.v1.Payload + 17, // 23: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState.MemoEntry.value:type_name -> temporal.api.common.v1.Payload + 24, // [24:24] is the sub-list for method output_type + 24, // [24:24] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_init() } @@ -653,7 +725,7 @@ func file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_init() { if File_temporal_server_chasm_lib_scheduler_proto_v1_message_proto != nil { return } - file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[3].OneofWrappers = []any{ + file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[4].OneofWrappers = []any{ (*BackfillerState_BackfillRequest)(nil), (*BackfillerState_TriggerRequest)(nil), } @@ -663,7 +735,7 @@ func file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDesc), len(file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_rawDesc)), NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.go-helpers.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.go-helpers.pb.go index 08689dcc627..c0bf15355a8 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.go-helpers.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.go-helpers.pb.go @@ -448,3 +448,225 @@ func (this *ListScheduleMatchingTimesResponse) Equal(that interface{}) bool { return proto.Equal(this, that1) } + +// Marshal an object of type CreateFromMigrationStateRequest to the protobuf v3 wire format +func (val *CreateFromMigrationStateRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateFromMigrationStateRequest from the protobuf v3 wire format +func (val *CreateFromMigrationStateRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateFromMigrationStateRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateFromMigrationStateRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateFromMigrationStateRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateFromMigrationStateRequest + switch t := that.(type) { + case *CreateFromMigrationStateRequest: + that1 = t + case CreateFromMigrationStateRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateFromMigrationStateResponse to the protobuf v3 wire format +func (val *CreateFromMigrationStateResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateFromMigrationStateResponse from the protobuf v3 wire format +func (val *CreateFromMigrationStateResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateFromMigrationStateResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateFromMigrationStateResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateFromMigrationStateResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateFromMigrationStateResponse + switch t := that.(type) { + case *CreateFromMigrationStateResponse: + that1 = t + case CreateFromMigrationStateResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateSentinelRequest to the protobuf v3 wire format +func (val *CreateSentinelRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateSentinelRequest from the protobuf v3 wire format +func (val *CreateSentinelRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateSentinelRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateSentinelRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateSentinelRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateSentinelRequest + switch t := that.(type) { + case *CreateSentinelRequest: + that1 = t + case CreateSentinelRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateSentinelResponse to the protobuf v3 wire format +func (val *CreateSentinelResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateSentinelResponse from the protobuf v3 wire format +func (val *CreateSentinelResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateSentinelResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateSentinelResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateSentinelResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateSentinelResponse + switch t := that.(type) { + case *CreateSentinelResponse: + that1 = t + case CreateSentinelResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type MigrateToWorkflowRequest to the protobuf v3 wire format +func (val *MigrateToWorkflowRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type MigrateToWorkflowRequest from the protobuf v3 wire format +func (val *MigrateToWorkflowRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *MigrateToWorkflowRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two MigrateToWorkflowRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *MigrateToWorkflowRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *MigrateToWorkflowRequest + switch t := that.(type) { + case *MigrateToWorkflowRequest: + that1 = t + case MigrateToWorkflowRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type MigrateToWorkflowResponse to the protobuf v3 wire format +func (val *MigrateToWorkflowResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type MigrateToWorkflowResponse from the protobuf v3 wire format +func (val *MigrateToWorkflowResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *MigrateToWorkflowResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two MigrateToWorkflowResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *MigrateToWorkflowResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *MigrateToWorkflowResponse + switch t := that.(type) { + case *MigrateToWorkflowResponse: + that1 = t + case MigrateToWorkflowResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.pb.go index ff24d4739c3..1329a1557b3 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/request_response.pb.go @@ -605,11 +605,305 @@ func (x *ListScheduleMatchingTimesResponse) GetFrontendResponse() *v1.ListSchedu return nil } +type CreateFromMigrationStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal namespace ID (UUID). + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + State *SchedulerMigrationState `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateFromMigrationStateRequest) Reset() { + *x = CreateFromMigrationStateRequest{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateFromMigrationStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateFromMigrationStateRequest) ProtoMessage() {} + +func (x *CreateFromMigrationStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateFromMigrationStateRequest.ProtoReflect.Descriptor instead. +func (*CreateFromMigrationStateRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescGZIP(), []int{12} +} + +func (x *CreateFromMigrationStateRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CreateFromMigrationStateRequest) GetState() *SchedulerMigrationState { + if x != nil { + return x.State + } + return nil +} + +type CreateFromMigrationStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateFromMigrationStateResponse) Reset() { + *x = CreateFromMigrationStateResponse{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateFromMigrationStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateFromMigrationStateResponse) ProtoMessage() {} + +func (x *CreateFromMigrationStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateFromMigrationStateResponse.ProtoReflect.Descriptor instead. +func (*CreateFromMigrationStateResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescGZIP(), []int{13} +} + +type CreateSentinelRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal namespace ID (UUID). + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` + ScheduleId string `protobuf:"bytes,3,opt,name=schedule_id,json=scheduleId,proto3" json:"schedule_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSentinelRequest) Reset() { + *x = CreateSentinelRequest{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSentinelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSentinelRequest) ProtoMessage() {} + +func (x *CreateSentinelRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSentinelRequest.ProtoReflect.Descriptor instead. +func (*CreateSentinelRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescGZIP(), []int{14} +} + +func (x *CreateSentinelRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CreateSentinelRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *CreateSentinelRequest) GetScheduleId() string { + if x != nil { + return x.ScheduleId + } + return "" +} + +type CreateSentinelResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSentinelResponse) Reset() { + *x = CreateSentinelResponse{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSentinelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSentinelResponse) ProtoMessage() {} + +func (x *CreateSentinelResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSentinelResponse.ProtoReflect.Descriptor instead. +func (*CreateSentinelResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescGZIP(), []int{15} +} + +type MigrateToWorkflowRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace ID of the schedule to migrate. + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + // The schedule ID to migrate from CHASM to workflow-backed. + ScheduleId string `protobuf:"bytes,2,opt,name=schedule_id,json=scheduleId,proto3" json:"schedule_id,omitempty"` + // The identity of the caller initiating the migration. + Identity string `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` + // A unique request ID for idempotency. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MigrateToWorkflowRequest) Reset() { + *x = MigrateToWorkflowRequest{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MigrateToWorkflowRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MigrateToWorkflowRequest) ProtoMessage() {} + +func (x *MigrateToWorkflowRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MigrateToWorkflowRequest.ProtoReflect.Descriptor instead. +func (*MigrateToWorkflowRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescGZIP(), []int{16} +} + +func (x *MigrateToWorkflowRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *MigrateToWorkflowRequest) GetScheduleId() string { + if x != nil { + return x.ScheduleId + } + return "" +} + +func (x *MigrateToWorkflowRequest) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *MigrateToWorkflowRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type MigrateToWorkflowResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MigrateToWorkflowResponse) Reset() { + *x = MigrateToWorkflowResponse{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MigrateToWorkflowResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MigrateToWorkflowResponse) ProtoMessage() {} + +func (x *MigrateToWorkflowResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MigrateToWorkflowResponse.ProtoReflect.Descriptor instead. +func (*MigrateToWorkflowResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescGZIP(), []int{17} +} + var File_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto protoreflect.FileDescriptor const file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDesc = "" + "\n" + - "Ctemporal/server/chasm/lib/scheduler/proto/v1/request_response.proto\x12,temporal.server.chasm.lib.scheduler.proto.v1\x1a6temporal/api/workflowservice/v1/request_response.proto\"\x9d\x01\n" + + "Ctemporal/server/chasm/lib/scheduler/proto/v1/request_response.proto\x12,temporal.server.chasm.lib.scheduler.proto.v1\x1a:temporal/server/chasm/lib/scheduler/proto/v1/message.proto\x1a6temporal/api/workflowservice/v1/request_response.proto\"\x9d\x01\n" + "\x15CreateScheduleRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12a\n" + "\x10frontend_request\x18\x02 \x01(\v26.temporal.api.workflowservice.v1.CreateScheduleRequestR\x0ffrontendRequest\"~\n" + @@ -639,7 +933,25 @@ const file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_r "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12l\n" + "\x10frontend_request\x18\x02 \x01(\v2A.temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequestR\x0ffrontendRequest\"\x94\x01\n" + "!ListScheduleMatchingTimesResponse\x12o\n" + - "\x11frontend_response\x18\x01 \x01(\v2B.temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponseR\x10frontendResponseBGZEgo.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpbb\x06proto3" + "\x11frontend_response\x18\x01 \x01(\v2B.temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponseR\x10frontendResponse\"\xa1\x01\n" + + "\x1fCreateFromMigrationStateRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12[\n" + + "\x05state\x18\x02 \x01(\v2E.temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationStateR\x05state\"\"\n" + + " CreateFromMigrationStateResponse\"y\n" + + "\x15CreateSentinelRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1c\n" + + "\tnamespace\x18\x02 \x01(\tR\tnamespace\x12\x1f\n" + + "\vschedule_id\x18\x03 \x01(\tR\n" + + "scheduleId\"\x18\n" + + "\x16CreateSentinelResponse\"\x99\x01\n" + + "\x18MigrateToWorkflowRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12\x1f\n" + + "\vschedule_id\x18\x02 \x01(\tR\n" + + "scheduleId\x12\x1a\n" + + "\bidentity\x18\x03 \x01(\tR\bidentity\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"\x1b\n" + + "\x19MigrateToWorkflowResponseBGZEgo.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpbb\x06proto3" var ( file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescOnce sync.Once @@ -653,7 +965,7 @@ func file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_ra return file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDescData } -var file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_goTypes = []any{ (*CreateScheduleRequest)(nil), // 0: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleRequest (*CreateScheduleResponse)(nil), // 1: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse @@ -667,37 +979,45 @@ var file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_goT (*DescribeScheduleResponse)(nil), // 9: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse (*ListScheduleMatchingTimesRequest)(nil), // 10: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesRequest (*ListScheduleMatchingTimesResponse)(nil), // 11: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse - (*v1.CreateScheduleRequest)(nil), // 12: temporal.api.workflowservice.v1.CreateScheduleRequest - (*v1.CreateScheduleResponse)(nil), // 13: temporal.api.workflowservice.v1.CreateScheduleResponse - (*v1.UpdateScheduleRequest)(nil), // 14: temporal.api.workflowservice.v1.UpdateScheduleRequest - (*v1.UpdateScheduleResponse)(nil), // 15: temporal.api.workflowservice.v1.UpdateScheduleResponse - (*v1.PatchScheduleRequest)(nil), // 16: temporal.api.workflowservice.v1.PatchScheduleRequest - (*v1.PatchScheduleResponse)(nil), // 17: temporal.api.workflowservice.v1.PatchScheduleResponse - (*v1.DeleteScheduleRequest)(nil), // 18: temporal.api.workflowservice.v1.DeleteScheduleRequest - (*v1.DeleteScheduleResponse)(nil), // 19: temporal.api.workflowservice.v1.DeleteScheduleResponse - (*v1.DescribeScheduleRequest)(nil), // 20: temporal.api.workflowservice.v1.DescribeScheduleRequest - (*v1.DescribeScheduleResponse)(nil), // 21: temporal.api.workflowservice.v1.DescribeScheduleResponse - (*v1.ListScheduleMatchingTimesRequest)(nil), // 22: temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest - (*v1.ListScheduleMatchingTimesResponse)(nil), // 23: temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse + (*CreateFromMigrationStateRequest)(nil), // 12: temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateRequest + (*CreateFromMigrationStateResponse)(nil), // 13: temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateResponse + (*CreateSentinelRequest)(nil), // 14: temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelRequest + (*CreateSentinelResponse)(nil), // 15: temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelResponse + (*MigrateToWorkflowRequest)(nil), // 16: temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowRequest + (*MigrateToWorkflowResponse)(nil), // 17: temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowResponse + (*v1.CreateScheduleRequest)(nil), // 18: temporal.api.workflowservice.v1.CreateScheduleRequest + (*v1.CreateScheduleResponse)(nil), // 19: temporal.api.workflowservice.v1.CreateScheduleResponse + (*v1.UpdateScheduleRequest)(nil), // 20: temporal.api.workflowservice.v1.UpdateScheduleRequest + (*v1.UpdateScheduleResponse)(nil), // 21: temporal.api.workflowservice.v1.UpdateScheduleResponse + (*v1.PatchScheduleRequest)(nil), // 22: temporal.api.workflowservice.v1.PatchScheduleRequest + (*v1.PatchScheduleResponse)(nil), // 23: temporal.api.workflowservice.v1.PatchScheduleResponse + (*v1.DeleteScheduleRequest)(nil), // 24: temporal.api.workflowservice.v1.DeleteScheduleRequest + (*v1.DeleteScheduleResponse)(nil), // 25: temporal.api.workflowservice.v1.DeleteScheduleResponse + (*v1.DescribeScheduleRequest)(nil), // 26: temporal.api.workflowservice.v1.DescribeScheduleRequest + (*v1.DescribeScheduleResponse)(nil), // 27: temporal.api.workflowservice.v1.DescribeScheduleResponse + (*v1.ListScheduleMatchingTimesRequest)(nil), // 28: temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest + (*v1.ListScheduleMatchingTimesResponse)(nil), // 29: temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse + (*SchedulerMigrationState)(nil), // 30: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState } var file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_depIdxs = []int32{ - 12, // 0: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.CreateScheduleRequest - 13, // 1: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.CreateScheduleResponse - 14, // 2: temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.UpdateScheduleRequest - 15, // 3: temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.UpdateScheduleResponse - 16, // 4: temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PatchScheduleRequest - 17, // 5: temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.PatchScheduleResponse - 18, // 6: temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DeleteScheduleRequest - 19, // 7: temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DeleteScheduleResponse - 20, // 8: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DescribeScheduleRequest - 21, // 9: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DescribeScheduleResponse - 22, // 10: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest - 23, // 11: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 18, // 0: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.CreateScheduleRequest + 19, // 1: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.CreateScheduleResponse + 20, // 2: temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.UpdateScheduleRequest + 21, // 3: temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.UpdateScheduleResponse + 22, // 4: temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PatchScheduleRequest + 23, // 5: temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.PatchScheduleResponse + 24, // 6: temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DeleteScheduleRequest + 25, // 7: temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DeleteScheduleResponse + 26, // 8: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DescribeScheduleRequest + 27, // 9: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DescribeScheduleResponse + 28, // 10: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest + 29, // 11: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse + 30, // 12: temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateRequest.state:type_name -> temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrationState + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_init() } @@ -705,13 +1025,14 @@ func file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_in if File_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto != nil { return } + file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_scheduler_proto_v1_request_response_proto_rawDesc)), NumEnums: 0, - NumMessages: 12, + NumMessages: 18, NumExtensions: 0, NumServices: 0, }, diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/service.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/service.pb.go index 59d97b75521..8c5afe3c650 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/service.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/service.pb.go @@ -10,6 +10,7 @@ import ( reflect "reflect" unsafe "unsafe" + _ "go.temporal.io/server/api/common/v1" _ "go.temporal.io/server/api/routing/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -26,14 +27,17 @@ var File_temporal_server_chasm_lib_scheduler_proto_v1_service_proto protoreflect const file_temporal_server_chasm_lib_scheduler_proto_v1_service_proto_rawDesc = "" + "\n" + - ":temporal/server/chasm/lib/scheduler/proto/v1/service.proto\x12,temporal.server.chasm.lib.scheduler.proto.v1\x1aCtemporal/server/chasm/lib/scheduler/proto/v1/request_response.proto\x1a.temporal/server/api/routing/v1/extension.proto2\xc2\t\n" + - "\x10SchedulerService\x12\xbf\x01\n" + - "\x0eCreateSchedule\x12C.temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleRequest\x1aD.temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x12\xbf\x01\n" + - "\x0eUpdateSchedule\x12C.temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleRequest\x1aD.temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x12\xbc\x01\n" + - "\rPatchSchedule\x12B.temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleRequest\x1aC.temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x12\xbf\x01\n" + - "\x0eDeleteSchedule\x12C.temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleRequest\x1aD.temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x12\xc5\x01\n" + - "\x10DescribeSchedule\x12E.temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleRequest\x1aF.temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x12\xe0\x01\n" + - "\x19ListScheduleMatchingTimes\x12N.temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesRequest\x1aO.temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse\"\"\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_idBGZEgo.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpbb\x06proto3" + ":temporal/server/chasm/lib/scheduler/proto/v1/service.proto\x12,temporal.server.chasm.lib.scheduler.proto.v1\x1aCtemporal/server/chasm/lib/scheduler/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\xb6\x0e\n" + + "\x10SchedulerService\x12\xc5\x01\n" + + "\x0eCreateSchedule\x12C.temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleRequest\x1aD.temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x8a\xb5\x18\x02\b\x01\x12\xc5\x01\n" + + "\x0eUpdateSchedule\x12C.temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleRequest\x1aD.temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x8a\xb5\x18\x02\b\x01\x12\xc2\x01\n" + + "\rPatchSchedule\x12B.temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleRequest\x1aC.temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x8a\xb5\x18\x02\b\x01\x12\xc5\x01\n" + + "\x0eDeleteSchedule\x12C.temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleRequest\x1aD.temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x8a\xb5\x18\x02\b\x01\x12\xcb\x01\n" + + "\x10DescribeSchedule\x12E.temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleRequest\x1aF.temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x8a\xb5\x18\x02\b\x01\x12\xe6\x01\n" + + "\x19ListScheduleMatchingTimes\x12N.temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesRequest\x1aO.temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.schedule_id\x8a\xb5\x18\x02\b\x01\x12\xe2\x01\n" + + "\x18CreateFromMigrationState\x12M.temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateRequest\x1aN.temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateResponse\"'\x92\xc4\x03#\x1a!state.scheduler_state.schedule_id\x12\xae\x01\n" + + "\x0eCreateSentinel\x12C.temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelRequest\x1aD.temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelResponse\"\x11\x92\xc4\x03\r\x1a\vschedule_id\x12\xb7\x01\n" + + "\x11MigrateToWorkflow\x12F.temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowRequest\x1aG.temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowResponse\"\x11\x92\xc4\x03\r\x1a\vschedule_idBGZEgo.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpbb\x06proto3" var file_temporal_server_chasm_lib_scheduler_proto_v1_service_proto_goTypes = []any{ (*CreateScheduleRequest)(nil), // 0: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleRequest @@ -42,12 +46,18 @@ var file_temporal_server_chasm_lib_scheduler_proto_v1_service_proto_goTypes = [] (*DeleteScheduleRequest)(nil), // 3: temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleRequest (*DescribeScheduleRequest)(nil), // 4: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleRequest (*ListScheduleMatchingTimesRequest)(nil), // 5: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesRequest - (*CreateScheduleResponse)(nil), // 6: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse - (*UpdateScheduleResponse)(nil), // 7: temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse - (*PatchScheduleResponse)(nil), // 8: temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse - (*DeleteScheduleResponse)(nil), // 9: temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse - (*DescribeScheduleResponse)(nil), // 10: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse - (*ListScheduleMatchingTimesResponse)(nil), // 11: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse + (*CreateFromMigrationStateRequest)(nil), // 6: temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateRequest + (*CreateSentinelRequest)(nil), // 7: temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelRequest + (*MigrateToWorkflowRequest)(nil), // 8: temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowRequest + (*CreateScheduleResponse)(nil), // 9: temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse + (*UpdateScheduleResponse)(nil), // 10: temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse + (*PatchScheduleResponse)(nil), // 11: temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse + (*DeleteScheduleResponse)(nil), // 12: temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse + (*DescribeScheduleResponse)(nil), // 13: temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse + (*ListScheduleMatchingTimesResponse)(nil), // 14: temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse + (*CreateFromMigrationStateResponse)(nil), // 15: temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateResponse + (*CreateSentinelResponse)(nil), // 16: temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelResponse + (*MigrateToWorkflowResponse)(nil), // 17: temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowResponse } var file_temporal_server_chasm_lib_scheduler_proto_v1_service_proto_depIdxs = []int32{ 0, // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.CreateSchedule:input_type -> temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleRequest @@ -56,14 +66,20 @@ var file_temporal_server_chasm_lib_scheduler_proto_v1_service_proto_depIdxs = [] 3, // 3: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.DeleteSchedule:input_type -> temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleRequest 4, // 4: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.DescribeSchedule:input_type -> temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleRequest 5, // 5: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.ListScheduleMatchingTimes:input_type -> temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesRequest - 6, // 6: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.CreateSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse - 7, // 7: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.UpdateSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse - 8, // 8: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.PatchSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse - 9, // 9: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.DeleteSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse - 10, // 10: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.DescribeSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse - 11, // 11: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.ListScheduleMatchingTimes:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse - 6, // [6:12] is the sub-list for method output_type - 0, // [0:6] is the sub-list for method input_type + 6, // 6: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.CreateFromMigrationState:input_type -> temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateRequest + 7, // 7: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.CreateSentinel:input_type -> temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelRequest + 8, // 8: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.MigrateToWorkflow:input_type -> temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowRequest + 9, // 9: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.CreateSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.CreateScheduleResponse + 10, // 10: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.UpdateSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.UpdateScheduleResponse + 11, // 11: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.PatchSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.PatchScheduleResponse + 12, // 12: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.DeleteSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.DeleteScheduleResponse + 13, // 13: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.DescribeSchedule:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.DescribeScheduleResponse + 14, // 14: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.ListScheduleMatchingTimes:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.ListScheduleMatchingTimesResponse + 15, // 15: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.CreateFromMigrationState:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.CreateFromMigrationStateResponse + 16, // 16: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.CreateSentinel:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.CreateSentinelResponse + 17, // 17: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService.MigrateToWorkflow:output_type -> temporal.server.chasm.lib.scheduler.proto.v1.MigrateToWorkflowResponse + 9, // [9:18] is the sub-list for method output_type + 0, // [0:9] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/service_client.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/service_client.pb.go index 7e7f7e2bb62..571b68416a0 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/service_client.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/service_client.pb.go @@ -316,3 +316,132 @@ func (c *SchedulerServiceLayeredClient) ListScheduleMatchingTimes( } return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) } +func (c *SchedulerServiceLayeredClient) callCreateFromMigrationStateNoRetry( + ctx context.Context, + request *CreateFromMigrationStateRequest, + opts ...grpc.CallOption, +) (*CreateFromMigrationStateResponse, error) { + var response *CreateFromMigrationStateResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("SchedulerService.CreateFromMigrationState"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetState().GetSchedulerState().GetScheduleId(), c.numShards) + op := func(ctx context.Context, client SchedulerServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.CreateFromMigrationState(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *SchedulerServiceLayeredClient) CreateFromMigrationState( + ctx context.Context, + request *CreateFromMigrationStateRequest, + opts ...grpc.CallOption, +) (*CreateFromMigrationStateResponse, error) { + call := func(ctx context.Context) (*CreateFromMigrationStateResponse, error) { + return c.callCreateFromMigrationStateNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *SchedulerServiceLayeredClient) callCreateSentinelNoRetry( + ctx context.Context, + request *CreateSentinelRequest, + opts ...grpc.CallOption, +) (*CreateSentinelResponse, error) { + var response *CreateSentinelResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("SchedulerService.CreateSentinel"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetScheduleId(), c.numShards) + op := func(ctx context.Context, client SchedulerServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.CreateSentinel(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *SchedulerServiceLayeredClient) CreateSentinel( + ctx context.Context, + request *CreateSentinelRequest, + opts ...grpc.CallOption, +) (*CreateSentinelResponse, error) { + call := func(ctx context.Context) (*CreateSentinelResponse, error) { + return c.callCreateSentinelNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *SchedulerServiceLayeredClient) callMigrateToWorkflowNoRetry( + ctx context.Context, + request *MigrateToWorkflowRequest, + opts ...grpc.CallOption, +) (*MigrateToWorkflowResponse, error) { + var response *MigrateToWorkflowResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("SchedulerService.MigrateToWorkflow"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetScheduleId(), c.numShards) + op := func(ctx context.Context, client SchedulerServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.MigrateToWorkflow(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *SchedulerServiceLayeredClient) MigrateToWorkflow( + ctx context.Context, + request *MigrateToWorkflowRequest, + opts ...grpc.CallOption, +) (*MigrateToWorkflowResponse, error) { + call := func(ctx context.Context) (*MigrateToWorkflowResponse, error) { + return c.callMigrateToWorkflowNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/service_grpc.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/service_grpc.pb.go index f8a7fde1ef2..a4b5845bbff 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/service_grpc.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/service_grpc.pb.go @@ -26,6 +26,9 @@ const ( SchedulerService_DeleteSchedule_FullMethodName = "/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/DeleteSchedule" SchedulerService_DescribeSchedule_FullMethodName = "/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/DescribeSchedule" SchedulerService_ListScheduleMatchingTimes_FullMethodName = "/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/ListScheduleMatchingTimes" + SchedulerService_CreateFromMigrationState_FullMethodName = "/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/CreateFromMigrationState" + SchedulerService_CreateSentinel_FullMethodName = "/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/CreateSentinel" + SchedulerService_MigrateToWorkflow_FullMethodName = "/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/MigrateToWorkflow" ) // SchedulerServiceClient is the client API for SchedulerService service. @@ -38,6 +41,9 @@ type SchedulerServiceClient interface { DeleteSchedule(ctx context.Context, in *DeleteScheduleRequest, opts ...grpc.CallOption) (*DeleteScheduleResponse, error) DescribeSchedule(ctx context.Context, in *DescribeScheduleRequest, opts ...grpc.CallOption) (*DescribeScheduleResponse, error) ListScheduleMatchingTimes(ctx context.Context, in *ListScheduleMatchingTimesRequest, opts ...grpc.CallOption) (*ListScheduleMatchingTimesResponse, error) + CreateFromMigrationState(ctx context.Context, in *CreateFromMigrationStateRequest, opts ...grpc.CallOption) (*CreateFromMigrationStateResponse, error) + CreateSentinel(ctx context.Context, in *CreateSentinelRequest, opts ...grpc.CallOption) (*CreateSentinelResponse, error) + MigrateToWorkflow(ctx context.Context, in *MigrateToWorkflowRequest, opts ...grpc.CallOption) (*MigrateToWorkflowResponse, error) } type schedulerServiceClient struct { @@ -102,6 +108,33 @@ func (c *schedulerServiceClient) ListScheduleMatchingTimes(ctx context.Context, return out, nil } +func (c *schedulerServiceClient) CreateFromMigrationState(ctx context.Context, in *CreateFromMigrationStateRequest, opts ...grpc.CallOption) (*CreateFromMigrationStateResponse, error) { + out := new(CreateFromMigrationStateResponse) + err := c.cc.Invoke(ctx, SchedulerService_CreateFromMigrationState_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *schedulerServiceClient) CreateSentinel(ctx context.Context, in *CreateSentinelRequest, opts ...grpc.CallOption) (*CreateSentinelResponse, error) { + out := new(CreateSentinelResponse) + err := c.cc.Invoke(ctx, SchedulerService_CreateSentinel_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *schedulerServiceClient) MigrateToWorkflow(ctx context.Context, in *MigrateToWorkflowRequest, opts ...grpc.CallOption) (*MigrateToWorkflowResponse, error) { + out := new(MigrateToWorkflowResponse) + err := c.cc.Invoke(ctx, SchedulerService_MigrateToWorkflow_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // SchedulerServiceServer is the server API for SchedulerService service. // All implementations must embed UnimplementedSchedulerServiceServer // for forward compatibility @@ -112,6 +145,9 @@ type SchedulerServiceServer interface { DeleteSchedule(context.Context, *DeleteScheduleRequest) (*DeleteScheduleResponse, error) DescribeSchedule(context.Context, *DescribeScheduleRequest) (*DescribeScheduleResponse, error) ListScheduleMatchingTimes(context.Context, *ListScheduleMatchingTimesRequest) (*ListScheduleMatchingTimesResponse, error) + CreateFromMigrationState(context.Context, *CreateFromMigrationStateRequest) (*CreateFromMigrationStateResponse, error) + CreateSentinel(context.Context, *CreateSentinelRequest) (*CreateSentinelResponse, error) + MigrateToWorkflow(context.Context, *MigrateToWorkflowRequest) (*MigrateToWorkflowResponse, error) mustEmbedUnimplementedSchedulerServiceServer() } @@ -137,6 +173,15 @@ func (UnimplementedSchedulerServiceServer) DescribeSchedule(context.Context, *De func (UnimplementedSchedulerServiceServer) ListScheduleMatchingTimes(context.Context, *ListScheduleMatchingTimesRequest) (*ListScheduleMatchingTimesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListScheduleMatchingTimes not implemented") } +func (UnimplementedSchedulerServiceServer) CreateFromMigrationState(context.Context, *CreateFromMigrationStateRequest) (*CreateFromMigrationStateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateFromMigrationState not implemented") +} +func (UnimplementedSchedulerServiceServer) CreateSentinel(context.Context, *CreateSentinelRequest) (*CreateSentinelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateSentinel not implemented") +} +func (UnimplementedSchedulerServiceServer) MigrateToWorkflow(context.Context, *MigrateToWorkflowRequest) (*MigrateToWorkflowResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method MigrateToWorkflow not implemented") +} func (UnimplementedSchedulerServiceServer) mustEmbedUnimplementedSchedulerServiceServer() {} // UnsafeSchedulerServiceServer may be embedded to opt out of forward compatibility for this service. @@ -258,6 +303,60 @@ func _SchedulerService_ListScheduleMatchingTimes_Handler(srv interface{}, ctx co return interceptor(ctx, in, info, handler) } +func _SchedulerService_CreateFromMigrationState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateFromMigrationStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SchedulerServiceServer).CreateFromMigrationState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SchedulerService_CreateFromMigrationState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SchedulerServiceServer).CreateFromMigrationState(ctx, req.(*CreateFromMigrationStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SchedulerService_CreateSentinel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSentinelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SchedulerServiceServer).CreateSentinel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SchedulerService_CreateSentinel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SchedulerServiceServer).CreateSentinel(ctx, req.(*CreateSentinelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SchedulerService_MigrateToWorkflow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MigrateToWorkflowRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SchedulerServiceServer).MigrateToWorkflow(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SchedulerService_MigrateToWorkflow_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SchedulerServiceServer).MigrateToWorkflow(ctx, req.(*MigrateToWorkflowRequest)) + } + return interceptor(ctx, in, info, handler) +} + // SchedulerService_ServiceDesc is the grpc.ServiceDesc for SchedulerService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -289,6 +388,18 @@ var SchedulerService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListScheduleMatchingTimes", Handler: _SchedulerService_ListScheduleMatchingTimes_Handler, }, + { + MethodName: "CreateFromMigrationState", + Handler: _SchedulerService_CreateFromMigrationState_Handler, + }, + { + MethodName: "CreateSentinel", + Handler: _SchedulerService_CreateSentinel_Handler, + }, + { + MethodName: "MigrateToWorkflow", + Handler: _SchedulerService_MigrateToWorkflow_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "temporal/server/chasm/lib/scheduler/proto/v1/service.proto", diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.go-helpers.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.go-helpers.pb.go index 304e4a6a61c..16df8371800 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.go-helpers.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.go-helpers.pb.go @@ -42,6 +42,43 @@ func (this *SchedulerIdleTask) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type SchedulerCallbacksTask to the protobuf v3 wire format +func (val *SchedulerCallbacksTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type SchedulerCallbacksTask from the protobuf v3 wire format +func (val *SchedulerCallbacksTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *SchedulerCallbacksTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two SchedulerCallbacksTask values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *SchedulerCallbacksTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *SchedulerCallbacksTask + switch t := that.(type) { + case *SchedulerCallbacksTask: + that1 = t + case SchedulerCallbacksTask: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type GeneratorTask to the protobuf v3 wire format func (val *GeneratorTask) Marshal() ([]byte, error) { return proto.Marshal(val) @@ -189,3 +226,40 @@ func (this *BackfillerTask) Equal(that interface{}) bool { return proto.Equal(this, that1) } + +// Marshal an object of type SchedulerMigrateToWorkflowTask to the protobuf v3 wire format +func (val *SchedulerMigrateToWorkflowTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type SchedulerMigrateToWorkflowTask from the protobuf v3 wire format +func (val *SchedulerMigrateToWorkflowTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *SchedulerMigrateToWorkflowTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two SchedulerMigrateToWorkflowTask values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *SchedulerMigrateToWorkflowTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *SchedulerMigrateToWorkflowTask + switch t := that.(type) { + case *SchedulerMigrateToWorkflowTask: + that1 = t + case SchedulerMigrateToWorkflowTask: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.pb.go b/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.pb.go index 9846c2239ce..3564a6aac35 100644 --- a/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.pb.go +++ b/chasm/lib/scheduler/gen/schedulerpb/v1/tasks.pb.go @@ -71,6 +71,45 @@ func (x *SchedulerIdleTask) GetIdleTimeTotal() *durationpb.Duration { return nil } +// Ensures that callbacks for all running buffered starts are attached. Used only +// during migration from V1, as workflows started by CHASM scheduler are started +// with callbacks attached. +type SchedulerCallbacksTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SchedulerCallbacksTask) Reset() { + *x = SchedulerCallbacksTask{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SchedulerCallbacksTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SchedulerCallbacksTask) ProtoMessage() {} + +func (x *SchedulerCallbacksTask) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SchedulerCallbacksTask.ProtoReflect.Descriptor instead. +func (*SchedulerCallbacksTask) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{1} +} + // Buffers actions based on the schedule's specification. type GeneratorTask struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -80,7 +119,7 @@ type GeneratorTask struct { func (x *GeneratorTask) Reset() { *x = GeneratorTask{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -92,7 +131,7 @@ func (x *GeneratorTask) String() string { func (*GeneratorTask) ProtoMessage() {} func (x *GeneratorTask) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -105,7 +144,7 @@ func (x *GeneratorTask) ProtoReflect() protoreflect.Message { // Deprecated: Use GeneratorTask.ProtoReflect.Descriptor instead. func (*GeneratorTask) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{1} + return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{2} } // Processes buffered actions, deciding whether to execute, delay, or discard. @@ -117,7 +156,7 @@ type InvokerProcessBufferTask struct { func (x *InvokerProcessBufferTask) Reset() { *x = InvokerProcessBufferTask{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[2] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -129,7 +168,7 @@ func (x *InvokerProcessBufferTask) String() string { func (*InvokerProcessBufferTask) ProtoMessage() {} func (x *InvokerProcessBufferTask) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[2] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -142,7 +181,7 @@ func (x *InvokerProcessBufferTask) ProtoReflect() protoreflect.Message { // Deprecated: Use InvokerProcessBufferTask.ProtoReflect.Descriptor instead. func (*InvokerProcessBufferTask) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{2} + return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{3} } // Drives execution of pending buffered actions to completion by starting, @@ -155,7 +194,7 @@ type InvokerExecuteTask struct { func (x *InvokerExecuteTask) Reset() { *x = InvokerExecuteTask{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[3] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -167,7 +206,7 @@ func (x *InvokerExecuteTask) String() string { func (*InvokerExecuteTask) ProtoMessage() {} func (x *InvokerExecuteTask) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[3] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -180,7 +219,7 @@ func (x *InvokerExecuteTask) ProtoReflect() protoreflect.Message { // Deprecated: Use InvokerExecuteTask.ProtoReflect.Descriptor instead. func (*InvokerExecuteTask) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{3} + return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{4} } // Buffers actions based on a manually-requested backfill. @@ -192,7 +231,7 @@ type BackfillerTask struct { func (x *BackfillerTask) Reset() { *x = BackfillerTask{} - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[4] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -204,7 +243,7 @@ func (x *BackfillerTask) String() string { func (*BackfillerTask) ProtoMessage() {} func (x *BackfillerTask) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[4] + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -217,7 +256,44 @@ func (x *BackfillerTask) ProtoReflect() protoreflect.Message { // Deprecated: Use BackfillerTask.ProtoReflect.Descriptor instead. func (*BackfillerTask) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{4} + return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{5} +} + +// Triggers migration from CHASM (V2) to workflow-backed (V1) scheduler. +type SchedulerMigrateToWorkflowTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SchedulerMigrateToWorkflowTask) Reset() { + *x = SchedulerMigrateToWorkflowTask{} + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SchedulerMigrateToWorkflowTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SchedulerMigrateToWorkflowTask) ProtoMessage() {} + +func (x *SchedulerMigrateToWorkflowTask) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SchedulerMigrateToWorkflowTask.ProtoReflect.Descriptor instead. +func (*SchedulerMigrateToWorkflowTask) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP(), []int{6} } var File_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto protoreflect.FileDescriptor @@ -226,11 +302,13 @@ const file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDesc = "" "\n" + "8temporal/server/chasm/lib/scheduler/proto/v1/tasks.proto\x12,temporal.server.chasm.lib.scheduler.proto.v1\x1a\x1egoogle/protobuf/duration.proto\"V\n" + "\x11SchedulerIdleTask\x12A\n" + - "\x0fidle_time_total\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\ridleTimeTotal\"\x0f\n" + + "\x0fidle_time_total\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\ridleTimeTotal\"\x18\n" + + "\x16SchedulerCallbacksTask\"\x0f\n" + "\rGeneratorTask\"\x1a\n" + "\x18InvokerProcessBufferTask\"\x14\n" + "\x12InvokerExecuteTask\"\x10\n" + - "\x0eBackfillerTaskBGZEgo.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpbb\x06proto3" + "\x0eBackfillerTask\" \n" + + "\x1eSchedulerMigrateToWorkflowTaskBGZEgo.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpbb\x06proto3" var ( file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescOnce sync.Once @@ -244,17 +322,19 @@ func file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescGZIP() return file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDescData } -var file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_goTypes = []any{ - (*SchedulerIdleTask)(nil), // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerIdleTask - (*GeneratorTask)(nil), // 1: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorTask - (*InvokerProcessBufferTask)(nil), // 2: temporal.server.chasm.lib.scheduler.proto.v1.InvokerProcessBufferTask - (*InvokerExecuteTask)(nil), // 3: temporal.server.chasm.lib.scheduler.proto.v1.InvokerExecuteTask - (*BackfillerTask)(nil), // 4: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerTask - (*durationpb.Duration)(nil), // 5: google.protobuf.Duration + (*SchedulerIdleTask)(nil), // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerIdleTask + (*SchedulerCallbacksTask)(nil), // 1: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerCallbacksTask + (*GeneratorTask)(nil), // 2: temporal.server.chasm.lib.scheduler.proto.v1.GeneratorTask + (*InvokerProcessBufferTask)(nil), // 3: temporal.server.chasm.lib.scheduler.proto.v1.InvokerProcessBufferTask + (*InvokerExecuteTask)(nil), // 4: temporal.server.chasm.lib.scheduler.proto.v1.InvokerExecuteTask + (*BackfillerTask)(nil), // 5: temporal.server.chasm.lib.scheduler.proto.v1.BackfillerTask + (*SchedulerMigrateToWorkflowTask)(nil), // 6: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerMigrateToWorkflowTask + (*durationpb.Duration)(nil), // 7: google.protobuf.Duration } var file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_depIdxs = []int32{ - 5, // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerIdleTask.idle_time_total:type_name -> google.protobuf.Duration + 7, // 0: temporal.server.chasm.lib.scheduler.proto.v1.SchedulerIdleTask.idle_time_total:type_name -> google.protobuf.Duration 1, // [1:1] is the sub-list for method output_type 1, // [1:1] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name @@ -273,7 +353,7 @@ func file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDesc), len(file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/chasm/lib/scheduler/generator.go b/chasm/lib/scheduler/generator.go index 9146c0c7cf8..38bba1de74b 100644 --- a/chasm/lib/scheduler/generator.go +++ b/chasm/lib/scheduler/generator.go @@ -20,18 +20,20 @@ type Generator struct { Scheduler chasm.ParentPtr[*Scheduler] } -// NewGenerator returns an intialized Generator component, which should +// NewGenerator returns an initialized Generator component, which should // be parented under a Scheduler root node. func NewGenerator(ctx chasm.MutableContext) *Generator { + return newGeneratorWithState(ctx, &schedulerpb.GeneratorState{ + LastProcessedTime: nil, + }) +} + +func newGeneratorWithState(ctx chasm.MutableContext, state *schedulerpb.GeneratorState) *Generator { generator := &Generator{ - GeneratorState: &schedulerpb.GeneratorState{ - LastProcessedTime: nil, - }, + GeneratorState: state, } - // Kick off initial generator run as an immediate task. generator.Generate(ctx) - return generator } diff --git a/chasm/lib/scheduler/generator_tasks.go b/chasm/lib/scheduler/generator_tasks.go index def3746c07d..7d7d5a7f707 100644 --- a/chasm/lib/scheduler/generator_tasks.go +++ b/chasm/lib/scheduler/generator_tasks.go @@ -16,7 +16,7 @@ import ( ) type ( - GeneratorTaskExecutorOptions struct { + GeneratorTaskHandlerOptions struct { fx.In Config *Config @@ -26,7 +26,8 @@ type ( SpecBuilder *scheduler.SpecBuilder } - GeneratorTaskExecutor struct { + GeneratorTaskHandler struct { + chasm.PureTaskHandlerBase config *Config metricsHandler metrics.Handler baseLogger log.Logger @@ -35,8 +36,8 @@ type ( } ) -func NewGeneratorTaskExecutor(opts GeneratorTaskExecutorOptions) *GeneratorTaskExecutor { - return &GeneratorTaskExecutor{ +func NewGeneratorTaskHandler(opts GeneratorTaskHandlerOptions) *GeneratorTaskHandler { + return &GeneratorTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, baseLogger: opts.BaseLogger, @@ -45,7 +46,7 @@ func NewGeneratorTaskExecutor(opts GeneratorTaskExecutorOptions) *GeneratorTaskE } } -func (g *GeneratorTaskExecutor) Execute( +func (g *GeneratorTaskHandler) Execute( ctx chasm.MutableContext, generator *Generator, _ chasm.TaskAttributes, @@ -141,13 +142,13 @@ func (g *GeneratorTaskExecutor) Execute( return nil } -func (g *GeneratorTaskExecutor) logSchedule(logger log.Logger, msg string, scheduler *Scheduler) { +func (g *GeneratorTaskHandler) logSchedule(logger log.Logger, msg string, sched *Scheduler) { logger.Debug(msg, - tag.Stringer("spec", jsonStringer{scheduler.Schedule.Spec}), - tag.Stringer("policies", jsonStringer{scheduler.Schedule.Policies})) + tag.Stringer("spec", jsonStringer{sched.Schedule.Spec}), + tag.Stringer("policies", jsonStringer{sched.Schedule.Policies})) } -func (g *GeneratorTaskExecutor) Validate( +func (g *GeneratorTaskHandler) Validate( ctx chasm.Context, generator *Generator, attrs chasm.TaskAttributes, diff --git a/chasm/lib/scheduler/generator_tasks_test.go b/chasm/lib/scheduler/generator_tasks_test.go index 14145c50e65..674fb88dc10 100644 --- a/chasm/lib/scheduler/generator_tasks_test.go +++ b/chasm/lib/scheduler/generator_tasks_test.go @@ -3,8 +3,9 @@ package scheduler_test import ( "errors" "testing" + "time" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/require" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" @@ -16,129 +17,139 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -type generatorTasksSuite struct { - schedulerSuite - executor *scheduler.GeneratorTaskExecutor -} - -func TestGeneratorTasksSuite(t *testing.T) { - suite.Run(t, &generatorTasksSuite{}) -} - -func (s *generatorTasksSuite) SetupTest() { - s.schedulerSuite.SetupTest() - s.executor = scheduler.NewGeneratorTaskExecutor(scheduler.GeneratorTaskExecutorOptions{ +func newGeneratorHandler(env *testEnv) *scheduler.GeneratorTaskHandler { + return scheduler.NewGeneratorTaskHandler(scheduler.GeneratorTaskHandlerOptions{ Config: defaultConfig(), MetricsHandler: metrics.NoopMetricsHandler, - BaseLogger: s.logger, - SpecProcessor: s.specProcessor, + BaseLogger: env.Logger, + SpecProcessor: env.SpecProcessor, SpecBuilder: legacyscheduler.NewSpecBuilder(), }) } -func (s *generatorTasksSuite) TestExecute_ProcessTimeRangeFails() { - sched := s.scheduler - ctx := s.newMutableContext() +func TestGeneratorTask_Execute_ProcessTimeRangeFails(t *testing.T) { + // Create a custom mock spec processor that fails on ProcessTimeRange. + ctrl := gomock.NewController(t) + specProcessor := scheduler.NewMockSpecProcessor(ctrl) + now := time.Now() - // If ProcessTimeRange fails, we should fail the task as an internal error. - s.specProcessor.EXPECT().ProcessTimeRange( + // First call during newTestEnv's CloseTransaction should succeed. + specProcessor.EXPECT().ProcessTimeRange( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(&scheduler.ProcessedTimeRange{ + NextWakeupTime: now.Add(defaultInterval), + LastActionTime: now, + }, nil).Times(1) + + // Second call during test should fail. + specProcessor.EXPECT().ProcessTimeRange( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), ).Return(nil, errors.New("processTimeRange bug")) - // Execute the generate task. - generator := sched.Generator.Get(ctx) - err := s.executor.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) + specProcessor.EXPECT().NextTime(gomock.Any(), gomock.Any()).Return(legacyscheduler.GetNextTimeResult{ + Next: now.Add(defaultInterval), + Nominal: now.Add(defaultInterval), + }, nil).AnyTimes() + + env := newTestEnv(t, withSpecProcessor(specProcessor)) + handler := newGeneratorHandler(env) + + ctx := env.MutableContext() + generator := env.Scheduler.Generator.Get(ctx) + + // If ProcessTimeRange fails, we should fail the task as an internal error. + err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) var target *queueerrors.UnprocessableTaskError - s.ErrorAs(err, &target) - s.Equal("failed to process a time range: processTimeRange bug", target.Message) + require.ErrorAs(t, err, &target) + require.Equal(t, "failed to process a time range: processTimeRange bug", target.Message) } -func (s *generatorTasksSuite) TestExecuteBufferTask_Basic() { - ctx := s.newMutableContext() - sched := s.scheduler +func TestGeneratorTask_ExecuteBufferTask_Basic(t *testing.T) { + env := newTestEnv(t) + handler := newGeneratorHandler(env) + ctx := env.MutableContext() + sched := env.Scheduler generator := sched.Generator.Get(ctx) - // Use a real SpecProcessor implementation. - specProcessor := newTestSpecProcessor(s.controller) - s.executor.SpecProcessor = specProcessor - // Move high water mark back in time (Generator always compares high water mark // against system time) to generate buffered actions. highWatermark := ctx.Now(generator).UTC().Add(-defaultInterval * 5) generator.LastProcessedTime = timestamppb.New(highWatermark) // Execute the generate task. - err := s.executor.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) - s.NoError(err) + err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) + require.NoError(t, err) // We expect 5 buffered starts. invoker := sched.Invoker.Get(ctx) - s.Equal(5, len(invoker.BufferedStarts)) + require.Len(t, invoker.BufferedStarts, 5) - // Validate RequestId -> WorkflowId mapping + // Validate RequestId -> WorkflowId mapping. for _, start := range invoker.BufferedStarts { - s.Equal(start.WorkflowId, invoker.RunningWorkflowID(start.RequestId)) + require.Equal(t, start.WorkflowId, invoker.RunningWorkflowID(start.RequestId)) } // Generator's high water mark should have advanced. newHighWatermark := generator.LastProcessedTime.AsTime() - s.True(newHighWatermark.After(highWatermark)) + require.True(t, newHighWatermark.After(highWatermark)) // Ensure we scheduled a physical side-effect task on the tree at immediate time. // The InvokerExecuteTask is a side-effect task that starts workflows. // The InvokerProcessBufferTask (pure) executes inline during CloseTransaction. - _, err = s.node.CloseTransaction() - s.NoError(err) - s.True(s.hasTask(&tasks.ChasmTask{}, chasm.TaskScheduledTimeImmediate)) + require.NoError(t, env.CloseTransaction()) + require.True(t, env.HasTask(&tasks.ChasmTask{}, chasm.TaskScheduledTimeImmediate)) } -func (s *generatorTasksSuite) TestUpdateFutureActionTimes_UnlimitedActions() { - ctx := s.newMutableContext() - sched := s.scheduler - generator := sched.Generator.Get(ctx) +func TestGeneratorTask_UpdateFutureActionTimes_UnlimitedActions(t *testing.T) { + env := newTestEnv(t) + handler := newGeneratorHandler(env) - s.executor.SpecProcessor = newTestSpecProcessor(s.controller) + ctx := env.MutableContext() + generator := env.Scheduler.Generator.Get(ctx) - err := s.executor.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) - s.NoError(err) + err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) + require.NoError(t, err) - s.NotEmpty(generator.FutureActionTimes) - s.Require().Len(generator.FutureActionTimes, 10) + require.NotEmpty(t, generator.FutureActionTimes) + require.Len(t, generator.FutureActionTimes, 10) } -func (s *generatorTasksSuite) TestUpdateFutureActionTimes_LimitedActions() { - ctx := s.newMutableContext() - sched := s.scheduler +func TestGeneratorTask_UpdateFutureActionTimes_LimitedActions(t *testing.T) { + env := newTestEnv(t) + handler := newGeneratorHandler(env) + + ctx := env.MutableContext() + sched := env.Scheduler generator := sched.Generator.Get(ctx) sched.Schedule.State.LimitedActions = true sched.Schedule.State.RemainingActions = 2 - s.executor.SpecProcessor = newTestSpecProcessor(s.controller) - err := s.executor.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) - s.NoError(err) + err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) + require.NoError(t, err) - s.Len(generator.FutureActionTimes, 2) + require.Len(t, generator.FutureActionTimes, 2) } -func (s *generatorTasksSuite) TestUpdateFutureActionTimes_SkipsBeforeUpdateTime() { - ctx := s.newMutableContext() - sched := s.scheduler - generator := sched.Generator.Get(ctx) +func TestGeneratorTask_UpdateFutureActionTimes_SkipsBeforeUpdateTime(t *testing.T) { + env := newTestEnv(t) + handler := newGeneratorHandler(env) - s.executor.SpecProcessor = newTestSpecProcessor(s.controller) + ctx := env.MutableContext() + sched := env.Scheduler + generator := sched.Generator.Get(ctx) // UpdateTime acts as a floor - action times at or before it are skipped. baseTime := ctx.Now(generator).UTC() updateTime := baseTime.Add(defaultInterval / 2) sched.Info.UpdateTime = timestamppb.New(updateTime) - err := s.executor.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) - s.NoError(err) + err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{}) + require.NoError(t, err) - s.Require().NotEmpty(generator.FutureActionTimes) + require.NotEmpty(t, generator.FutureActionTimes) for _, futureTime := range generator.FutureActionTimes { - s.True(futureTime.AsTime().After(updateTime)) + require.True(t, futureTime.AsTime().After(updateTime)) } } diff --git a/chasm/lib/scheduler/handler.go b/chasm/lib/scheduler/handler.go index 069ca2c6697..710cee1743a 100644 --- a/chasm/lib/scheduler/handler.go +++ b/chasm/lib/scheduler/handler.go @@ -3,12 +3,14 @@ package scheduler import ( "context" "errors" + "fmt" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" legacyscheduler "go.temporal.io/server/service/worker/scheduler" ) @@ -75,6 +77,101 @@ func (h *handler) CreateSchedule(ctx context.Context, req *schedulerpb.CreateSch }, err } +// CreateFromMigrationState creates a CHASM schedule from migrated V1 state. +// Used during migration from workflow-backed schedules to CHASM schedules. +func (h *handler) CreateFromMigrationState(ctx context.Context, req *schedulerpb.CreateFromMigrationStateRequest) (resp *schedulerpb.CreateFromMigrationStateResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + scheduleID := req.GetState().GetSchedulerState().GetScheduleId() + _, err = chasm.StartExecution( + ctx, + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: scheduleID, + }, + CreateSchedulerFromMigration, + req, + ) + + var alreadyStartedErr *chasm.ExecutionAlreadyStartedError + if errors.As(err, &alreadyStartedErr) { + // Check if the existing schedule is a sentinel. Sentinels are + // auto-deleted SentinelIdleTime after schedule creation; the + // V1 schedule will keep retrying migration until it expires. + _, readErr := chasm.ReadComponent( + ctx, + chasm.NewComponentRef[*Scheduler]( + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: scheduleID, + }, + ), + func(s *Scheduler, ctx chasm.Context, _ *struct{}) (*struct{}, error) { + if s.IsSentinel() { + return nil, ErrSentinel + } + return nil, nil + }, + (*struct{})(nil), + ) + if readErr != nil { + if errors.Is(readErr, ErrSentinel) { + h.logger.Warn( + fmt.Sprintf("Migration blocked by sentinel schedule; sentinel will auto-delete %v after schedule creation", SentinelIdleTime), + tag.NewStringTag("schedule-id", scheduleID), + ) + return nil, ErrSentinelBlocked + } + return nil, readErr + } + return nil, serviceerror.NewAlreadyExistsf("schedule %q is already registered", scheduleID) + } + + return &schedulerpb.CreateFromMigrationStateResponse{}, err +} + +func (h *handler) CreateSentinel(ctx context.Context, req *schedulerpb.CreateSentinelRequest) (resp *schedulerpb.CreateSentinelResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + _, err = chasm.StartExecution( + ctx, + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.ScheduleId, + }, + CreateSentinelFn, + req, + ) + + var alreadyStartedErr *chasm.ExecutionAlreadyStartedError + if errors.As(err, &alreadyStartedErr) { + // If a sentinel already exists, succeed idempotently. + // If a real scheduler exists, fail. + _, readErr := chasm.ReadComponent( + ctx, + chasm.NewComponentRef[*Scheduler]( + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.ScheduleId, + }, + ), + func(s *Scheduler, ctx chasm.Context, _ *struct{}) (*struct{}, error) { + if s.IsSentinel() { + return nil, nil + } + return nil, serviceerror.NewAlreadyExistsf("schedule %q is already registered", req.ScheduleId) + }, + (*struct{})(nil), + ) + return &schedulerpb.CreateSentinelResponse{}, readErr + } + if err != nil { + return nil, err + } + + return &schedulerpb.CreateSentinelResponse{}, nil +} + func (h *handler) UpdateSchedule(ctx context.Context, req *schedulerpb.UpdateScheduleRequest) (resp *schedulerpb.UpdateScheduleResponse, err error) { defer log.CapturePanic(h.logger, &err) @@ -126,6 +223,26 @@ func (h *handler) DeleteSchedule(ctx context.Context, req *schedulerpb.DeleteSch return resp, err } +func (h *handler) MigrateToWorkflow(ctx context.Context, req *schedulerpb.MigrateToWorkflowRequest) (resp *schedulerpb.MigrateToWorkflowResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + resp, _, err = chasm.UpdateComponent( + ctx, + chasm.NewComponentRef[*Scheduler]( + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.ScheduleId, + }, + ), + (*Scheduler).MigrateToWorkflow, + req, + ) + if errors.Is(err, ErrSentinel) { + return nil, ErrSentinelBlocked + } + return resp, err +} + func (h *handler) DescribeSchedule(ctx context.Context, req *schedulerpb.DescribeScheduleRequest) (resp *schedulerpb.DescribeScheduleResponse, err error) { defer log.CapturePanic(h.logger, &err) diff --git a/chasm/lib/scheduler/handler_test.go b/chasm/lib/scheduler/handler_test.go index 2d22c9ea49f..03ca797714c 100644 --- a/chasm/lib/scheduler/handler_test.go +++ b/chasm/lib/scheduler/handler_test.go @@ -8,8 +8,9 @@ import ( "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler" - "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" legacyscheduler "go.temporal.io/server/service/worker/scheduler" + "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -95,3 +96,63 @@ func TestSentinelHandler_DeleteSchedule(t *testing.T) { return err }) } + +func TestSentinelHandler_MigrateToWorkflow(t *testing.T) { + runSentinelHandlerTestCase(t, func(sentinel *scheduler.Scheduler, ctx chasm.MutableContext, _ *legacyscheduler.SpecBuilder) error { + _, err := sentinel.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + return err + }) +} + +func TestHandler_CreateFromMigrationState_Sentinel(t *testing.T) { + env := newTestEnv(t, withMockEngine()) + sentinel, ctx, _ := setupSentinelForTest(t) + + h := scheduler.NewTestHandler(env.Logger) + + // StartExecution returns already-started because the sentinel occupies the key. + env.MockEngine.EXPECT().StartExecution(gomock.Any(), gomock.Any(), gomock.Any()). + Return(chasm.StartExecutionResult{}, chasm.NewExecutionAlreadyStartedErr("already exists", "", "")) + + // ReadComponent invokes the read function with the sentinel. + env.ExpectReadComponent(ctx, sentinel) + + engineCtx := env.EngineContext() + _, err := h.TestCreateFromMigrationState(engineCtx, &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: namespaceID, + State: &schedulerpb.SchedulerMigrationState{ + SchedulerState: &schedulerpb.SchedulerState{ + ScheduleId: scheduleID, + }, + }, + }) + + require.Error(t, err) + require.ErrorIs(t, err, scheduler.ErrSentinelBlocked) + var unavailableErr *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailableErr) +} + +func TestHandler_MigrateToWorkflow_Sentinel(t *testing.T) { + env := newTestEnv(t, withMockEngine()) + sentinel, ctx, _ := setupSentinelForTest(t) + + h := scheduler.NewTestHandler(env.Logger) + + // UpdateComponent invokes the update function with the sentinel. + env.ExpectUpdateComponent(ctx, sentinel) + + engineCtx := env.EngineContext() + _, err := h.TestMigrateToWorkflow(engineCtx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + + require.Error(t, err) + require.ErrorIs(t, err, scheduler.ErrSentinelBlocked) + var unavailableErr *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailableErr) +} diff --git a/chasm/lib/scheduler/helper_test.go b/chasm/lib/scheduler/helper_test.go index 24ce5aea80d..f28ff945cec 100644 --- a/chasm/lib/scheduler/helper_test.go +++ b/chasm/lib/scheduler/helper_test.go @@ -2,6 +2,7 @@ package scheduler_test import ( "context" + "reflect" "testing" "time" @@ -20,6 +21,7 @@ import ( legacyscheduler "go.temporal.io/server/service/worker/scheduler" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" ) const ( @@ -79,7 +81,7 @@ func defaultConfig() *scheduler.Config { func newTestLibrary(logger log.Logger, specProcessor scheduler.SpecProcessor) *scheduler.Library { config := defaultConfig() specBuilder := legacyscheduler.NewSpecBuilder() - invokerOpts := scheduler.InvokerTaskExecutorOptions{ + invokerOpts := scheduler.InvokerTaskHandlerOptions{ Config: config, MetricsHandler: metrics.NoopMetricsHandler, BaseLogger: logger, @@ -87,27 +89,233 @@ func newTestLibrary(logger log.Logger, specProcessor scheduler.SpecProcessor) *s } return scheduler.NewLibrary( nil, - scheduler.NewSchedulerIdleTaskExecutor(scheduler.SchedulerIdleTaskExecutorOptions{ + scheduler.NewSchedulerIdleTaskHandler(scheduler.SchedulerIdleTaskHandlerOptions{ Config: config, }), - scheduler.NewGeneratorTaskExecutor(scheduler.GeneratorTaskExecutorOptions{ + scheduler.NewSchedulerCallbacksTaskHandler(scheduler.SchedulerCallbacksTaskHandlerOptions{ + Config: config, + }), + scheduler.NewGeneratorTaskHandler(scheduler.GeneratorTaskHandlerOptions{ Config: config, MetricsHandler: metrics.NoopMetricsHandler, BaseLogger: logger, SpecProcessor: specProcessor, SpecBuilder: specBuilder, }), - scheduler.NewInvokerExecuteTaskExecutor(invokerOpts), - scheduler.NewInvokerProcessBufferTaskExecutor(invokerOpts), - scheduler.NewBackfillerTaskExecutor(scheduler.BackfillerTaskExecutorOptions{ + scheduler.NewInvokerExecuteTaskHandler(invokerOpts), + scheduler.NewInvokerProcessBufferTaskHandler(invokerOpts), + scheduler.NewBackfillerTaskHandler(scheduler.BackfillerTaskHandlerOptions{ Config: config, MetricsHandler: metrics.NoopMetricsHandler, BaseLogger: logger, SpecProcessor: specProcessor, }), + scheduler.NewSchedulerMigrateToWorkflowTaskHandler(scheduler.SchedulerMigrateToWorkflowTaskHandlerOptions{ + Config: config, + MetricsHandler: metrics.NoopMetricsHandler, + BaseLogger: logger, + }), + ) +} + +// testEnv holds all components needed for scheduler tests. +type testEnv struct { + t *testing.T // only used within these setup helpers + Ctrl *gomock.Controller + Registry *chasm.Registry + Node *chasm.Node + NodeBackend *chasm.MockNodeBackend + TimeSource *clock.EventTimeSource + Scheduler *scheduler.Scheduler + SpecProcessor scheduler.SpecProcessor + MockEngine *chasm.MockEngine + Logger log.Logger +} + +// testEnvConfig holds configuration options for testEnv. +type testEnvConfig struct { + specProcessor scheduler.SpecProcessor + withMockEngine bool +} + +// testEnvOption is a functional option for configuring testEnv. +type testEnvOption func(*testEnvConfig) + +// withSpecProcessor configures testEnv with a custom SpecProcessor. +// By default, testEnv uses a real SpecProcessor. Use this option only +// when you need to mock specific SpecProcessor behavior (e.g., simulating failures). +func withSpecProcessor(sp scheduler.SpecProcessor) testEnvOption { + return func(c *testEnvConfig) { + c.specProcessor = sp + } +} + +// withMockEngine configures testEnv to include a mock CHASM engine for side-effect tasks. +func withMockEngine() testEnvOption { + return func(c *testEnvConfig) { + c.withMockEngine = true + } +} + +// newRealSpecProcessor creates a real SpecProcessor for tests. +func newRealSpecProcessor(ctrl *gomock.Controller, logger log.Logger) scheduler.SpecProcessor { + mockMetrics := metrics.NewMockHandler(ctrl) + mockMetrics.EXPECT().Counter(gomock.Any()).Return(metrics.NoopCounterMetricFunc).AnyTimes() + mockMetrics.EXPECT().WithTags(gomock.Any()).Return(mockMetrics).AnyTimes() + mockMetrics.EXPECT().Timer(gomock.Any()).Return(metrics.NoopTimerMetricFunc).AnyTimes() + + return scheduler.NewSpecProcessor( + defaultConfig(), + mockMetrics, + logger, + legacyscheduler.NewSpecBuilder(), ) } +// newTestEnv creates a new test environment with the given options. +func newTestEnv(t *testing.T, opts ...testEnvOption) *testEnv { + config := &testEnvConfig{} + for _, opt := range opts { + opt(config) + } + + ctrl := gomock.NewController(t) + logger := testlogger.NewTestLogger(t, testlogger.FailOnExpectedErrorOnly) + nodePathEncoder := chasm.DefaultPathEncoder + + // Configure spec processor: use custom if provided, otherwise use real. + var specProcessor scheduler.SpecProcessor + if config.specProcessor != nil { + specProcessor = config.specProcessor + } else { + specProcessor = newRealSpecProcessor(ctrl, logger) + } + + registry := chasm.NewRegistry(logger) + if err := registry.Register(&chasm.CoreLibrary{}); err != nil { + t.Fatalf("failed to register core library: %v", err) + } + if err := registry.Register(newTestLibrary(logger, specProcessor)); err != nil { + t.Fatalf("failed to register scheduler library: %v", err) + } + + timeSource := clock.NewEventTimeSource() + now := time.Now() + timeSource.Update(now) + + tv := testvars.New(t) + nodeBackend := &chasm.MockNodeBackend{ + HandleNextTransitionCount: func() int64 { return 2 }, + HandleGetCurrentVersion: func() int64 { return 1 }, + HandleGetWorkflowKey: tv.Any().WorkflowKey, + HandleIsWorkflow: func() bool { return false }, + HandleCurrentVersionedTransition: func() *persistencespb.VersionedTransition { + return &persistencespb.VersionedTransition{ + NamespaceFailoverVersion: 1, + TransitionCount: 1, + } + }, + } + + node := chasm.NewEmptyTree(registry, timeSource, nodeBackend, nodePathEncoder, logger, metrics.NoopMetricsHandler) + ctx := chasm.NewMutableContext(context.Background(), node) + sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) + if err != nil { + t.Fatalf("failed to create scheduler: %v", err) + } + if err = node.SetRootComponent(sched); err != nil { + t.Fatalf("failed to set root component: %v", err) + } + + // Advance Generator's high water mark to 'now'. + generator := sched.Generator.Get(ctx) + generator.LastProcessedTime = timestamppb.New(now) + + _, err = node.CloseTransaction() + if err != nil { + t.Fatalf("failed to close initial transaction: %v", err) + } + + env := &testEnv{ + t: t, + Ctrl: ctrl, + Registry: registry, + Node: node, + NodeBackend: nodeBackend, + TimeSource: timeSource, + Scheduler: sched, + SpecProcessor: specProcessor, + Logger: logger, + } + + if config.withMockEngine { + env.MockEngine = chasm.NewMockEngine(ctrl) + } + + return env +} + +// MutableContext returns a new mutable CHASM context. +func (e *testEnv) MutableContext() chasm.MutableContext { + return chasm.NewMutableContext(context.Background(), e.Node) +} + +// ReadContext returns a new read-only CHASM context. +func (e *testEnv) ReadContext() chasm.Context { + return chasm.NewContext(context.Background(), e.Node) +} + +// CloseTransaction closes the current CHASM transaction. +func (e *testEnv) CloseTransaction() error { + _, err := e.Node.CloseTransaction() + return err +} + +// HasTask returns true if the given task type was added with the given visibilityTime. +func (e *testEnv) HasTask(task any, visibilityTime time.Time) bool { + taskType := reflect.TypeOf(task) + for _, tasks := range e.NodeBackend.TasksByCategory { + for _, t := range tasks { + if reflect.TypeOf(t) == taskType && + t.GetVisibilityTime().Equal(visibilityTime) { + return true + } + } + } + return false +} + +// EngineContext returns a context with a mock engine. Requires withMockEngine(). +func (e *testEnv) EngineContext() context.Context { + if e.MockEngine == nil { + e.t.Fatal("EngineContext requires withMockEngine() option") + } + return chasm.NewEngineContext(context.Background(), e.MockEngine) +} + +// ExpectReadComponent sets up mock expectations for reading a component. +func (e *testEnv) ExpectReadComponent(ctx chasm.Context, returnedComponent chasm.Component) { + if e.MockEngine == nil { + e.t.Fatal("ExpectReadComponent requires withMockEngine() option") + } + e.MockEngine.EXPECT().ReadComponent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ chasm.ComponentRef, readFn func(chasm.Context, chasm.Component) error, _ ...chasm.TransitionOption) error { + return readFn(ctx, returnedComponent) + }).Times(1) +} + +// ExpectUpdateComponent sets up mock expectations for updating a component. +func (e *testEnv) ExpectUpdateComponent(ctx chasm.MutableContext, componentToUpdate chasm.Component) { + if e.MockEngine == nil { + e.t.Fatal("ExpectUpdateComponent requires withMockEngine() option") + } + e.MockEngine.EXPECT().UpdateComponent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ chasm.ComponentRef, updateFn func(chasm.MutableContext, chasm.Component) error, _ ...chasm.TransitionOption) ([]byte, error) { + err := updateFn(ctx, componentToUpdate) + return nil, err + }).Times(1) +} + type testInfra struct { node *chasm.Node nodeBackend *chasm.MockNodeBackend @@ -145,7 +353,7 @@ func setupTestInfra(t *testing.T, specProcessor scheduler.SpecProcessor) *testIn } } - node := chasm.NewEmptyTree(registry, timeSource, nodeBackend, nodePathEncoder, logger) + node := chasm.NewEmptyTree(registry, timeSource, nodeBackend, nodePathEncoder, logger, metrics.NoopMetricsHandler) return &testInfra{ node: node, nodeBackend: nodeBackend, @@ -173,7 +381,10 @@ func setupSchedulerForTest(t *testing.T) (*scheduler.Scheduler, chasm.MutableCon if err != nil { t.Fatalf("failed to create scheduler: %v", err) } - infra.node.SetRootComponent(sched) + err = infra.node.SetRootComponent(sched) + if err != nil { + t.Fatalf("failed to set root component: %v", err) + } _, err = infra.node.CloseTransaction() if err != nil { t.Fatalf("failed to close initial transaction: %v", err) @@ -190,8 +401,11 @@ func setupSentinelForTest(t *testing.T) (*scheduler.Scheduler, chasm.MutableCont infra := setupTestInfra(t, specProcessor) ctx := chasm.NewMutableContext(context.Background(), infra.node) sentinel := scheduler.NewSentinel(ctx, namespace, namespaceID, scheduleID) - infra.node.SetRootComponent(sentinel) - _, err := infra.node.CloseTransaction() + err := infra.node.SetRootComponent(sentinel) + if err != nil { + t.Fatalf("failed to set root component: %v", err) + } + _, err = infra.node.CloseTransaction() if err != nil { t.Fatalf("failed to close initial transaction: %v", err) } diff --git a/chasm/lib/scheduler/invoker.go b/chasm/lib/scheduler/invoker.go index b37e2cc49dd..74280ecc693 100644 --- a/chasm/lib/scheduler/invoker.go +++ b/chasm/lib/scheduler/invoker.go @@ -27,14 +27,20 @@ func (i *Invoker) LifecycleState(ctx chasm.Context) chasm.LifecycleState { return chasm.LifecycleStateRunning } -// NewInvoker returns an intialized Invoker component, which should +// NewInvoker returns an initialized Invoker component, which should // be parented under a Scheduler root component. func NewInvoker(ctx chasm.MutableContext) *Invoker { - return &Invoker{ - InvokerState: &schedulerpb.InvokerState{ - BufferedStarts: []*schedulespb.BufferedStart{}, - }, + return newInvokerWithState(ctx, &schedulerpb.InvokerState{ + BufferedStarts: []*schedulespb.BufferedStart{}, + }) +} + +func newInvokerWithState(ctx chasm.MutableContext, state *schedulerpb.InvokerState) *Invoker { + i := &Invoker{ + InvokerState: state, } + i.addTasks(ctx) + return i } // EnqueueBufferedStarts adds new BufferedStarts to the invocation queue, diff --git a/chasm/lib/scheduler/invoker_execute_task_test.go b/chasm/lib/scheduler/invoker_execute_task_test.go index 6a68831db9c..34732f6b5a3 100644 --- a/chasm/lib/scheduler/invoker_execute_task_test.go +++ b/chasm/lib/scheduler/invoker_execute_task_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" @@ -20,32 +20,35 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -type invokerExecuteTaskSuite struct { - schedulerSuite - executor *scheduler.InvokerExecuteTaskExecutor - +// invokerExecuteTestEnv extends testEnv with mock clients for invoker execute tests. +type invokerExecuteTestEnv struct { + *testEnv + handler *scheduler.InvokerExecuteTaskHandler mockFrontendClient *workflowservicemock.MockWorkflowServiceClient mockHistoryClient *historyservicemock.MockHistoryServiceClient } -func TestInvokerExecuteTaskSuite(t *testing.T) { - suite.Run(t, &invokerExecuteTaskSuite{}) -} - -func (s *invokerExecuteTaskSuite) SetupTest() { - s.schedulerSuite.SetupTest() +func newInvokerExecuteTestEnv(t *testing.T) *invokerExecuteTestEnv { + env := newTestEnv(t, withMockEngine()) - s.mockFrontendClient = workflowservicemock.NewMockWorkflowServiceClient(s.controller) - s.mockHistoryClient = historyservicemock.NewMockHistoryServiceClient(s.controller) + mockFrontendClient := workflowservicemock.NewMockWorkflowServiceClient(env.Ctrl) + mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(env.Ctrl) - s.executor = scheduler.NewInvokerExecuteTaskExecutor(scheduler.InvokerTaskExecutorOptions{ + handler := scheduler.NewInvokerExecuteTaskHandler(scheduler.InvokerTaskHandlerOptions{ Config: defaultConfig(), MetricsHandler: metrics.NoopMetricsHandler, - BaseLogger: s.logger, - SpecProcessor: s.specProcessor, - HistoryClient: s.mockHistoryClient, - FrontendClient: s.mockFrontendClient, + BaseLogger: env.Logger, + SpecProcessor: env.SpecProcessor, + HistoryClient: mockHistoryClient, + FrontendClient: mockFrontendClient, }) + + return &invokerExecuteTestEnv{ + testEnv: env, + handler: handler, + mockFrontendClient: mockFrontendClient, + mockHistoryClient: mockHistoryClient, + } } type executeTestCase struct { @@ -62,12 +65,72 @@ type executeTestCase struct { ExpectedOverlapSkipped int64 ExpectedMissedCatchupWindow int64 - ValidateInvoker func(invoker *scheduler.Invoker) + ValidateInvoker func(t *testing.T, invoker *scheduler.Invoker, env *invokerExecuteTestEnv) +} + +func runExecuteTestCase(t *testing.T, env *invokerExecuteTestEnv, c *executeTestCase) { + ctx := env.MutableContext() + invoker := env.Scheduler.Invoker.Get(ctx) + + // Set up initial state. Note: InitialRunningWorkflows is now represented by + // BufferedStarts that have RunId set but no Completed field. + invoker.BufferedStarts = c.InitialBufferedStarts + invoker.CancelWorkflows = c.InitialCancelWorkflows + invoker.TerminateWorkflows = c.InitialTerminateWorkflows + + // Add initial running workflows as BufferedStarts with RunId set. + for _, wf := range c.InitialRunningWorkflows { + invoker.BufferedStarts = append(invoker.BufferedStarts, &schedulespb.BufferedStart{ + RequestId: wf.WorkflowId + "-req", + WorkflowId: wf.WorkflowId, + RunId: wf.RunId, + Attempt: 1, + }) + } + + // Set LastProcessedTime to current time to ensure time checks pass. + invoker.LastProcessedTime = timestamppb.New(env.TimeSource.Now()) + + // Set expectations. The read and update calls will also update the Scheduler + // component, within the same transition. + env.ExpectReadComponent(ctx, invoker) + env.ExpectUpdateComponent(ctx, invoker) + + // Create engine context for side effect task execution. + engineCtx := env.EngineContext() + err := env.handler.Execute(engineCtx, chasm.ComponentRef{}, chasm.TaskAttributes{}, &schedulerpb.InvokerExecuteTask{}) + require.NoError(t, err) + require.NoError(t, env.CloseTransaction()) + + // Validate the results. + // BufferedStarts now includes both pending and running starts (they're kept after starting). + require.Len(t, invoker.GetBufferedStarts(), c.ExpectedBufferedStarts) + + // Count running workflows from BufferedStarts (has RunId but no Completed). + runningCount := 0 + for _, start := range invoker.GetBufferedStarts() { + if start.GetRunId() != "" && start.GetCompleted() == nil { + runningCount++ + } + } + require.Equal(t, c.ExpectedRunningWorkflows, runningCount) + + require.Len(t, invoker.TerminateWorkflows, c.ExpectedTerminateWorkflows) + require.Len(t, invoker.CancelWorkflows, c.ExpectedCancelWorkflows) + require.Equal(t, c.ExpectedActionCount, env.Scheduler.Info.ActionCount) + require.Equal(t, c.ExpectedOverlapSkipped, env.Scheduler.Info.OverlapSkipped) + require.Equal(t, c.ExpectedMissedCatchupWindow, env.Scheduler.Info.MissedCatchupWindow) + + // Callbacks. + if c.ValidateInvoker != nil { + c.ValidateInvoker(t, invoker, env) + } } // Execute success case. -func (s *invokerExecuteTaskSuite) TestExecuteTask_Basic() { - startTime := timestamppb.New(s.timeSource.Now()) +func TestExecuteTask_Basic(t *testing.T) { + env := newInvokerExecuteTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -90,7 +153,7 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_Basic() { } // Expect both buffered starts to result in workflow executions. - s.mockFrontendClient.EXPECT(). + env.mockFrontendClient.EXPECT(). StartWorkflowExecution(gomock.Any(), gomock.Any()). Times(2). Return(&workflowservice.StartWorkflowExecutionResponse{ @@ -99,7 +162,7 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_Basic() { // After execution, both BufferedStarts are kept (with RunId set). // They become "running" workflows. - s.runExecuteTestCase(&executeTestCase{ + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 2, // kept after starting ExpectedRunningWorkflows: 2, @@ -108,17 +171,20 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_Basic() { } // Execute is scheduled with an empty buffer. -func (s *invokerExecuteTaskSuite) TestExecuteTask_Empty() { - s.runExecuteTestCase(&executeTestCase{ +func TestExecuteTask_Empty(t *testing.T) { + env := newInvokerExecuteTestEnv(t) + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: nil, }) } // A buffered start fails with a retryable error. -func (s *invokerExecuteTaskSuite) TestExecuteTask_RetryableFailure() { +func TestExecuteTask_RetryableFailure(t *testing.T) { + env := newInvokerExecuteTestEnv(t) + // Set up the Invoker's buffer with a two starts. One will succeed immediately, // one will fail. - startTime := timestamppb.New(s.timeSource.Now()) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -141,11 +207,11 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_RetryableFailure() { } // Fail the first start, and succeed the second. - s.mockFrontendClient.EXPECT(). + env.mockFrontendClient.EXPECT(). StartWorkflowExecution(gomock.Any(), startWorkflowExecutionRequestIDMatches("fail")). Times(1). Return(nil, serviceerror.NewDeadlineExceeded("deadline exceeded")) - s.mockFrontendClient.EXPECT(). + env.mockFrontendClient.EXPECT(). StartWorkflowExecution(gomock.Any(), startWorkflowExecutionRequestIDMatches("pass")). Times(1). Return(&workflowservice.StartWorkflowExecutionResponse{ @@ -155,29 +221,30 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_RetryableFailure() { // After execution: // - Failed start stays in buffer with backoff (pending) // - Successful start stays in buffer with RunId set (running) - s.runExecuteTestCase(&executeTestCase{ + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 2, // both kept: 1 failed (backoff) + 1 running ExpectedRunningWorkflows: 1, ExpectedActionCount: 1, - ValidateInvoker: func(invoker *scheduler.Invoker) { - // Find the failed start (no RunId, has backoff) + ValidateInvoker: func(t *testing.T, invoker *scheduler.Invoker, env *invokerExecuteTestEnv) { + // Find the failed start (no RunId, has backoff). for _, start := range invoker.BufferedStarts { if start.GetRunId() == "" { backoffTime := start.BackoffTime.AsTime() - s.True(backoffTime.After(s.timeSource.Now())) - s.Equal(int64(2), start.Attempt) + require.True(t, backoffTime.After(env.TimeSource.Now())) + require.Equal(t, int64(2), start.Attempt) return } } - s.Fail("expected to find failed start with backoff") + require.Fail(t, "expected to find failed start with backoff") }, }) } // A buffered start fails when a duplicate workflow has already been started. -func (s *invokerExecuteTaskSuite) TestExecuteTask_AlreadyStarted() { - startTime := timestamppb.New(s.timeSource.Now()) +func TestExecuteTask_AlreadyStarted(t *testing.T) { + env := newInvokerExecuteTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -191,12 +258,12 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_AlreadyStarted() { } // Fail with WorkflowExecutionAlreadyStarted. - s.mockFrontendClient.EXPECT(). + env.mockFrontendClient.EXPECT(). StartWorkflowExecution(gomock.Any(), gomock.Any()). Times(1). Return(nil, serviceerror.NewWorkflowExecutionAlreadyStarted("workflow already started", "", "")) - s.runExecuteTestCase(&executeTestCase{ + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 0, ExpectedRunningWorkflows: 0, @@ -205,8 +272,9 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_AlreadyStarted() { } // A buffered start fails from having exceeded its maximum retry limit. -func (s *invokerExecuteTaskSuite) TestExecuteTask_ExceedsMaxAttempts() { - startTime := timestamppb.New(s.timeSource.Now()) +func TestExecuteTask_ExceedsMaxAttempts(t *testing.T) { + env := newInvokerExecuteTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -219,7 +287,7 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_ExceedsMaxAttempts() { }, } - s.runExecuteTestCase(&executeTestCase{ + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 0, ExpectedRunningWorkflows: 0, @@ -228,7 +296,8 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_ExceedsMaxAttempts() { } // An execute task runs with cancels/terminations queued, which fail to execute. -func (s *invokerExecuteTaskSuite) TestExecuteTask_CancelTerminateFailure() { +func TestExecuteTask_CancelTerminateFailure(t *testing.T) { + env := newInvokerExecuteTestEnv(t) cancelWorkflows := []*commonpb.WorkflowExecution{ { WorkflowId: "wf", @@ -243,14 +312,14 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_CancelTerminateFailure() { } // Fail both service calls. - s.mockHistoryClient.EXPECT().RequestCancelWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). + env.mockHistoryClient.EXPECT().RequestCancelWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). Return(nil, serviceerror.NewInternal("internal failure")) - s.mockHistoryClient.EXPECT().TerminateWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). + env.mockHistoryClient.EXPECT().TerminateWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). Return(nil, serviceerror.NewInternal("internal failure")) // Terminate and Cancel are both attempted only once. Regardless of the service // call's outcome, they should have been removed from the Invoker's queue. - s.runExecuteTestCase(&executeTestCase{ + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: nil, InitialCancelWorkflows: cancelWorkflows, InitialTerminateWorkflows: terminateWorkflows, @@ -263,7 +332,8 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_CancelTerminateFailure() { } // An Execute task runs with cancels/terminations queued, resulting in success. -func (s *invokerExecuteTaskSuite) TestExecuteTask_CancelTerminateSucceed() { +func TestExecuteTask_CancelTerminateSucceed(t *testing.T) { + env := newInvokerExecuteTestEnv(t) cancelWorkflows := []*commonpb.WorkflowExecution{ { WorkflowId: "wf", @@ -278,12 +348,12 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_CancelTerminateSucceed() { } // Succeed both service calls. - s.mockHistoryClient.EXPECT().RequestCancelWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). + env.mockHistoryClient.EXPECT().RequestCancelWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). Return(nil, nil) - s.mockHistoryClient.EXPECT().TerminateWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). + env.mockHistoryClient.EXPECT().TerminateWorkflowExecution(gomock.Any(), gomock.Any()).Times(1). Return(nil, nil) - s.runExecuteTestCase(&executeTestCase{ + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: nil, InitialCancelWorkflows: cancelWorkflows, InitialTerminateWorkflows: terminateWorkflows, @@ -297,8 +367,9 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_CancelTerminateSucceed() { // Tests when the ExecuteTask should yield by completing and committing any // completed work. -func (s *invokerExecuteTaskSuite) TestExecuteTask_ExceedsMaxActionsPerExecution() { - startTime := timestamppb.New(s.timeSource.Now()) +func TestExecuteTask_ExceedsMaxActionsPerExecution(t *testing.T) { + env := newInvokerExecuteTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) var bufferedStarts []*schedulespb.BufferedStart maxStarts := scheduler.DefaultTweakables.MaxActionsPerExecution for i := range maxStarts * 2 { @@ -316,7 +387,7 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_ExceedsMaxActionsPerExecution( // Expect up to the maximum buffered start limit to result in workflow // executions. - s.mockFrontendClient.EXPECT(). + env.mockFrontendClient.EXPECT(). StartWorkflowExecution(gomock.Any(), gomock.Any()). Times(maxStarts). Return(&workflowservice.StartWorkflowExecutionResponse{ @@ -324,7 +395,7 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_ExceedsMaxActionsPerExecution( }, nil) // All BufferedStarts are kept: maxStarts get RunId set (running), the rest stay pending. - s.runExecuteTestCase(&executeTestCase{ + runExecuteTestCase(t, env, &executeTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: maxStarts * 2, // all kept: started + pending ExpectedRunningWorkflows: maxStarts, // only started ones @@ -332,66 +403,6 @@ func (s *invokerExecuteTaskSuite) TestExecuteTask_ExceedsMaxActionsPerExecution( }) } -func (s *invokerExecuteTaskSuite) runExecuteTestCase(c *executeTestCase) { - ctx := s.newMutableContext() - invoker := s.scheduler.Invoker.Get(ctx) - - // Set up initial state. Note: InitialRunningWorkflows is now represented by - // BufferedStarts that have RunId set but no Completed field. - invoker.BufferedStarts = c.InitialBufferedStarts - invoker.CancelWorkflows = c.InitialCancelWorkflows - invoker.TerminateWorkflows = c.InitialTerminateWorkflows - - // Add initial running workflows as BufferedStarts with RunId set - for _, wf := range c.InitialRunningWorkflows { - invoker.BufferedStarts = append(invoker.BufferedStarts, &schedulespb.BufferedStart{ - RequestId: wf.WorkflowId + "-req", - WorkflowId: wf.WorkflowId, - RunId: wf.RunId, - Attempt: 1, - }) - } - - // Set LastProcessedTime to current time to ensure time checks pass - invoker.LastProcessedTime = timestamppb.New(s.timeSource.Now()) - - // Set expectations. The read and update calls will also update the Scheduler - // component, within the same transition. - s.ExpectReadComponent(ctx, invoker) - s.ExpectUpdateComponent(ctx, invoker) - - // Create engine context for side effect task execution - engineCtx := s.newEngineContext() - err := s.executor.Execute(engineCtx, chasm.ComponentRef{}, chasm.TaskAttributes{}, &schedulerpb.InvokerExecuteTask{}) - s.NoError(err) - _, err = s.node.CloseTransaction() - s.NoError(err) - - // Validate the results. - // BufferedStarts now includes both pending and running starts (they're kept after starting). - s.Equal(c.ExpectedBufferedStarts, len(invoker.GetBufferedStarts())) - - // Count running workflows from BufferedStarts (has RunId but no Completed) - runningCount := 0 - for _, start := range invoker.GetBufferedStarts() { - if start.GetRunId() != "" && start.GetCompleted() == nil { - runningCount++ - } - } - s.Equal(c.ExpectedRunningWorkflows, runningCount) - - s.Equal(c.ExpectedTerminateWorkflows, len(invoker.TerminateWorkflows)) - s.Equal(c.ExpectedCancelWorkflows, len(invoker.CancelWorkflows)) - s.Equal(c.ExpectedActionCount, s.scheduler.Info.ActionCount) - s.Equal(c.ExpectedOverlapSkipped, s.scheduler.Info.OverlapSkipped) - s.Equal(c.ExpectedMissedCatchupWindow, s.scheduler.Info.MissedCatchupWindow) - - // Callbacks. - if c.ValidateInvoker != nil { - c.ValidateInvoker(invoker) - } -} - type startWorkflowExecutionRequestIDMatcher struct { RequestID string } diff --git a/chasm/lib/scheduler/invoker_process_buffer_task_test.go b/chasm/lib/scheduler/invoker_process_buffer_task_test.go index b176c3ccbeb..9178a44667d 100644 --- a/chasm/lib/scheduler/invoker_process_buffer_task_test.go +++ b/chasm/lib/scheduler/invoker_process_buffer_task_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" schedulespb "go.temporal.io/server/api/schedule/v1" @@ -16,22 +16,12 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -type invokerProcessBufferTaskSuite struct { - schedulerSuite - executor *scheduler.InvokerProcessBufferTaskExecutor -} - -func TestInvokerProcessBufferTaskSuite(t *testing.T) { - suite.Run(t, &invokerProcessBufferTaskSuite{}) -} - -func (s *invokerProcessBufferTaskSuite) SetupTest() { - s.schedulerSuite.SetupTest() - s.executor = scheduler.NewInvokerProcessBufferTaskExecutor(scheduler.InvokerTaskExecutorOptions{ +func newProcessBufferHandler(env *testEnv) *scheduler.InvokerProcessBufferTaskHandler { + return scheduler.NewInvokerProcessBufferTaskHandler(scheduler.InvokerTaskHandlerOptions{ Config: defaultConfig(), MetricsHandler: metrics.NoopMetricsHandler, - BaseLogger: s.logger, - SpecProcessor: s.specProcessor, + BaseLogger: env.Logger, + SpecProcessor: env.SpecProcessor, }) } @@ -48,12 +38,65 @@ type processBufferTestCase struct { ExpectedOverlapSkipped int64 ExpectedMissedCatchupWindow int64 - ValidateInvoker func(invoker *scheduler.Invoker) + ValidateInvoker func(t *testing.T, invoker *scheduler.Invoker) +} + +func runProcessBufferTestCase(t *testing.T, env *testEnv, c *processBufferTestCase) { + ctx := env.MutableContext() + invoker := env.Scheduler.Invoker.Get(ctx) + + // Set up initial state. Note: InitialRunningWorkflows is now represented by + // BufferedStarts that have RunId set but no Completed field. + invoker.BufferedStarts = c.InitialBufferedStarts + invoker.CancelWorkflows = c.InitialCancelWorkflows + invoker.TerminateWorkflows = c.InitialTerminateWorkflows + + // Add initial running workflows as BufferedStarts with RunId set. + for _, wf := range c.InitialRunningWorkflows { + invoker.BufferedStarts = append(invoker.BufferedStarts, &schedulespb.BufferedStart{ + RequestId: wf.WorkflowId + "-req", + WorkflowId: wf.WorkflowId, + RunId: wf.RunId, + Attempt: 1, + }) + } + + // Set LastProcessedTime to current time to ensure time checks pass. + invoker.LastProcessedTime = timestamppb.New(env.TimeSource.Now()) + + handler := newProcessBufferHandler(env) + err := handler.Execute(ctx, invoker, chasm.TaskAttributes{}, &schedulerpb.InvokerProcessBufferTask{}) + require.NoError(t, err) + require.NoError(t, env.CloseTransaction()) + + // Validate the results. + // Count BufferedStarts (excluding running ones added from InitialRunningWorkflows). + require.Len(t, invoker.GetBufferedStarts(), c.ExpectedBufferedStarts+len(c.InitialRunningWorkflows)) + + // Count running workflows from BufferedStarts (has RunId but no Completed). + runningCount := 0 + for _, start := range invoker.GetBufferedStarts() { + if start.GetRunId() != "" && start.GetCompleted() == nil { + runningCount++ + } + } + require.Equal(t, c.ExpectedRunningWorkflows, runningCount) + + require.Len(t, invoker.TerminateWorkflows, c.ExpectedTerminateWorkflows) + require.Len(t, invoker.CancelWorkflows, c.ExpectedCancelWorkflows) + require.Equal(t, c.ExpectedOverlapSkipped, env.Scheduler.Info.OverlapSkipped) + require.Equal(t, c.ExpectedMissedCatchupWindow, env.Scheduler.Info.MissedCatchupWindow) + + // Callbacks. + if c.ValidateInvoker != nil { + c.ValidateInvoker(t, invoker) + } } // ProcessBuffer attempts all buffered starts with ALLOW_ALL policy. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_AllowAll() { - startTime := timestamppb.New(s.timeSource.Now()) +func TestProcessBufferTask_AllowAll(t *testing.T) { + env := newTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -81,21 +124,22 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_AllowAll() { }, } - s.runProcessBufferTestCase(&processBufferTestCase{ + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 3, ExpectedOverlapSkipped: 0, - ValidateInvoker: func(invoker *scheduler.Invoker) { - s.Equal(3, len(util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool { + ValidateInvoker: func(t *testing.T, invoker *scheduler.Invoker) { + require.Len(t, util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool { return start.Attempt > 0 - }))) + }), 3) }, }) } // ProcessBuffer processes a start that missed the catchup window. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_MissedCatchupWindow() { - now := s.timeSource.Now() +func TestProcessBufferTask_MissedCatchupWindow(t *testing.T) { + env := newTestEnv(t) + now := env.TimeSource.Now() startTime := now.Add(-defaultCatchupWindow * 2) startTimestamp := timestamppb.New(startTime) bufferedStarts := []*schedulespb.BufferedStart{ @@ -109,7 +153,7 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_MissedCatchupWindo }, } - s.runProcessBufferTestCase(&processBufferTestCase{ + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 0, ExpectedOverlapSkipped: 0, @@ -118,8 +162,9 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_MissedCatchupWindo } // ProcessBuffer defers a start (from overlap policy) by placing it into NewBuffer. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BufferOne() { - startTime := timestamppb.New(s.timeSource.Now()) +func TestProcessBufferTask_BufferOne(t *testing.T) { + env := newTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -147,32 +192,34 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BufferOne() { }, } - s.runProcessBufferTestCase(&processBufferTestCase{ + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: bufferedStarts, // Because no workflows are running, we'll immediately kick off one // BufferedStart, and then buffer the next. This leaves us with 1 ready start, // and 1 still buffered. ExpectedBufferedStarts: 2, ExpectedOverlapSkipped: 1, - ValidateInvoker: func(invoker *scheduler.Invoker) { - // Only one start should be set for execution (Attempt > 0) - s.Equal(1, len(util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool { + ValidateInvoker: func(t *testing.T, invoker *scheduler.Invoker) { + // Only one start should be set for execution (Attempt > 0). + require.Len(t, util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool { return start.Attempt > 0 - }))) + }), 1) }, }) } // ProcessBuffer is scheduled with an empty buffer. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_Empty() { - s.runProcessBufferTestCase(&processBufferTestCase{ +func TestProcessBufferTask_Empty(t *testing.T) { + env := newTestEnv(t) + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: nil, }) } // ProcessBuffer is scheduled with a buffer of starts all backing off. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BackingOff() { - startTime := timestamppb.New(s.timeSource.Now()) +func TestProcessBufferTask_BackingOff(t *testing.T) { + env := newTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) backoffTime := startTime.AsTime().Add(30 * time.Minute) bufferedStarts := []*schedulespb.BufferedStart{ { @@ -197,16 +244,17 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BackingOff() { }, } - s.runProcessBufferTestCase(&processBufferTestCase{ + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 2, }) } // ProcessBuffer is scheduled with a start that was backing off, but ready to retry. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BackingOffReady() { - startTime := timestamppb.New(s.timeSource.Now()) - backoffTime := s.timeSource.Now().Add(-1 * time.Minute) +func TestProcessBufferTask_BackingOffReady(t *testing.T) { + env := newTestEnv(t) + startTime := timestamppb.New(env.TimeSource.Now()) + backoffTime := env.TimeSource.Now().Add(-1 * time.Minute) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -220,20 +268,22 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_BackingOffReady() }, } - s.runProcessBufferTestCase(&processBufferTestCase{ + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: bufferedStarts, ExpectedBufferedStarts: 1, - ValidateInvoker: func(invoker *scheduler.Invoker) { - // The start should be ready for execution (Attempt > 0) - s.Equal(1, len(util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool { + ValidateInvoker: func(t *testing.T, invoker *scheduler.Invoker) { + // The start should be ready for execution (Attempt > 0). + require.Len(t, util.FilterSlice(invoker.GetBufferedStarts(), func(start *schedulespb.BufferedStart) bool { return start.Attempt > 0 - }))) + }), 1) }, }) } // A buffered start with an overlap policy to terminate other workflows is processed. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsTerminate() { +func TestProcessBufferTask_NeedsTerminate(t *testing.T) { + env := newTestEnv(t) + // Add a running workflow to the Scheduler. initialRunningWorkflows := []*commonpb.WorkflowExecution{{ WorkflowId: "existing-wf", @@ -241,7 +291,7 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsTerminate() { }} // Set up the BufferedStart with a policy that will terminate existing workflows. - startTime := timestamppb.New(s.timeSource.Now()) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -253,7 +303,7 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsTerminate() { }, } - s.runProcessBufferTestCase(&processBufferTestCase{ + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: bufferedStarts, InitialRunningWorkflows: initialRunningWorkflows, // Buffer should still contain the buffered start. The existing workflow will still @@ -266,7 +316,9 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsTerminate() { } // A buffered start with an overlap policy to cancel other workflows is processed. -func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsCancel() { +func TestProcessBufferTask_NeedsCancel(t *testing.T) { + env := newTestEnv(t) + // Add a running workflow to the Scheduler. initialRunningWorkflows := []*commonpb.WorkflowExecution{{ WorkflowId: "existing-wf", @@ -274,7 +326,7 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsCancel() { }} // Set up the BufferedStart with a policy that will cancel existing workflows. - startTime := timestamppb.New(s.timeSource.Now()) + startTime := timestamppb.New(env.TimeSource.Now()) bufferedStarts := []*schedulespb.BufferedStart{ { NominalTime: startTime, @@ -286,7 +338,7 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsCancel() { }, } - s.runProcessBufferTestCase(&processBufferTestCase{ + runProcessBufferTestCase(t, env, &processBufferTestCase{ InitialBufferedStarts: bufferedStarts, InitialRunningWorkflows: initialRunningWorkflows, // Buffer should still contain the buffered start. The existing workflow will still @@ -297,55 +349,3 @@ func (s *invokerProcessBufferTaskSuite) TestProcessBufferTask_NeedsCancel() { ExpectedCancelWorkflows: 1, }) } - -func (s *invokerProcessBufferTaskSuite) runProcessBufferTestCase(c *processBufferTestCase) { - ctx := s.newMutableContext() - invoker := s.scheduler.Invoker.Get(ctx) - - // Set up initial state. Note: InitialRunningWorkflows is now represented by - // BufferedStarts that have RunId set but no Completed field. - invoker.BufferedStarts = c.InitialBufferedStarts - invoker.CancelWorkflows = c.InitialCancelWorkflows - invoker.TerminateWorkflows = c.InitialTerminateWorkflows - - // Add initial running workflows as BufferedStarts with RunId set - for _, wf := range c.InitialRunningWorkflows { - invoker.BufferedStarts = append(invoker.BufferedStarts, &schedulespb.BufferedStart{ - RequestId: wf.WorkflowId + "-req", - WorkflowId: wf.WorkflowId, - RunId: wf.RunId, - Attempt: 1, - }) - } - - // Set LastProcessedTime to current time to ensure time checks pass - invoker.LastProcessedTime = timestamppb.New(s.timeSource.Now()) - - err := s.executor.Execute(ctx, invoker, chasm.TaskAttributes{}, &schedulerpb.InvokerProcessBufferTask{}) - s.NoError(err) - _, err = s.node.CloseTransaction() - s.NoError(err) - - // Validate the results - // Count BufferedStarts (excluding running ones added from InitialRunningWorkflows) - s.Len(invoker.GetBufferedStarts(), c.ExpectedBufferedStarts+len(c.InitialRunningWorkflows)) - - // Count running workflows from BufferedStarts (has RunId but no Completed) - runningCount := 0 - for _, start := range invoker.GetBufferedStarts() { - if start.GetRunId() != "" && start.GetCompleted() == nil { - runningCount++ - } - } - s.Equal(c.ExpectedRunningWorkflows, runningCount) - - s.Equal(c.ExpectedTerminateWorkflows, len(invoker.TerminateWorkflows)) - s.Equal(c.ExpectedCancelWorkflows, len(invoker.CancelWorkflows)) - s.Equal(c.ExpectedOverlapSkipped, s.scheduler.Info.OverlapSkipped) - s.Equal(c.ExpectedMissedCatchupWindow, s.scheduler.Info.MissedCatchupWindow) - - // Callbacks - if c.ValidateInvoker != nil { - c.ValidateInvoker(invoker) - } -} diff --git a/chasm/lib/scheduler/invoker_tasks.go b/chasm/lib/scheduler/invoker_tasks.go index 94d6d05baa9..80162793594 100644 --- a/chasm/lib/scheduler/invoker_tasks.go +++ b/chasm/lib/scheduler/invoker_tasks.go @@ -29,7 +29,7 @@ import ( ) type ( - InvokerTaskExecutorOptions struct { + InvokerTaskHandlerOptions struct { fx.In Config *Config @@ -45,7 +45,8 @@ type ( FrontendClient workflowservice.WorkflowServiceClient } - InvokerExecuteTaskExecutor struct { + InvokerExecuteTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*schedulerpb.InvokerExecuteTask] config *Config metricsHandler metrics.Handler baseLogger log.Logger @@ -53,7 +54,8 @@ type ( frontendClient workflowservice.WorkflowServiceClient } - InvokerProcessBufferTaskExecutor struct { + InvokerProcessBufferTaskHandler struct { + chasm.PureTaskHandlerBase config *Config metricsHandler metrics.Handler baseLogger log.Logger @@ -62,7 +64,7 @@ type ( } // Per-task context. - invokerTaskExecutorContext struct { + invokerTaskHandlerContext struct { context.Context actionsTaken int @@ -79,10 +81,6 @@ const ( // Lower bound for the deadline in which buffered actions are dropped. startWorkflowMinDeadline = 5 * time.Second - // Because the catchup window doesn't apply to a manual start, pick a custom - // execution deadline before timing out a start. - manualStartExecutionDeadline = 1 * time.Hour - // Upper bound on how many times starting an individual buffered action should be retried. InvokerMaxStartAttempts = 10 // TODO - dial this up/remove it ) @@ -92,8 +90,8 @@ var ( _ error = &rateLimitedError{} ) -func NewInvokerExecuteTaskExecutor(opts InvokerTaskExecutorOptions) *InvokerExecuteTaskExecutor { - return &InvokerExecuteTaskExecutor{ +func NewInvokerExecuteTaskHandler(opts InvokerTaskHandlerOptions) *InvokerExecuteTaskHandler { + return &InvokerExecuteTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, baseLogger: opts.BaseLogger, @@ -102,8 +100,8 @@ func NewInvokerExecuteTaskExecutor(opts InvokerTaskExecutorOptions) *InvokerExec } } -func NewInvokerProcessBufferTaskExecutor(opts InvokerTaskExecutorOptions) *InvokerProcessBufferTaskExecutor { - return &InvokerProcessBufferTaskExecutor{ +func NewInvokerProcessBufferTaskHandler(opts InvokerTaskHandlerOptions) *InvokerProcessBufferTaskHandler { + return &InvokerProcessBufferTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, baseLogger: opts.BaseLogger, @@ -112,7 +110,7 @@ func NewInvokerProcessBufferTaskExecutor(opts InvokerTaskExecutorOptions) *Invok } } -func (e *InvokerExecuteTaskExecutor) Validate( +func (h *InvokerExecuteTaskHandler) Validate( _ chasm.Context, invoker *Invoker, _ chasm.TaskAttributes, @@ -127,7 +125,7 @@ func (e *InvokerExecuteTaskExecutor) Validate( return valid, nil } -func (e *InvokerExecuteTaskExecutor) Execute( +func (h *InvokerExecuteTaskHandler) Execute( ctx context.Context, invokerRef chasm.ComponentRef, _ chasm.TaskAttributes, @@ -175,8 +173,8 @@ func (e *InvokerExecuteTaskExecutor) Execute( return fmt.Errorf("failed to read component: %w", err) } - logger := newTaggedLogger(e.baseLogger, scheduler) - metricsHandler := newTaggedMetricsHandler(e.metricsHandler, scheduler) + logger := newTaggedLogger(h.baseLogger, scheduler) + metricsHandler := newTaggedMetricsHandler(h.metricsHandler, scheduler) // Terminate, cancel, and start workflows. The result struct contains the // complete outcome of all requests executed in a single batch. @@ -184,10 +182,10 @@ func (e *InvokerExecuteTaskExecutor) Execute( // Invoker will never have work pending for more than one of these calls (terminate, // cancel, start) at a time, so it isn't sensible to run them in parallel. The // structure below is simply for code simplicity. - ictx := e.newInvokerTaskExecutorContext(ctx, scheduler) - result = result.Append(e.terminateWorkflows(ictx, logger, metricsHandler, scheduler, invoker.GetTerminateWorkflows())) - result = result.Append(e.cancelWorkflows(ictx, logger, metricsHandler, scheduler, invoker.GetCancelWorkflows())) - sres, startResults := e.startWorkflows(ictx, logger, metricsHandler, scheduler, invoker, lastCompletionState, callback) + ictx := h.newInvokerTaskHandlerContext(ctx, scheduler) + result = result.Append(h.terminateWorkflows(ictx, logger, metricsHandler, scheduler, invoker.GetTerminateWorkflows())) + result = result.Append(h.cancelWorkflows(ictx, logger, metricsHandler, scheduler, invoker.GetCancelWorkflows())) + sres, startResults := h.startWorkflows(ictx, logger, metricsHandler, scheduler, invoker, lastCompletionState, callback) result = result.Append(sres) // Record action results on the Invoker (internal state), as well as the @@ -214,7 +212,7 @@ func (e *InvokerExecuteTaskExecutor) Execute( // takeNextAction increments the context's actionTaken counter, returning true if // the action should be executed, and false if the task should instead yield. -func (i *invokerTaskExecutorContext) takeNextAction() bool { +func (i *invokerTaskHandlerContext) takeNextAction() bool { allowed := i.actionsTaken < i.maxActions if allowed { i.actionsTaken++ @@ -223,8 +221,8 @@ func (i *invokerTaskExecutorContext) takeNextAction() bool { } // cancelWorkflows does a best-effort attempt to cancel all workflow executions provided in targets. -func (e *InvokerExecuteTaskExecutor) cancelWorkflows( - ctx invokerTaskExecutorContext, +func (h *InvokerExecuteTaskHandler) cancelWorkflows( + ctx invokerTaskHandlerContext, logger log.Logger, metricsHandler metrics.Handler, scheduler *Scheduler, @@ -241,7 +239,7 @@ func (e *InvokerExecuteTaskExecutor) cancelWorkflows( // Run all cancels concurrently. newCtx := ctx.Clone() wg.Go(func() { - err := e.cancelWorkflow(newCtx, scheduler, wf) + err := h.cancelWorkflow(newCtx, scheduler, wf) resultMutex.Lock() defer resultMutex.Unlock() @@ -261,8 +259,8 @@ func (e *InvokerExecuteTaskExecutor) cancelWorkflows( } // terminateWorkflows does a best-effort attempt to terminate all workflow executions provided in targets. -func (e *InvokerExecuteTaskExecutor) terminateWorkflows( - ctx invokerTaskExecutorContext, +func (h *InvokerExecuteTaskHandler) terminateWorkflows( + ctx invokerTaskHandlerContext, logger log.Logger, metricsHandler metrics.Handler, scheduler *Scheduler, @@ -279,7 +277,7 @@ func (e *InvokerExecuteTaskExecutor) terminateWorkflows( // Run all terminates concurrently. newCtx := ctx.Clone() wg.Go(func() { - err := e.terminateWorkflow(newCtx, scheduler, wf) + err := h.terminateWorkflow(newCtx, scheduler, wf) resultMutex.Lock() defer resultMutex.Unlock() @@ -299,8 +297,8 @@ func (e *InvokerExecuteTaskExecutor) terminateWorkflows( } // startWorkflows executes the provided list of starts, returning a result with their outcomes. -func (e *InvokerExecuteTaskExecutor) startWorkflows( - ctx invokerTaskExecutorContext, +func (h *InvokerExecuteTaskHandler) startWorkflows( + ctx invokerTaskHandlerContext, logger log.Logger, metricsHandler metrics.Handler, scheduler *Scheduler, @@ -336,7 +334,7 @@ func (e *InvokerExecuteTaskExecutor) startWorkflows( // Run all starts concurrently. newCtx := ctx.Clone() wg.Go(func() { - startResult, err := e.startWorkflow(newCtx, metricsHandler, scheduler, start, lastCompletionState, callback) + startResult, err := h.startWorkflow(newCtx, metricsHandler, scheduler, start, lastCompletionState, callback) resultMutex.Lock() defer resultMutex.Unlock() @@ -352,7 +350,7 @@ func (e *InvokerExecuteTaskExecutor) startWorkflows( if isRetryableError(err) { // Apply backoff to start and retry. - e.applyBackoff(start, err) + h.applyBackoff(start, err) result.RetryableStarts = append(result.RetryableStarts, start) } else { // Drop the start from the buffer. @@ -372,7 +370,7 @@ func (e *InvokerExecuteTaskExecutor) startWorkflows( return } -func (e *InvokerProcessBufferTaskExecutor) Validate( +func (h *InvokerProcessBufferTaskHandler) Validate( ctx chasm.Context, invoker *Invoker, attrs chasm.TaskAttributes, @@ -384,7 +382,7 @@ func (e *InvokerProcessBufferTaskExecutor) Validate( ) } -func (e *InvokerProcessBufferTaskExecutor) Execute( +func (h *InvokerProcessBufferTaskHandler) Execute( ctx chasm.MutableContext, invoker *Invoker, _ chasm.TaskAttributes, @@ -399,7 +397,7 @@ func (e *InvokerProcessBufferTaskExecutor) Execute( } // Compute actions to take from the current buffer. - result := e.processBuffer(ctx, invoker, scheduler) + result := h.processBuffer(ctx, invoker, scheduler) // Update Scheduler metadata. scheduler.recordActionResult(&schedulerActionResult{ @@ -416,7 +414,7 @@ func (e *InvokerProcessBufferTaskExecutor) Execute( // processBuffer resolves the Invoker's buffered starts that haven't yet begun // execution. This is where the decision is made to drive execution to // completion, or skip/drop a start. -func (e *InvokerProcessBufferTaskExecutor) processBuffer( +func (h *InvokerProcessBufferTaskHandler) processBuffer( ctx chasm.MutableContext, invoker *Invoker, scheduler *Scheduler, @@ -457,7 +455,7 @@ func (e *InvokerProcessBufferTaskExecutor) processBuffer( continue } - if ctx.Now(invoker).After(e.startWorkflowDeadline(ctx, scheduler, start)) { + if ctx.Now(invoker).After(h.startWorkflowDeadline(ctx, scheduler, start)) { // Drop expired starts. result.missedCatchupWindow++ result.discardStarts = append(result.discardStarts, start) @@ -485,7 +483,7 @@ func (e *InvokerProcessBufferTaskExecutor) processBuffer( } // applyBackoff updates start's BackoffTime based on err and the retry policy. -func (e *InvokerExecuteTaskExecutor) applyBackoff(start *schedulespb.BufferedStart, err error) { +func (h *InvokerExecuteTaskHandler) applyBackoff(start *schedulespb.BufferedStart, err error) { if err == nil { return } @@ -497,7 +495,7 @@ func (e *InvokerExecuteTaskExecutor) applyBackoff(start *schedulespb.BufferedSta } else { // Otherwise, use the backoff policy. Elapsed time is left at 0 because we bound // on number of attempts. - delay = e.config.RetryPolicy().ComputeNextDelay(0, int(start.Attempt), nil) + delay = h.config.RetryPolicy().ComputeNextDelay(0, int(start.Attempt), nil) } start.BackoffTime = timestamppb.New(time.Now().Add(delay)) @@ -506,7 +504,7 @@ func (e *InvokerExecuteTaskExecutor) applyBackoff(start *schedulespb.BufferedSta // startWorkflowDeadline returns the latest time at which a buffered workflow // should be started, instead of dropped. The deadline puts an upper bound on // the number of retry attempts per buffered start. -func (e *InvokerProcessBufferTaskExecutor) startWorkflowDeadline( +func (h *InvokerProcessBufferTaskHandler) startWorkflowDeadline( ctx chasm.Context, scheduler *Scheduler, start *schedulespb.BufferedStart, @@ -522,7 +520,7 @@ func (e *InvokerProcessBufferTaskExecutor) startWorkflowDeadline( // Set request deadline based on the schedule's catchup window, which is the // latest time that it's acceptable to start this workflow. - tweakables := e.config.Tweakables(scheduler.Namespace) + tweakables := h.config.Tweakables(scheduler.Namespace) timeout = catchupWindow(scheduler, tweakables) timeout = max(timeout, startWorkflowMinDeadline) @@ -530,7 +528,7 @@ func (e *InvokerProcessBufferTaskExecutor) startWorkflowDeadline( return start.ActualTime.AsTime().Add(timeout) } -func (e *InvokerExecuteTaskExecutor) startWorkflow( +func (h *InvokerExecuteTaskHandler) startWorkflow( ctx context.Context, metricsHandler metrics.Handler, scheduler *Scheduler, @@ -546,7 +544,7 @@ func (e *InvokerExecuteTaskExecutor) startWorkflow( // Get rate limiter permission once per buffered start, on the first attempt only. if start.Attempt == 1 { - delay, err := e.getRateLimiterPermission() + delay, err := h.getRateLimiterPermission() if err != nil { return nil, err } @@ -589,7 +587,7 @@ func (e *InvokerExecuteTaskExecutor) startWorkflow( }, } - result, err := e.frontendClient.StartWorkflowExecution(ctx, request) + result, err := h.frontendClient.StartWorkflowExecution(ctx, request) if err != nil { return nil, err } @@ -600,6 +598,7 @@ func (e *InvokerExecuteTaskExecutor) startWorkflow( // BufferedStarts in recordExecuteResult. start.RunId = result.RunId start.StartTime = timestamppb.New(actualStartTime) + start.HasCallback = true // Record time taken from action eligible to workflow started. if !start.Manual { @@ -619,7 +618,7 @@ func (e *InvokerExecuteTaskExecutor) startWorkflow( }, nil } -func (e *InvokerExecuteTaskExecutor) terminateWorkflow( +func (h *InvokerExecuteTaskHandler) terminateWorkflow( ctx context.Context, scheduler *Scheduler, target *commonpb.WorkflowExecution, @@ -634,11 +633,11 @@ func (e *InvokerExecuteTaskExecutor) terminateWorkflow( FirstExecutionRunId: target.RunId, }, } - _, err := e.historyClient.TerminateWorkflowExecution(ctx, request) + _, err := h.historyClient.TerminateWorkflowExecution(ctx, request) return err } -func (e *InvokerExecuteTaskExecutor) cancelWorkflow( +func (h *InvokerExecuteTaskHandler) cancelWorkflow( ctx context.Context, scheduler *Scheduler, target *commonpb.WorkflowExecution, @@ -653,14 +652,14 @@ func (e *InvokerExecuteTaskExecutor) cancelWorkflow( FirstExecutionRunId: target.RunId, }, } - _, err := e.historyClient.RequestCancelWorkflowExecution(ctx, request) + _, err := h.historyClient.RequestCancelWorkflowExecution(ctx, request) return err } // getRateLimiterPermission returns a delay for which the caller should wait // before proceeding. If an error is returned, execution should not proceed, and // reservation should be retried. -func (e *InvokerExecuteTaskExecutor) getRateLimiterPermission() (delay time.Duration, err error) { +func (h *InvokerExecuteTaskHandler) getRateLimiterPermission() (delay time.Duration, err error) { // For now, we're only going to rate limit via APS. return } @@ -694,22 +693,22 @@ func (r *rateLimitedError) Error() string { return fmt.Sprintf("rate limited for %s", r.delay) } -func (e *InvokerExecuteTaskExecutor) newInvokerTaskExecutorContext( +func (h *InvokerExecuteTaskHandler) newInvokerTaskHandlerContext( ctx context.Context, scheduler *Scheduler, -) invokerTaskExecutorContext { - tweakables := e.config.Tweakables(scheduler.Namespace) +) invokerTaskHandlerContext { + tweakables := h.config.Tweakables(scheduler.Namespace) maxActions := tweakables.MaxActionsPerExecution - return invokerTaskExecutorContext{ + return invokerTaskHandlerContext{ Context: ctx, actionsTaken: 0, maxActions: maxActions, } } -func (i invokerTaskExecutorContext) Clone() invokerTaskExecutorContext { - return invokerTaskExecutorContext{ +func (i invokerTaskHandlerContext) Clone() invokerTaskHandlerContext { + return invokerTaskHandlerContext{ Context: i.Context, actionsTaken: i.actionsTaken, maxActions: i.maxActions, diff --git a/chasm/lib/scheduler/library.go b/chasm/lib/scheduler/library.go index 37d881b5f86..183dd486af4 100644 --- a/chasm/lib/scheduler/library.go +++ b/chasm/lib/scheduler/library.go @@ -12,29 +12,41 @@ type ( handler *handler - SchedulerIdleTaskExecutor *SchedulerIdleTaskExecutor - GeneratorTaskExecutor *GeneratorTaskExecutor - InvokerExecuteTaskExecutor *InvokerExecuteTaskExecutor - InvokerProcessBufferTaskExecutor *InvokerProcessBufferTaskExecutor - BackfillerTaskExecutor *BackfillerTaskExecutor + SchedulerIdleTaskHandler *SchedulerIdleTaskHandler + SchedulerCallbacksTaskHandler *SchedulerCallbacksTaskHandler + GeneratorTaskHandler *GeneratorTaskHandler + InvokerExecuteTaskHandler *InvokerExecuteTaskHandler + InvokerProcessBufferTaskHandler *InvokerProcessBufferTaskHandler + BackfillerTaskHandler *BackfillerTaskHandler + MigrateToWorkflowTaskHandler *SchedulerMigrateToWorkflowTaskHandler } ) +// NewNilLibrary creates a Library with all nil handlers. Useful for +// registration-only contexts like tdbg where no task execution is needed. +func NewNilLibrary() *Library { + return &Library{} +} + func NewLibrary( handler *handler, - SchedulerIdleTaskExecutor *SchedulerIdleTaskExecutor, - GeneratorTaskExecutor *GeneratorTaskExecutor, - InvokerExecuteTaskExecutor *InvokerExecuteTaskExecutor, - InvokerProcessBufferTaskExecutor *InvokerProcessBufferTaskExecutor, - BackfillerTaskExecutor *BackfillerTaskExecutor, + SchedulerIdleTaskHandler *SchedulerIdleTaskHandler, + SchedulerCallbacksTaskHandler *SchedulerCallbacksTaskHandler, + GeneratorTaskHandler *GeneratorTaskHandler, + InvokerExecuteTaskHandler *InvokerExecuteTaskHandler, + InvokerProcessBufferTaskHandler *InvokerProcessBufferTaskHandler, + BackfillerTaskHandler *BackfillerTaskHandler, + MigrateToWorkflowTaskHandler *SchedulerMigrateToWorkflowTaskHandler, ) *Library { return &Library{ - handler: handler, - SchedulerIdleTaskExecutor: SchedulerIdleTaskExecutor, - GeneratorTaskExecutor: GeneratorTaskExecutor, - InvokerExecuteTaskExecutor: InvokerExecuteTaskExecutor, - InvokerProcessBufferTaskExecutor: InvokerProcessBufferTaskExecutor, - BackfillerTaskExecutor: BackfillerTaskExecutor, + handler: handler, + SchedulerIdleTaskHandler: SchedulerIdleTaskHandler, + SchedulerCallbacksTaskHandler: SchedulerCallbacksTaskHandler, + GeneratorTaskHandler: GeneratorTaskHandler, + InvokerExecuteTaskHandler: InvokerExecuteTaskHandler, + InvokerProcessBufferTaskHandler: InvokerProcessBufferTaskHandler, + BackfillerTaskHandler: BackfillerTaskHandler, + MigrateToWorkflowTaskHandler: MigrateToWorkflowTaskHandler, } } @@ -59,28 +71,31 @@ func (l *Library) Tasks() []*chasm.RegistrableTask { return []*chasm.RegistrableTask{ chasm.NewRegistrablePureTask( "idle", - l.SchedulerIdleTaskExecutor, - l.SchedulerIdleTaskExecutor, + l.SchedulerIdleTaskHandler, + ), + chasm.NewRegistrableSideEffectTask( + "callbacks", + l.SchedulerCallbacksTaskHandler, ), chasm.NewRegistrablePureTask( "generate", - l.GeneratorTaskExecutor, - l.GeneratorTaskExecutor, + l.GeneratorTaskHandler, ), chasm.NewRegistrableSideEffectTask( "execute", - l.InvokerExecuteTaskExecutor, - l.InvokerExecuteTaskExecutor, + l.InvokerExecuteTaskHandler, ), chasm.NewRegistrablePureTask( "processBuffer", - l.InvokerProcessBufferTaskExecutor, - l.InvokerProcessBufferTaskExecutor, + l.InvokerProcessBufferTaskHandler, ), chasm.NewRegistrablePureTask( "backfill", - l.BackfillerTaskExecutor, - l.BackfillerTaskExecutor, + l.BackfillerTaskHandler, + ), + chasm.NewRegistrableSideEffectTask( + "migrateToWorkflow", + l.MigrateToWorkflowTaskHandler, ), } } diff --git a/chasm/lib/scheduler/migration/migration.go b/chasm/lib/scheduler/migration/migration.go index a97127b140f..e9c9c3bbfd8 100644 --- a/chasm/lib/scheduler/migration/migration.go +++ b/chasm/lib/scheduler/migration/migration.go @@ -14,8 +14,9 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -// LegacyToSchedulerMigrationState converts legacy (workflow-backed) scheduler state to a -// SchedulerMigrationState proto. This is the primary V1-to-V2 migration function. +// LegacyToCreateFromMigrationStateRequest converts legacy (workflow-backed) scheduler +// state to a CreateFromMigrationStateRequest proto. This is the primary V1-to-V2 +// migration function. // // The migrationTime parameter is used for initializing timestamps that don't have a // direct mapping from V1 state (e.g., StartTime for running workflows). @@ -34,14 +35,14 @@ import ( // // Note: In V2, RunningWorkflows and RecentActions are computed on-demand from // BufferedStarts by the Invoker, rather than being stored separately in ScheduleInfo. -func LegacyToSchedulerMigrationState( +func LegacyToCreateFromMigrationStateRequest( schedule *schedulepb.Schedule, info *schedulepb.ScheduleInfo, state *schedulespb.InternalState, - searchAttributes map[string]*commonpb.Payload, - memo map[string]*commonpb.Payload, + searchAttributes *commonpb.SearchAttributes, + memo *commonpb.Memo, migrationTime time.Time, -) *schedulerpb.SchedulerMigrationState { +) *schedulerpb.CreateFromMigrationStateRequest { // V2 computes RunningWorkflows/RecentActions on-demand from BufferedStarts infoClone := common.CloneProto(info) infoClone.RunningWorkflows = nil @@ -80,6 +81,7 @@ func LegacyToSchedulerMigrationState( recentActionsBufferedStarts := convertRecentActionsToBufferedStarts( info.RecentActions, + info.RunningWorkflows, state.NamespaceId, state.ScheduleId, state.ConflictToken, @@ -97,18 +99,24 @@ func LegacyToSchedulerMigrationState( backfillers := convertBackfillsLegacyToCHASM(state.OngoingBackfills) lastCompletion := convertLastCompletionLegacyToCHASM(state.LastCompletionResult, state.ContinuedFailure) - return &schedulerpb.SchedulerMigrationState{ - SchedulerState: schedulerState, - GeneratorState: generatorState, - InvokerState: invokerState, - Backfillers: backfillers, - LastCompletionResult: lastCompletion, - SearchAttributes: searchAttributes, - Memo: memo, + return &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: state.NamespaceId, + State: &schedulerpb.SchedulerMigrationState{ + SchedulerState: schedulerState, + GeneratorState: generatorState, + InvokerState: invokerState, + Backfillers: backfillers, + LastCompletionResult: lastCompletion, + SearchAttributes: searchAttributes.GetIndexedFields(), + Memo: memo.GetFields(), + }, } } -func CHASMToSchedulerMigrationState( +// CHASMToLegacyStartScheduleArgs converts CHASM scheduler state to V1 StartScheduleArgs. +// This is the primary V2-to-V1 migration function. The migrationTime parameter is used +// to initialize missing timestamps. +func CHASMToLegacyStartScheduleArgs( scheduler *schedulerpb.SchedulerState, generator *schedulerpb.GeneratorState, invoker *schedulerpb.InvokerState, @@ -116,29 +124,9 @@ func CHASMToSchedulerMigrationState( lastCompletionResult *schedulerpb.LastCompletionResult, searchAttributes map[string]*commonpb.Payload, memo map[string]*commonpb.Payload, -) *schedulerpb.SchedulerMigrationState { - return &schedulerpb.SchedulerMigrationState{ - SchedulerState: common.CloneProto(scheduler), - GeneratorState: common.CloneProto(generator), - InvokerState: common.CloneProto(invoker), - Backfillers: common.CloneProtoMap(backfillers), - LastCompletionResult: common.CloneProto(lastCompletionResult), - SearchAttributes: searchAttributes, - Memo: memo, - } -} - -// SchedulerMigrationStateToLegacyStartScheduleArgs converts migration state to V1 StartScheduleArgs.. The migrationTime parameter is used to initialize -// missing timestamps. -func SchedulerMigrationStateToLegacyStartScheduleArgs( - migrationState *schedulerpb.SchedulerMigrationState, migrationTime time.Time, ) *schedulespb.StartScheduleArgs { - if migrationState == nil { - migrationState = &schedulerpb.SchedulerMigrationState{} - } - - schedulerState := common.CloneProto(migrationState.GetSchedulerState()) + schedulerState := common.CloneProto(scheduler) if schedulerState == nil { schedulerState = &schedulerpb.SchedulerState{} } @@ -154,23 +142,23 @@ func SchedulerMigrationStateToLegacyStartScheduleArgs( } var invokerBuffered []*schedulespb.BufferedStart - if migrationState.GetInvokerState() != nil { - invokerBuffered = migrationState.GetInvokerState().GetBufferedStarts() + if invoker != nil { + invokerBuffered = invoker.GetBufferedStarts() } bufferedStarts, running, recent := splitBufferedStartsForLegacy(invokerBuffered) - ongoingBackfills, triggerStarts := convertBackfillersCHASMToLegacy(migrationState.GetBackfillers(), migrationTime) + ongoingBackfills, triggerStarts := convertBackfillersCHASMToLegacy(backfillers, migrationTime) bufferedStarts = append(bufferedStarts, triggerStarts...) var generatorLastProcessed *timestamppb.Timestamp - if migrationState.GetGeneratorState() != nil { - generatorLastProcessed = migrationState.GetGeneratorState().GetLastProcessedTime() + if generator != nil { + generatorLastProcessed = generator.GetLastProcessedTime() } lastProcessedTime := common.CloneProto(generatorLastProcessed) if lastProcessedTime == nil { lastProcessedTime = timestamppb.New(migrationTime) } - resultPayloads, continuedFailure := convertLastCompletionCHASMToLegacy(migrationState.GetLastCompletionResult()) + resultPayloads, continuedFailure := convertLastCompletionCHASMToLegacy(lastCompletionResult) info.RunningWorkflows = running info.RecentActions = recent @@ -261,16 +249,22 @@ func convertRunningWorkflowsToBufferedStarts( RunId: wf.RunId, // RequestId will be used with AttachRequestID to register Nexus // callbacks for tracking workflow completion after migration. + // Include the RunId in the tag to ensure each running workflow + // gets a unique RequestId (important for ALLOW_ALL overlap + // policy where multiple workflows may be running concurrently). RequestId: schedulescommon.GenerateRequestID( namespaceID, scheduleID, conflictToken, - "migrated-running", + "migrated-running-"+wf.RunId, migrationTime, migrationTime, ), Attempt: 1, Completed: nil, + // Migrated running workflows must have a Nexus callback attached once the + // migrated schedule target has been created. + HasCallback: false, } } @@ -280,8 +274,15 @@ func convertRunningWorkflowsToBufferedStarts( // convertRecentActionsToBufferedStarts converts V1's RecentActions list to V2's // BufferedStarts format. In V2, completed actions are represented as BufferedStarts with // RunId, StartTime, and Completed fields all populated. +// +// runningWorkflows is the set of currently running workflow executions (from +// info.RunningWorkflows). These are excluded because they are already converted +// separately by convertRunningWorkflowsToBufferedStarts. In V1, recordAction +// adds the same workflow to both RecentActions and RunningWorkflows, so without +// this filter the same execution would appear twice in the CHASM BufferedStarts. func convertRecentActionsToBufferedStarts( recentActions []*schedulepb.ScheduleActionResult, + runningWorkflows []*commonpb.WorkflowExecution, namespaceID, scheduleID string, conflictToken int64, migrationTime time.Time, @@ -290,12 +291,25 @@ func convertRecentActionsToBufferedStarts( return nil } + // Build a set of running workflow run IDs to exclude from recent actions, + // since those are already converted by convertRunningWorkflowsToBufferedStarts. + runningRunIDs := make(map[string]struct{}, len(runningWorkflows)) + for _, wf := range runningWorkflows { + runningRunIDs[wf.GetRunId()] = struct{}{} + } + bufferedStarts := make([]*schedulespb.BufferedStart, 0, len(recentActions)) for _, action := range recentActions { if action.StartWorkflowResult == nil { continue } + // Skip actions for workflows that are still running — those are handled + // by convertRunningWorkflowsToBufferedStarts. + if _, ok := runningRunIDs[action.StartWorkflowResult.GetRunId()]; ok { + continue + } + bufferedStarts = append(bufferedStarts, &schedulespb.BufferedStart{ NominalTime: action.ScheduleTime, ActualTime: action.ActualTime, diff --git a/chasm/lib/scheduler/migration/migration_test.go b/chasm/lib/scheduler/migration/migration_test.go index e91a920953f..41a17192fec 100644 --- a/chasm/lib/scheduler/migration/migration_test.go +++ b/chasm/lib/scheduler/migration/migration_test.go @@ -34,7 +34,7 @@ func newTestSchedule() *schedulepb.Schedule { } } -func TestLegacyToSchedulerMigrationState(t *testing.T) { +func TestLegacyToCreateFromMigrationStateRequest(t *testing.T) { now := time.Now().UTC() state := &schedulespb.InternalState{ Namespace: "test-ns", @@ -75,11 +75,15 @@ func TestLegacyToSchedulerMigrationState(t *testing.T) { }, }, } - searchAttrs := map[string]*commonpb.Payload{"Attr": {Data: []byte("value")}} - memo := map[string]*commonpb.Payload{"Memo": {Data: []byte("memo")}} + searchAttrs := &commonpb.SearchAttributes{IndexedFields: map[string]*commonpb.Payload{"Attr": {Data: []byte("value")}}} + memo := &commonpb.Memo{Fields: map[string]*commonpb.Payload{"Memo": {Data: []byte("memo")}}} - migrationState := LegacyToSchedulerMigrationState(newTestSchedule(), info, state, searchAttrs, memo, now) + req := LegacyToCreateFromMigrationStateRequest(newTestSchedule(), info, state, searchAttrs, memo, now) + require.NotNil(t, req) + require.Equal(t, "test-ns-id", req.NamespaceId) + + migrationState := req.State // Scheduler state require.NotNil(t, migrationState) require.NotNil(t, migrationState.SchedulerState) @@ -109,6 +113,7 @@ func TestLegacyToSchedulerMigrationState(t *testing.T) { running++ require.Equal(t, "wf-1", start.WorkflowId) require.Equal(t, "run-1", start.RunId) + require.False(t, start.HasCallback) case start.Completed != nil: completed++ require.Equal(t, "wf-2", start.WorkflowId) @@ -136,8 +141,8 @@ func TestLegacyToSchedulerMigrationState(t *testing.T) { require.Equal(t, "last failure", migrationState.LastCompletionResult.Failure.Message) // Search attributes and memo - require.Equal(t, searchAttrs, migrationState.SearchAttributes) - require.Equal(t, memo, migrationState.Memo) + require.Equal(t, searchAttrs.GetIndexedFields(), migrationState.SearchAttributes) + require.Equal(t, memo.GetFields(), migrationState.Memo) } func TestCHASMToLegacyStartScheduleArgs(t *testing.T) { @@ -207,15 +212,7 @@ func TestCHASMToLegacyStartScheduleArgs(t *testing.T) { Failure: &failurepb.Failure{Message: "last failure"}, } - migrationState := &schedulerpb.SchedulerMigrationState{ - SchedulerState: scheduler, - GeneratorState: generator, - InvokerState: invoker, - Backfillers: backfillers, - LastCompletionResult: lastCompletion, - } - - args := SchedulerMigrationStateToLegacyStartScheduleArgs(migrationState, now) + args := CHASMToLegacyStartScheduleArgs(scheduler, generator, invoker, backfillers, lastCompletion, nil, nil, now) require.Equal(t, "ns-id", args.State.NamespaceId) require.Equal(t, "sched-id", args.State.ScheduleId) @@ -243,3 +240,97 @@ func TestCHASMToLegacyStartScheduleArgs(t *testing.T) { } require.True(t, triggerFound) } + +func TestLegacyToCreateFromMigrationStateRequest_DeduplicatesRunningWorkflows(t *testing.T) { + // V1's recordAction puts the same workflow in both RecentActions (with + // RUNNING status) and RunningWorkflows. The migration should not create + // duplicate BufferedStarts for the same execution. + now := time.Now().UTC() + state := &schedulespb.InternalState{ + Namespace: "test-ns", + NamespaceId: "test-ns-id", + ScheduleId: "test-sched-id", + ConflictToken: 1, + } + info := &schedulepb.ScheduleInfo{ + RunningWorkflows: []*commonpb.WorkflowExecution{ + {WorkflowId: "wf-1", RunId: "run-1"}, + }, + RecentActions: []*schedulepb.ScheduleActionResult{ + { + // Completed action - should be kept. + ScheduleTime: timestamppb.New(now.Add(-2 * time.Hour)), + ActualTime: timestamppb.New(now.Add(-2 * time.Hour)), + StartWorkflowResult: &commonpb.WorkflowExecution{WorkflowId: "wf-old", RunId: "run-old"}, + StartWorkflowStatus: enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, + }, + { + // Same workflow as RunningWorkflows - should be deduplicated. + ScheduleTime: timestamppb.New(now.Add(-time.Hour)), + ActualTime: timestamppb.New(now.Add(-time.Hour)), + StartWorkflowResult: &commonpb.WorkflowExecution{WorkflowId: "wf-1", RunId: "run-1"}, + StartWorkflowStatus: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, + }, + }, + } + + req := LegacyToCreateFromMigrationStateRequest(newTestSchedule(), info, state, nil, nil, now) + + // Should have 2 BufferedStarts: 1 running (from RunningWorkflows) + 1 completed (from RecentActions). + // The running entry in RecentActions should be excluded since it duplicates RunningWorkflows. + require.Len(t, req.State.InvokerState.BufferedStarts, 2) + + var running, completed int + for _, start := range req.State.InvokerState.BufferedStarts { + switch { + case start.RunId != "" && start.Completed == nil: + running++ + require.Equal(t, "wf-1", start.WorkflowId) + require.Equal(t, "run-1", start.RunId) + case start.Completed != nil: + completed++ + require.Equal(t, "wf-old", start.WorkflowId) + require.Equal(t, "run-old", start.RunId) + default: + t.Fatalf("unexpected buffered start state: RunId=%q, Completed=%v", start.RunId, start.Completed) + } + } + require.Equal(t, 1, running, "expected exactly 1 running workflow (not duplicated)") + require.Equal(t, 1, completed, "expected 1 completed workflow from recent actions") + + // Verify the round-trip: converting back to legacy should also have no + // duplicate RunIds in RecentActions. + _, _, recentActions := splitBufferedStartsForLegacy(req.State.InvokerState.BufferedStarts) + seen := make(map[string]bool) + for _, action := range recentActions { + runID := action.GetStartWorkflowResult().GetRunId() + require.False(t, seen[runID], "duplicate RunId %q in round-tripped RecentActions", runID) + seen[runID] = true + } +} + +func TestConvertRunningWorkflowsToBufferedStarts_UniqueRequestIDs(t *testing.T) { + // With ALLOW_ALL overlap policy, multiple workflows can be running + // concurrently. Each must get a unique RequestId so that + // recordCompletedAction matches the correct BufferedStart. + now := time.Now().UTC() + running := []*commonpb.WorkflowExecution{ + {WorkflowId: "wf-1", RunId: "run-aaa"}, + {WorkflowId: "wf-2", RunId: "run-bbb"}, + {WorkflowId: "wf-3", RunId: "run-ccc"}, + } + + starts := convertRunningWorkflowsToBufferedStarts( + running, "ns-id", "sched-id", 1, now, + ) + require.Len(t, starts, 3) + + requestIDs := make(map[string]string) // requestId -> runId + for _, start := range starts { + if prev, ok := requestIDs[start.RequestId]; ok { + t.Fatalf("duplicate RequestId %q: used by both RunId %q and %q", + start.RequestId, prev, start.RunId) + } + requestIDs[start.RequestId] = start.RunId + } +} diff --git a/chasm/lib/scheduler/proto/v1/message.proto b/chasm/lib/scheduler/proto/v1/message.proto index 5fd0b05c294..999dd406dc9 100644 --- a/chasm/lib/scheduler/proto/v1/message.proto +++ b/chasm/lib/scheduler/proto/v1/message.proto @@ -2,108 +2,121 @@ syntax = "proto3"; package temporal.server.chasm.lib.scheduler.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; - +import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/failure/v1/message.proto"; import "temporal/api/schedule/v1/message.proto"; import "temporal/server/api/schedule/v1/message.proto"; -import "google/protobuf/timestamp.proto"; +option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; // CHASM scheduler top-level state. message SchedulerState { - // Scheduler request parameters and metadata. - temporal.api.schedule.v1.Schedule schedule = 2; - temporal.api.schedule.v1.ScheduleInfo info = 3; - - // State common to all generators is stored in the top-level machine. - string namespace = 5; - string namespace_id = 6; - string schedule_id = 7; - - // Implemented as a sequence number. Used for optimistic locking against - // update requests. - int64 conflict_token = 8; - - // The closed flag is set true after a schedule completes, and the idle timer - // expires. - bool closed = 9; - - // When true, this scheduler is a sentinel that exists only to reserve the - // schedule ID. All API operations return NotFound. - bool sentinel = 10; + // Scheduler request parameters and metadata. + temporal.api.schedule.v1.Schedule schedule = 2; + temporal.api.schedule.v1.ScheduleInfo info = 3; + + // State common to all generators is stored in the top-level machine. + string namespace = 5; + string namespace_id = 6; + string schedule_id = 7; + + // Implemented as a sequence number. Used for optimistic locking against + // update requests. + int64 conflict_token = 8; + + // The closed flag is set true after a schedule completes, and the idle timer + // expires. + bool closed = 9; + + // When true, this scheduler is a sentinel that exists only to reserve the + // schedule ID. All API operations return NotFound. + bool sentinel = 10; + + // Set when a migration to workflow-backed scheduler (V1) is pending. + // Unpause operations are blocked while this is set. + WorkflowMigrationState workflow_migration = 11; +} + +// WorkflowMigrationState tracks the state of an in-progress V2-to-V1 migration. +message WorkflowMigrationState { + // The schedule's paused state before migration was initiated. Used to + // restore the correct paused state when passing state to the V1 workflow. + bool pre_migration_paused = 1; + + // The schedule's notes before migration was initiated. + string pre_migration_notes = 2; } // CHASM scheduler's Generator internal state. message GeneratorState { - // High water mark. - google.protobuf.Timestamp last_processed_time = 3; + // High water mark. + google.protobuf.Timestamp last_processed_time = 3; - // A list of upcoming times an action will be triggered. - repeated google.protobuf.Timestamp future_action_times = 4; + // A list of upcoming times an action will be triggered. + repeated google.protobuf.Timestamp future_action_times = 4; } // CHASM scheduler's Invoker internal state. message InvokerState { - // Buffered starts that will be started by the Invoker. - repeated temporal.server.api.schedule.v1.BufferedStart buffered_starts = 2; + // Buffered starts that will be started by the Invoker. + repeated temporal.server.api.schedule.v1.BufferedStart buffered_starts = 2; - // Workflow executions that will be cancelled due to overlap policy. - repeated temporal.api.common.v1.WorkflowExecution cancel_workflows = 3; + // Workflow executions that will be cancelled due to overlap policy. + repeated temporal.api.common.v1.WorkflowExecution cancel_workflows = 3; - // Workflow executions that will be terminated due to overlap policy. - repeated temporal.api.common.v1.WorkflowExecution terminate_workflows = 4; + // Workflow executions that will be terminated due to overlap policy. + repeated temporal.api.common.v1.WorkflowExecution terminate_workflows = 4; - // High water mark, used for evaluating when to fire tasks that are backing - // off from a retry. LastProcessedTime is stored as state so that task - // generation will be consistent, regardless of when generation occurs, such - // as after applying a replicated state (as opposed to evaluating based on - // present time). - google.protobuf.Timestamp last_processed_time = 5; + // High water mark, used for evaluating when to fire tasks that are backing + // off from a retry. LastProcessedTime is stored as state so that task + // generation will be consistent, regardless of when generation occurs, such + // as after applying a replicated state (as opposed to evaluating based on + // present time). + google.protobuf.Timestamp last_processed_time = 5; - reserved 6; + reserved 6; } // CHASM scheduler's Backfiller internal state. Backfill requests are 1:1 // with Backfiller nodes. Backfiller nodes also handle immediate trigger requests. message BackfillerState { - oneof request { - temporal.api.schedule.v1.BackfillRequest backfill_request = 1; + oneof request { + temporal.api.schedule.v1.BackfillRequest backfill_request = 1; - // When set, immediately buffer a single manual action. - temporal.api.schedule.v1.TriggerImmediatelyRequest trigger_request = 2; - } + // When set, immediately buffer a single manual action. + temporal.api.schedule.v1.TriggerImmediatelyRequest trigger_request = 2; + } - // Every Backfiller should be assigned a unique ID upon creation, used - // for deduplication. - string backfill_id = 6; + // Every Backfiller should be assigned a unique ID upon creation, used + // for deduplication. + string backfill_id = 6; - // High water mark. - google.protobuf.Timestamp last_processed_time = 7; + // High water mark. + google.protobuf.Timestamp last_processed_time = 7; - // Attempt count, incremented when the buffer is full and the Backfiller - // needs to back off before retrying to fill. - int64 attempt = 8; + // Attempt count, incremented when the buffer is full and the Backfiller + // needs to back off before retrying to fill. + int64 attempt = 8; } // CHASM scheduler retains the payload data for the last completed workflow. Both // last success and failure are stored simultaneously. message LastCompletionResult { - temporal.api.common.v1.Payload success = 1; - temporal.api.failure.v1.Failure failure = 2; + temporal.api.common.v1.Payload success = 1; + temporal.api.failure.v1.Failure failure = 2; } // SchedulerMigrationState is a stack-agnostic interchange format for migrating // scheduler state between V1 (workflow-backed) and V2 (CHASM) implementations. message SchedulerMigrationState { - SchedulerState scheduler_state = 1; - GeneratorState generator_state = 2; - InvokerState invoker_state = 3; - map backfillers = 4; - LastCompletionResult last_completion_result = 5; - - // Visibility data. - map search_attributes = 6; - map memo = 7; + SchedulerState scheduler_state = 1; + GeneratorState generator_state = 2; + InvokerState invoker_state = 3; + map backfillers = 4; + LastCompletionResult last_completion_result = 5; + + // Visibility data. + map search_attributes = 6; + map memo = 7; } diff --git a/chasm/lib/scheduler/proto/v1/request_response.proto b/chasm/lib/scheduler/proto/v1/request_response.proto index 21bc9cf7190..9dab1efa974 100644 --- a/chasm/lib/scheduler/proto/v1/request_response.proto +++ b/chasm/lib/scheduler/proto/v1/request_response.proto @@ -2,72 +2,106 @@ syntax = "proto3"; package temporal.server.chasm.lib.scheduler.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; - +import "chasm/lib/scheduler/proto/v1/message.proto"; import "temporal/api/workflowservice/v1/request_response.proto"; +option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; + message CreateScheduleRequest { - // Internal namespace ID (UUID). - string namespace_id = 1; + // Internal namespace ID (UUID). + string namespace_id = 1; - temporal.api.workflowservice.v1.CreateScheduleRequest frontend_request = 2; + temporal.api.workflowservice.v1.CreateScheduleRequest frontend_request = 2; } message CreateScheduleResponse { - temporal.api.workflowservice.v1.CreateScheduleResponse frontend_response = 1; + temporal.api.workflowservice.v1.CreateScheduleResponse frontend_response = 1; } message UpdateScheduleRequest { - // Internal namespace ID (UUID). - string namespace_id = 1; + // Internal namespace ID (UUID). + string namespace_id = 1; - temporal.api.workflowservice.v1.UpdateScheduleRequest frontend_request = 2; + temporal.api.workflowservice.v1.UpdateScheduleRequest frontend_request = 2; } message UpdateScheduleResponse { - temporal.api.workflowservice.v1.UpdateScheduleResponse frontend_response = 1; + temporal.api.workflowservice.v1.UpdateScheduleResponse frontend_response = 1; } message PatchScheduleRequest { - // Internal namespace ID (UUID). - string namespace_id = 1; + // Internal namespace ID (UUID). + string namespace_id = 1; - temporal.api.workflowservice.v1.PatchScheduleRequest frontend_request = 2; + temporal.api.workflowservice.v1.PatchScheduleRequest frontend_request = 2; } message PatchScheduleResponse { - temporal.api.workflowservice.v1.PatchScheduleResponse frontend_response = 1; + temporal.api.workflowservice.v1.PatchScheduleResponse frontend_response = 1; } message DeleteScheduleRequest { - // Internal namespace ID (UUID). - string namespace_id = 1; + // Internal namespace ID (UUID). + string namespace_id = 1; - temporal.api.workflowservice.v1.DeleteScheduleRequest frontend_request = 2; + temporal.api.workflowservice.v1.DeleteScheduleRequest frontend_request = 2; } message DeleteScheduleResponse { - temporal.api.workflowservice.v1.DeleteScheduleResponse frontend_response = 1; + temporal.api.workflowservice.v1.DeleteScheduleResponse frontend_response = 1; } message DescribeScheduleRequest { - // Internal namespace ID (UUID). - string namespace_id = 1; + // Internal namespace ID (UUID). + string namespace_id = 1; - temporal.api.workflowservice.v1.DescribeScheduleRequest frontend_request = 2; + temporal.api.workflowservice.v1.DescribeScheduleRequest frontend_request = 2; } message DescribeScheduleResponse { - temporal.api.workflowservice.v1.DescribeScheduleResponse frontend_response = 1; + temporal.api.workflowservice.v1.DescribeScheduleResponse frontend_response = 1; } message ListScheduleMatchingTimesRequest { - // Internal namespace ID (UUID). - string namespace_id = 1; + // Internal namespace ID (UUID). + string namespace_id = 1; - temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest frontend_request = 2; + temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest frontend_request = 2; } message ListScheduleMatchingTimesResponse { - temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse frontend_response = 1; + temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse frontend_response = 1; } + +message CreateFromMigrationStateRequest { + // Internal namespace ID (UUID). + string namespace_id = 1; + + SchedulerMigrationState state = 2; +} + +message CreateFromMigrationStateResponse {} + +message CreateSentinelRequest { + // Internal namespace ID (UUID). + string namespace_id = 1; + + string namespace = 2; + + string schedule_id = 3; +} + +message CreateSentinelResponse {} + +message MigrateToWorkflowRequest { + // The namespace ID of the schedule to migrate. + string namespace_id = 1; + // The schedule ID to migrate from CHASM to workflow-backed. + string schedule_id = 2; + // The identity of the caller initiating the migration. + string identity = 3; + // A unique request ID for idempotency. + string request_id = 4; +} + +message MigrateToWorkflowResponse {} diff --git a/chasm/lib/scheduler/proto/v1/service.proto b/chasm/lib/scheduler/proto/v1/service.proto index 265236b9783..65c21e949c8 100644 --- a/chasm/lib/scheduler/proto/v1/service.proto +++ b/chasm/lib/scheduler/proto/v1/service.proto @@ -2,33 +2,50 @@ syntax = "proto3"; package temporal.server.chasm.lib.scheduler.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; - import "chasm/lib/scheduler/proto/v1/request_response.proto"; +import "temporal/server/api/common/v1/api_category.proto"; import "temporal/server/api/routing/v1/extension.proto"; -service SchedulerService { - rpc CreateSchedule(CreateScheduleRequest) returns (CreateScheduleResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; - } - - rpc UpdateSchedule(UpdateScheduleRequest) returns (UpdateScheduleResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; - } - - rpc PatchSchedule(PatchScheduleRequest) returns (PatchScheduleResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; - } - - rpc DeleteSchedule(DeleteScheduleRequest) returns (DeleteScheduleResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; - } - - rpc DescribeSchedule(DescribeScheduleRequest) returns (DescribeScheduleResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; - } +option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; - rpc ListScheduleMatchingTimes(ListScheduleMatchingTimesRequest) returns (ListScheduleMatchingTimesResponse) { - option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; - } +service SchedulerService { + rpc CreateSchedule(CreateScheduleRequest) returns (CreateScheduleResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc UpdateSchedule(UpdateScheduleRequest) returns (UpdateScheduleResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc PatchSchedule(PatchScheduleRequest) returns (PatchScheduleResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DeleteSchedule(DeleteScheduleRequest) returns (DeleteScheduleResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DescribeSchedule(DescribeScheduleRequest) returns (DescribeScheduleResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc ListScheduleMatchingTimes(ListScheduleMatchingTimesRequest) returns (ListScheduleMatchingTimesResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.schedule_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + rpc CreateFromMigrationState(CreateFromMigrationStateRequest) returns (CreateFromMigrationStateResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "state.scheduler_state.schedule_id"; + } + + rpc CreateSentinel(CreateSentinelRequest) returns (CreateSentinelResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "schedule_id"; + } + rpc MigrateToWorkflow(MigrateToWorkflowRequest) returns (MigrateToWorkflowResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "schedule_id"; + } } diff --git a/chasm/lib/scheduler/proto/v1/tasks.proto b/chasm/lib/scheduler/proto/v1/tasks.proto index d5fb30d362b..077e71fe0be 100644 --- a/chasm/lib/scheduler/proto/v1/tasks.proto +++ b/chasm/lib/scheduler/proto/v1/tasks.proto @@ -2,10 +2,10 @@ syntax = "proto3"; package temporal.server.chasm.lib.scheduler.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; - import "google/protobuf/duration.proto"; +option go_package = "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb;schedulerpb"; + // Fires when the scheduler's idle period has lapsed, and the scheduler should // be closed. message SchedulerIdleTask { @@ -14,6 +14,11 @@ message SchedulerIdleTask { google.protobuf.Duration idle_time_total = 1; } +// Ensures that callbacks for all running buffered starts are attached. Used only +// during migration from V1, as workflows started by CHASM scheduler are started +// with callbacks attached. +message SchedulerCallbacksTask {} + // Buffers actions based on the schedule's specification. message GeneratorTask {} @@ -26,3 +31,6 @@ message InvokerExecuteTask {} // Buffers actions based on a manually-requested backfill. message BackfillerTask {} + +// Triggers migration from CHASM (V2) to workflow-backed (V1) scheduler. +message SchedulerMigrateToWorkflowTask {} diff --git a/chasm/lib/scheduler/scheduler.go b/chasm/lib/scheduler/scheduler.go index cd65a8e38b0..40445112532 100644 --- a/chasm/lib/scheduler/scheduler.go +++ b/chasm/lib/scheduler/scheduler.go @@ -17,6 +17,7 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common" + "go.temporal.io/server/common/contextutil" "go.temporal.io/server/common/payload" "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/searchattribute/sadefs" @@ -90,6 +91,8 @@ var ( ErrTooManyBackfillers = serviceerror.NewFailedPrecondition("too many concurrent backfillers") ErrInvalidQuery = serviceerror.NewInvalidArgument("missing or invalid query") ErrSentinel = serviceerror.NewNotFound("schedule is a sentinel") + ErrSentinelBlocked = serviceerror.NewUnavailable("schedule is a sentinel; please retry after sentinel expires") + ErrMigrationPending = serviceerror.NewUnavailable("schedule has a pending migration to workflow; please retry later") ) // NewScheduler returns an initialized CHASM scheduler root component. @@ -136,22 +139,42 @@ func NewScheduler( } // NewSentinel returns a sentinel CHASM scheduler that exists only to reserve -// the schedule ID. Sentinels have no sub-components and return NotFound -// on all API operations. +// the schedule ID. Sentinels have no sub-components (other than Info for idle +// tracking) and return NotFound on all API operations. An idle task auto-closes +// the sentinel after SentinelIdleTime. func NewSentinel( ctx chasm.MutableContext, namespace, namespaceID, scheduleID string, ) *Scheduler { - return &Scheduler{ + s := &Scheduler{ SchedulerState: &schedulerpb.SchedulerState{ Namespace: namespace, NamespaceId: namespaceID, ScheduleId: scheduleID, Sentinel: true, ConflictToken: scheduler.InitialConflictToken, + Info: &schedulepb.ScheduleInfo{}, }, cacheConflictToken: scheduler.InitialConflictToken, } + s.Info.CreateTime = timestamppb.New(ctx.Now(s)) + + ctx.AddTask(s, chasm.TaskAttributes{ + ScheduledTime: ctx.Now(s).Add(SentinelIdleTime), + }, &schedulerpb.SchedulerIdleTask{ + IdleTimeTotal: durationpb.New(SentinelIdleTime), + }) + + return s +} + +// CreateSentinelFn is the chasm.StartExecution factory for creating sentinel +// schedulers. Used by the V1 path to reserve the CHASM key space. +func CreateSentinelFn( + ctx chasm.MutableContext, + req *schedulerpb.CreateSentinelRequest, +) (*Scheduler, error) { + return NewSentinel(ctx, req.Namespace, req.NamespaceId, req.ScheduleId), nil } // IsSentinel returns true if this is a sentinel scheduler. @@ -218,6 +241,46 @@ func CreateScheduler( return sched, nil } +// CreateSchedulerFromMigration initializes a CHASM scheduler from migrated V1 state. +// Unlike CreateScheduler, this preserves the conflict token and other state from V1. +// +// The migrated state components (scheduler, generator, invoker, backfillers) are +// directly initialized from the request, preserving all state including the +// conflict token for client compatibility. +func CreateSchedulerFromMigration( + ctx chasm.MutableContext, + req *schedulerpb.CreateFromMigrationStateRequest, +) (*Scheduler, error) { + state := req.GetState() + + sched := &Scheduler{ + SchedulerState: state.GetSchedulerState(), + cacheConflictToken: state.GetSchedulerState().GetConflictToken(), + Backfillers: make(chasm.Map[string, *Backfiller]), + LastCompletionResult: chasm.NewDataField(ctx, state.GetLastCompletionResult()), + } + sched.setNullableFields() + + sched.Invoker = chasm.NewComponentField(ctx, newInvokerWithState(ctx, state.GetInvokerState())) + sched.Generator = chasm.NewComponentField(ctx, newGeneratorWithState(ctx, state.GetGeneratorState())) + + for backfillID, backfillerState := range state.GetBackfillers() { + sched.Backfillers[backfillID] = chasm.NewComponentField(ctx, newBackfillerWithState(ctx, backfillerState)) + } + + visibility := chasm.NewVisibility(ctx) + sched.Visibility = chasm.NewComponentField(ctx, visibility) + visibility.MergeCustomSearchAttributes(ctx, state.GetSearchAttributes()) + visibility.MergeCustomMemo(ctx, state.GetMemo()) + + // Schedule a callbacks task to attach Nexus callbacks to any migrated + // running workflows. The task self-invalidates if there's no work to do. + ctx.AddTask(sched, chasm.TaskAttributes{}, &schedulerpb.SchedulerCallbacksTask{}) + + return sched, nil +} + +// LifecycleState implements the chasm.Component interface. func (s *Scheduler) LifecycleState(ctx chasm.Context) chasm.LifecycleState { if s.Closed { return chasm.LifecycleStateCompleted @@ -226,6 +289,29 @@ func (s *Scheduler) LifecycleState(ctx chasm.Context) chasm.LifecycleState { return chasm.LifecycleStateRunning } +func (s *Scheduler) ContextMetadata(_ chasm.Context) map[string]string { + md := make(map[string]string, 2) + if wfType := s.Schedule.GetAction().GetStartWorkflow().GetWorkflowType().GetName(); wfType != "" { + md[contextutil.MetadataKeyWorkflowType] = wfType + } + if tq := s.Schedule.GetAction().GetStartWorkflow().GetTaskQueue().GetName(); tq != "" { + md[contextutil.MetadataKeyWorkflowTaskQueue] = tq + } + if len(md) == 0 { + return nil + } + return md +} + +// Terminate implements the chasm.RootComponent interface. +func (s *Scheduler) Terminate( + _ chasm.MutableContext, + _ chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + // TODO: Implement terminate logic. + return chasm.TerminateComponentResponse{}, nil +} + // NewRangeBackfiller returns an intialized Backfiller component, which should // be parented under a Scheduler root node. func (s *Scheduler) NewRangeBackfiller( @@ -378,12 +464,15 @@ func (s *Scheduler) getIdleExpiration( // The idle timer to close off the component is started only for schedules with // no more work to do. Paused schedules are held open indefinitely. if idleTime == 0 || - s.Schedule.State.Paused || + s.GetSchedule().GetState().GetPaused() || (!nextWakeup.IsZero() && s.useScheduledAction(false)) || s.hasMoreAllowAllBackfills(ctx) { return time.Time{}, false } + if s.IsSentinel() { + return s.Info.GetCreateTime().AsTime().Add(idleTime), true + } return s.getLastEventTime(ctx).Add(idleTime), true } @@ -601,7 +690,7 @@ func (s *Scheduler) ListMatchingTimes( var out []*timestamppb.Timestamp t1 := timestamp.TimeValue(frontendReq.StartTime) - for i := 0; i < maxListMatchingTimesCount; i++ { + for range maxListMatchingTimesCount { t1 = cspec.GetNextTime(s.jitterSeed(), t1).Next if t1.IsZero() || t1.After(timestamp.TimeValue(frontendReq.EndTime)) { break @@ -630,6 +719,38 @@ func (s *Scheduler) Delete( }, nil } +// MigrateToWorkflow pauses the schedule and schedules a side-effect task to +// start the V1 workflow. This is the CHASM-side operation for V2-to-V1 migration. +// It is idempotent: if a migration is already pending, it returns success +// without taking any action. +func (s *Scheduler) MigrateToWorkflow( + ctx chasm.MutableContext, + req *schedulerpb.MigrateToWorkflowRequest, +) (*schedulerpb.MigrateToWorkflowResponse, error) { + if s.Sentinel { + return nil, ErrSentinel + } + if s.Closed { + return nil, ErrClosed + } + if s.WorkflowMigration != nil { + return &schedulerpb.MigrateToWorkflowResponse{}, nil + } + + // Save pre-migration paused state, mark migration as pending, then pause. + s.WorkflowMigration = &schedulerpb.WorkflowMigrationState{ + PreMigrationPaused: s.Schedule.State.Paused, + PreMigrationNotes: s.Schedule.State.Notes, + } + s.Schedule.State.Paused = true + s.Schedule.State.Notes = "paused for migration to workflow-backed scheduler" + + // Schedule a side-effect task to export state and start the V1 workflow. + ctx.AddTask(s, chasm.TaskAttributes{}, &schedulerpb.SchedulerMigrateToWorkflowTask{}) + + return &schedulerpb.MigrateToWorkflowResponse{}, nil +} + // Update replaces the schedule with a new one for UpdateSchedule requests. func (s *Scheduler) Update( ctx chasm.MutableContext, @@ -638,14 +759,16 @@ func (s *Scheduler) Update( if s.Sentinel { return nil, ErrSentinel } + // UpdateComponent does not reject mutations on completed executions, + // so we must check explicitly here. + if s.Closed { + return nil, ErrClosed + } if !s.validateConflictToken(req.FrontendRequest.ConflictToken) { return nil, ErrConflictTokenMismatch } // Update custom search attributes. - // - // TODO - we could also easily support allowing the customer to update their - // memo here. if req.FrontendRequest.GetSearchAttributes() != nil { // To preserve compatibility with V1 scheduler, we do a full replacement // of search attributes, dropping any that aren't a part of the update's @@ -661,8 +784,21 @@ func (s *Scheduler) Update( s.Visibility = chasm.NewComponentField(ctx, visibility) } + // Reject updates outright when a migration is pending so that changes are + // not silently lost during the migration window. + if s.WorkflowMigration != nil { + return nil, ErrMigrationPending + } + + // Update custom memo. + if req.FrontendRequest.GetMemo() != nil { + visibility := s.Visibility.Get(ctx) + visibility.ReplaceCustomMemo(ctx, req.FrontendRequest.GetMemo().GetFields()) + } + s.Schedule = req.FrontendRequest.Schedule s.setNullableFields() + s.Info.UpdateTime = timestamppb.New(ctx.Now(s)) s.updateConflictToken() @@ -683,12 +819,20 @@ func (s *Scheduler) Patch( if s.Sentinel { return nil, ErrSentinel } + // UpdateComponent does not reject mutations on completed executions, + // so we must check explicitly here. + if s.Closed { + return nil, ErrClosed + } // Handle paused status. if req.FrontendRequest.Patch.Pause != "" { s.Schedule.State.Paused = true s.Schedule.State.Notes = req.FrontendRequest.Patch.Pause } if req.FrontendRequest.Patch.Unpause != "" { + if s.WorkflowMigration != nil { + return nil, ErrMigrationPending + } s.Schedule.State.Paused = false s.Schedule.State.Notes = req.FrontendRequest.Patch.Unpause } diff --git a/chasm/lib/scheduler/scheduler_idle_tasks_test.go b/chasm/lib/scheduler/scheduler_idle_tasks_test.go index 2fe7015c71e..075e838b719 100644 --- a/chasm/lib/scheduler/scheduler_idle_tasks_test.go +++ b/chasm/lib/scheduler/scheduler_idle_tasks_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/require" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" @@ -12,14 +12,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -type idleTasksSuite struct { - schedulerSuite -} - -func TestIdleTasksSuite(t *testing.T) { - suite.Run(t, &idleTasksSuite{}) -} - type idleValidateTestCase struct { configIdleTime time.Duration taskIdleTimeTotal time.Duration @@ -30,66 +22,9 @@ type idleValidateTestCase struct { expectedValid bool } -func (s *idleTasksSuite) TestExecute() { - ctx := s.newMutableContext() - sched := s.scheduler - - // Create executor with default config - executor := scheduler.NewSchedulerIdleTaskExecutor(scheduler.SchedulerIdleTaskExecutorOptions{ - Config: defaultConfig(), - }) - - // Verify scheduler starts open - s.False(sched.Closed) - - // Execute the idle task - err := executor.Execute(ctx, sched, chasm.TaskAttributes{}, &schedulerpb.SchedulerIdleTask{}) - s.NoError(err) - - // Verify scheduler is now closed - s.True(sched.Closed) -} - -func (s *idleTasksSuite) TestValidate_SchedulerNotIdle() { - now := s.timeSource.Now() - s.runValidateTestCase(&idleValidateTestCase{ - configIdleTime: 10 * time.Minute, - taskIdleTimeTotal: 10 * time.Minute, - scheduledTime: now, - setupScheduler: func(sched *scheduler.Scheduler, ctx chasm.Context) { - // Make scheduler not idle by setting it as paused - sched.Schedule.State.Paused = true - }, - expectedValid: false, - }) -} - -func (s *idleTasksSuite) TestValidate_ValidIdleTask() { - now := s.timeSource.Now() - s.runValidateTestCase(&idleValidateTestCase{ - configIdleTime: 10 * time.Minute, - taskIdleTimeTotal: 10 * time.Minute, - scheduledTime: now, - idleMatchesScheduledTime: true, - expectedValid: true, - }) -} - -func (s *idleTasksSuite) TestValidate_SchedulerAlreadyClosed() { - now := s.timeSource.Now() - s.runValidateTestCase(&idleValidateTestCase{ - configIdleTime: 10 * time.Minute, - taskIdleTimeTotal: 10 * time.Minute, - scheduledTime: now, - schedulerClosed: true, - idleMatchesScheduledTime: true, - expectedValid: false, // Should return !scheduler.Closed (false when closed) - }) -} - -func (s *idleTasksSuite) runValidateTestCase(c *idleValidateTestCase) { - ctx := s.newMutableContext() - sched := s.scheduler +func runIdleValidateTestCase(t *testing.T, env *testEnv, c *idleValidateTestCase) { + ctx := env.MutableContext() + sched := env.Scheduler sched.Closed = c.schedulerClosed @@ -105,7 +40,7 @@ func (s *idleTasksSuite) runValidateTestCase(c *idleValidateTestCase) { }, } - executor := scheduler.NewSchedulerIdleTaskExecutor(scheduler.SchedulerIdleTaskExecutorOptions{ + handler := scheduler.NewSchedulerIdleTaskHandler(scheduler.SchedulerIdleTaskHandlerOptions{ Config: config, }) @@ -124,7 +59,67 @@ func (s *idleTasksSuite) runValidateTestCase(c *idleValidateTestCase) { ScheduledTime: scheduledTime, } - isValid, err := executor.Validate(ctx, sched, taskAttrs, task) - s.NoError(err) - s.Equal(c.expectedValid, isValid) + isValid, err := handler.Validate(ctx, sched, taskAttrs, task) + require.NoError(t, err) + require.Equal(t, c.expectedValid, isValid) +} + +func TestIdleTask_Execute(t *testing.T) { + env := newTestEnv(t) + ctx := env.MutableContext() + sched := env.Scheduler + + handler := scheduler.NewSchedulerIdleTaskHandler(scheduler.SchedulerIdleTaskHandlerOptions{ + Config: defaultConfig(), + }) + + // Verify scheduler starts open. + require.False(t, sched.Closed) + + // Execute the idle task. + err := handler.Execute(ctx, sched, chasm.TaskAttributes{}, &schedulerpb.SchedulerIdleTask{}) + require.NoError(t, err) + + // Verify scheduler is now closed. + require.True(t, sched.Closed) +} + +func TestIdleTask_Validate_SchedulerNotIdle(t *testing.T) { + env := newTestEnv(t) + now := env.TimeSource.Now() + runIdleValidateTestCase(t, env, &idleValidateTestCase{ + configIdleTime: 10 * time.Minute, + taskIdleTimeTotal: 10 * time.Minute, + scheduledTime: now, + setupScheduler: func(sched *scheduler.Scheduler, ctx chasm.Context) { + // Make scheduler not idle by setting it as paused. + sched.Schedule.State.Paused = true + }, + expectedValid: false, + }) +} + +func TestIdleTask_Validate_ValidIdleTask(t *testing.T) { + env := newTestEnv(t) + now := env.TimeSource.Now() + runIdleValidateTestCase(t, env, &idleValidateTestCase{ + configIdleTime: 10 * time.Minute, + taskIdleTimeTotal: 10 * time.Minute, + scheduledTime: now, + idleMatchesScheduledTime: true, + expectedValid: true, + }) +} + +func TestIdleTask_Validate_SchedulerAlreadyClosed(t *testing.T) { + env := newTestEnv(t) + now := env.TimeSource.Now() + runIdleValidateTestCase(t, env, &idleValidateTestCase{ + configIdleTime: 10 * time.Minute, + taskIdleTimeTotal: 10 * time.Minute, + scheduledTime: now, + schedulerClosed: true, + idleMatchesScheduledTime: true, + expectedValid: false, // Should return !scheduler.Closed (false when closed). + }) } diff --git a/chasm/lib/scheduler/scheduler_migrate_task.go b/chasm/lib/scheduler/scheduler_migrate_task.go new file mode 100644 index 00000000000..2892ebad0de --- /dev/null +++ b/chasm/lib/scheduler/scheduler_migrate_task.go @@ -0,0 +1,201 @@ +package scheduler + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + 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" + "go.temporal.io/api/workflowservice/v1" + schedulespb "go.temporal.io/server/api/schedule/v1" + "go.temporal.io/server/chasm" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + "go.temporal.io/server/chasm/lib/scheduler/migration" + "go.temporal.io/server/common" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/payload" + "go.temporal.io/server/common/primitives" + "go.temporal.io/server/common/resource" + "go.temporal.io/server/common/sdk" + "go.temporal.io/server/common/searchattribute" + "go.temporal.io/server/common/searchattribute/sadefs" + legacyscheduler "go.temporal.io/server/service/worker/scheduler" + "go.uber.org/fx" +) + +type ( + SchedulerMigrateToWorkflowTaskHandlerOptions struct { + fx.In + + Config *Config + MetricsHandler metrics.Handler + BaseLogger log.Logger + HistoryClient resource.HistoryClient + } + + SchedulerMigrateToWorkflowTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*schedulerpb.SchedulerMigrateToWorkflowTask] + config *Config + metricsHandler metrics.Handler + baseLogger log.Logger + historyClient resource.HistoryClient + } +) + +func NewSchedulerMigrateToWorkflowTaskHandler( + opts SchedulerMigrateToWorkflowTaskHandlerOptions, +) *SchedulerMigrateToWorkflowTaskHandler { + return &SchedulerMigrateToWorkflowTaskHandler{ + config: opts.Config, + metricsHandler: opts.MetricsHandler, + baseLogger: opts.BaseLogger, + historyClient: opts.HistoryClient, + } +} + +func (h *SchedulerMigrateToWorkflowTaskHandler) Validate( + _ chasm.Context, + scheduler *Scheduler, + _ chasm.TaskAttributes, + _ *schedulerpb.SchedulerMigrateToWorkflowTask, +) (bool, error) { + if scheduler.Closed { + return false, nil + } + return scheduler.WorkflowMigration != nil, nil +} + +func (h *SchedulerMigrateToWorkflowTaskHandler) Execute( + ctx context.Context, + schedulerRef chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *schedulerpb.SchedulerMigrateToWorkflowTask, +) error { + // Read state and convert to V1 args inside the ReadComponent callback, + // where we have access to the CHASM context for consistent time. + type readResult struct { + args *schedulespb.StartScheduleArgs + namespace string + namespaceID string + scheduleID string + searchAttributes map[string]*commonpb.Payload + memo map[string]*commonpb.Payload + now time.Time + } + var result readResult + + _, err := chasm.ReadComponent( + ctx, + schedulerRef, + func(s *Scheduler, ctx chasm.Context, _ any) (struct{}, error) { + now := ctx.Now(s) + schedulerState := common.CloneProto(s.SchedulerState) + generatorState := common.CloneProto(s.Generator.Get(ctx).GeneratorState) + invokerState := common.CloneProto(s.Invoker.Get(ctx).InvokerState) + + bStates := make(map[string]*schedulerpb.BackfillerState, len(s.Backfillers)) + for id, field := range s.Backfillers { + bStates[id] = common.CloneProto(field.Get(ctx).BackfillerState) + } + + lastCompletionResult := common.CloneProto(s.LastCompletionResult.Get(ctx)) + + visibility := s.Visibility.Get(ctx) + searchAttributes := visibility.CustomSearchAttributes(ctx) + memo := visibility.CustomMemo(ctx) + + // Restore the pre-migration paused state so the V1 workflow receives + // the correct schedule state (not the migration-imposed pause). + // Validation guarantees WorkflowMigration and State are always set + // when this task runs. + schedulerState.Schedule.State.Paused = schedulerState.WorkflowMigration.PreMigrationPaused + schedulerState.Schedule.State.Notes = schedulerState.WorkflowMigration.PreMigrationNotes + + result = readResult{ + args: migration.CHASMToLegacyStartScheduleArgs( + schedulerState, + generatorState, + invokerState, + bStates, + lastCompletionResult, + searchAttributes, + memo, + now, + ), + namespace: schedulerState.GetNamespace(), + namespaceID: schedulerState.GetNamespaceId(), + scheduleID: schedulerState.GetScheduleId(), + searchAttributes: searchAttributes, + memo: memo, + now: now, + } + return struct{}{}, nil + }, + nil, + ) + if err != nil { + return fmt.Errorf("failed to read scheduler state: %w", err) + } + + // Serialize the V1 workflow input. + inputPayloads, err := sdk.PreferProtoDataConverter.ToPayloads(result.args) + if err != nil { + return fmt.Errorf("failed to serialize schedule args: %w", err) + } + + // Build the start request to match createScheduleWorkflow in the frontend + // as closely as possible. Include TemporalNamespaceDivision so the V1 + // workflow is discoverable via ListSchedules. + sa := &commonpb.SearchAttributes{IndexedFields: result.searchAttributes} + searchattribute.AddSearchAttribute(&sa, sadefs.TemporalNamespaceDivision, payload.EncodeString(legacyscheduler.NamespaceDivision)) + workflowID := legacyscheduler.WorkflowIDPrefix + result.scheduleID + startReq := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: result.namespace, + WorkflowId: workflowID, + WorkflowType: &commonpb.WorkflowType{Name: legacyscheduler.WorkflowType}, + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + Input: inputPayloads, + Identity: fmt.Sprintf("temporal-scheduler-migration-%s-%s", result.namespace, result.scheduleID), + WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + Memo: &commonpb.Memo{Fields: result.memo}, + SearchAttributes: sa, + Priority: &commonpb.Priority{}, + } + + _, err = h.historyClient.StartWorkflowExecution( + ctx, + common.CreateHistoryStartWorkflowRequest(result.namespaceID, startReq, nil, nil, result.now), + ) + if err != nil { + // Treat already-started as success for idempotency. + var alreadyStartedErr *serviceerror.WorkflowExecutionAlreadyStarted + if !errors.As(err, &alreadyStartedErr) { + return fmt.Errorf("failed to start V1 scheduler workflow: %w", err) + } + } + + // Mark the CHASM scheduler as closed now that the V1 workflow is running. + _, _, err = chasm.UpdateComponent( + ctx, + schedulerRef, + func(s *Scheduler, ctx chasm.MutableContext, _ any) (chasm.NoValue, error) { + s.Closed = true + s.WorkflowMigration = nil + return nil, nil + }, + nil, + ) + if err != nil { + return fmt.Errorf("failed to close CHASM scheduler after migration: %w", err) + } + + return nil +} diff --git a/chasm/lib/scheduler/scheduler_migrate_test.go b/chasm/lib/scheduler/scheduler_migrate_test.go new file mode 100644 index 00000000000..66f3c8f3df2 --- /dev/null +++ b/chasm/lib/scheduler/scheduler_migrate_test.go @@ -0,0 +1,169 @@ +package scheduler_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + schedulepb "go.temporal.io/api/schedule/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm/lib/scheduler" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" +) + +func TestMigrateToWorkflow_PausesSchedule(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + + require.False(t, sched.Schedule.State.Paused) + + _, err := sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) + + require.True(t, sched.Schedule.State.Paused) + require.Equal(t, "paused for migration to workflow-backed scheduler", sched.Schedule.State.Notes) + require.NotNil(t, sched.WorkflowMigration) +} + +func TestMigrateToWorkflow_SavesPreMigrationState(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + + // Pause the schedule before migration with custom notes. + sched.Schedule.State.Paused = true + sched.Schedule.State.Notes = "user paused" + + _, err := sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) + + require.NotNil(t, sched.WorkflowMigration) + require.True(t, sched.WorkflowMigration.PreMigrationPaused) + require.Equal(t, "user paused", sched.WorkflowMigration.PreMigrationNotes) +} + +func TestMigrateToWorkflow_SavesPreMigrationState_Unpaused(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + + require.False(t, sched.Schedule.State.Paused) + + _, err := sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) + + require.NotNil(t, sched.WorkflowMigration) + require.False(t, sched.WorkflowMigration.PreMigrationPaused) + require.Empty(t, sched.WorkflowMigration.PreMigrationNotes) +} + +func TestMigrateToWorkflow_Idempotent(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + + _, err := sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) + + // Second call succeeds without error (no-op). + _, err = sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) +} + +func TestMigrateToWorkflow_Sentinel(t *testing.T) { + sentinel, ctx, _ := setupSentinelForTest(t) + + _, err := sentinel.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + + var notFoundErr *serviceerror.NotFound + require.ErrorAs(t, err, ¬FoundErr) +} + +func TestPatch_UnpauseBlockedDuringMigration(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + + // Initiate migration. + _, err := sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) + + // Attempt to unpause should fail. + _, err = sched.Patch(ctx, &schedulerpb.PatchScheduleRequest{ + NamespaceId: namespaceID, + FrontendRequest: &workflowservice.PatchScheduleRequest{ + Namespace: namespace, + ScheduleId: scheduleID, + Patch: &schedulepb.SchedulePatch{ + Unpause: "resuming", + }, + }, + }) + + var unavailableErr *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailableErr) + require.ErrorIs(t, err, scheduler.ErrMigrationPending) +} + +func TestPatch_PauseAllowedDuringMigration(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + + _, err := sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) + + // Pause with different notes should succeed. + _, err = sched.Patch(ctx, &schedulerpb.PatchScheduleRequest{ + NamespaceId: namespaceID, + FrontendRequest: &workflowservice.PatchScheduleRequest{ + Namespace: namespace, + ScheduleId: scheduleID, + Patch: &schedulepb.SchedulePatch{ + Pause: "user pause during migration", + }, + }, + }) + require.NoError(t, err) + require.True(t, sched.Schedule.State.Paused) +} + +func TestUpdate_RejectedDuringMigration(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + + _, err := sched.MigrateToWorkflow(ctx, &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: scheduleID, + }) + require.NoError(t, err) + + // Update should be rejected outright when a migration is pending. + newSchedule := defaultSchedule() + newSchedule.State.Paused = false + newSchedule.State.Notes = "user unpaused" + + _, err = sched.Update(ctx, &schedulerpb.UpdateScheduleRequest{ + NamespaceId: namespaceID, + FrontendRequest: &workflowservice.UpdateScheduleRequest{ + Namespace: namespace, + ScheduleId: scheduleID, + Schedule: newSchedule, + }, + }) + var unavailableErr *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailableErr) + require.ErrorIs(t, err, scheduler.ErrMigrationPending) +} diff --git a/chasm/lib/scheduler/scheduler_suite_test.go b/chasm/lib/scheduler/scheduler_suite_test.go deleted file mode 100644 index b6d3f44f5e1..00000000000 --- a/chasm/lib/scheduler/scheduler_suite_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package scheduler_test - -import ( - "context" - "reflect" - "time" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - persistencespb "go.temporal.io/server/api/persistence/v1" - "go.temporal.io/server/chasm" - "go.temporal.io/server/chasm/lib/scheduler" - "go.temporal.io/server/common/clock" - "go.temporal.io/server/common/log" - "go.temporal.io/server/common/testing/protorequire" - "go.temporal.io/server/common/testing/testlogger" - "go.temporal.io/server/common/testing/testvars" - legacyscheduler "go.temporal.io/server/service/worker/scheduler" - "go.uber.org/mock/gomock" - "google.golang.org/protobuf/types/known/timestamppb" -) - -// schedulerSuite sets up a suite that has a basic CHASM tree ready -// for use with the Scheduler library. -type schedulerSuite struct { - suite.Suite - *require.Assertions - protorequire.ProtoAssertions - - controller *gomock.Controller - nodeBackend *chasm.MockNodeBackend - specProcessor *scheduler.MockSpecProcessor - mockEngine *chasm.MockEngine - node *chasm.Node - - scheduler *scheduler.Scheduler - - registry *chasm.Registry - timeSource *clock.EventTimeSource - nodePathEncoder chasm.NodePathEncoder - logger log.Logger -} - -// SetupSuite initializes the CHASM tree to a default scheduler. -func (s *schedulerSuite) SetupTest() { - s.Assertions = require.New(s.T()) - s.ProtoAssertions = protorequire.New(s.T()) - - s.controller = gomock.NewController(s.T()) - s.specProcessor = scheduler.NewMockSpecProcessor(s.controller) - s.mockEngine = chasm.NewMockEngine(s.controller) - s.logger = testlogger.NewTestLogger(s.T(), testlogger.FailOnExpectedErrorOnly) - s.nodePathEncoder = chasm.DefaultPathEncoder - - s.registry = chasm.NewRegistry(s.logger) - err := s.registry.Register(newTestLibrary(s.logger, s.specProcessor)) - s.NoError(err) - - // Register the Core library as well, which we use for Visibility. - err = s.registry.Register(&chasm.CoreLibrary{}) - s.NoError(err) - - // Advance here, because otherwise ctx.Now().IsZero() will be true. - s.timeSource = clock.NewEventTimeSource() - now := time.Now() - s.timeSource.Update(now) - - // Stub NodeBackend for NewEmptytree - tv := testvars.New(s.T()) - s.nodeBackend = &chasm.MockNodeBackend{ - HandleNextTransitionCount: func() int64 { return 2 }, - HandleGetCurrentVersion: func() int64 { return 1 }, - HandleGetWorkflowKey: tv.Any().WorkflowKey, - HandleIsWorkflow: func() bool { return false }, - HandleCurrentVersionedTransition: func() *persistencespb.VersionedTransition { - return &persistencespb.VersionedTransition{ - NamespaceFailoverVersion: 1, - TransitionCount: 1, - } - }, - } - - s.node = chasm.NewEmptyTree(s.registry, s.timeSource, s.nodeBackend, s.nodePathEncoder, s.logger) - ctx := s.newMutableContext() - s.scheduler, err = scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) - s.node.SetRootComponent(s.scheduler) - - // Advance Generator's high water mark to 'now'. - generator := s.scheduler.Generator.Get(ctx) - generator.LastProcessedTime = timestamppb.New(now) - - // Set up future action times. - futureTime := now.Add(time.Hour) - s.specProcessor.EXPECT().NextTime(s.scheduler, gomock.Any()).Return(legacyscheduler.GetNextTimeResult{ - Next: futureTime, - Nominal: futureTime, - }, nil).MaxTimes(1) - s.specProcessor.EXPECT().NextTime(s.scheduler, gomock.Any()).Return(legacyscheduler.GetNextTimeResult{}, nil).AnyTimes() - - // Allow ProcessTimeRange to be called once during setup. - s.specProcessor.EXPECT().ProcessTimeRange( - gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return(&scheduler.ProcessedTimeRange{ - NextWakeupTime: futureTime, - LastActionTime: now, - }, nil).Times(1) - - _, err = s.node.CloseTransaction() - s.NoError(err) -} - -// hasTask returns true if the given task type was added at the end of the -// transaction with the given visibilityTime. -func (s *schedulerSuite) hasTask(task any, visibilityTime time.Time) bool { - taskType := reflect.TypeOf(task) - for _, tasks := range s.nodeBackend.TasksByCategory { - for _, task := range tasks { - if reflect.TypeOf(task) == taskType && - task.GetVisibilityTime().Equal(visibilityTime) { - return true - } - } - } - - return false -} - -func (s *schedulerSuite) newMutableContext() chasm.MutableContext { - return chasm.NewMutableContext(context.Background(), s.node) -} - -func (s *schedulerSuite) newEngineContext() context.Context { - return chasm.NewEngineContext(context.Background(), s.mockEngine) -} - -func (s *schedulerSuite) ExpectReadComponent(ctx chasm.Context, returnedComponent chasm.Component) { - s.mockEngine.EXPECT().ReadComponent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ context.Context, _ chasm.ComponentRef, readFn func(chasm.Context, chasm.Component) error, _ ...chasm.TransitionOption) error { - return readFn(ctx, returnedComponent) - }).Times(1) -} - -func (s *schedulerSuite) ExpectUpdateComponent(ctx chasm.MutableContext, componentToUpdate chasm.Component) { - s.mockEngine.EXPECT().UpdateComponent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ context.Context, _ chasm.ComponentRef, updateFn func(chasm.MutableContext, chasm.Component) error, _ ...chasm.TransitionOption) ([]byte, error) { - err := updateFn(ctx, componentToUpdate) - return nil, err - }).Times(1) -} diff --git a/chasm/lib/scheduler/scheduler_tasks.go b/chasm/lib/scheduler/scheduler_tasks.go index a0d92f41460..2dbb4c6956b 100644 --- a/chasm/lib/scheduler/scheduler_tasks.go +++ b/chasm/lib/scheduler/scheduler_tasks.go @@ -1,30 +1,44 @@ package scheduler import ( + "context" + "errors" + "fmt" "time" + commonpb "go.temporal.io/api/common/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/server/api/historyservice/v1" + schedulespb "go.temporal.io/server/api/schedule/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/resource" "go.uber.org/fx" + "google.golang.org/protobuf/types/known/timestamppb" ) -type SchedulerIdleTaskExecutorOptions struct { +type SchedulerIdleTaskHandlerOptions struct { fx.In Config *Config } -type SchedulerIdleTaskExecutor struct { +type SchedulerIdleTaskHandler struct { + chasm.PureTaskHandlerBase config *Config } -func NewSchedulerIdleTaskExecutor(opts SchedulerIdleTaskExecutorOptions) *SchedulerIdleTaskExecutor { - return &SchedulerIdleTaskExecutor{ +func NewSchedulerIdleTaskHandler(opts SchedulerIdleTaskHandlerOptions) *SchedulerIdleTaskHandler { + return &SchedulerIdleTaskHandler{ config: opts.Config, } } -func (r *SchedulerIdleTaskExecutor) Execute( +func (r *SchedulerIdleTaskHandler) Execute( ctx chasm.MutableContext, scheduler *Scheduler, _ chasm.TaskAttributes, @@ -34,7 +48,7 @@ func (r *SchedulerIdleTaskExecutor) Execute( return nil } -func (r *SchedulerIdleTaskExecutor) Validate( +func (r *SchedulerIdleTaskHandler) Validate( ctx chasm.Context, scheduler *Scheduler, taskAttrs chasm.TaskAttributes, @@ -51,3 +65,212 @@ func (r *SchedulerIdleTaskExecutor) Validate( return !scheduler.Closed, nil } + +type SchedulerCallbacksTaskHandlerOptions struct { + fx.In + + Config *Config + HistoryClient resource.HistoryClient + FrontendClient workflowservice.WorkflowServiceClient +} + +type SchedulerCallbacksTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*schedulerpb.SchedulerCallbacksTask] + config *Config + historyClient resource.HistoryClient + frontendClient workflowservice.WorkflowServiceClient +} + +func NewSchedulerCallbacksTaskHandler(opts SchedulerCallbacksTaskHandlerOptions) *SchedulerCallbacksTaskHandler { + return &SchedulerCallbacksTaskHandler{ + config: opts.Config, + historyClient: opts.HistoryClient, + frontendClient: opts.FrontendClient, + } +} + +// watchResult holds the outcome of watchRunningStart for a single BufferedStart. +// A nil completed field means the callback was successfully attached and the +// workflow is still running. +type watchResult struct { + completed *schedulespb.CompletedResult +} + +func (r *SchedulerCallbacksTaskHandler) Execute( + ctx context.Context, + schedulerRef chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *schedulerpb.SchedulerCallbacksTask, +) error { + var scheduler *Scheduler + var starts []*schedulespb.BufferedStart + var callback *commonpb.Callback + + // Read scheduler state and generate the Nexus callback token. + _, err := chasm.ReadComponent( + ctx, + schedulerRef, + func(s *Scheduler, ctx chasm.Context, _ any) (struct{}, error) { + scheduler = &Scheduler{ + SchedulerState: common.CloneProto(s.SchedulerState), + } + + invoker := s.Invoker.Get(ctx) + for _, start := range invoker.BufferedStarts { + if needsCallback(start) { + starts = append(starts, common.CloneProto(start)) + } + } + + cb, err := chasm.GenerateNexusCallback(ctx, s) + if err != nil { + return struct{}{}, err + } + callback = common.CloneProto(cb) + + return struct{}{}, nil + }, + nil, + ) + if err != nil { + return fmt.Errorf("failed to read component: %w", err) + } + + // Attach callbacks and check workflow status. + results := make(map[string]*watchResult, len(starts)) + for _, start := range starts { + result, err := r.watchRunningStart(ctx, scheduler, start, callback) + if err != nil { + return err + } + results[start.RequestId] = result + } + + // Apply results to the invoker's BufferedStarts. + _, _, err = chasm.UpdateComponent( + ctx, + schedulerRef, + func(s *Scheduler, ctx chasm.MutableContext, _ any) (chasm.NoValue, error) { + invoker := s.Invoker.Get(ctx) + for _, start := range invoker.BufferedStarts { + if result, ok := results[start.RequestId]; ok { + start.HasCallback = true + if result.completed != nil { + start.Completed = result.completed + } + } + } + return nil, nil + }, + nil, + ) + if err != nil { + return fmt.Errorf("failed to update component state: %w", err) + } + + return nil +} + +// watchRunningStart will attach a Nexus completion callback to a running +// BufferedStart. If the start's workflow has already closed, the start is updated +// to indicate it has completed. Intended for migration/anti-entropy cases. +func (r *SchedulerCallbacksTaskHandler) watchRunningStart( + ctx context.Context, + scheduler *Scheduler, + start *schedulespb.BufferedStart, + callback *commonpb.Callback, +) (*watchResult, error) { + // Describe the workflow to ensure it exists and is still running. + descResp, err := r.historyClient.DescribeWorkflowExecution(ctx, &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: scheduler.NamespaceId, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: scheduler.Namespace, + Execution: &commonpb.WorkflowExecution{ + WorkflowId: start.WorkflowId, + RunId: start.RunId, + }, + }, + }) + if err != nil { + var notFoundErr *serviceerror.NotFound + if errors.As(err, ¬FoundErr) { + return &watchResult{ + completed: &schedulespb.CompletedResult{ + Status: enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, + CloseTime: timestamppb.Now(), + }, + }, nil + } + return nil, err + } + + wfInfo := descResp.GetWorkflowExecutionInfo() + wfProgressing := wfInfo.GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING || + wfInfo.GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_PAUSED + + if !wfProgressing { + return &watchResult{ + completed: &schedulespb.CompletedResult{ + Status: wfInfo.GetStatus(), + CloseTime: wfInfo.GetCloseTime(), + }, + }, nil + } + + // Workflow is still running. Attach a Nexus completion callback by issuing + // a StartWorkflowExecution with USE_EXISTING conflict policy. REJECT_DUPLICATE + // reuse policy prevents accidentally starting a new workflow if the original + // completes between the describe and this call. + requestSpec := scheduler.GetSchedule().GetAction().GetStartWorkflow() + + _, err = r.frontendClient.StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + Namespace: scheduler.Namespace, + WorkflowId: start.WorkflowId, + RequestId: start.RequestId, + Identity: scheduler.identity(), + WorkflowType: requestSpec.GetWorkflowType(), + TaskQueue: requestSpec.GetTaskQueue(), + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, + WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, + CompletionCallbacks: []*commonpb.Callback{callback}, + OnConflictOptions: &workflowpb.OnConflictOptions{ + AttachRequestId: true, + AttachCompletionCallbacks: true, + }, + }) + if err != nil { + // WorkflowExecutionAlreadyStarted: workflow completed between describe + // and this attach call (REJECT_DUPLICATE rejects completed workflows). + if isAlreadyStartedError(err) { + return &watchResult{ + completed: &schedulespb.CompletedResult{ + Status: enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, + CloseTime: timestamppb.Now(), + }, + }, nil + } + return nil, err + } + + // Callback attached successfully. + return &watchResult{}, nil +} + +func (r *SchedulerCallbacksTaskHandler) Validate( + ctx chasm.Context, + scheduler *Scheduler, + taskAttrs chasm.TaskAttributes, + task *schedulerpb.SchedulerCallbacksTask, +) (bool, error) { + invoker := scheduler.Invoker.Get(ctx) + for _, start := range invoker.BufferedStarts { + if needsCallback(start) { + return true, nil + } + } + return false, nil +} + +func needsCallback(start *schedulespb.BufferedStart) bool { + return !start.HasCallback && start.GetRunId() != "" && start.GetCompleted() == nil +} diff --git a/chasm/lib/scheduler/scheduler_test.go b/chasm/lib/scheduler/scheduler_test.go index b3d8e0332a5..0aa537a1026 100644 --- a/chasm/lib/scheduler/scheduler_test.go +++ b/chasm/lib/scheduler/scheduler_test.go @@ -1,9 +1,21 @@ package scheduler_test import ( + "context" "testing" + "time" "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + schedulepb "go.temporal.io/api/schedule/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + workflowpb "go.temporal.io/api/workflow/v1" + "go.temporal.io/api/workflowservice/v1" + schedulespb "go.temporal.io/server/api/schedule/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/scheduler" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common/testing/protorequire" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -27,3 +39,276 @@ func TestListInfo(t *testing.T) { require.NotEmpty(t, listInfo.FutureActionTimes) require.Equal(t, expectedFutureTimes, listInfo.FutureActionTimes) } + +func TestCreateSchedulerFromMigration(t *testing.T) { + now := time.Now().UTC() + _, _, node := setupSchedulerForTest(t) + + req := &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: namespaceID, + State: &schedulerpb.SchedulerMigrationState{ + SchedulerState: &schedulerpb.SchedulerState{ + Schedule: defaultSchedule(), + Info: &schedulepb.ScheduleInfo{ActionCount: 10}, + Namespace: namespace, + NamespaceId: namespaceID, + ScheduleId: scheduleID, + ConflictToken: 42, + }, + GeneratorState: &schedulerpb.GeneratorState{ + LastProcessedTime: timestamppb.New(now), + }, + InvokerState: &schedulerpb.InvokerState{ + BufferedStarts: []*schedulespb.BufferedStart{ + { + NominalTime: timestamppb.New(now), + ActualTime: timestamppb.New(now), + OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_SKIP, + RequestId: "req-1", + WorkflowId: "wf-1", + }, + { + NominalTime: timestamppb.New(now.Add(time.Minute)), + ActualTime: timestamppb.New(now.Add(time.Minute)), + OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, + RequestId: "req-2", + WorkflowId: "wf-2", + }, + }, + }, + Backfillers: map[string]*schedulerpb.BackfillerState{ + "bf-1": { + BackfillId: "bf-1", + Request: &schedulerpb.BackfillerState_BackfillRequest{ + BackfillRequest: &schedulepb.BackfillRequest{ + StartTime: timestamppb.New(now.Add(-time.Hour)), + EndTime: timestamppb.New(now), + OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, + }, + }, + }, + }, + LastCompletionResult: &schedulerpb.LastCompletionResult{ + Success: &commonpb.Payload{Data: []byte("result-data")}, + }, + SearchAttributes: map[string]*commonpb.Payload{ + "CustomAttr": {Data: []byte("attr-value")}, + }, + Memo: map[string]*commonpb.Payload{ + "MemoKey": {Data: []byte("memo-value")}, + }, + }, + } + + ctx := chasm.NewMutableContext(context.Background(), node) + sched, err := scheduler.CreateSchedulerFromMigration(ctx, req) + require.NoError(t, err) + + // Scheduler state + require.Equal(t, namespace, sched.Namespace) + require.Equal(t, namespaceID, sched.NamespaceId) + require.Equal(t, scheduleID, sched.ScheduleId) + require.Equal(t, int64(42), sched.ConflictToken) + require.False(t, sched.Closed) + + // Generator + generator := sched.Generator.Get(ctx) + require.Equal(t, now, generator.LastProcessedTime.AsTime()) + + // Invoker buffered starts + invoker := sched.Invoker.Get(ctx) + require.Len(t, invoker.BufferedStarts, 2) + require.Equal(t, "req-1", invoker.BufferedStarts[0].RequestId) + require.Equal(t, "req-2", invoker.BufferedStarts[1].RequestId) + + // Backfillers + require.Len(t, sched.Backfillers, 1) + bf := sched.Backfillers["bf-1"].Get(ctx) + require.Equal(t, "bf-1", bf.BackfillId) + + // Last completion result + lastResult := sched.LastCompletionResult.Get(ctx) + require.Equal(t, []byte("result-data"), lastResult.Success.Data) + + require.NoError(t, node.SetRootComponent(sched)) + _, err = node.CloseTransaction() + require.NoError(t, err) +} + +func TestUpdate_WithMemo(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + memoValue := &commonpb.Payload{Data: []byte("test-value")} + + _, err := sched.Update(ctx, &schedulerpb.UpdateScheduleRequest{ + NamespaceId: namespaceID, + FrontendRequest: &workflowservice.UpdateScheduleRequest{ + Namespace: namespace, + ScheduleId: scheduleID, + Schedule: defaultSchedule(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{"key1": memoValue}, + }, + }, + }) + require.NoError(t, err) + + visibility := sched.Visibility.Get(ctx) + memo := visibility.CustomMemo(ctx) + protorequire.ProtoEqual(t, memoValue, memo["key1"]) +} + +func TestUpdate_WithNilMemo(t *testing.T) { + sched, ctx, node := setupSchedulerForTest(t) + + // Set initial memo. + visibility := sched.Visibility.Get(ctx) + visibility.MergeCustomMemo(ctx, map[string]*commonpb.Payload{ + "existing": {Data: []byte("value")}, + }) + _, err := node.CloseTransaction() + require.NoError(t, err) + + // Update without memo (nil) should preserve existing memo. + ctx = chasm.NewMutableContext(context.Background(), node) + _, err = sched.Update(ctx, &schedulerpb.UpdateScheduleRequest{ + NamespaceId: namespaceID, + FrontendRequest: &workflowservice.UpdateScheduleRequest{ + Namespace: namespace, + ScheduleId: scheduleID, + Schedule: defaultSchedule(), + }, + }) + require.NoError(t, err) + + visibility = sched.Visibility.Get(ctx) + memo := visibility.CustomMemo(ctx) + protorequire.ProtoEqual(t, &commonpb.Payload{Data: []byte("value")}, memo["existing"]) +} + +func TestUpdate_MemoReplaceSemantics(t *testing.T) { + sched, ctx, node := setupSchedulerForTest(t) + + // Set initial memo with keys A and B. + visibility := sched.Visibility.Get(ctx) + visibility.MergeCustomMemo(ctx, map[string]*commonpb.Payload{ + "A": {Data: []byte("1")}, + "B": {Data: []byte("2")}, + }) + _, err := node.CloseTransaction() + require.NoError(t, err) + + // Update with only C: should fully replace memo (A and B are gone). + ctx = chasm.NewMutableContext(context.Background(), node) + _, err = sched.Update(ctx, &schedulerpb.UpdateScheduleRequest{ + NamespaceId: namespaceID, + FrontendRequest: &workflowservice.UpdateScheduleRequest{ + Namespace: namespace, + ScheduleId: scheduleID, + Schedule: defaultSchedule(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "C": {Data: []byte("3")}, + }, + }, + }, + }) + require.NoError(t, err) + + visibility = sched.Visibility.Get(ctx) + memo := visibility.CustomMemo(ctx) + require.Nil(t, memo["A"], "A should be gone after replace") + require.Nil(t, memo["B"], "B should be gone after replace") + protorequire.ProtoEqual(t, &commonpb.Payload{Data: []byte("3")}, memo["C"]) + + // Update with empty memo: should clear all memo fields. + _, err = node.CloseTransaction() + require.NoError(t, err) + ctx = chasm.NewMutableContext(context.Background(), node) + _, err = sched.Update(ctx, &schedulerpb.UpdateScheduleRequest{ + NamespaceId: namespaceID, + FrontendRequest: &workflowservice.UpdateScheduleRequest{ + Namespace: namespace, + ScheduleId: scheduleID, + Schedule: defaultSchedule(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{}, + }, + }, + }) + require.NoError(t, err) + + visibility = sched.Visibility.Get(ctx) + memo = visibility.CustomMemo(ctx) + require.Empty(t, memo, "memo should be empty after replace with empty map") +} + +func TestCreateSchedulerFromMigration_EmptyState(t *testing.T) { + _, _, node := setupSchedulerForTest(t) + + req := &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: namespaceID, + State: &schedulerpb.SchedulerMigrationState{ + SchedulerState: &schedulerpb.SchedulerState{ + Schedule: defaultSchedule(), + Info: &schedulepb.ScheduleInfo{}, + Namespace: namespace, + NamespaceId: namespaceID, + ScheduleId: scheduleID, + ConflictToken: 1, + }, + GeneratorState: &schedulerpb.GeneratorState{}, + InvokerState: &schedulerpb.InvokerState{}, + }, + } + + ctx := chasm.NewMutableContext(context.Background(), node) + sched, err := scheduler.CreateSchedulerFromMigration(ctx, req) + require.NoError(t, err) + + require.Equal(t, int64(1), sched.ConflictToken) + require.Empty(t, sched.Backfillers) + + invoker := sched.Invoker.Get(ctx) + require.Empty(t, invoker.BufferedStarts) + + require.NoError(t, node.SetRootComponent(sched)) + _, err = node.CloseTransaction() + require.NoError(t, err) +} + +func TestContextMetadata(t *testing.T) { + t.Run("returns workflow type and task queue", func(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + sched.Schedule.Action = &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowType: &commonpb.WorkflowType{Name: "my-workflow"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: "my-task-queue"}, + }, + }, + } + + md := sched.ContextMetadata(ctx) + require.Equal(t, map[string]string{ + "workflow-type": "my-workflow", + "workflow-task-queue": "my-task-queue", + }, md) + }) + + t.Run("returns only workflow type when task queue is empty", func(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + // defaultSchedule sets WorkflowType but no TaskQueue + md := sched.ContextMetadata(ctx) + require.Equal(t, map[string]string{ + "workflow-type": "scheduled-wf-type", + }, md) + }) + + t.Run("returns nil when action is empty", func(t *testing.T) { + sched, ctx, _ := setupSchedulerForTest(t) + sched.Schedule.Action = nil + + md := sched.ContextMetadata(ctx) + require.Nil(t, md) + }) +} diff --git a/chasm/lib/scheduler/sentinel_test.go b/chasm/lib/scheduler/sentinel_test.go new file mode 100644 index 00000000000..b5e31d77000 --- /dev/null +++ b/chasm/lib/scheduler/sentinel_test.go @@ -0,0 +1,96 @@ +package scheduler_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/scheduler" + "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +func TestNewSentinel(t *testing.T) { + sentinel, ctx, _ := setupSentinelForTest(t) + + require.True(t, sentinel.IsSentinel()) + require.NotNil(t, sentinel.Info.GetCreateTime()) + require.False(t, sentinel.Info.CreateTime.AsTime().IsZero()) + + // Sentinels should have no Visibility component, which prevents them from + // appearing in ListSchedules results. + _, ok := sentinel.Visibility.TryGet(ctx) + require.False(t, ok) +} + +func TestSentinelIdleTask_Validate_Valid(t *testing.T) { + sentinel, ctx, _ := setupSentinelForTest(t) + + executor := scheduler.NewSchedulerIdleTaskHandler(scheduler.SchedulerIdleTaskHandlerOptions{ + Config: defaultConfig(), + }) + + task := &schedulerpb.SchedulerIdleTask{ + IdleTimeTotal: durationpb.New(scheduler.SentinelIdleTime), + } + taskAttrs := chasm.TaskAttributes{ + ScheduledTime: sentinel.Info.CreateTime.AsTime().Add(scheduler.SentinelIdleTime), + } + + isValid, err := executor.Validate(ctx, sentinel, taskAttrs, task) + require.NoError(t, err) + require.True(t, isValid) +} + +func TestSentinelIdleTask_Validate_InvalidAfterClosed(t *testing.T) { + sentinel, ctx, _ := setupSentinelForTest(t) + sentinel.Closed = true + + executor := scheduler.NewSchedulerIdleTaskHandler(scheduler.SchedulerIdleTaskHandlerOptions{ + Config: defaultConfig(), + }) + + task := &schedulerpb.SchedulerIdleTask{ + IdleTimeTotal: durationpb.New(scheduler.SentinelIdleTime), + } + taskAttrs := chasm.TaskAttributes{ + ScheduledTime: sentinel.Info.CreateTime.AsTime().Add(scheduler.SentinelIdleTime), + } + + isValid, err := executor.Validate(ctx, sentinel, taskAttrs, task) + require.NoError(t, err) + require.False(t, isValid) +} + +func TestSentinelIdleTask_Validate_MismatchedScheduledTime(t *testing.T) { + sentinel, ctx, _ := setupSentinelForTest(t) + + executor := scheduler.NewSchedulerIdleTaskHandler(scheduler.SchedulerIdleTaskHandlerOptions{ + Config: defaultConfig(), + }) + + task := &schedulerpb.SchedulerIdleTask{ + IdleTimeTotal: durationpb.New(scheduler.SentinelIdleTime), + } + taskAttrs := chasm.TaskAttributes{ + ScheduledTime: sentinel.Info.CreateTime.AsTime().Add(99 * time.Hour), + } + + isValid, err := executor.Validate(ctx, sentinel, taskAttrs, task) + require.NoError(t, err) + require.False(t, isValid) +} + +func TestSentinelIdleTask_Execute(t *testing.T) { + sentinel, ctx, _ := setupSentinelForTest(t) + + executor := scheduler.NewSchedulerIdleTaskHandler(scheduler.SchedulerIdleTaskHandlerOptions{ + Config: defaultConfig(), + }) + + require.False(t, sentinel.Closed) + err := executor.Execute(ctx, sentinel, chasm.TaskAttributes{}, &schedulerpb.SchedulerIdleTask{}) + require.NoError(t, err) + require.True(t, sentinel.Closed) +} diff --git a/chasm/lib/scheduler/spec_processor_test.go b/chasm/lib/scheduler/spec_processor_test.go index 5a1c80a278b..e4431b5ecd2 100644 --- a/chasm/lib/scheduler/spec_processor_test.go +++ b/chasm/lib/scheduler/spec_processor_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/require" enumspb "go.temporal.io/api/enums/v1" schedulepb "go.temporal.io/api/schedule/v1" "go.temporal.io/server/chasm" @@ -18,26 +18,13 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -type ( - testSpecProcessor struct { - scheduler.SpecProcessor - mockMetrics *metrics.MockHandler - } - - specProcessorSuite struct { - schedulerSuite - processor *testSpecProcessor - } -) - -func TestSpecProcessorSuite(t *testing.T) { - suite.Run(t, &specProcessorSuite{}) -} -func (s *specProcessorSuite) SetupTest() { - s.schedulerSuite.SetupTest() - s.processor = newTestSpecProcessor(s.controller) +// testSpecProcessor wraps a real SpecProcessor for testing. +type testSpecProcessor struct { + scheduler.SpecProcessor + mockMetrics *metrics.MockHandler } +// newTestSpecProcessor creates a real SpecProcessor for tests that need actual scheduling logic. func newTestSpecProcessor(ctrl *gomock.Controller) *testSpecProcessor { mockMetrics := metrics.NewMockHandler(ctrl) mockMetrics.EXPECT().Counter(gomock.Any()).Return(metrics.NoopCounterMetricFunc).AnyTimes() @@ -58,10 +45,13 @@ func newTestSpecProcessor(ctrl *gomock.Controller) *testSpecProcessor { } } -func (s *specProcessorSuite) TestProcessTimeRange_LimitedActions() { - ctx := chasm.NewMutableContext(context.Background(), s.node) +func TestProcessTimeRange_LimitedActions(t *testing.T) { + env := newTestEnv(t) + ctx := chasm.NewMutableContext(context.Background(), env.Node) sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) + require.NoError(t, err) + processor := newTestSpecProcessor(env.Ctrl) + end := time.Now() start := end.Add(-defaultInterval) @@ -69,33 +59,35 @@ func (s *specProcessorSuite) TestProcessTimeRange_LimitedActions() { sched.Schedule.State.LimitedActions = true sched.Schedule.State.RemainingActions = 1 - res, err := s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(1, len(res.BufferedStarts)) + res, err := processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 1) // When a schedule has an action limit that has been exceeded, we don't bother // buffering additional actions. sched.Schedule.State.RemainingActions = 0 - res, err = s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(0, len(res.BufferedStarts)) + res, err = processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Empty(t, res.BufferedStarts) // Manual starts should always be allowed. backfillID := "backfill" - res, err = s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), backfillID, true, nil) - s.NoError(err) - s.Equal(1, len(res.BufferedStarts)) + res, err = processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), backfillID, true, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 1) bufferedStart := res.BufferedStarts[0] - s.True(bufferedStart.Manual) - s.Contains(bufferedStart.RequestId, backfillID) - s.NotEmpty(bufferedStart.WorkflowId) + require.True(t, bufferedStart.Manual) + require.Contains(t, bufferedStart.RequestId, backfillID) + require.NotEmpty(t, bufferedStart.WorkflowId) } -func (s *specProcessorSuite) TestProcessTimeRange_UpdateAfterHighWatermark() { - ctx := chasm.NewMutableContext(context.Background(), s.node) +func TestProcessTimeRange_UpdateAfterHighWatermark(t *testing.T) { + env := newTestEnv(t) + ctx := chasm.NewMutableContext(context.Background(), env.Node) sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) + require.NoError(t, err) + processor := newTestSpecProcessor(env.Ctrl) // Below window would give 6 actions, but the update time halves that. base := time.Now() @@ -105,15 +97,16 @@ func (s *specProcessorSuite) TestProcessTimeRange_UpdateAfterHighWatermark() { // Actions taking place in time before the last update time should be dropped. sched.Info.UpdateTime = timestamppb.Now() - res, err := s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(3, len(res.BufferedStarts)) + res, err := processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 3) } // Tests that an update between a nominal time and jittered time for a start, that doesn't // modify that start, will still start it. -func (s *specProcessorSuite) TestProcessTimeRange_UpdateBetweenNominalAndJitter() { - ctx := chasm.NewMutableContext(context.Background(), s.node) +func TestProcessTimeRange_UpdateBetweenNominalAndJitter(t *testing.T) { + env := newTestEnv(t) + ctx := chasm.NewMutableContext(context.Background(), env.Node) schedule := defaultSchedule() schedule.Policies.CatchupWindow = durationpb.New(2 * time.Hour) schedule.Spec = &schedulepb.ScheduleSpec{ @@ -123,7 +116,8 @@ func (s *specProcessorSuite) TestProcessTimeRange_UpdateBetweenNominalAndJitter( Jitter: durationpb.New(1 * time.Hour), } sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, schedule, nil) - s.NoError(err) + require.NoError(t, err) + processor := newTestSpecProcessor(env.Ctrl) // Generate a start with a long jitter period. base := time.Date(2025, 03, 31, 1, 0, 0, 0, time.UTC) @@ -135,36 +129,41 @@ func (s *specProcessorSuite) TestProcessTimeRange_UpdateBetweenNominalAndJitter( sched.Info.UpdateTime = timestamppb.New(updateTime) // A single start should have been buffered. - res, err := s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(1, len(res.BufferedStarts)) + res, err := processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 1) // Validates the test case. actualTime := res.BufferedStarts[0].GetActualTime().AsTime() nominalTime := res.BufferedStarts[0].GetNominalTime().AsTime() - s.True(nominalTime.Before(updateTime)) - s.True(actualTime.After(updateTime)) + require.True(t, nominalTime.Before(updateTime)) + require.True(t, actualTime.After(updateTime)) } -func (s *specProcessorSuite) TestProcessTimeRange_CatchupWindow() { - ctx := chasm.NewMutableContext(context.Background(), s.node) +func TestProcessTimeRange_CatchupWindow(t *testing.T) { + env := newTestEnv(t) + ctx := chasm.NewMutableContext(context.Background(), env.Node) sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) + require.NoError(t, err) + processor := newTestSpecProcessor(env.Ctrl) // When an action would fall outside of the schedule's catchup window, it should // be dropped. end := time.Now() start := end.Add(-defaultCatchupWindow * 2) - res, err := s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(5, len(res.BufferedStarts)) + res, err := processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 5) } -func (s *specProcessorSuite) TestProcessTimeRange_Limit() { - ctx := chasm.NewMutableContext(context.Background(), s.node) +func TestProcessTimeRange_Limit(t *testing.T) { + env := newTestEnv(t) + ctx := chasm.NewMutableContext(context.Background(), env.Node) sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) + require.NoError(t, err) + processor := newTestSpecProcessor(env.Ctrl) + end := time.Now() start := end.Add(-defaultInterval * 5) @@ -173,71 +172,77 @@ func (s *specProcessorSuite) TestProcessTimeRange_Limit() { // exhausted. limit := 2 - res, err := s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, &limit) - s.NoError(err) - s.Equal(2, len(res.BufferedStarts)) - s.Equal(0, limit) + res, err := processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, &limit) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 2) + require.Equal(t, 0, limit) } -func (s *specProcessorSuite) TestProcessTimeRange_OverlapPolicy() { - ctx := chasm.NewMutableContext(context.Background(), s.node) +func TestProcessTimeRange_OverlapPolicy(t *testing.T) { + env := newTestEnv(t) + ctx := chasm.NewMutableContext(context.Background(), env.Node) sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) + require.NoError(t, err) + processor := newTestSpecProcessor(env.Ctrl) + end := time.Now() start := end.Add(-defaultInterval * 5) // Check that a default overlap policy (SKIP) is applied, even when left unspecified. sched.Schedule.Policies.OverlapPolicy = enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED - res, err := s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(5, len(res.BufferedStarts)) + res, err := processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 5) for _, b := range res.BufferedStarts { - s.Equal(enumspb.SCHEDULE_OVERLAP_POLICY_SKIP, b.OverlapPolicy) + require.Equal(t, enumspb.SCHEDULE_OVERLAP_POLICY_SKIP, b.OverlapPolicy) } // Check that a specified overlap policy is applied. overlapPolicy := enumspb.SCHEDULE_OVERLAP_POLICY_BUFFER_ALL sched.Schedule.Policies.OverlapPolicy = overlapPolicy - res, err = s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(5, len(res.BufferedStarts)) + res, err = processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 5) for _, b := range res.BufferedStarts { - s.Equal(overlapPolicy, b.OverlapPolicy) + require.Equal(t, overlapPolicy, b.OverlapPolicy) } } -func (s *specProcessorSuite) TestProcessTimeRange_Basic() { - ctx := chasm.NewMutableContext(context.Background(), s.node) +func TestProcessTimeRange_Basic(t *testing.T) { + env := newTestEnv(t) + ctx := chasm.NewMutableContext(context.Background(), env.Node) sched, err := scheduler.NewScheduler(ctx, namespace, namespaceID, scheduleID, defaultSchedule(), nil) - s.NoError(err) + require.NoError(t, err) + processor := newTestSpecProcessor(env.Ctrl) + end := time.Now() start := end.Add(-defaultInterval * 5) // Validate returned BufferedStarts for unique action times and request IDs. - res, err := s.processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) - s.NoError(err) - s.Equal(5, len(res.BufferedStarts)) + res, err := processor.ProcessTimeRange(sched, start, end, enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, sched.WorkflowID(), "", false, nil) + require.NoError(t, err) + require.Len(t, res.BufferedStarts, 5) uniqueTimes := make(map[time.Time]bool) uniqueIDs := make(map[string]bool) for _, b := range res.BufferedStarts { - s.False(b.Manual) + require.False(t, b.Manual) actualTime := b.ActualTime.AsTime() - s.False(uniqueTimes[actualTime]) - s.False(uniqueIDs[b.RequestId]) + require.False(t, uniqueTimes[actualTime]) + require.False(t, uniqueIDs[b.RequestId]) uniqueTimes[actualTime] = true uniqueIDs[b.RequestId] = true // Validate WorkflowId format: scheduled-wf-{RFC3339 timestamp} nominalTime := b.NominalTime.AsTime() expectedTimestamp := nominalTime.Truncate(time.Second).Format(time.RFC3339) - s.Equal("scheduled-wf-"+expectedTimestamp, b.WorkflowId) + require.Equal(t, "scheduled-wf-"+expectedTimestamp, b.WorkflowId) } // Validate next wakeup time. - s.GreaterOrEqual(res.NextWakeupTime, end) - s.Less(res.NextWakeupTime, end.Add(defaultInterval*2)) + require.GreaterOrEqual(t, res.NextWakeupTime, end) + require.Less(t, res.NextWakeupTime, end.Add(defaultInterval*2)) } diff --git a/chasm/lib/tests/gen/testspb/v1/message.pb.go b/chasm/lib/tests/gen/testspb/v1/message.pb.go index aae3768197c..9bd7f2b343c 100644 --- a/chasm/lib/tests/gen/testspb/v1/message.pb.go +++ b/chasm/lib/tests/gen/testspb/v1/message.pb.go @@ -30,6 +30,7 @@ type TestPayloadStore struct { // (-- api-linter: core::0142::time-field-type=disabled --) ExpirationTimes map[string]*timestamppb.Timestamp `protobuf:"bytes,3,rep,name=expiration_times,json=expirationTimes,proto3" json:"expiration_times,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` Closed bool `protobuf:"varint,4,opt,name=closed,proto3" json:"closed,omitempty"` + Canceled bool `protobuf:"varint,5,opt,name=canceled,proto3" json:"canceled,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -92,6 +93,13 @@ func (x *TestPayloadStore) GetClosed() bool { return false } +func (x *TestPayloadStore) GetCanceled() bool { + if x != nil { + return x.Canceled + } + return false +} + type TestPayloadTTLPureTask struct { state protoimpl.MessageState `protogen:"open.v1"` PayloadKey string `protobuf:"bytes,1,opt,name=payload_key,json=payloadKey,proto3" json:"payload_key,omitempty"` @@ -184,14 +192,15 @@ var File_temporal_server_chasm_lib_tests_proto_v1_message_proto protoreflect.Fil const file_temporal_server_chasm_lib_tests_proto_v1_message_proto_rawDesc = "" + "\n" + - "6temporal/server/chasm/lib/tests/proto/v1/message.proto\x12(temporal.server.chasm.lib.tests.proto.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc6\x02\n" + + "6temporal/server/chasm/lib/tests/proto/v1/message.proto\x12(temporal.server.chasm.lib.tests.proto.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe2\x02\n" + "\x10TestPayloadStore\x12\x1f\n" + "\vtotal_count\x18\x01 \x01(\x03R\n" + "totalCount\x12\x1d\n" + "\n" + "total_size\x18\x02 \x01(\x03R\ttotalSize\x12z\n" + "\x10expiration_times\x18\x03 \x03(\v2O.temporal.server.chasm.lib.tests.proto.v1.TestPayloadStore.ExpirationTimesEntryR\x0fexpirationTimes\x12\x16\n" + - "\x06closed\x18\x04 \x01(\bR\x06closed\x1a^\n" + + "\x06closed\x18\x04 \x01(\bR\x06closed\x12\x1a\n" + + "\bcanceled\x18\x05 \x01(\bR\bcanceled\x1a^\n" + "\x14ExpirationTimesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + "\x05value\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05value:\x028\x01\"9\n" + diff --git a/chasm/lib/tests/gen/testspb/v1/service.pb.go b/chasm/lib/tests/gen/testspb/v1/service.pb.go index 67641ad5216..236ecca1ee7 100644 --- a/chasm/lib/tests/gen/testspb/v1/service.pb.go +++ b/chasm/lib/tests/gen/testspb/v1/service.pb.go @@ -10,6 +10,7 @@ import ( reflect "reflect" unsafe "unsafe" + _ "go.temporal.io/server/api/common/v1" _ "go.temporal.io/server/api/routing/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -26,9 +27,9 @@ var File_temporal_server_chasm_lib_tests_proto_v1_service_proto protoreflect.Fil const file_temporal_server_chasm_lib_tests_proto_v1_service_proto_rawDesc = "" + "\n" + - "6temporal/server/chasm/lib/tests/proto/v1/service.proto\x12(temporal.server.chasm.lib.tests.proto.v1\x1a?temporal/server/chasm/lib/tests/proto/v1/request_response.proto\x1a.temporal/server/api/routing/v1/extension.proto2\x8c\x01\n" + - "\vTestService\x12}\n" + - "\x04Test\x125.temporal.server.chasm.lib.tests.proto.v1.TestRequest\x1a6.temporal.server.chasm.lib.tests.proto.v1.TestResponse\"\x06\x92\xc4\x03\x02\b\x01B;Z9go.temporal.io/server/chasm/lib/tests/gen/testspb;testspbb\x06proto3" + "6temporal/server/chasm/lib/tests/proto/v1/service.proto\x12(temporal.server.chasm.lib.tests.proto.v1\x1a?temporal/server/chasm/lib/tests/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\x93\x01\n" + + "\vTestService\x12\x83\x01\n" + + "\x04Test\x125.temporal.server.chasm.lib.tests.proto.v1.TestRequest\x1a6.temporal.server.chasm.lib.tests.proto.v1.TestResponse\"\f\x92\xc4\x03\x02\b\x01\x8a\xb5\x18\x02\b\x01B;Z9go.temporal.io/server/chasm/lib/tests/gen/testspb;testspbb\x06proto3" var file_temporal_server_chasm_lib_tests_proto_v1_service_proto_goTypes = []any{ (*TestRequest)(nil), // 0: temporal.server.chasm.lib.tests.proto.v1.TestRequest diff --git a/chasm/lib/tests/handler.go b/chasm/lib/tests/handler.go index 6be05248d6d..1c5d0d04600 100644 --- a/chasm/lib/tests/handler.go +++ b/chasm/lib/tests/handler.go @@ -28,7 +28,8 @@ type ( } DescribePayloadStoreResponse struct { - State *testspb.TestPayloadStore + State *testspb.TestPayloadStore + ApproximateStateSize int } ClosePayloadStoreRequest struct { @@ -38,6 +39,13 @@ type ( ClosePayloadStoreResponse struct{} + CancelPayloadStoreRequest struct { + NamespaceID namespace.ID + StoreID string + } + + CancelPayloadStoreResponse struct{} + AddPayloadRequest struct { NamespaceID namespace.ID StoreID string @@ -69,6 +77,13 @@ type ( RemovePayloadResponse struct { State *testspb.TestPayloadStore } + + DeletePayloadStoreRequest struct { + NamespaceID namespace.ID + StoreID string + Reason string + Identity string + } ) func NewPayloadStoreHandler( @@ -100,7 +115,7 @@ func DescribePayloadStoreHandler( ctx context.Context, request DescribePayloadStoreRequest, ) (DescribePayloadStoreResponse, error) { - state, err := chasm.ReadComponent( + return chasm.ReadComponent( ctx, chasm.NewComponentRef[*PayloadStore]( chasm.ExecutionKey{ @@ -111,12 +126,6 @@ func DescribePayloadStoreHandler( (*PayloadStore).Describe, request, ) - if err != nil { - return DescribePayloadStoreResponse{}, err - } - return DescribePayloadStoreResponse{ - State: state, - }, nil } func ClosePayloadStoreHandler( @@ -132,6 +141,24 @@ func ClosePayloadStoreHandler( }, ), (*PayloadStore).Close, + nil, + ) + return resp, err +} + +func CancelPayloadStoreHandler( + ctx context.Context, + request CancelPayloadStoreRequest, +) (CancelPayloadStoreResponse, error) { + resp, _, err := chasm.UpdateComponent( + ctx, + chasm.NewComponentRef[*PayloadStore]( + chasm.ExecutionKey{ + NamespaceID: request.NamespaceID.String(), + BusinessID: request.StoreID, + }, + ), + (*PayloadStore).Cancel, request, ) return resp, err @@ -183,6 +210,25 @@ func GetPayloadHandler( }, nil } +func DeletePayloadStoreHandler( + ctx context.Context, + request DeletePayloadStoreRequest, +) error { + return chasm.DeleteExecution[*PayloadStore]( + ctx, + chasm.ExecutionKey{ + NamespaceID: request.NamespaceID.String(), + BusinessID: request.StoreID, + }, + chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: request.Reason, + Identity: request.Identity, + }, + }, + ) +} + func RemovePayloadHandler( ctx context.Context, request RemovePayloadRequest, diff --git a/chasm/lib/tests/library.go b/chasm/lib/tests/library.go index d8f64094763..432dad23967 100644 --- a/chasm/lib/tests/library.go +++ b/chasm/lib/tests/library.go @@ -11,10 +11,20 @@ type ( } ) +const ( + libraryName = "tests" + componentName = "payloadStore" +) + +var ( + Archetype = chasm.FullyQualifiedName(libraryName, componentName) + ArchetypeID = chasm.GenerateTypeID(Archetype) +) + var Library = &library{} func (l *library) Name() string { - return "tests" + return libraryName } func (l *library) NexusServices() []*nexus.Service { @@ -27,7 +37,8 @@ func (l *library) NexusServiceProcessors() []*chasm.NexusServiceProcessor { func (l *library) Components() []*chasm.RegistrableComponent { return []*chasm.RegistrableComponent{ - chasm.NewRegistrableComponent[*PayloadStore]("payloadStore", + chasm.NewRegistrableComponent[*PayloadStore]( + componentName, chasm.WithBusinessIDAlias("PayloadStoreId"), chasm.WithSearchAttributes( PayloadTotalCountSearchAttribute, @@ -35,6 +46,9 @@ func (l *library) Components() []*chasm.RegistrableComponent { ExecutionStatusSearchAttribute, chasm.SearchAttributeTaskQueue, ), + chasm.WithContextValues(map[any]any{ + componentCtxKey: componentCtxVal, + }), ), } } @@ -43,13 +57,11 @@ func (l *library) Tasks() []*chasm.RegistrableTask { return []*chasm.RegistrableTask{ chasm.NewRegistrablePureTask( "payloadTTLPureTask", - &PayloadTTLPureTaskValidator{}, - &PayloadTTLPureTaskExecutor{}, + &PayloadTTLPureTaskHandler{}, ), chasm.NewRegistrableSideEffectTask( "payloadTTLSideEffectTask", - &PayloadTTLSideEffectTaskValidator{}, - &PayloadTTLSideEffectTaskExecutor{}, + &PayloadTTLSideEffectTaskHandler{}, ), } } diff --git a/chasm/lib/tests/payload.go b/chasm/lib/tests/payload.go index e05ee200ff4..4764adfb1d2 100644 --- a/chasm/lib/tests/payload.go +++ b/chasm/lib/tests/payload.go @@ -6,6 +6,7 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/tests/gen/testspb/v1" "go.temporal.io/server/common" + "go.temporal.io/server/common/softassert" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -41,11 +42,22 @@ type ( Payloads chasm.Map[string, *commonpb.Payload] Visibility chasm.Field[*chasm.Visibility] } + + componentContextKey string +) + +const ( + componentCtxKey componentContextKey = "key" + componentCtxVal string = "value" ) func NewPayloadStore( mutableContext chasm.MutableContext, ) (*PayloadStore, error) { + if err := assertContextValue(mutableContext); err != nil { + return nil, err + } + store := &PayloadStore{ State: &testspb.TestPayloadStore{ TotalCount: 0, @@ -60,25 +72,69 @@ func NewPayloadStore( return store, nil } +func assertContextValue(chasmContext chasm.Context) error { + if val := chasmContext.Value(componentCtxKey); val != componentCtxVal { + return softassert.UnexpectedInternalErr( + chasmContext.Logger(), + "registered component key value pair not available in context", + nil, + ) + } + + return nil +} + func (s *PayloadStore) Describe( - _ chasm.Context, + chasmContext chasm.Context, _ DescribePayloadStoreRequest, -) (*testspb.TestPayloadStore, error) { - return common.CloneProto(s.State), nil +) (DescribePayloadStoreResponse, error) { + if err := assertContextValue(chasmContext); err != nil { + return DescribePayloadStoreResponse{}, err + } + + state := common.CloneProto(s.State) + executionInfo := chasmContext.ExecutionInfo() + + return DescribePayloadStoreResponse{ + State: state, + ApproximateStateSize: executionInfo.ApproximateStateSize, + }, nil } func (s *PayloadStore) Close( - _ chasm.MutableContext, - _ ClosePayloadStoreRequest, + chasmContext chasm.MutableContext, + _ chasm.NoValue, ) (ClosePayloadStoreResponse, error) { + if err := assertContextValue(chasmContext); err != nil { + return ClosePayloadStoreResponse{}, err + } + s.State.Closed = true return ClosePayloadStoreResponse{}, nil } +func (s *PayloadStore) Cancel( + _ chasm.MutableContext, + _ CancelPayloadStoreRequest, +) (CancelPayloadStoreResponse, error) { + s.State.Canceled = true + return CancelPayloadStoreResponse{}, nil +} + +func (s *PayloadStore) ContextMetadata(_ chasm.Context) map[string]string { + return map[string]string{ + string(componentCtxKey): componentCtxVal, + } +} + func (s *PayloadStore) AddPayload( mutableContext chasm.MutableContext, request AddPayloadRequest, ) (*testspb.TestPayloadStore, error) { + if err := assertContextValue(mutableContext); err != nil { + return nil, err + } + if _, ok := s.Payloads[request.PayloadKey]; ok { return nil, serviceerror.NewAlreadyExistsf("payload already exists with key: %s", request.PayloadKey) } @@ -106,13 +162,17 @@ func (s *PayloadStore) AddPayload( ) } - return s.Describe(mutableContext, DescribePayloadStoreRequest{}) + return common.CloneProto(s.State), nil } func (s *PayloadStore) GetPayload( chasmContext chasm.Context, key string, ) (*commonpb.Payload, error) { + if err := assertContextValue(chasmContext); err != nil { + return nil, err + } + if field, ok := s.Payloads[key]; ok { return field.Get(chasmContext), nil } @@ -123,6 +183,10 @@ func (s *PayloadStore) RemovePayload( mutableContext chasm.MutableContext, key string, ) (*testspb.TestPayloadStore, error) { + if err := assertContextValue(mutableContext); err != nil { + return nil, err + } + if _, ok := s.Payloads[key]; !ok { return nil, serviceerror.NewNotFoundf("payload not found with key: %s", key) } @@ -134,32 +198,69 @@ func (s *PayloadStore) RemovePayload( delete(s.Payloads, key) delete(s.State.ExpirationTimes, key) - return s.Describe(mutableContext, DescribePayloadStoreRequest{}) + return common.CloneProto(s.State), nil } func (s *PayloadStore) LifecycleState( - _ chasm.Context, + chasmContext chasm.Context, ) chasm.LifecycleState { + if err := assertContextValue(chasmContext); err != nil { + // nolint:forbidigo // Panic here for testing. + panic("registered component key value pair not available in context") + } + + if s.State.Canceled { + return chasm.LifecycleStateFailed + } if s.State.Closed { return chasm.LifecycleStateCompleted } return chasm.LifecycleStateRunning } +func (s *PayloadStore) Terminate( + mutableContext chasm.MutableContext, + _ chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + if err := assertContextValue(mutableContext); err != nil { + return chasm.TerminateComponentResponse{}, err + } + + if _, err := s.Close(mutableContext, nil); err != nil { + return chasm.TerminateComponentResponse{}, err + } + return chasm.TerminateComponentResponse{}, nil +} + // SearchAttributes implements chasm.VisibilitySearchAttributesProvider interface func (s *PayloadStore) SearchAttributes( - ctx chasm.Context, + chasmContext chasm.Context, ) []chasm.SearchAttributeKeyValue { + if err := assertContextValue(chasmContext); err != nil { + // nolint:forbidigo // Panic here for testing. + panic("registered component key value pair not available in context") + } + + status := s.LifecycleState(chasmContext).String() + if s.State.Canceled { + status = "Canceled" + } + return []chasm.SearchAttributeKeyValue{ PayloadTotalCountSearchAttribute.Value(s.State.TotalCount), PayloadTotalSizeSearchAttribute.Value(s.State.TotalSize), - ExecutionStatusSearchAttribute.Value(s.LifecycleState(ctx).String()), + ExecutionStatusSearchAttribute.Value(status), chasm.SearchAttributeTemporalScheduledByID.Value(TestScheduleID), chasm.SearchAttributeTaskQueue.Value(DefaultPayloadStoreTaskQueue), } } // Memo implements chasm.VisibilityMemoProvider interface -func (s *PayloadStore) Memo(_ chasm.Context) proto.Message { +func (s *PayloadStore) Memo(chasmContext chasm.Context) proto.Message { + if err := assertContextValue(chasmContext); err != nil { + // nolint:forbidigo // Panic here for testing. + panic("registered component key value pair not available in context") + } + return s.State } diff --git a/chasm/lib/tests/proto/v1/message.proto b/chasm/lib/tests/proto/v1/message.proto index 118db953883..98f077d4c21 100644 --- a/chasm/lib/tests/proto/v1/message.proto +++ b/chasm/lib/tests/proto/v1/message.proto @@ -2,23 +2,23 @@ syntax = "proto3"; package temporal.server.chasm.lib.tests.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/tests/gen/testspb;testspb"; - import "google/protobuf/timestamp.proto"; +option go_package = "go.temporal.io/server/chasm/lib/tests/gen/testspb;testspb"; + message TestPayloadStore { - int64 total_count = 1; - int64 total_size = 2; - // (-- api-linter: core::0142::time-field-type=disabled --) - map expiration_times = 3; - bool closed = 4; + int64 total_count = 1; + int64 total_size = 2; + // (-- api-linter: core::0142::time-field-type=disabled --) + map expiration_times = 3; + bool closed = 4; + bool canceled = 5; } message TestPayloadTTLPureTask { - string payload_key = 1; + string payload_key = 1; } message TestPayloadTTLSideEffectTask { - string payload_key = 1; + string payload_key = 1; } - diff --git a/chasm/lib/tests/proto/v1/request_response.proto b/chasm/lib/tests/proto/v1/request_response.proto index ba8f0b4697d..43b0a48659b 100644 --- a/chasm/lib/tests/proto/v1/request_response.proto +++ b/chasm/lib/tests/proto/v1/request_response.proto @@ -5,10 +5,10 @@ package temporal.server.chasm.lib.tests.proto.v1; option go_package = "go.temporal.io/server/chasm/lib/tests/gen/testspb;testspb"; message TestRequest { - string request_id = 1; + string request_id = 1; } message TestResponse { - string request_id = 1; - bool has_engine_ctx = 2; + string request_id = 1; + bool has_engine_ctx = 2; } diff --git a/chasm/lib/tests/proto/v1/service.proto b/chasm/lib/tests/proto/v1/service.proto index 5d84064c713..782e4aa27e1 100644 --- a/chasm/lib/tests/proto/v1/service.proto +++ b/chasm/lib/tests/proto/v1/service.proto @@ -2,13 +2,15 @@ syntax = "proto3"; package temporal.server.chasm.lib.tests.proto.v1; -option go_package = "go.temporal.io/server/chasm/lib/tests/gen/testspb;testspb"; - import "chasm/lib/tests/proto/v1/request_response.proto"; +import "temporal/server/api/common/v1/api_category.proto"; import "temporal/server/api/routing/v1/extension.proto"; +option go_package = "go.temporal.io/server/chasm/lib/tests/gen/testspb;testspb"; + service TestService { - rpc Test(TestRequest) returns (TestResponse) { - option (temporal.server.api.routing.v1.routing).random = true; - } + rpc Test(TestRequest) returns (TestResponse) { + option (temporal.server.api.routing.v1.routing).random = true; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } } diff --git a/chasm/lib/tests/tasks.go b/chasm/lib/tests/tasks.go index ab0953830b4..d501a114724 100644 --- a/chasm/lib/tests/tasks.go +++ b/chasm/lib/tests/tasks.go @@ -7,36 +7,36 @@ import ( "go.temporal.io/server/chasm/lib/tests/gen/testspb/v1" ) -type ( - PayloadTTLPureTaskExecutor struct{} - PayloadTTLPureTaskValidator struct{} -) +type PayloadTTLPureTaskHandler struct{ chasm.PureTaskHandlerBase } -func (e *PayloadTTLPureTaskExecutor) Execute( +func (h *PayloadTTLPureTaskHandler) Execute( mutableContext chasm.MutableContext, store *PayloadStore, _ chasm.TaskAttributes, task *testspb.TestPayloadTTLPureTask, ) error { + if err := assertContextValue(mutableContext); err != nil { + return err + } + _, err := store.RemovePayload(mutableContext, task.PayloadKey) return err } -func (v *PayloadTTLPureTaskValidator) Validate( - _ chasm.Context, +func (h *PayloadTTLPureTaskHandler) Validate( + chasmContext chasm.Context, store *PayloadStore, attributes chasm.TaskAttributes, task *testspb.TestPayloadTTLPureTask, ) (bool, error) { - return validateTask(store, attributes, task.PayloadKey) + return validateTask(chasmContext, store, attributes, task.PayloadKey) } -type ( - PayloadTTLSideEffectTaskExecutor struct{} - PayloadTTLSideEffectTaskValidator struct{} -) +type PayloadTTLSideEffectTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*testspb.TestPayloadTTLSideEffectTask] +} -func (e *PayloadTTLSideEffectTaskExecutor) Execute( +func (h *PayloadTTLSideEffectTaskHandler) Execute( ctx context.Context, ref chasm.ComponentRef, _ chasm.TaskAttributes, @@ -51,20 +51,25 @@ func (e *PayloadTTLSideEffectTaskExecutor) Execute( return err } -func (v *PayloadTTLSideEffectTaskValidator) Validate( - _ chasm.Context, +func (h *PayloadTTLSideEffectTaskHandler) Validate( + chasmContext chasm.Context, store *PayloadStore, attributes chasm.TaskAttributes, task *testspb.TestPayloadTTLSideEffectTask, ) (bool, error) { - return validateTask(store, attributes, task.PayloadKey) + return validateTask(chasmContext, store, attributes, task.PayloadKey) } func validateTask( + chasmContext chasm.Context, store *PayloadStore, attributes chasm.TaskAttributes, payloadKey string, ) (bool, error) { + if err := assertContextValue(chasmContext); err != nil { + return false, err + } + expirationTime, ok := store.State.ExpirationTimes[payloadKey] if !ok { return false, nil diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index bf609a40b28..08f915ee24c 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -46,6 +46,18 @@ func (w *Workflow) LifecycleState( return chasm.LifecycleStateRunning } +func (w *Workflow) ContextMetadata(_ chasm.Context) map[string]string { + // TODO: Export workflow metadata from the CHASM workflow root instead of CloseTransaction(). + return nil +} + +func (w *Workflow) Terminate( + _ chasm.MutableContext, + _ chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + return chasm.TerminateComponentResponse{}, serviceerror.NewInternal("workflow root Terminate should not be called") +} + // ProcessCloseCallbacks triggers "WorkflowClosed" callbacks using the CHASM implementation. // It iterates through all callbacks and schedules WorkflowClosed ones that are in STANDBY state. func (w *Workflow) ProcessCloseCallbacks(ctx chasm.MutableContext) error { diff --git a/chasm/library_core.go b/chasm/library_core.go index 696280b61b6..a0b1725cfad 100644 --- a/chasm/library_core.go +++ b/chasm/library_core.go @@ -20,7 +20,6 @@ func (b *CoreLibrary) Tasks() []*RegistrableTask { NewRegistrableSideEffectTask( "visTask", defaultVisibilityTaskHandler, - defaultVisibilityTaskHandler, ), } } diff --git a/chasm/ms_pointer.go b/chasm/ms_pointer.go index ff35e4afe55..95856ae3dd5 100644 --- a/chasm/ms_pointer.go +++ b/chasm/ms_pointer.go @@ -21,5 +21,5 @@ func NewMSPointer(backend NodeBackend) MSPointer { // GetNexusCompletion retrieves the Nexus operation completion data for the given request ID from the underlying mutable state. func (m MSPointer) GetNexusCompletion(ctx Context, requestID string) (nexusrpc.CompleteOperationOptions, error) { - return m.backend.GetNexusCompletion(ctx.getContext(), requestID) + return m.backend.GetNexusCompletion(ctx.goContext(), requestID) } diff --git a/chasm/node_backend_mock.go b/chasm/node_backend_mock.go index 8fbd37e58ba..7f89c612186 100644 --- a/chasm/node_backend_mock.go +++ b/chasm/node_backend_mock.go @@ -18,15 +18,16 @@ import ( // fields (thread-safe). type MockNodeBackend struct { // Optional function overrides. If nil, methods return zero-values. - HandleGetExecutionState func() *persistencespb.WorkflowExecutionState - HandleGetExecutionInfo func() *persistencespb.WorkflowExecutionInfo - HandleGetCurrentVersion func() int64 - HandleNextTransitionCount func() int64 - HandleCurrentVersionedTransition func() *persistencespb.VersionedTransition - HandleGetWorkflowKey func() definition.WorkflowKey - HandleUpdateWorkflowStateStatus func(state enumsspb.WorkflowExecutionState, status enumspb.WorkflowExecutionStatus) (bool, error) - HandleIsWorkflow func() bool - HandleGetNexusCompletion func(ctx context.Context, requestID string) (nexusrpc.CompleteOperationOptions, error) + HandleGetExecutionState func() *persistencespb.WorkflowExecutionState + HandleGetExecutionInfo func() *persistencespb.WorkflowExecutionInfo + HandleGetApproximatePersistedSize func() int + HandleGetCurrentVersion func() int64 + HandleNextTransitionCount func() int64 + HandleCurrentVersionedTransition func() *persistencespb.VersionedTransition + HandleGetWorkflowKey func() definition.WorkflowKey + HandleUpdateWorkflowStateStatus func(state enumsspb.WorkflowExecutionState, status enumspb.WorkflowExecutionStatus) (bool, error) + HandleIsWorkflow func() bool + HandleGetNexusCompletion func(ctx context.Context, requestID string) (nexusrpc.CompleteOperationOptions, error) // Recorded calls (protected by mu). mu sync.Mutex @@ -52,6 +53,13 @@ func (m *MockNodeBackend) GetExecutionInfo() *persistencespb.WorkflowExecutionIn return &persistencespb.WorkflowExecutionInfo{} } +func (m *MockNodeBackend) GetApproximatePersistedSize() int { + if m.HandleGetApproximatePersistedSize != nil { + return m.HandleGetApproximatePersistedSize() + } + return 0 +} + func (m *MockNodeBackend) GetCurrentVersion() int64 { if m.HandleGetCurrentVersion != nil { return m.HandleGetCurrentVersion() diff --git a/chasm/ref.go b/chasm/ref.go index b13d42b6645..11adc1b0ba2 100644 --- a/chasm/ref.go +++ b/chasm/ref.go @@ -51,7 +51,7 @@ type ComponentRef struct { componentPath []string componentInitialVT *persistencespb.VersionedTransition - validationFn func(NodeBackend, Context, Component) error + validationFn func(NodeBackend, Context, Component, *Registry) error } // NewComponentRef creates a new ComponentRef with a registered root component go type. @@ -67,6 +67,19 @@ func NewComponentRef[C Component]( } } +// NewComponentRefByArchetypeID creates a new ComponentRef with a known archetype ID. +// This should only be used by CHASM framework internals. +// CHASM library developers should use [NewComponentRef] instead. +func NewComponentRefByArchetypeID( + executionKey ExecutionKey, + archetypeID ArchetypeID, +) ComponentRef { + return ComponentRef{ + ExecutionKey: executionKey, + archetypeID: archetypeID, + } +} + func (r *ComponentRef) ArchetypeID( registry *Registry, ) (ArchetypeID, error) { diff --git a/chasm/ref_test.go b/chasm/ref_test.go index c3cbc3ffa18..0d1242e5bfa 100644 --- a/chasm/ref_test.go +++ b/chasm/ref_test.go @@ -57,6 +57,22 @@ func (s *componentRefSuite) TestArchetypeID() { s.Equal(rc.componentID, archetypeID) } +func (s *componentRefSuite) TestNewComponentRefByArchetypeID() { + executionKey := ExecutionKey{ + NamespaceID: primitives.NewUUID().String(), + BusinessID: primitives.NewUUID().String(), + RunID: primitives.NewUUID().String(), + } + expectArchetypeID := WorkflowArchetypeID + ref := NewComponentRefByArchetypeID(executionKey, expectArchetypeID) + + s.Equal(executionKey, ref.ExecutionKey) + + archetypeID, err := ref.ArchetypeID(s.registry) + s.NoError(err) + s.Equal(expectArchetypeID, archetypeID) +} + func (s *componentRefSuite) TestSerializeDeserialize() { _, err := DeserializeComponentRef(nil) s.ErrorIs(err, ErrInvalidComponentRef) diff --git a/chasm/registrable_component.go b/chasm/registrable_component.go index 8ceffa23c9a..15b7f659e41 100644 --- a/chasm/registrable_component.go +++ b/chasm/registrable_component.go @@ -24,6 +24,8 @@ type ( detached bool searchAttributesMapper *VisibilitySearchAttributesMapper + + contextValues map[any]any } RegistrableComponentOption func(*RegistrableComponent) @@ -154,6 +156,29 @@ func WithSearchAttributes( } } +// WithContextValues allows specifying key-value pairs that will be available in the Context +// via the Value() method whenever the chasm framework starts, updates, reads, polls, executes or +// validates tasks on a component. +// +// This is useful for propagating values needed for those processing logic but are not avaiable via the +// component's struct definition, such as configurations. +// +// Keys need to be globally unique across components. Conflicting keys across will cause component registration to fail. +// +// Manually added key-value pairs via ContextWithValue() will take precedence over registered context values. +func WithContextValues( + keyVals map[any]any, +) RegistrableComponentOption { + return func(rc *RegistrableComponent) { + if rc.contextValues == nil { + rc.contextValues = make(map[any]any, len(keyVals)) + } + for k, v := range keyVals { + rc.contextValues[k] = v + } + } +} + func (rc *RegistrableComponent) registerToLibrary( library namer, ) (string, uint32, error) { diff --git a/chasm/registrable_task.go b/chasm/registrable_task.go index d86552d525c..f6e54e4c4bc 100644 --- a/chasm/registrable_task.go +++ b/chasm/registrable_task.go @@ -1,18 +1,21 @@ package chasm import ( + "context" "fmt" "reflect" ) type ( RegistrableTask struct { - taskType string - goType reflect.Type - componentGoType reflect.Type // It is not clear how this one is used. - validateFn reflect.Value // The Validate() method of the TaskValidator interface. - executeFn reflect.Value // The Execute() method of the TaskExecutor interface. - isPureTask bool + taskType string + goType reflect.Type + componentGoType reflect.Type // It is not clear how this one is used. + validateFn validateFn + pureTaskExecuteFn pureTaskExecuteFn + sideEffectTaskExecuteFn sideEffectTaskExecuteFn + sideEffectTaskDiscardFn sideEffectTaskDiscardFn + isPureTask bool // Those two fields are initialized when the component is registered to a library. library namer @@ -20,39 +23,95 @@ type ( } RegistrableTaskOption func(*RegistrableTask) + + validateFn func(Context, any, TaskAttributes, any, *Registry) (bool, error) + pureTaskExecuteFn func(MutableContext, any, TaskAttributes, any, *Registry) error + sideEffectTaskExecuteFn func(context.Context, ComponentRef, TaskAttributes, any) error + sideEffectTaskDiscardFn func(context.Context, ComponentRef, TaskAttributes, any) error ) -// NOTE: C is not Component but any. +// NewRegistrableSideEffectTask creates a new registrable side-effect task. NOTE: C is not Component but any. +// The handler's Discard method is called on standby clusters when a task has been pending past the discard delay. func NewRegistrableSideEffectTask[C any, T any]( taskType string, - validator TaskValidator[C, T], - executor SideEffectTaskExecutor[C, T], + handler SideEffectTaskHandler[C, T], opts ...RegistrableTaskOption, ) *RegistrableTask { return newRegistrableTask( taskType, reflect.TypeFor[T](), reflect.TypeFor[C](), - reflect.ValueOf(validator).MethodByName("Validate"), - reflect.ValueOf(executor).MethodByName("Execute"), + func( + ctx Context, + component any, + taskAttrs TaskAttributes, + taskData any, + registry *Registry, + ) (bool, error) { + return handler.Validate( + ctx, + component.(C), + taskAttrs, + taskData.(T), + ) + }, + nil, // pureTaskExecuteFn is not used for side effect tasks + func( + ctx context.Context, + componentRef ComponentRef, + taskAttrs TaskAttributes, + taskData any, + ) error { + return handler.Execute(ctx, componentRef, taskAttrs, taskData.(T)) + }, false, + func(ctx context.Context, ref ComponentRef, attrs TaskAttributes, task any) error { + return handler.Discard(ctx, ref, attrs, task.(T)) + }, opts..., ) } func NewRegistrablePureTask[C any, T any]( taskType string, - validator TaskValidator[C, T], - executor PureTaskExecutor[C, T], + handler PureTaskHandler[C, T], opts ...RegistrableTaskOption, ) *RegistrableTask { return newRegistrableTask( taskType, reflect.TypeFor[T](), reflect.TypeFor[C](), - reflect.ValueOf(validator).MethodByName("Validate"), - reflect.ValueOf(executor).MethodByName("Execute"), + func( + ctx Context, + component any, + taskAttrs TaskAttributes, + taskData any, + registry *Registry, + ) (bool, error) { + return handler.Validate( + ctx, + component.(C), + taskAttrs, + taskData.(T), + ) + }, + func( + ctx MutableContext, + component any, + taskAttrs TaskAttributes, + taskData any, + registry *Registry, + ) error { + return handler.Execute( + ctx, + component.(C), + taskAttrs, + taskData.(T), + ) + }, + nil, // sideEffectTaskExecuteFn is not used for pure tasks true, + nil, // sideEffectTaskDiscardFn is not used for pure tasks opts..., ) } @@ -60,17 +119,22 @@ func NewRegistrablePureTask[C any, T any]( func newRegistrableTask( taskType string, goType, componentGoType reflect.Type, - validateFn, executeFn reflect.Value, + validateFn validateFn, + pureTaskExecuteFn pureTaskExecuteFn, + sideEffectTaskExecuteFn sideEffectTaskExecuteFn, isPureTask bool, + sideEffectTaskDiscardFn sideEffectTaskDiscardFn, opts ...RegistrableTaskOption, ) *RegistrableTask { rt := &RegistrableTask{ - taskType: taskType, - goType: goType, - componentGoType: componentGoType, - validateFn: validateFn, - executeFn: executeFn, - isPureTask: isPureTask, + taskType: taskType, + goType: goType, + componentGoType: componentGoType, + validateFn: validateFn, + pureTaskExecuteFn: pureTaskExecuteFn, + sideEffectTaskExecuteFn: sideEffectTaskExecuteFn, + sideEffectTaskDiscardFn: sideEffectTaskDiscardFn, + isPureTask: isPureTask, } for _, opt := range opts { diff --git a/chasm/registry.go b/chasm/registry.go index 4735e387667..910cdcf4991 100644 --- a/chasm/registry.go +++ b/chasm/registry.go @@ -26,6 +26,10 @@ type ( rcByFqn map[string]*RegistrableComponent // fully qualified type name -> component rcByID map[uint32]*RegistrableComponent // component type ID -> component rcByGoType map[reflect.Type]*RegistrableComponent // component go type -> component + // rcContextValues is aggregated context values from all components, + // used for easy lookup when Context.Value(key) is called. + // Registration process will check for key conflicts and return error if same key is registered by multiple components. + rcContextValues map[any]valueWithFqn // rt stands for RegistrableTask. rtByFqn map[string]*RegistrableTask // fully qualified type name -> task @@ -39,6 +43,13 @@ type ( } ) +// valueWithFqn is a wrapper struct that associates a value with +// the fully qualified name (FQN) of the component that registered it. +type valueWithFqn struct { + v any + fqn string +} + func NewRegistry(logger log.Logger) *Registry { return &Registry{ libraries: make(map[string]Library), @@ -48,6 +59,7 @@ func NewRegistry(logger log.Logger) *Registry { rtByFqn: make(map[string]*RegistrableTask), rtByID: make(map[uint32]*RegistrableTask), rtByGoType: make(map[reflect.Type]*RegistrableTask), + rcContextValues: make(map[any]valueWithFqn), nexusServices: make(map[string]*nexus.Service), NexusEndpointProcessor: NewNexusEndpointProcessor(), logger: logger, @@ -244,6 +256,16 @@ func (r *Registry) registerComponent( return fmt.Errorf("component ID %d collision between %s and %s", id, fqn, existingComponent.fqType()) } + for key, value := range rc.contextValues { + if existingValue, ok := r.rcContextValues[key]; ok { + return fmt.Errorf("context value key %v registered by component %s conflicts with component %s", key, fqn, existingValue.fqn) + } + r.rcContextValues[key] = valueWithFqn{ + v: value, + fqn: fqn, + } + } + // rc.goType implements Component interface; therefore, it must be a struct. // This check to protect against the interface itself being registered. if !(rc.goType.Kind() == reflect.Struct || @@ -358,3 +380,10 @@ func (r *Registry) NexusServices() map[string]*nexus.Service { maps.Copy(services, r.nexusServices) return services } + +func (r *Registry) componentContextValue(key any) any { + if v, ok := r.rcContextValues[key]; ok { + return v.v + } + return nil +} diff --git a/chasm/registry_test.go b/chasm/registry_test.go index 6cdee3a500b..4562e33f7e0 100644 --- a/chasm/registry_test.go +++ b/chasm/registry_test.go @@ -117,15 +117,13 @@ func (s *RegistryTestSuite) TestRegistry_RegisterTasks_Success() { lib.EXPECT().NexusServiceProcessors().Return(nil) lib.EXPECT().Tasks().Return([]*chasm.RegistrableTask{ - chasm.NewRegistrableSideEffectTask[*chasm.MockComponent, testTask1]( + chasm.NewRegistrableSideEffectTask( "Task1", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockSideEffectTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockSideEffectTaskHandler[*chasm.MockComponent, testTask1](ctrl), ), - chasm.NewRegistrablePureTask[testTaskComponentInterface, testTask2]( + chasm.NewRegistrablePureTask( "Task2", - chasm.NewMockTaskValidator[testTaskComponentInterface, testTask2](ctrl), - chasm.NewMockPureTaskExecutor[testTaskComponentInterface, testTask2](ctrl), + chasm.NewMockPureTaskHandler[testTaskComponentInterface, testTask2](ctrl), ), }) @@ -375,8 +373,7 @@ func (s *RegistryTestSuite) TestRegistry_RegisterTasks_Error() { lib.EXPECT().Tasks().Return([]*chasm.RegistrableTask{ chasm.NewRegistrablePureTask[*chasm.MockComponent, testTask1]( "", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockPureTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockPureTaskHandler[*chasm.MockComponent, testTask1](ctrl), ), }) err := r.Register(lib) @@ -388,8 +385,7 @@ func (s *RegistryTestSuite) TestRegistry_RegisterTasks_Error() { lib.EXPECT().Tasks().Return([]*chasm.RegistrableTask{ chasm.NewRegistrablePureTask[*chasm.MockComponent, testTask1]( "bad.task.name", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockPureTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockPureTaskHandler[*chasm.MockComponent, testTask1](ctrl), ), }) r := chasm.NewRegistry(s.logger) @@ -402,13 +398,11 @@ func (s *RegistryTestSuite) TestRegistry_RegisterTasks_Error() { lib.EXPECT().Tasks().Return([]*chasm.RegistrableTask{ chasm.NewRegistrablePureTask[*chasm.MockComponent, testTask1]( "Task1", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockPureTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockPureTaskHandler[*chasm.MockComponent, testTask1](ctrl), ), chasm.NewRegistrableSideEffectTask[*chasm.MockComponent, testTask1]( "Task1", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockSideEffectTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockSideEffectTaskHandler[*chasm.MockComponent, testTask1](ctrl), ), }) r := chasm.NewRegistry(s.logger) @@ -421,13 +415,11 @@ func (s *RegistryTestSuite) TestRegistry_RegisterTasks_Error() { lib.EXPECT().Tasks().Return([]*chasm.RegistrableTask{ chasm.NewRegistrablePureTask[*chasm.MockComponent, testTask1]( "Task1", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockPureTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockPureTaskHandler[*chasm.MockComponent, testTask1](ctrl), ), chasm.NewRegistrablePureTask[*chasm.MockComponent, testTask1]( "Task2", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockPureTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockPureTaskHandler[*chasm.MockComponent, testTask1](ctrl), ), }) r := chasm.NewRegistry(s.logger) @@ -445,8 +437,7 @@ func (s *RegistryTestSuite) TestRegistry_RegisterTasks_Error() { lib2.EXPECT().NexusServiceProcessors().Return(nil) task := chasm.NewRegistrablePureTask[*chasm.MockComponent, testTask1]( "Task1", - chasm.NewMockTaskValidator[*chasm.MockComponent, testTask1](ctrl), - chasm.NewMockPureTaskExecutor[*chasm.MockComponent, testTask1](ctrl), + chasm.NewMockPureTaskHandler[*chasm.MockComponent, testTask1](ctrl), ) lib2.EXPECT().Tasks().Return([]*chasm.RegistrableTask{task}) r2 := chasm.NewRegistry(s.logger) @@ -464,8 +455,7 @@ func (s *RegistryTestSuite) TestRegistry_RegisterTasks_Error() { lib.EXPECT().Tasks().Return([]*chasm.RegistrableTask{ chasm.NewRegistrablePureTask[*chasm.MockComponent, string]( "Task1", - chasm.NewMockTaskValidator[*chasm.MockComponent, string](ctrl), - chasm.NewMockPureTaskExecutor[*chasm.MockComponent, string](ctrl), + chasm.NewMockPureTaskHandler[*chasm.MockComponent, string](ctrl), ), }) r := chasm.NewRegistry(s.logger) diff --git a/chasm/search_attribute.go b/chasm/search_attribute.go index b8c62387208..db2e966b9b1 100644 --- a/chasm/search_attribute.go +++ b/chasm/search_attribute.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/server/common/searchattribute/sadefs" ) @@ -75,6 +76,8 @@ var ( ) var ( + // CHASM search attribute of type Text is not supported at this moment. + // Note that it's currently assumed that string type values are Keyword search attributes. _ SearchAttribute = (*SearchAttributeBool)(nil) _ SearchAttribute = (*SearchAttributeDateTime)(nil) _ SearchAttribute = (*SearchAttributeInt)(nil) @@ -369,7 +372,7 @@ func (s SearchAttributeKeyword) Value(value string) SearchAttributeKeyValue { return SearchAttributeKeyValue{ Alias: s.alias, Field: s.field, - Value: VisibilityValueString(value), + Value: VisibilityValueKeyword(value), } } @@ -422,6 +425,26 @@ func NewSearchAttributesMap(values map[string]VisibilityValue) SearchAttributesM return SearchAttributesMap{values: values} } +// newSearchAttributesMapFromProto creates a new SearchAttributesMap from commonpb.SearchAttributes. +func newSearchAttributesMapFromProto( + searchAttributes *commonpb.SearchAttributes, +) (SearchAttributesMap, error) { + if len(searchAttributes.GetIndexedFields()) == 0 { + return SearchAttributesMap{}, nil + } + result := SearchAttributesMap{ + values: make(map[string]VisibilityValue), + } + for saName, saPayload := range searchAttributes.IndexedFields { + value, err := visibilityValueFromPayload(saPayload) + if err != nil { + return SearchAttributesMap{}, nil + } + result.values[saName] = value + } + return result, nil +} + // SearchAttributeValue returns the value for a given SearchAttribute with compile-time type safety. // The return type T is inferred from the SearchAttribute's type parameter. // For example, SearchAttributeBool will return a bool value. diff --git a/chasm/search_attribute_test.go b/chasm/search_attribute_test.go index 0f539135e7d..88de59df6c4 100644 --- a/chasm/search_attribute_test.go +++ b/chasm/search_attribute_test.go @@ -4,7 +4,10 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/common/searchattribute/sadefs" ) func TestSearchAttributesMap_Get(t *testing.T) { @@ -23,7 +26,7 @@ func TestSearchAttributesMap_Get(t *testing.T) { "completed": VisibilityValueBool(true), "count": VisibilityValueInt64(42), "score": VisibilityValueFloat64(3.14), - "status": VisibilityValueString("active"), + "status": VisibilityValueKeyword("active"), "timestamp": VisibilityValueTime(now), "tags": VisibilityValueStringSlice([]string{"tag1", "tag2"}), } @@ -31,51 +34,140 @@ func TestSearchAttributesMap_Get(t *testing.T) { t.Run("GetBool", func(t *testing.T) { val, ok := SearchAttributeValue(m, boolAttr) - assert.True(t, ok) - assert.True(t, val) + require.True(t, ok) + require.True(t, val) }) t.Run("GetInt64", func(t *testing.T) { val, ok := SearchAttributeValue(m, intAttr) - assert.True(t, ok) - assert.Equal(t, int64(42), val) + require.True(t, ok) + require.Equal(t, int64(42), val) }) t.Run("GetFloat64", func(t *testing.T) { val, ok := SearchAttributeValue(m, doubleAttr) - assert.True(t, ok) - assert.InDelta(t, 3.14, val, 0.0001) + require.True(t, ok) + require.InDelta(t, 3.14, val, 0.0001) }) t.Run("GetString", func(t *testing.T) { val, ok := SearchAttributeValue(m, keywordAttr) - assert.True(t, ok) - assert.Equal(t, "active", val) + require.True(t, ok) + require.Equal(t, "active", val) }) t.Run("GetTime", func(t *testing.T) { val, ok := SearchAttributeValue(m, datetimeAttr) - assert.True(t, ok) - assert.True(t, now.Equal(val)) + require.True(t, ok) + require.True(t, now.Equal(val)) }) t.Run("GetStringSlice", func(t *testing.T) { val, ok := SearchAttributeValue(m, keywordListAttr) - assert.True(t, ok) - assert.Equal(t, []string{"tag1", "tag2"}, val) + require.True(t, ok) + require.Equal(t, []string{"tag1", "tag2"}, val) }) t.Run("NotFound", func(t *testing.T) { missingAttr := NewSearchAttributeBool("missing", SearchAttributeFieldBool02) val, ok := SearchAttributeValue(m, missingAttr) - assert.False(t, ok) - assert.False(t, val) + require.False(t, ok) + require.False(t, val) }) t.Run("NilMap", func(t *testing.T) { emptyMap := NewSearchAttributesMap(nil) val, ok := SearchAttributeValue(emptyMap, boolAttr) - assert.False(t, ok) - assert.False(t, val) + require.False(t, ok) + require.False(t, val) + }) +} + +func TestNewSearchAttributesMapFromProto(t *testing.T) { + t.Run("NilSearchAttributes", func(t *testing.T) { + m, err := newSearchAttributesMapFromProto(nil) + require.NoError(t, err) + require.Empty(t, m.values) + }) + + t.Run("EmptyIndexedFields", func(t *testing.T) { + m, err := newSearchAttributesMapFromProto(&commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{}, + }) + require.NoError(t, err) + require.Empty(t, m.values) + }) + + t.Run("SingleBoolValue", func(t *testing.T) { + sa := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "completed": sadefs.MustEncodeValue(true, enumspb.INDEXED_VALUE_TYPE_BOOL), + }, + } + m, err := newSearchAttributesMapFromProto(sa) + require.NoError(t, err) + + boolAttr := NewSearchAttributeBool("completed", SearchAttributeFieldBool01) + val, ok := SearchAttributeValue(m, boolAttr) + require.True(t, ok) + require.True(t, val) + }) + + t.Run("MultipleValueTypes", func(t *testing.T) { + now := time.Now().UTC().Truncate(time.Millisecond) + sa := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "completed": sadefs.MustEncodeValue(true, enumspb.INDEXED_VALUE_TYPE_BOOL), + "count": sadefs.MustEncodeValue(int64(42), enumspb.INDEXED_VALUE_TYPE_INT), + "score": sadefs.MustEncodeValue(3.14, enumspb.INDEXED_VALUE_TYPE_DOUBLE), + "status": sadefs.MustEncodeValue("active", enumspb.INDEXED_VALUE_TYPE_KEYWORD), + "timestamp": sadefs.MustEncodeValue(now, enumspb.INDEXED_VALUE_TYPE_DATETIME), + "tags": sadefs.MustEncodeValue([]string{"tag1", "tag2"}, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST), + }, + } + m, err := newSearchAttributesMapFromProto(sa) + require.NoError(t, err) + + boolAttr := NewSearchAttributeBool("completed", SearchAttributeFieldBool01) + boolVal, ok := SearchAttributeValue(m, boolAttr) + require.True(t, ok) + require.True(t, boolVal) + + intAttr := NewSearchAttributeInt("count", SearchAttributeFieldInt01) + intVal, ok := SearchAttributeValue(m, intAttr) + require.True(t, ok) + require.Equal(t, int64(42), intVal) + + doubleAttr := NewSearchAttributeDouble("score", SearchAttributeFieldDouble01) + doubleVal, ok := SearchAttributeValue(m, doubleAttr) + require.True(t, ok) + require.InDelta(t, 3.14, doubleVal, 0.0001) + + keywordAttr := NewSearchAttributeKeyword("status", SearchAttributeFieldKeyword01) + keywordVal, ok := SearchAttributeValue(m, keywordAttr) + require.True(t, ok) + require.Equal(t, "active", keywordVal) + + datetimeAttr := NewSearchAttributeDateTime("timestamp", SearchAttributeFieldDateTime01) + timeVal, ok := SearchAttributeValue(m, datetimeAttr) + require.True(t, ok) + require.True(t, now.Equal(timeVal)) + + keywordListAttr := NewSearchAttributeKeywordList("tags", SearchAttributeFieldKeywordList01) + listVal, ok := SearchAttributeValue(m, keywordListAttr) + require.True(t, ok) + require.Equal(t, []string{"tag1", "tag2"}, listVal) + }) + + t.Run("InvalidPayload", func(t *testing.T) { + sa := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "bad": {Data: []byte("not valid")}, + }, + } + m, err := newSearchAttributesMapFromProto(sa) + // Current implementation returns nil error on decode failure + require.NoError(t, err) + require.Empty(t, m.values) }) } diff --git a/chasm/task.go b/chasm/task.go index 7fc0bec6360..43698837b3f 100644 --- a/chasm/task.go +++ b/chasm/task.go @@ -4,23 +4,51 @@ package chasm import ( "context" + "errors" "time" ) +// ErrTaskDiscarded is the error returned by the default [SideEffectTaskHandlerBase] Discard implementation, +// indicating that a side-effect task on a standby cluster has been pending past the discard delay. +var ErrTaskDiscarded = errors.New("standby task pending for too long") + type ( + // TaskAttributes specifies scheduling metadata for a task. TaskAttributes struct { + // ScheduledTime is when the task should fire. Use [TaskScheduledTimeImmediate] (zero value) + // for tasks that should execute as soon as possible. ScheduledTime time.Time - Destination string + // Destination is an optional routing key for outbound tasks (e.g., a URL host for HTTP + // callbacks). When non-empty, the task is categorized as outbound; when empty, it is + // categorized as a transfer task. Destination must only be set on immediate tasks. + Destination string } - SideEffectTaskExecutor[C any, T any] interface { + // SideEffectTaskHandler handles side effect tasks that run outside of the state lock and have access to a Go + // context to perform I/O and access chasm engine methods such as [UpdateComponent]. Implementations must embed + // [SideEffectTaskHandlerBase]. + SideEffectTaskHandler[C any, T any] interface { + TaskValidator[C, T] Execute(context.Context, ComponentRef, TaskAttributes, T) error + // Discard implements custom discard behavior on standby clusters. When a side-effect task has been + // pending on standby past the discard delay, the framework calls Discard instead of silently dropping + // the task. For example, the activity dispatch handler implements this to spill tasks to matching. + // The ctx carries engine access, but implementations must avoid mutating component state on standby + // clusters. + Discard(context.Context, ComponentRef, TaskAttributes, T) error + sideEffectTaskHandler() } - PureTaskExecutor[C any, T any] interface { + // PureTaskHandler handles pure tasks that run while holding execution state write lock and should not do I/O. + // Implementations must embed [PureTaskHandlerBase]. + PureTaskHandler[C any, T any] interface { + TaskValidator[C, T] Execute(MutableContext, C, TaskAttributes, T) error + pureTaskHandler() } + // TaskValidator is implemented by both [SideEffectTaskHandler] and [PureTaskHandler] to gate + // whether a task should proceed with execution. TaskValidator[C any, T any] interface { // Validate determines whether a task should proceed with execution based on the current context, component // state, task attributes, and task data. @@ -45,13 +73,17 @@ type ( } ) +// TaskScheduledTimeImmediate is the zero time value used to indicate that a task should execute immediately. var TaskScheduledTimeImmediate = time.Time{} +// IsImmediate reports whether the task is scheduled for immediate execution (zero or unset scheduled time). func (a *TaskAttributes) IsImmediate() bool { return a.ScheduledTime.IsZero() || a.ScheduledTime.Equal(TaskScheduledTimeImmediate) } +// IsValid reports whether the task attributes are well-formed. A Destination may only be set on +// immediate tasks; deferred tasks with a Destination are invalid. func (a *TaskAttributes) IsValid() bool { return a.Destination == "" || a.IsImmediate() } diff --git a/chasm/task_handler_base.go b/chasm/task_handler_base.go new file mode 100644 index 00000000000..1c7312468d2 --- /dev/null +++ b/chasm/task_handler_base.go @@ -0,0 +1,18 @@ +package chasm + +import "context" + +// SideEffectTaskHandlerBase provides a default Discard implementation that returns ErrTaskDiscarded. +// Embed this in side-effect task handler structs to satisfy the SideEffectTaskHandler interface. +type SideEffectTaskHandlerBase[T any] struct{} + +func (SideEffectTaskHandlerBase[T]) Discard(_ context.Context, _ ComponentRef, _ TaskAttributes, _ T) error { + return ErrTaskDiscarded +} + +func (SideEffectTaskHandlerBase[T]) sideEffectTaskHandler() {} + +// PureTaskHandlerBase must be embedded in all pure task handler implementations. +type PureTaskHandlerBase struct{} + +func (PureTaskHandlerBase) pureTaskHandler() {} diff --git a/chasm/task_mock.go b/chasm/task_mock.go index e7721421979..4b41f5f50aa 100644 --- a/chasm/task_mock.go +++ b/chasm/task_mock.go @@ -16,32 +16,46 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockSideEffectTaskExecutor is a mock of SideEffectTaskExecutor interface. -type MockSideEffectTaskExecutor[C any, T any] struct { +// MockSideEffectTaskHandler is a mock of SideEffectTaskHandler interface. +type MockSideEffectTaskHandler[C any, T any] struct { ctrl *gomock.Controller - recorder *MockSideEffectTaskExecutorMockRecorder[C, T] + recorder *MockSideEffectTaskHandlerMockRecorder[C, T] isgomock struct{} } -// MockSideEffectTaskExecutorMockRecorder is the mock recorder for MockSideEffectTaskExecutor. -type MockSideEffectTaskExecutorMockRecorder[C any, T any] struct { - mock *MockSideEffectTaskExecutor[C, T] +// MockSideEffectTaskHandlerMockRecorder is the mock recorder for MockSideEffectTaskHandler. +type MockSideEffectTaskHandlerMockRecorder[C any, T any] struct { + mock *MockSideEffectTaskHandler[C, T] } -// NewMockSideEffectTaskExecutor creates a new mock instance. -func NewMockSideEffectTaskExecutor[C any, T any](ctrl *gomock.Controller) *MockSideEffectTaskExecutor[C, T] { - mock := &MockSideEffectTaskExecutor[C, T]{ctrl: ctrl} - mock.recorder = &MockSideEffectTaskExecutorMockRecorder[C, T]{mock} +// NewMockSideEffectTaskHandler creates a new mock instance. +func NewMockSideEffectTaskHandler[C any, T any](ctrl *gomock.Controller) *MockSideEffectTaskHandler[C, T] { + mock := &MockSideEffectTaskHandler[C, T]{ctrl: ctrl} + mock.recorder = &MockSideEffectTaskHandlerMockRecorder[C, T]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockSideEffectTaskExecutor[C, T]) EXPECT() *MockSideEffectTaskExecutorMockRecorder[C, T] { +func (m *MockSideEffectTaskHandler[C, T]) EXPECT() *MockSideEffectTaskHandlerMockRecorder[C, T] { return m.recorder } +// Discard mocks base method. +func (m *MockSideEffectTaskHandler[C, T]) Discard(arg0 context.Context, arg1 ComponentRef, arg2 TaskAttributes, arg3 T) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Discard", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// Discard indicates an expected call of Discard. +func (mr *MockSideEffectTaskHandlerMockRecorder[C, T]) Discard(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discard", reflect.TypeOf((*MockSideEffectTaskHandler[C, T])(nil).Discard), arg0, arg1, arg2, arg3) +} + // Execute mocks base method. -func (m *MockSideEffectTaskExecutor[C, T]) Execute(arg0 context.Context, arg1 ComponentRef, arg2 TaskAttributes, arg3 T) error { +func (m *MockSideEffectTaskHandler[C, T]) Execute(arg0 context.Context, arg1 ComponentRef, arg2 TaskAttributes, arg3 T) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Execute", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) @@ -49,37 +63,64 @@ func (m *MockSideEffectTaskExecutor[C, T]) Execute(arg0 context.Context, arg1 Co } // Execute indicates an expected call of Execute. -func (mr *MockSideEffectTaskExecutorMockRecorder[C, T]) Execute(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockSideEffectTaskHandlerMockRecorder[C, T]) Execute(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockSideEffectTaskHandler[C, T])(nil).Execute), arg0, arg1, arg2, arg3) +} + +// Validate mocks base method. +func (m *MockSideEffectTaskHandler[C, T]) Validate(arg0 Context, arg1 C, arg2 TaskAttributes, arg3 T) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Validate indicates an expected call of Validate. +func (mr *MockSideEffectTaskHandlerMockRecorder[C, T]) Validate(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockSideEffectTaskHandler[C, T])(nil).Validate), arg0, arg1, arg2, arg3) +} + +// sideEffectTaskHandler mocks base method. +func (m *MockSideEffectTaskHandler[C, T]) sideEffectTaskHandler() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "sideEffectTaskHandler") +} + +// sideEffectTaskHandler indicates an expected call of sideEffectTaskHandler. +func (mr *MockSideEffectTaskHandlerMockRecorder[C, T]) sideEffectTaskHandler() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockSideEffectTaskExecutor[C, T])(nil).Execute), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "sideEffectTaskHandler", reflect.TypeOf((*MockSideEffectTaskHandler[C, T])(nil).sideEffectTaskHandler)) } -// MockPureTaskExecutor is a mock of PureTaskExecutor interface. -type MockPureTaskExecutor[C any, T any] struct { +// MockPureTaskHandler is a mock of PureTaskHandler interface. +type MockPureTaskHandler[C any, T any] struct { ctrl *gomock.Controller - recorder *MockPureTaskExecutorMockRecorder[C, T] + recorder *MockPureTaskHandlerMockRecorder[C, T] isgomock struct{} } -// MockPureTaskExecutorMockRecorder is the mock recorder for MockPureTaskExecutor. -type MockPureTaskExecutorMockRecorder[C any, T any] struct { - mock *MockPureTaskExecutor[C, T] +// MockPureTaskHandlerMockRecorder is the mock recorder for MockPureTaskHandler. +type MockPureTaskHandlerMockRecorder[C any, T any] struct { + mock *MockPureTaskHandler[C, T] } -// NewMockPureTaskExecutor creates a new mock instance. -func NewMockPureTaskExecutor[C any, T any](ctrl *gomock.Controller) *MockPureTaskExecutor[C, T] { - mock := &MockPureTaskExecutor[C, T]{ctrl: ctrl} - mock.recorder = &MockPureTaskExecutorMockRecorder[C, T]{mock} +// NewMockPureTaskHandler creates a new mock instance. +func NewMockPureTaskHandler[C any, T any](ctrl *gomock.Controller) *MockPureTaskHandler[C, T] { + mock := &MockPureTaskHandler[C, T]{ctrl: ctrl} + mock.recorder = &MockPureTaskHandlerMockRecorder[C, T]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPureTaskExecutor[C, T]) EXPECT() *MockPureTaskExecutorMockRecorder[C, T] { +func (m *MockPureTaskHandler[C, T]) EXPECT() *MockPureTaskHandlerMockRecorder[C, T] { return m.recorder } // Execute mocks base method. -func (m *MockPureTaskExecutor[C, T]) Execute(arg0 MutableContext, arg1 C, arg2 TaskAttributes, arg3 T) error { +func (m *MockPureTaskHandler[C, T]) Execute(arg0 MutableContext, arg1 C, arg2 TaskAttributes, arg3 T) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Execute", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) @@ -87,9 +128,36 @@ func (m *MockPureTaskExecutor[C, T]) Execute(arg0 MutableContext, arg1 C, arg2 T } // Execute indicates an expected call of Execute. -func (mr *MockPureTaskExecutorMockRecorder[C, T]) Execute(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockPureTaskHandlerMockRecorder[C, T]) Execute(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockPureTaskHandler[C, T])(nil).Execute), arg0, arg1, arg2, arg3) +} + +// Validate mocks base method. +func (m *MockPureTaskHandler[C, T]) Validate(arg0 Context, arg1 C, arg2 TaskAttributes, arg3 T) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Validate indicates an expected call of Validate. +func (mr *MockPureTaskHandlerMockRecorder[C, T]) Validate(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockPureTaskHandler[C, T])(nil).Validate), arg0, arg1, arg2, arg3) +} + +// pureTaskHandler mocks base method. +func (m *MockPureTaskHandler[C, T]) pureTaskHandler() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "pureTaskHandler") +} + +// pureTaskHandler indicates an expected call of pureTaskHandler. +func (mr *MockPureTaskHandlerMockRecorder[C, T]) pureTaskHandler() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockPureTaskExecutor[C, T])(nil).Execute), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "pureTaskHandler", reflect.TypeOf((*MockPureTaskHandler[C, T])(nil).pureTaskHandler)) } // MockTaskValidator is a mock of TaskValidator interface. diff --git a/chasm/test_component_test.go b/chasm/test_component_test.go index 39165021a40..18fbfc528ec 100644 --- a/chasm/test_component_test.go +++ b/chasm/test_component_test.go @@ -80,6 +80,7 @@ var ( _ VisibilitySearchAttributesProvider = (*TestComponent)(nil) _ VisibilityMemoProvider = (*TestComponent)(nil) + _ RootComponent = (*TestComponent)(nil) ) func (tc *TestComponent) LifecycleState(_ Context) LifecycleState { @@ -109,6 +110,11 @@ func (tc *TestComponent) Fail(_ MutableContext) { tc.ComponentData.Status = enumspb.WORKFLOW_EXECUTION_STATUS_FAILED } +func (tc *TestComponent) ContextMetadata(_ Context) map[string]string { + // TODO: Export context metadata from this test root. + return nil +} + // SearchAttributes implements VisibilitySearchAttributesProvider interface. func (tc *TestComponent) SearchAttributes(_ Context) []SearchAttributeKeyValue { return []SearchAttributeKeyValue{ diff --git a/chasm/test_library_test.go b/chasm/test_library_test.go index ce9763cc8ab..29a3d06234c 100644 --- a/chasm/test_library_test.go +++ b/chasm/test_library_test.go @@ -10,12 +10,10 @@ type TestLibrary struct { controller *gomock.Controller - mockSideEffectTaskValidator *MockTaskValidator[any, *TestSideEffectTask] - mockSideEffectTaskExecutor *MockSideEffectTaskExecutor[any, *TestSideEffectTask] - mockOutboundSideEffectTaskValidator *MockTaskValidator[any, TestOutboundSideEffectTask] - mockOutboundSideEffectTaskExecutor *MockSideEffectTaskExecutor[any, TestOutboundSideEffectTask] - mockPureTaskValidator *MockTaskValidator[any, *TestPureTask] - mockPureTaskExecutor *MockPureTaskExecutor[any, *TestPureTask] + mockSideEffectTaskHandler *MockSideEffectTaskHandler[any, *TestSideEffectTask] + mockDiscardableSideEffectHandler *MockSideEffectTaskHandler[any, *TestDiscardableSideEffectTask] + mockOutboundSideEffectTaskHandler *MockSideEffectTaskHandler[any, TestOutboundSideEffectTask] + mockPureTaskHandler *MockPureTaskHandler[any, *TestPureTask] } func newTestLibrary( @@ -24,12 +22,10 @@ func newTestLibrary( return &TestLibrary{ controller: controller, - mockSideEffectTaskValidator: NewMockTaskValidator[any, *TestSideEffectTask](controller), - mockSideEffectTaskExecutor: NewMockSideEffectTaskExecutor[any, *TestSideEffectTask](controller), - mockOutboundSideEffectTaskValidator: NewMockTaskValidator[any, TestOutboundSideEffectTask](controller), - mockOutboundSideEffectTaskExecutor: NewMockSideEffectTaskExecutor[any, TestOutboundSideEffectTask](controller), - mockPureTaskValidator: NewMockTaskValidator[any, *TestPureTask](controller), - mockPureTaskExecutor: NewMockPureTaskExecutor[any, *TestPureTask](controller), + mockSideEffectTaskHandler: NewMockSideEffectTaskHandler[any, *TestSideEffectTask](controller), + mockDiscardableSideEffectHandler: NewMockSideEffectTaskHandler[any, *TestDiscardableSideEffectTask](controller), + mockOutboundSideEffectTaskHandler: NewMockSideEffectTaskHandler[any, TestOutboundSideEffectTask](controller), + mockPureTaskHandler: NewMockPureTaskHandler[any, *TestPureTask](controller), } } @@ -54,19 +50,20 @@ func (l *TestLibrary) Tasks() []*RegistrableTask { return []*RegistrableTask{ NewRegistrableSideEffectTask( testSideEffectTaskName, - l.mockSideEffectTaskValidator, - l.mockSideEffectTaskExecutor, + l.mockSideEffectTaskHandler, + ), + NewRegistrableSideEffectTask( + testDiscardableSideEffectTaskName, + l.mockDiscardableSideEffectHandler, ), NewRegistrableSideEffectTask( // NOTE this task is registered as a struct, instead of pointer to struct. testOutboundSideEffectTaskName, - l.mockOutboundSideEffectTaskValidator, - l.mockOutboundSideEffectTaskExecutor, + l.mockOutboundSideEffectTaskHandler, ), NewRegistrablePureTask( testPureTaskName, - l.mockPureTaskValidator, - l.mockPureTaskExecutor, + l.mockPureTaskHandler, ), } } diff --git a/chasm/test_task_test.go b/chasm/test_task_test.go index 03336c4446e..e048e8d62ee 100644 --- a/chasm/test_task_test.go +++ b/chasm/test_task_test.go @@ -8,6 +8,8 @@ import ( type ( TestSideEffectTask = commonpb.Payload + TestDiscardableSideEffectTask struct{} + TestOutboundSideEffectTask struct{} TestPureTask struct { diff --git a/chasm/test_var_test.go b/chasm/test_var_test.go index 0d6184cd55a..2cd91679878 100644 --- a/chasm/test_var_test.go +++ b/chasm/test_var_test.go @@ -7,9 +7,10 @@ const ( testSubComponent11Name = "test_sub_component_11" testSubComponent2Name = "test_sub_component_2" - testSideEffectTaskName = "test_side_effect_task" - testOutboundSideEffectTaskName = "test_outbound_side_effect_task" - testPureTaskName = "test_pure_task" + testSideEffectTaskName = "test_side_effect_task" + testDiscardableSideEffectTaskName = "test_discardable_side_effect_task" + testOutboundSideEffectTaskName = "test_outbound_side_effect_task" + testPureTaskName = "test_pure_task" ) var ( @@ -18,9 +19,10 @@ var ( testSubComponent11FQN = FullyQualifiedName(testLibraryName, testSubComponent11Name) testSubComponent2FQN = FullyQualifiedName(testLibraryName, testSubComponent2Name) - testSideEffectTaskFQN = FullyQualifiedName(testLibraryName, testSideEffectTaskName) - testOutboundSideEffectTaskFQN = FullyQualifiedName(testLibraryName, testOutboundSideEffectTaskName) - testPureTaskFQN = FullyQualifiedName(testLibraryName, testPureTaskName) + testSideEffectTaskFQN = FullyQualifiedName(testLibraryName, testSideEffectTaskName) + testDiscardableSideEffectTaskFQN = FullyQualifiedName(testLibraryName, testDiscardableSideEffectTaskName) + testOutboundSideEffectTaskFQN = FullyQualifiedName(testLibraryName, testOutboundSideEffectTaskName) + testPureTaskFQN = FullyQualifiedName(testLibraryName, testPureTaskName) ) var ( @@ -29,7 +31,8 @@ var ( testSubComponent11TypeID = GenerateTypeID(testSubComponent11FQN) testSubComponent2TypeID = GenerateTypeID(testSubComponent2FQN) - testSideEffectTaskTypeID = GenerateTypeID(testSideEffectTaskFQN) - testOutboundSideEffectTaskTypeID = GenerateTypeID(testOutboundSideEffectTaskFQN) - testPureTaskTypeID = GenerateTypeID(testPureTaskFQN) + testSideEffectTaskTypeID = GenerateTypeID(testSideEffectTaskFQN) + testDiscardableSideEffectTaskTypeID = GenerateTypeID(testDiscardableSideEffectTaskFQN) + testOutboundSideEffectTaskTypeID = GenerateTypeID(testOutboundSideEffectTaskFQN) + testPureTaskTypeID = GenerateTypeID(testPureTaskFQN) ) diff --git a/chasm/tree.go b/chasm/tree.go index 81270034f99..b4385a994de 100644 --- a/chasm/tree.go +++ b/chasm/tree.go @@ -21,6 +21,7 @@ import ( "go.temporal.io/server/common/definition" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/nexus/nexusrpc" "go.temporal.io/server/common/persistence/serialization" "go.temporal.io/server/common/persistence/transitionhistory" @@ -117,15 +118,21 @@ type ( // // We can consider extending the force terminate concept to sub-components as well, and make the field durable. terminated bool + + // deleteAfterClose suppresses the close visibility task when an execution is being + // terminated as part of a delete operation. Like terminated, this is in-memory only + // and only needed for the current transaction. Set via SetDeleteAfterClose. + deleteAfterClose bool } // nodeBase is a set of dependencies and states shared by all nodes in a CHASM tree. nodeBase struct { - registry *Registry - timeSource clock.TimeSource - backend NodeBackend - pathEncoder NodePathEncoder - logger log.Logger + registry *Registry + timeSource clock.TimeSource + backend NodeBackend + pathEncoder NodePathEncoder + logger log.Logger + metricsHandler metrics.Handler // Following fields are changes accumulated in this transaction, // and will get cleaned up after CloseTransaction(). @@ -190,6 +197,7 @@ type ( // TODO: Add methods needed from MutateState here. GetExecutionState() *persistencespb.WorkflowExecutionState GetExecutionInfo() *persistencespb.WorkflowExecutionInfo + GetApproximatePersistedSize() int GetCurrentVersion() int64 NextTransitionCount() int64 CurrentVersionedTransition() *persistencespb.VersionedTransition @@ -230,21 +238,21 @@ type ( // If serializedNodes is empty, the tree will be considered as a legacy Workflow execution without any CHASM nodes. func NewTreeFromDB( serializedNodes map[string]*persistencespb.ChasmNode, // This is coming from MS map[nodePath]ChasmNode. - registry *Registry, timeSource clock.TimeSource, backend NodeBackend, pathEncoder NodePathEncoder, logger log.Logger, + metricsHandler metrics.Handler, ) (*Node, error) { if len(serializedNodes) == 0 { - root := NewEmptyTree(registry, timeSource, backend, pathEncoder, logger) + root := NewEmptyTree(registry, timeSource, backend, pathEncoder, logger, metricsHandler) // NewEmptyTree initializes the serializedNode to an empty component node, root.serializedNode.Metadata.GetComponentAttributes().TypeId = WorkflowArchetypeID return root, nil } - root := newTreeHelper(registry, timeSource, backend, pathEncoder, logger) + root := newTreeHelper(registry, timeSource, backend, pathEncoder, logger, metricsHandler) for encodedPath, serializedNode := range serializedNodes { nodePath, err := pathEncoder.Decode(encodedPath) if err != nil { @@ -253,7 +261,7 @@ func NewTreeFromDB( root.setSerializedNode(nodePath, encodedPath, serializedNode) } - if err := newTreeInitSearchAttributesAndMemo(root); err != nil { + if err := newTreeInitSearchAttributesAndMemo(root, registry); err != nil { return nil, err } return root, nil @@ -266,8 +274,9 @@ func NewEmptyTree( backend NodeBackend, pathEncoder NodePathEncoder, logger log.Logger, + metricsHandler metrics.Handler, ) *Node { - root := newTreeHelper(registry, timeSource, backend, pathEncoder, logger) + root := newTreeHelper(registry, timeSource, backend, pathEncoder, logger, metricsHandler) // If serializedNodes is empty, it means that this new tree. // Initialize empty serializedNode. @@ -287,13 +296,15 @@ func newTreeHelper( backend NodeBackend, pathEncoder NodePathEncoder, logger log.Logger, + metricsHandler metrics.Handler, ) *Node { base := &nodeBase{ - registry: registry, - timeSource: timeSource, - backend: backend, - pathEncoder: pathEncoder, - logger: logger, + registry: registry, + timeSource: timeSource, + backend: backend, + pathEncoder: pathEncoder, + logger: logger, + metricsHandler: metricsHandler, mutation: NodesMutation{ UpdatedNodes: make(map[string]*persistencespb.ChasmNode), @@ -315,6 +326,7 @@ func newTreeHelper( func newTreeInitSearchAttributesAndMemo( root *Node, + registry *Registry, ) error { immutableContext := NewContext(context.Background(), root) rootComponent, err := root.Component(immutableContext, ComponentRef{}) @@ -346,7 +358,7 @@ func searchAttributeKeyValuesToMap(saSlice []SearchAttributeKeyValue) map[string } func (n *Node) SetRootComponent( - rootComponent Component, + rootComponent RootComponent, ) error { root := n.root() root.setValue(rootComponent) @@ -407,7 +419,7 @@ func (n *Node) Component( return nil, errComponentNotFound } - validationContext := NewContext(chasmContext.getContext(), node) + validationContext := NewContext(chasmContext.goContext(), node) if err := node.prepareComponentValue(validationContext); err != nil { return nil, err } @@ -425,7 +437,7 @@ func (n *Node) Component( } if ref.validationFn != nil { - if err := ref.validationFn(node.root().backend, validationContext, componentValue); err != nil { + if err := ref.validationFn(node.root().backend, validationContext, componentValue, node.registry); err != nil { return nil, err } } @@ -444,7 +456,7 @@ func (n *Node) Component( // the case of a newly created node, a detached node, or an OperationIntentObserve // intent, the check is skipped. func (n *Node) validateAccess(ctx Context) error { - intent := operationIntentFromContext(ctx.getContext()) + intent := operationIntentFromContext(ctx.goContext()) if intent != OperationIntentProgress { // Read-only operations are always allowed. return nil @@ -1101,7 +1113,7 @@ func (n *Node) deleteChildren( ) error { for childName, childNode := range n.children { if _, childToKeep := childrenToKeep[childName]; !childToKeep { - if err := childNode.delete(); err != nil { + if err := childNode.delete(false); err != nil { return err } } @@ -1430,13 +1442,14 @@ func (n *Node) CloseTransaction() (NodesMutation, error) { TransitionCount: n.backend.NextTransitionCount(), } - rootLifecycleChanged, err := n.closeTransactionHandleRootLifecycleChange() + immutableContext := NewContext(context.TODO(), n) + rootLifecycleChanged, err := n.closeTransactionHandleRootLifecycleChange(immutableContext) if err != nil { return NodesMutation{}, err } if n.isActiveStateDirty { - if err := n.closeTransactionForceUpdateVisibility(rootLifecycleChanged); err != nil { + if err := n.closeTransactionForceUpdateVisibility(immutableContext, rootLifecycleChanged); err != nil { return NodesMutation{}, err } } @@ -1500,7 +1513,9 @@ func (n *Node) executeImmediatePureTasks() error { return nil } -func (n *Node) closeTransactionHandleRootLifecycleChange() (bool, error) { +func (n *Node) closeTransactionHandleRootLifecycleChange( + immutableContext Context, +) (bool, error) { if n.backend.IsWorkflow() { // Workflow manages its lifecycle directly in mutable state. return false, nil @@ -1522,12 +1537,11 @@ func (n *Node) closeTransactionHandleRootLifecycleChange() (bool, error) { ) } - chasmContext := NewContext(context.Background(), n) - rootComponent, err := n.Component(chasmContext, ComponentRef{}) + rootComponent, err := n.Component(immutableContext, ComponentRef{}) if err != nil { return false, err } - lifecycleState := rootComponent.LifecycleState(chasmContext) + lifecycleState := rootComponent.LifecycleState(immutableContext) var newState enumsspb.WorkflowExecutionState var newStatus enumspb.WorkflowExecutionStatus @@ -1552,10 +1566,18 @@ func (n *Node) closeTransactionHandleRootLifecycleChange() (bool, error) { } func (n *Node) closeTransactionForceUpdateVisibility( + immutableContext Context, rootLifecycleChanged bool, ) error { + if n.deleteAfterClose { + return nil + } + + if !rootLifecycleChanged && + n.backend.GetExecutionState().State == enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED { + return nil + } - immutableContext := NewContext(context.TODO(), n) needUpdate := rootLifecycleChanged rootComponent, err := n.Component(immutableContext, ComponentRef{}) @@ -1829,18 +1851,13 @@ func (n *Node) validateTask( defer log.CapturePanic(n.logger, &retErr) - retValues := registableTask.validateFn.Call([]reflect.Value{ - reflect.ValueOf(validateContext), - reflect.ValueOf(n.value), - reflect.ValueOf(taskAttributes), - reflect.ValueOf(taskInstance), - }) - if !retValues[1].IsNil() { - //revive:disable-next-line:unchecked-type-assertion - return false, retValues[1].Interface().(error) - } - //revive:disable-next-line:unchecked-type-assertion - return retValues[0].Interface().(bool), nil + return registableTask.validateFn( + validateContext, + n.value, + taskAttributes, + taskInstance, + n.registry, + ) } func (n *Node) closeTransactionCleanupInvalidTasks( @@ -2088,7 +2105,10 @@ func (n *Node) andAllChildren() iter.Seq2[[]string, *Node] { return false } for _, child := range node.children { - if !walk(append(path, child.nodeName), child) { + childPath := make([]string, len(path)+1) + copy(childPath, path) + childPath[len(path)] = child.nodeName + if !walk(childPath, child) { return false } } @@ -2167,6 +2187,19 @@ func (n *Node) snapshotInternal( } } +// ApplySystemMutation should only used by internal persistence layer logic to force apply +// cluster specific chasm tree changes. +// DO NOT USE if you don't know why this method is introduced. +func (n *Node) ApplySystemMutation( + mutation NodesMutation, +) error { + if err := n.applyDeletions(mutation.DeletedNodes, true); err != nil { + return err + } + + return n.applyUpdates(mutation.UpdatedNodes, true) +} + // ApplyMutation is used by replication stack to apply node // mutations from the source cluster. // @@ -2176,11 +2209,11 @@ func (n *Node) snapshotInternal( func (n *Node) ApplyMutation( mutation NodesMutation, ) error { - if err := n.applyDeletions(mutation.DeletedNodes); err != nil { + if err := n.applyDeletions(mutation.DeletedNodes, false); err != nil { return err } - if err := n.applyUpdates(mutation.UpdatedNodes); err != nil { + if err := n.applyUpdates(mutation.UpdatedNodes, false); err != nil { return err } @@ -2194,7 +2227,7 @@ func (n *Node) ApplyMutation( // // TODO: combine this with the logic in CloseTransactionForceUpdateVisibility // right that force update logic only applies to the active cluster. - immutableContext := NewContext(context.Background(), n) + immutableContext := NewContext(context.TODO(), n) rootComponent, err := n.root().Component(immutableContext, ComponentRef{}) if err != nil { return err @@ -2257,6 +2290,7 @@ func (n *Node) ApplySnapshot( func (n *Node) applyDeletions( deletedNodes map[string]struct{}, + isSystemUpdates bool, ) error { for encodedPath := range deletedNodes { path, err := n.pathEncoder.Decode(encodedPath) @@ -2274,7 +2308,7 @@ func (n *Node) applyDeletions( continue } - if err := node.delete(); err != nil { + if err := node.delete(isSystemUpdates); err != nil { return err } } @@ -2284,6 +2318,7 @@ func (n *Node) applyDeletions( func (n *Node) applyUpdates( updatedNodes map[string]*persistencespb.ChasmNode, + isSystemUpdates bool, ) error { for encodedPath, updatedNode := range updatedNodes { path, err := n.pathEncoder.Decode(encodedPath) @@ -2296,7 +2331,11 @@ func (n *Node) applyUpdates( // Node doesn't exist, we need to create it. newNode := n.setSerializedNode(path, encodedPath, updatedNode) newNode.resetTaskStatus() - n.mutation.UpdatedNodes[encodedPath] = newNode.serializedNode + if isSystemUpdates { + n.systemMutation.UpdatedNodes[encodedPath] = newNode.serializedNode + } else { + n.mutation.UpdatedNodes[encodedPath] = newNode.serializedNode + } continue } @@ -2321,7 +2360,11 @@ func (n *Node) applyUpdates( ) } - n.mutation.UpdatedNodes[encodedPath] = updatedNode + if isSystemUpdates { + n.systemMutation.UpdatedNodes[encodedPath] = updatedNode + } else { + n.mutation.UpdatedNodes[encodedPath] = updatedNode + } node.setValue(nil) node.setValueState(valueStateNeedDeserialize) node.serializedNode = updatedNode @@ -2418,9 +2461,9 @@ func (n *Node) findNode( return childNode.findNode(path[1:]) } -func (n *Node) delete() error { +func (n *Node) delete(isSystemDelete bool) error { for _, childNode := range n.children { - if err := childNode.delete(); err != nil { + if err := childNode.delete(isSystemDelete); err != nil { return err } } @@ -2439,7 +2482,12 @@ func (n *Node) delete() error { if err != nil { return err } - n.mutation.DeletedNodes[encodedPath] = struct{}{} + + if isSystemDelete { + n.systemMutation.DeletedNodes[encodedPath] = struct{}{} + } else { + n.mutation.DeletedNodes[encodedPath] = struct{}{} + } n.cleanupCachedTasks() @@ -2496,13 +2544,29 @@ func (n *Node) IsStale( func (n *Node) Terminate( request TerminateComponentRequest, ) error { - mutableContext := NewMutableContext(context.Background(), n.root()) + if n.parent != nil { + return softassert.UnexpectedInternalErr( + n.logger, + "Terminate should only be called on the root node", + fmt.Errorf("node path: %v", n.path()), + ) + } + + mutableContext := NewMutableContext(context.TODO(), n.root()) component, err := n.Component(mutableContext, ComponentRef{}) if err != nil { return err } + rootComponent, ok := component.(RootComponent) + if !ok { + return softassert.UnexpectedInternalErr( + n.logger, + "root node must implement RootComponent interface", + fmt.Errorf("component type: %T", component), + ) + } - _, err = component.Terminate(mutableContext, request) + _, err = rootComponent.Terminate(mutableContext, request) if err != nil { return err } @@ -2511,6 +2575,12 @@ func (n *Node) Terminate( return nil } +// SetDeleteAfterClose suppresses the close visibility task when an execution is being +// terminated as part of a delete operation. Must be called before a [Terminate] call, like in DeleteExecution. +func (n *Node) SetDeleteAfterClose(deleteAfterClose bool) { + n.deleteAfterClose = deleteAfterClose +} + // ArchetypeID returns the framework's internal ID for the root component's fully qualified name. func (n *Node) ArchetypeID() ArchetypeID { // Root must be a component. @@ -2564,7 +2634,7 @@ func isComponentTaskExpired( // close). func (n *Node) EachPureTask( referenceTime time.Time, - callback func(executor NodePureTask, taskAttributes TaskAttributes, taskInstance any) (bool, error), + callback func(handler NodePureTask, taskAttributes TaskAttributes, taskInstance any) (bool, error), ) error { chasmContext := NewContext(context.Background(), n) @@ -2924,15 +2994,26 @@ func (n *Node) ExecutePureTask( defer log.CapturePanic(n.logger, &retErr) - result := registrableTask.executeFn.Call([]reflect.Value{ - reflect.ValueOf(executionContext), - reflect.ValueOf(component), - reflect.ValueOf(taskAttributes), - reflect.ValueOf(taskInstance), - }) - if !result[0].IsNil() { - //nolint:revive // type cast result is unchecked - return true, result[0].Interface().(error) + archetypeTag := metrics.ArchetypeTag("") + if name, ok := n.registry.ArchetypeDisplayName(n.ArchetypeID()); ok { + archetypeTag = metrics.ArchetypeTag(name) + } + chasmTaskTypeTag := metrics.ChasmTaskTypeTag(registrableTask.fqType()) + metricsHandler := n.metricsHandler.WithTags(archetypeTag) + + execErr := registrableTask.pureTaskExecuteFn( + executionContext, + component, + taskAttributes, + taskInstance, + n.registry, + ) + + metrics.ChasmPureTaskRequests.With(metricsHandler).Record(1, chasmTaskTypeTag) + + if execErr != nil { + metrics.ChasmPureTaskErrors.With(metricsHandler).Record(1, chasmTaskTypeTag) + return true, execErr } // TODO - a task validator must succeed validation after a task executes @@ -2946,7 +3027,7 @@ func (n *Node) ExecutePureTask( } // ValidatePureTask runs a pure task's associated validator, returning true -// if the task is valid. Intended for use by standby executors as part of +// if the task is valid. Intended for use by standby handlers as part of // EachPureTask's callback. // This method assumes the node's value has already been prepared (hydrated). func (n *Node) ValidatePureTask( @@ -2963,7 +3044,7 @@ func (n *Node) ValidatePureTask( // ValidateSideEffectTask runs a side effect task's associated validator, // returning the deserialized task instance if the task is valid. Intended for -// use by standby executors. +// use by standby handlers. // // If validation succeeds but the task is invalid, nil is returned to signify the // task can be skipped/deleted. @@ -3049,31 +3130,68 @@ func (n *Node) ValidateSideEffectTask( // ctx should have a CHASM engine already set. func (n *Node) ExecuteSideEffectTask( ctx context.Context, - registry *Registry, executionKey ExecutionKey, chasmTask *tasks.ChasmTask, validate func(NodeBackend, Context, Component) error, -) (retErr error) { +) error { + rt, err := n.lookupSideEffectTask(ctx, "ExecuteSideEffectTask", chasmTask) + if err != nil { + return err + } + return n.invokeSideEffectTaskFn(ctx, rt, executionKey, chasmTask, validate, rt.sideEffectTaskExecuteFn) +} +// ExecuteSideEffectDiscardTask executes the discard handler for the given ChasmTask. This is called on standby +// clusters when a side effect task has been pending past the discard delay, allowing custom discard behavior +// (e.g., spilling activity tasks to matching). +func (n *Node) ExecuteSideEffectDiscardTask( + ctx context.Context, + executionKey ExecutionKey, + chasmTask *tasks.ChasmTask, + validate func(NodeBackend, Context, Component) error, +) error { + rt, err := n.lookupSideEffectTask(ctx, "ExecuteSideEffectDiscardTask", chasmTask) + if err != nil { + return err + } + return n.invokeSideEffectTaskFn(ctx, rt, executionKey, chasmTask, validate, rt.sideEffectTaskDiscardFn) +} + +func (n *Node) lookupSideEffectTask( + ctx context.Context, + callerName string, + chasmTask *tasks.ChasmTask, +) (*RegistrableTask, error) { if engineFromContext(ctx) == nil { - return serviceerror.NewInternal("no CHASM engine set on context") + return nil, serviceerror.NewInternal("no CHASM engine set on context") } - taskInfo := chasmTask.Info - taskTypeID := taskInfo.TypeId - registrableTask, ok := registry.TaskByID(taskTypeID) + taskTypeID := chasmTask.Info.TypeId + registrableTask, ok := n.registry.TaskByID(taskTypeID) if !ok { - return softassert.UnexpectedInternalErr( + return nil, softassert.UnexpectedInternalErr( n.logger, "unknown task type id", fmt.Errorf("%d", taskTypeID)) } if registrableTask.isPureTask { - return softassert.UnexpectedInternalErr( + return nil, softassert.UnexpectedInternalErr( n.logger, - "ExecuteSideEffectTask called on a Pure task, task type: ", + callerName+" called on a Pure task", fmt.Errorf("%s", registrableTask.fqType())) } + return registrableTask, nil +} + +func (n *Node) invokeSideEffectTaskFn( + ctx context.Context, + registrableTask *RegistrableTask, + executionKey ExecutionKey, + chasmTask *tasks.ChasmTask, + validate func(NodeBackend, Context, Component) error, + taskFn func(context.Context, ComponentRef, TaskAttributes, any) error, +) (retErr error) { + taskInfo := chasmTask.Info defer func() { if rec := recover(); rec != nil { @@ -3108,7 +3226,7 @@ func (n *Node) ExecuteSideEffectTask( componentPath: taskInfo.Path, componentInitialVT: taskInfo.ComponentInitialVersionedTransition, - // Validate the Ref only once it is accessed by the task's executor. + // Validate the Ref only once it is accessed by the task's handler. validationFn: makeValidationFn(registrableTask, validate, taskAttributes, taskValue), } @@ -3116,18 +3234,7 @@ func (n *Node) ExecuteSideEffectTask( defer log.CapturePanic(n.logger, &retErr) - result := registrableTask.executeFn.Call([]reflect.Value{ - reflect.ValueOf(ctx), - reflect.ValueOf(ref), - reflect.ValueOf(taskAttributes), - taskValue, - }) - if !result[0].IsNil() { - //nolint:revive // type cast result is unchecked - return result[0].Interface().(error) - } - - return nil + return taskFn(ctx, ref, taskAttributes, taskValue.Interface()) } func (n *Node) ComponentByPath( @@ -3163,36 +3270,31 @@ func makeValidationFn( validate func(NodeBackend, Context, Component) error, taskAttributes TaskAttributes, taskValue reflect.Value, -) func(NodeBackend, Context, Component) error { - return func(backend NodeBackend, ctx Context, component Component) error { +) func(NodeBackend, Context, Component, *Registry) error { + return func(backend NodeBackend, ctx Context, component Component, registry *Registry) error { // Call the provided validation callback. err := validate(backend, ctx, component) if err != nil { return err } - // Side effect's task validator is invoked inside the task executor, + // Side effect's task validator is invoked inside the task handler, // so the panic wrapper ExecuteSideEffectTask() will cover this case. - // Call the TaskValidator interface. - result := registrableTask.validateFn.Call([]reflect.Value{ - reflect.ValueOf(ctx), - reflect.ValueOf(component), - reflect.ValueOf(taskAttributes), - taskValue, - }) - - // Handle err. - if !result[1].IsNil() { - //nolint:revive // type cast result is unchecked - return result[1].Interface().(error) + // Call the TaskValidator. + valid, err := registrableTask.validateFn( + ctx, + component, + taskAttributes, + taskValue.Interface(), + registry, + ) + if err != nil { + return err } - - // Handle bool result. - if !result[0].Bool() { + if !valid { return errTaskNotValid } - return nil } } diff --git a/chasm/tree_test.go b/chasm/tree_test.go index f53655468fe..a229842a814 100644 --- a/chasm/tree_test.go +++ b/chasm/tree_test.go @@ -22,6 +22,7 @@ import ( "go.temporal.io/server/common/clock" "go.temporal.io/server/common/definition" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/persistence/serialization" "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/testing/protoassert" @@ -47,6 +48,7 @@ type ( timeSource *clock.EventTimeSource nodePathEncoder NodePathEncoder logger log.Logger + metricsHandler metrics.Handler } ) @@ -61,6 +63,7 @@ func (s *nodeSuite) SetupTest() { s.testLibrary = newTestLibrary(s.controller) s.logger = testlogger.NewTestLogger(s.T(), testlogger.FailOnAnyUnexpectedError) + s.metricsHandler = metrics.NoopMetricsHandler s.registry = NewRegistry(s.logger) err := s.registry.Register(s.testLibrary) s.NoError(err) @@ -218,7 +221,7 @@ func (s *nodeSuite) TestSerializeNode_ClearSubDataField() { } func (s *nodeSuite) TestSetRootComponent_SetsArchetypeID() { - rootNode := NewEmptyTree(s.registry, s.timeSource, s.nodeBackend, s.nodePathEncoder, s.logger) + rootNode := NewEmptyTree(s.registry, s.timeSource, s.nodeBackend, s.nodePathEncoder, s.logger, s.metricsHandler) s.Equal(WorkflowArchetypeID, rootNode.ArchetypeID()) rootComponent := &TestComponent{ MSPointer: NewMSPointer(s.nodeBackend), @@ -1730,7 +1733,7 @@ func (s *nodeSuite) TestGetComponent() { NamespaceFailoverVersion: 1, TransitionCount: 1, }, - validationFn: func(_ NodeBackend, _ Context, _ Component) error { + validationFn: func(_ NodeBackend, _ Context, _ Component, _ *Registry) error { return errValidation }, }, @@ -1747,7 +1750,7 @@ func (s *nodeSuite) TestGetComponent() { NamespaceFailoverVersion: 1, TransitionCount: 1, }, - validationFn: func(_ NodeBackend, _ Context, _ Component) error { + validationFn: func(_ NodeBackend, _ Context, _ Component, _ *Registry) error { return nil }, }, @@ -2237,11 +2240,11 @@ func (s *nodeSuite) TestCloseTransaction_InvalidateComponentTasks() { _, err = root.Component(mutableContext, ComponentRef{}) s.NoError(err) - s.testLibrary.mockSideEffectTaskValidator.EXPECT(). + s.testLibrary.mockSideEffectTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil).Times(2) - s.testLibrary.mockOutboundSideEffectTaskValidator.EXPECT(). + s.testLibrary.mockOutboundSideEffectTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) - s.testLibrary.mockPureTaskValidator.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil).Times(2) mutation, err := root.CloseTransaction() @@ -2314,7 +2317,7 @@ func (s *nodeSuite) TestCloseTransaction_NewComponentTasks() { s.NoError(err) // Add a valid side effect task. - s.testLibrary.mockSideEffectTaskValidator.EXPECT(). + s.testLibrary.mockSideEffectTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) testComponent := c.(*TestComponent) mutableContext.AddTask(testComponent, TaskAttributes{}, &TestSideEffectTask{ @@ -2323,7 +2326,7 @@ func (s *nodeSuite) TestCloseTransaction_NewComponentTasks() { // Add an invalid outbound side effect task. // the invalid task should not be created. - s.testLibrary.mockOutboundSideEffectTaskValidator.EXPECT(). + s.testLibrary.mockOutboundSideEffectTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil).Times(1) mutableContext.AddTask( testComponent, @@ -2332,7 +2335,7 @@ func (s *nodeSuite) TestCloseTransaction_NewComponentTasks() { ) // Add a valid pure task. - s.testLibrary.mockPureTaskValidator.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) mutableContext.AddTask( testComponent, @@ -2346,7 +2349,7 @@ func (s *nodeSuite) TestCloseTransaction_NewComponentTasks() { // Add an invalid pure task. // the invalid task should not be created. - s.testLibrary.mockPureTaskValidator.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil).Times(1) mutableContext.AddTask( testComponent, @@ -2359,7 +2362,7 @@ func (s *nodeSuite) TestCloseTransaction_NewComponentTasks() { ) // Add a valid outbound side effect task to a sub-component. - s.testLibrary.mockOutboundSideEffectTaskValidator.EXPECT(). + s.testLibrary.mockOutboundSideEffectTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) subComponent2 := testComponent.SubComponent2.Get(mutableContext) mutableContext.AddTask( @@ -2784,11 +2787,11 @@ func (s *nodeSuite) TestExecuteImmediatePureTask() { ) // One valid task, one invalid task - s.testLibrary.mockPureTaskValidator.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Eq(taskAttributes), gomock.Any()).Return(false, nil).Times(1) - s.testLibrary.mockPureTaskValidator.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Eq(taskAttributes), gomock.Any()).Return(true, nil).Times(1) - s.testLibrary.mockPureTaskExecutor.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Execute( gomock.AssignableToTypeOf(&mutableCtx{}), gomock.Any(), @@ -2939,8 +2942,8 @@ func (s *nodeSuite) TestEachPureTask() { s.NotNil(root) processedTaskData := [][]byte{} - err = root.EachPureTask(now.Add(time.Minute), func(executor NodePureTask, taskAttributes TaskAttributes, task any) (bool, error) { - s.NotNil(executor) + err = root.EachPureTask(now.Add(time.Minute), func(handler NodePureTask, taskAttributes TaskAttributes, task any) (bool, error) { + s.NotNil(handler) s.NotNil(taskAttributes) testPureTask, ok := task.(*TestPureTask) @@ -3010,7 +3013,7 @@ func (s *nodeSuite) TestExecutePureTask() { ctx := context.Background() expectExecute := func(result error) { - s.testLibrary.mockPureTaskExecutor.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Execute( gomock.AssignableToTypeOf(&mutableCtx{}), gomock.AssignableToTypeOf(&TestComponent{}), @@ -3020,7 +3023,7 @@ func (s *nodeSuite) TestExecutePureTask() { } expectValidate := func(retValue bool, errValue error) { - s.testLibrary.mockPureTaskValidator.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Eq(taskAttributes), gomock.Any()).Return(retValue, errValue).Times(1) } @@ -3073,7 +3076,7 @@ func (s *nodeSuite) TestValidatePureTask() { ctx := context.Background() expectValidate := func(retValue bool, errValue error) { - s.testLibrary.mockPureTaskValidator.EXPECT(). + s.testLibrary.mockPureTaskHandler.EXPECT(). Validate(gomock.Any(), gomock.Any(), gomock.Eq(taskAttributes), gomock.Any()).Return(retValue, errValue).Times(1) } @@ -3178,7 +3181,7 @@ func (s *nodeSuite) TestExecuteSideEffectTask() { } expectValidate := func(valid bool, validationErr error) { backendValidtionFnCalled = false - s.testLibrary.mockSideEffectTaskValidator.EXPECT().Validate( + s.testLibrary.mockSideEffectTaskHandler.EXPECT().Validate( gomock.Any(), gomock.Any(), gomock.Any(), @@ -3186,7 +3189,7 @@ func (s *nodeSuite) TestExecuteSideEffectTask() { ).Return(valid, validationErr).Times(1) } expectExecute := func(result error) { - s.testLibrary.mockSideEffectTaskExecutor.EXPECT(). + s.testLibrary.mockSideEffectTaskHandler.EXPECT(). Execute( gomock.Any(), gomock.Any(), @@ -3211,7 +3214,7 @@ func (s *nodeSuite) TestExecuteSideEffectTask() { // Succeed task execution. expectValidate(true, nil) expectExecute(nil) - err = root.ExecuteSideEffectTask(ctx, s.registry, executionKey, chasmTask, dummyValidationFn) + err = root.ExecuteSideEffectTask(ctx, executionKey, chasmTask, dummyValidationFn) s.NoError(err) s.True(backendValidtionFnCalled) s.True(chasmTask.DeserializedTask.IsValid()) @@ -3219,7 +3222,7 @@ func (s *nodeSuite) TestExecuteSideEffectTask() { // Invalid task. expectValidate(false, nil) expectExecute(nil) - err = root.ExecuteSideEffectTask(ctx, s.registry, executionKey, chasmTask, dummyValidationFn) + err = root.ExecuteSideEffectTask(ctx, executionKey, chasmTask, dummyValidationFn) s.Error(err) s.IsType(&serviceerror.NotFound{}, err) s.True(chasmTask.DeserializedTask.IsValid()) @@ -3228,7 +3231,7 @@ func (s *nodeSuite) TestExecuteSideEffectTask() { validationErr := errors.New("validation error") expectValidate(false, validationErr) expectExecute(nil) - err = root.ExecuteSideEffectTask(ctx, s.registry, executionKey, chasmTask, dummyValidationFn) + err = root.ExecuteSideEffectTask(ctx, executionKey, chasmTask, dummyValidationFn) s.ErrorIs(validationErr, err) s.False(chasmTask.DeserializedTask.IsValid()) @@ -3236,12 +3239,169 @@ func (s *nodeSuite) TestExecuteSideEffectTask() { expectValidate(true, nil) executionErr := errors.New("execution error") expectExecute(executionErr) - err = root.ExecuteSideEffectTask(ctx, s.registry, executionKey, chasmTask, dummyValidationFn) + err = root.ExecuteSideEffectTask(ctx, executionKey, chasmTask, dummyValidationFn) s.ErrorIs(executionErr, err) s.True(backendValidtionFnCalled) s.False(chasmTask.DeserializedTask.IsValid()) } +func (s *nodeSuite) TestExecuteSideEffectDiscardTask() { + setup := func() (*Node, *tasks.ChasmTask, ExecutionKey, context.Context, Context) { + persistenceNodes := map[string]*persistencespb.ChasmNode{ + "": { + Metadata: &persistencespb.ChasmNodeMetadata{ + InitialVersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, + Attributes: &persistencespb.ChasmNodeMetadata_ComponentAttributes{ + ComponentAttributes: &persistencespb.ChasmComponentAttributes{ + TypeId: testComponentTypeID, + }, + }, + }, + }, + } + + root, err := s.newTestTree(persistenceNodes) + s.NoError(err) + s.NotNil(root) + + workflowKey := definition.NewWorkflowKey( + primitives.NewUUID().String(), + primitives.NewUUID().String(), + primitives.NewUUID().String(), + ) + chasmTask := &tasks.ChasmTask{ + WorkflowKey: workflowKey, + VisibilityTimestamp: s.timeSource.Now(), + TaskID: 123, + Category: tasks.CategoryOutbound, + Destination: "destination", + Info: &persistencespb.ChasmTaskInfo{ + ComponentInitialVersionedTransition: &persistencespb.VersionedTransition{ + TransitionCount: 1, + }, + ComponentLastUpdateVersionedTransition: &persistencespb.VersionedTransition{ + TransitionCount: 1, + }, + Path: rootPath, + TypeId: testDiscardableSideEffectTaskTypeID, + Data: &commonpb.DataBlob{ + Data: nil, + EncodingType: enumspb.ENCODING_TYPE_PROTO3, + }, + }, + } + executionKey := ExecutionKey{ + NamespaceID: chasmTask.NamespaceID, + BusinessID: chasmTask.WorkflowID, + RunID: chasmTask.RunID, + } + + mockEngine := NewMockEngine(s.controller) + ctx := NewEngineContext(context.Background(), mockEngine) + chasmContext := NewMutableContext(ctx, root) + + return root, chasmTask, executionKey, ctx, chasmContext + } + + s.Run("Success", func() { + root, chasmTask, executionKey, ctx, chasmContext := setup() + + var validationFnCalled bool + dummyValidationFn := func(_ NodeBackend, _ Context, _ Component) error { + validationFnCalled = true + return nil + } + + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Validate( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(true, nil).Times(1) + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Discard( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).DoAndReturn(func( + _ context.Context, ref ComponentRef, _ TaskAttributes, _ *TestDiscardableSideEffectTask, + ) error { + s.NotNil(ref.validationFn) + _, err := root.Component(chasmContext, ref) + return err + }).Times(1) + + err := root.ExecuteSideEffectDiscardTask(ctx, executionKey, chasmTask, dummyValidationFn) + s.NoError(err) + s.True(validationFnCalled) + s.True(chasmTask.DeserializedTask.IsValid()) + }) + + s.Run("InvalidTask", func() { + root, chasmTask, executionKey, ctx, chasmContext := setup() + + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Validate( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(false, nil).Times(1) + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Discard( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).DoAndReturn(func( + _ context.Context, ref ComponentRef, _ TaskAttributes, _ *TestDiscardableSideEffectTask, + ) error { + _, err := root.Component(chasmContext, ref) + return err + }).Times(1) + + err := root.ExecuteSideEffectDiscardTask(ctx, executionKey, chasmTask, func(_ NodeBackend, _ Context, _ Component) error { return nil }) + s.ErrorAs(err, new(*serviceerror.NotFound)) + }) + + s.Run("ValidationError", func() { + root, chasmTask, executionKey, ctx, chasmContext := setup() + + validationErr := errors.New("validation error") + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Validate( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(false, validationErr).Times(1) + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Discard( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).DoAndReturn(func( + _ context.Context, ref ComponentRef, _ TaskAttributes, _ *TestDiscardableSideEffectTask, + ) error { + _, err := root.Component(chasmContext, ref) + return err + }).Times(1) + + err := root.ExecuteSideEffectDiscardTask( + ctx, executionKey, chasmTask, func(_ NodeBackend, _ Context, _ Component) error { return nil }) + s.ErrorIs(err, validationErr) + }) + + s.Run("DiscardHandlerError", func() { + root, chasmTask, executionKey, ctx, chasmContext := setup() + + var validationFnCalled bool + dummyValidationFn := func(_ NodeBackend, _ Context, _ Component) error { + validationFnCalled = true + return nil + } + + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Validate( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(true, nil).Times(1) + discardErr := errors.New("discard error") + s.testLibrary.mockDiscardableSideEffectHandler.EXPECT().Discard( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).DoAndReturn(func( + _ context.Context, ref ComponentRef, _ TaskAttributes, _ *TestDiscardableSideEffectTask, + ) error { + s.NotNil(ref.validationFn) + if _, err := root.Component(chasmContext, ref); err != nil { + return err + } + return discardErr + }).Times(1) + + err := root.ExecuteSideEffectDiscardTask(ctx, executionKey, chasmTask, dummyValidationFn) + s.ErrorIs(err, discardErr) + s.True(validationFnCalled) + }) +} + func (s *nodeSuite) TestValidateSideEffectTask() { taskInfo := &persistencespb.ChasmTaskInfo{ ComponentInitialVersionedTransition: &persistencespb.VersionedTransition{ @@ -3278,7 +3438,7 @@ func (s *nodeSuite) TestValidateSideEffectTask() { ctx := NewEngineContext(context.Background(), mockEngine) expectValidate := func(componentType any, retValue bool, errValue error) { - s.testLibrary.mockSideEffectTaskValidator.EXPECT(). + s.testLibrary.mockSideEffectTaskHandler.EXPECT(). Validate( gomock.AssignableToTypeOf((*immutableCtx)(nil)), gomock.AssignableToTypeOf(componentType), @@ -3346,11 +3506,44 @@ func (s *nodeSuite) TestValidateSideEffectTask() { s.True(childChasmTask.DeserializedTask.IsValid()) } +func (s *nodeSuite) TestAndAllChildren_PathIndependence() { + // Build a tree deep enough to trigger Go's slice capacity doubling. + // append grows cap: 0→1→2→4. At depth 3, the path slice has len=3, cap=4, + // so a 4th append reuses the backing array. If node P at depth 3 has siblings + // S1 and S2 at depth 4, the second sibling's append overwrites S1's path. + // + // Tree: root → A → B → C → {S1, S2} + root := &Node{ + nodeName: "", + children: map[string]*Node{ + "A": {nodeName: "A", children: map[string]*Node{ + "B": {nodeName: "B", children: map[string]*Node{ + "C": {nodeName: "C", children: map[string]*Node{ + "S1": {nodeName: "S1", children: map[string]*Node{}}, + "S2": {nodeName: "S2", children: map[string]*Node{}}, + }}, + }}, + }}, + }, + } + + // Store raw path slices (not copies!) so we can detect mutation. + collected := make(map[string][]string) + for path, node := range root.andAllChildren() { + collected[node.nodeName] = path + } + + // Verify S1/S2 do not have a corrupted path + // because append reused the backing array at depth 3→4. + s.Equal([]string{"A", "B", "C", "S1"}, collected["S1"]) + s.Equal([]string{"A", "B", "C", "S2"}, collected["S2"]) +} + func (s *nodeSuite) newTestTree( serializedNodes map[string]*persistencespb.ChasmNode, ) (*Node, error) { if len(serializedNodes) == 0 { - return NewEmptyTree(s.registry, s.timeSource, s.nodeBackend, s.nodePathEncoder, s.logger), nil + return NewEmptyTree(s.registry, s.timeSource, s.nodeBackend, s.nodePathEncoder, s.logger, s.metricsHandler), nil } - return NewTreeFromDB(serializedNodes, s.registry, s.timeSource, s.nodeBackend, s.nodePathEncoder, s.logger) + return NewTreeFromDB(serializedNodes, s.registry, s.timeSource, s.nodeBackend, s.nodePathEncoder, s.logger, s.metricsHandler) } diff --git a/chasm/visibility.go b/chasm/visibility.go index a1c6a31a540..50f74530ddc 100644 --- a/chasm/visibility.go +++ b/chasm/visibility.go @@ -161,10 +161,12 @@ func NewVisibilityWithData( }, } - if len(customSearchAttributes) != 0 { + // Filter out nil/empty payload values for search attributes. + filteredSA := payload.MergeMapOfPayload(nil, customSearchAttributes) + if len(filteredSA) != 0 { visibility.SA = NewDataField( mutableContext, - &commonpb.SearchAttributes{IndexedFields: customSearchAttributes}, + &commonpb.SearchAttributes{IndexedFields: filteredSA}, ) } if len(customMemo) != 0 { @@ -228,12 +230,16 @@ func (v *Visibility) MergeCustomSearchAttributes( } // ReplaceCustomSearchAttributes replaces the existing custom search attribute fields with the provided ones. -// If `customSearchAttributes` is empty, the underlying search attributes node is deleted. +// Nil/empty payload values are filtered. +// If `customSearchAttributes` is empty or all values are nil after filtering, the underlying search attributes node is deleted. func (v *Visibility) ReplaceCustomSearchAttributes( mutableContext MutableContext, customSearchAttributes map[string]*commonpb.Payload, ) { - if len(customSearchAttributes) == 0 { + // Filter out nil/empty payload values. + filteredSA := payload.MergeMapOfPayload(nil, customSearchAttributes) + + if len(filteredSA) == 0 { _, ok := v.SA.TryGet(mutableContext) if !ok { // Already empty, no-op @@ -244,7 +250,7 @@ func (v *Visibility) ReplaceCustomSearchAttributes( } else { v.SA = NewDataField( mutableContext, - &commonpb.SearchAttributes{IndexedFields: customSearchAttributes}, + &commonpb.SearchAttributes{IndexedFields: filteredSA}, ) } @@ -330,7 +336,9 @@ func (v *Visibility) generateTask( ) } -type visibilityTaskHandler struct{} +type visibilityTaskHandler struct { + SideEffectTaskHandlerBase[*persistencespb.ChasmVisibilityTaskData] +} var defaultVisibilityTaskHandler = &visibilityTaskHandler{} @@ -350,5 +358,5 @@ func (v *visibilityTaskHandler) Execute( _ *persistencespb.ChasmVisibilityTaskData, ) error { //nolint:forbidigo - panic("chasm visibilityTaskExecutor should not be called directly") + panic("chasm visibilityTaskHandler should not be called directly") } diff --git a/chasm/visibility_manager.go b/chasm/visibility_manager.go index d046c1f46c9..fb36fd8e8b1 100644 --- a/chasm/visibility_manager.go +++ b/chasm/visibility_manager.go @@ -9,6 +9,7 @@ import ( commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/common/payload" "google.golang.org/protobuf/proto" ) @@ -18,16 +19,16 @@ type VisibilityManager interface { context.Context, reflect.Type, *ListExecutionsRequest, - ) (*ListExecutionsResponse[*commonpb.Payload], error) + ) (*visibilityservice.ListChasmExecutionsResponse, error) CountExecutions( context.Context, reflect.Type, *CountExecutionsRequest, - ) (*CountExecutionsResponse, error) + ) (*visibilityservice.CountChasmExecutionsResponse, error) } -type ExecutionInfo[M proto.Message] struct { +type VisibilityExecutionInfo[M proto.Message] struct { BusinessID string RunID string StartTime time.Time @@ -42,7 +43,6 @@ type ExecutionInfo[M proto.Message] struct { } type ListExecutionsRequest struct { - NamespaceID string NamespaceName string Query string PageSize int @@ -50,12 +50,11 @@ type ListExecutionsRequest struct { } type ListExecutionsResponse[M proto.Message] struct { - Executions []*ExecutionInfo[M] + Executions []*VisibilityExecutionInfo[M] NextPageToken []byte } type CountExecutionsRequest struct { - NamespaceID string NamespaceName string Query string } @@ -91,28 +90,32 @@ func ListExecutions[C Component, M proto.Message]( return nil, err } - // Convert response, unmarshaling ChasmMemo to type M - executions := make([]*ExecutionInfo[M], len(response.Executions)) + // Convert response: decode ChasmSearchAttributes and ChasmMemo to type M + executions := make([]*VisibilityExecutionInfo[M], len(response.Executions)) for i, execution := range response.Executions { + chasmSAs, err := newSearchAttributesMapFromProto(execution.ChasmSearchAttributes) + if err != nil { + return nil, err + } + chasmMemoInterface := reflect.New(reflect.TypeFor[M]().Elem()).Interface() chasmMemo, ok := chasmMemoInterface.(M) if !ok { return nil, serviceerror.NewInternalf("failed to cast chasm memo to type %s", reflect.TypeFor[M]().String()) } - err := payload.Decode(execution.ChasmMemo, chasmMemo) - if err != nil { + if err := payload.Decode(execution.ChasmMemo, chasmMemo); err != nil { return nil, serviceerror.NewInternalf("failed to decode chasm memo: %v", err) } - executions[i] = &ExecutionInfo[M]{ - BusinessID: execution.BusinessID, - RunID: execution.RunID, - StartTime: execution.StartTime, - CloseTime: execution.CloseTime, + executions[i] = &VisibilityExecutionInfo[M]{ + BusinessID: execution.BusinessId, + RunID: execution.RunId, + StartTime: execution.StartTime.AsTime(), + CloseTime: execution.CloseTime.AsTime(), HistoryLength: execution.HistoryLength, HistorySizeBytes: execution.HistorySizeBytes, StateTransitionCount: execution.StateTransitionCount, - ChasmSearchAttributes: execution.ChasmSearchAttributes, - CustomSearchAttributes: execution.CustomSearchAttributes, + ChasmSearchAttributes: chasmSAs, + CustomSearchAttributes: execution.CustomSearchAttributes.GetIndexedFields(), Memo: execution.Memo, ChasmMemo: chasmMemo, } @@ -137,7 +140,22 @@ func CountExecutions[C Component]( request *CountExecutionsRequest, ) (*CountExecutionsResponse, error) { archetypeType := reflect.TypeFor[C]() - return visibilityManagerFromContext(ctx).CountExecutions(ctx, archetypeType, request) + visResponse, err := visibilityManagerFromContext(ctx).CountExecutions(ctx, archetypeType, request) + if err != nil { + return nil, err + } + + response := &CountExecutionsResponse{ + Count: visResponse.Count, + Groups: make([]Group, len(visResponse.Groups)), + } + for k, group := range visResponse.Groups { + response.Groups[k] = Group{ + Values: group.GroupValues, + Count: group.Count, + } + } + return response, nil } type visibilityManagerCtxKeyType string diff --git a/chasm/visibility_manager_mock.go b/chasm/visibility_manager_mock.go index e45f51a6673..4a57c11377c 100644 --- a/chasm/visibility_manager_mock.go +++ b/chasm/visibility_manager_mock.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - common "go.temporal.io/api/common/v1" + visibilityservice "go.temporal.io/server/api/visibilityservice/v1" gomock "go.uber.org/mock/gomock" ) @@ -42,10 +42,10 @@ func (m *MockVisibilityManager) EXPECT() *MockVisibilityManagerMockRecorder { } // CountExecutions mocks base method. -func (m *MockVisibilityManager) CountExecutions(arg0 context.Context, arg1 reflect.Type, arg2 *CountExecutionsRequest) (*CountExecutionsResponse, error) { +func (m *MockVisibilityManager) CountExecutions(arg0 context.Context, arg1 reflect.Type, arg2 *CountExecutionsRequest) (*visibilityservice.CountChasmExecutionsResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountExecutions", arg0, arg1, arg2) - ret0, _ := ret[0].(*CountExecutionsResponse) + ret0, _ := ret[0].(*visibilityservice.CountChasmExecutionsResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -57,10 +57,10 @@ func (mr *MockVisibilityManagerMockRecorder) CountExecutions(arg0, arg1, arg2 an } // ListExecutions mocks base method. -func (m *MockVisibilityManager) ListExecutions(arg0 context.Context, arg1 reflect.Type, arg2 *ListExecutionsRequest) (*ListExecutionsResponse[*common.Payload], error) { +func (m *MockVisibilityManager) ListExecutions(arg0 context.Context, arg1 reflect.Type, arg2 *ListExecutionsRequest) (*visibilityservice.ListChasmExecutionsResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListExecutions", arg0, arg1, arg2) - ret0, _ := ret[0].(*ListExecutionsResponse[*common.Payload]) + ret0, _ := ret[0].(*visibilityservice.ListChasmExecutionsResponse) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/chasm/visibility_test.go b/chasm/visibility_test.go index 9a9436b795d..4308791e60d 100644 --- a/chasm/visibility_test.go +++ b/chasm/visibility_test.go @@ -131,6 +131,29 @@ func (s *visibilitySuite) TestMergeCustomSearchAttributes() { s.Nil(s.visibility.CustomSearchAttributes(s.mockContext)) } +func (s *visibilitySuite) TestNewVisibilityWithData_FilterNilSearchAttributes() { + stringKey, stringVal := "stringKey", "stringValue" + // SA with 1 valid and 2 nil values - nil values should be filtered out + customSearchAttributes := map[string]*commonpb.Payload{ + stringKey: s.mustEncode(stringVal), + "nilKey1": nil, + "nilKey2": nil, + } + // Memo with 1 valid and 2 nil values - nil values should NOT be filtered out + customMemo := map[string]*commonpb.Payload{ + stringKey: s.mustEncode(stringVal), + "nilKey1": nil, + "nilKey2": nil, + } + visibility := NewVisibilityWithData(s.mockMutableContext, customSearchAttributes, customMemo) + // SA should have only 1 field (nil values filtered out) + s.Len(visibility.SA.Get(s.mockContext).IndexedFields, 1) + s.NotNil(visibility.SA.Get(s.mockContext).IndexedFields[stringKey]) + // Memo should have all 3 fields (nil values NOT filtered) + s.Len(visibility.Memo.Get(s.mockContext).Fields, 3) + s.NotNil(visibility.Memo.Get(s.mockContext).Fields[stringKey]) +} + func (s *visibilitySuite) TestReplaceCustomSearchAttributes() { stringKey, stringVal := "stringKey", "stringValue" intKey, intVal := "intKey", 42 @@ -176,6 +199,36 @@ func (s *visibilitySuite) TestReplaceCustomSearchAttributes() { _, ok := s.visibility.SA.TryGet(s.mockContext) s.False(ok) s.Nil(s.visibility.CustomSearchAttributes(s.mockContext)) + + // Test that nil values are filtered out during replace. + s.visibility.ReplaceCustomSearchAttributes( + s.mockMutableContext, + map[string]*commonpb.Payload{ + stringKey: s.mustEncode(stringVal), + intKey: nil, // Should be filtered out + }, + ) + s.Len(s.mockMutableContext.Tasks, 4) + s.assertTaskPayload(5, s.mockMutableContext.Tasks[3].Payload) + + sa = s.visibility.CustomSearchAttributes(s.mockMutableContext) + s.Len(sa, 1, "nil values should be filtered out") + s.NotNil(sa[stringKey]) + s.Nil(sa[intKey]) + + // Test that replacing with all nil values removes the node. + s.visibility.ReplaceCustomSearchAttributes( + s.mockMutableContext, + map[string]*commonpb.Payload{ + stringKey: nil, + intKey: nil, + }, + ) + s.Len(s.mockMutableContext.Tasks, 5) + s.assertTaskPayload(6, s.mockMutableContext.Tasks[4].Payload) + _, ok = s.visibility.SA.TryGet(s.mockContext) + s.False(ok) + s.Nil(s.visibility.CustomSearchAttributes(s.mockContext)) } func (s *visibilitySuite) TestMergeCustomMemo() { diff --git a/chasm/visibility_value.go b/chasm/visibility_value.go index 7c2a6696d8c..0b79d230ec5 100644 --- a/chasm/visibility_value.go +++ b/chasm/visibility_value.go @@ -1,12 +1,12 @@ package chasm import ( + "fmt" "slices" "time" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" - "go.temporal.io/server/common/payload" "go.temporal.io/server/common/searchattribute/sadefs" ) @@ -16,52 +16,10 @@ type VisibilityValue interface { Value() any } -type VisibilityValueInt int - -func (v VisibilityValueInt) MustEncode() *commonpb.Payload { - p, _ := payload.Encode(int(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_INT) - return p -} - -func (v VisibilityValueInt) Equal(other VisibilityValue) bool { - ov, ok := other.(VisibilityValueInt) - if !ok { - return false - } - return v == ov -} - -func (v VisibilityValueInt) Value() any { - return int(v) -} - -type VisibilityValueInt32 int32 - -func (v VisibilityValueInt32) MustEncode() *commonpb.Payload { - p, _ := payload.Encode(int32(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_INT) - return p -} - -func (v VisibilityValueInt32) Equal(other VisibilityValue) bool { - ov, ok := other.(VisibilityValueInt32) - if !ok { - return false - } - return v == ov -} - -func (v VisibilityValueInt32) Value() any { - return int32(v) -} - type VisibilityValueInt64 int64 func (v VisibilityValueInt64) MustEncode() *commonpb.Payload { - p, _ := payload.Encode(int64(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_INT) - return p + return sadefs.MustEncodeValue(int64(v), enumspb.INDEXED_VALUE_TYPE_INT) } func (v VisibilityValueInt64) Equal(other VisibilityValue) bool { @@ -76,32 +34,28 @@ func (v VisibilityValueInt64) Value() any { return int64(v) } -type VisibilityValueString string +type VisibilityValueKeyword string -func (v VisibilityValueString) MustEncode() *commonpb.Payload { - p := payload.EncodeString(string(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_KEYWORD) - return p +func (v VisibilityValueKeyword) MustEncode() *commonpb.Payload { + return sadefs.MustEncodeValue(string(v), enumspb.INDEXED_VALUE_TYPE_KEYWORD) } -func (v VisibilityValueString) Equal(other VisibilityValue) bool { - ov, ok := other.(VisibilityValueString) +func (v VisibilityValueKeyword) Equal(other VisibilityValue) bool { + ov, ok := other.(VisibilityValueKeyword) if !ok { return false } return v == ov } -func (v VisibilityValueString) Value() any { +func (v VisibilityValueKeyword) Value() any { return string(v) } type VisibilityValueBool bool func (v VisibilityValueBool) MustEncode() *commonpb.Payload { - p, _ := payload.Encode(bool(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_BOOL) - return p + return sadefs.MustEncodeValue(bool(v), enumspb.INDEXED_VALUE_TYPE_BOOL) } func (v VisibilityValueBool) Equal(other VisibilityValue) bool { @@ -119,9 +73,7 @@ func (v VisibilityValueBool) Value() any { type VisibilityValueFloat64 float64 func (v VisibilityValueFloat64) MustEncode() *commonpb.Payload { - p, _ := payload.Encode(float64(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_DOUBLE) - return p + return sadefs.MustEncodeValue(float64(v), enumspb.INDEXED_VALUE_TYPE_DOUBLE) } func (v VisibilityValueFloat64) Equal(other VisibilityValue) bool { @@ -139,9 +91,7 @@ func (v VisibilityValueFloat64) Value() any { type VisibilityValueTime time.Time func (v VisibilityValueTime) MustEncode() *commonpb.Payload { - p, _ := payload.Encode(time.Time(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_DATETIME) - return p + return sadefs.MustEncodeValue(time.Time(v), enumspb.INDEXED_VALUE_TYPE_DATETIME) } func (v VisibilityValueTime) Equal(other VisibilityValue) bool { @@ -156,30 +106,10 @@ func (v VisibilityValueTime) Value() any { return time.Time(v) } -type VisibilityValueByteSlice []byte - -func (v VisibilityValueByteSlice) MustEncode() *commonpb.Payload { - return payload.EncodeBytes([]byte(v)) -} - -func (v VisibilityValueByteSlice) Equal(other VisibilityValue) bool { - ov, ok := other.(VisibilityValueByteSlice) - if !ok { - return false - } - return slices.Equal(v, ov) -} - -func (v VisibilityValueByteSlice) Value() any { - return []byte(v) -} - type VisibilityValueStringSlice []string func (v VisibilityValueStringSlice) MustEncode() *commonpb.Payload { - p, _ := payload.Encode([]string(v)) - sadefs.SetMetadataType(p, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) - return p + return sadefs.MustEncodeValue([]string(v), enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) } func (v VisibilityValueStringSlice) Equal(other VisibilityValue) bool { @@ -203,3 +133,33 @@ func isVisibilityValueEqual(v1, v2 VisibilityValue) bool { } return v1.Equal(v2) } + +// visibilityValueFromPayload decoded payload based on type set in its metadata. +func visibilityValueFromPayload(payload *commonpb.Payload) (VisibilityValue, error) { + value, err := sadefs.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, false) + if err != nil { + return nil, err + } + + switch val := value.(type) { + case int64: + return VisibilityValueInt64(val), nil + case float64: + return VisibilityValueFloat64(val), nil + case bool: + return VisibilityValueBool(val), nil + case time.Time: + return VisibilityValueTime(val), nil + case string: + // Try to parse as datetime first + if parsedTime, err := time.Parse(time.RFC3339, val); err == nil { + return VisibilityValueTime(parsedTime), nil + } + return VisibilityValueKeyword(val), nil + case []string: + return VisibilityValueStringSlice(val), nil + default: + // this should never happen given that DecodeValue did not return an error + return nil, fmt.Errorf("unexpected search attribute value type %T", value) + } +} diff --git a/chasm/visibility_value_test.go b/chasm/visibility_value_test.go index f1b41d4d6db..08c187a32bc 100644 --- a/chasm/visibility_value_test.go +++ b/chasm/visibility_value_test.go @@ -9,37 +9,6 @@ import ( ) func TestVisibilityValue(t *testing.T) { - t.Run("Int", func(t *testing.T) { - v := VisibilityValueInt(42) - p := v.MustEncode() - require.NotNil(t, p) - - var out int - err := payload.Decode(p, &out) - require.NoError(t, err) - require.Equal(t, 42, out) - - require.True(t, v.Equal(VisibilityValueInt(42))) - require.False(t, v.Equal(VisibilityValueInt(43))) - // Different underlying type should not be equal even if numerically same - require.False(t, v.Equal(VisibilityValueInt32(42))) - }) - - t.Run("Int32", func(t *testing.T) { - v := VisibilityValueInt32(123) - p := v.MustEncode() - require.NotNil(t, p) - - var out int32 - err := payload.Decode(p, &out) - require.NoError(t, err) - require.Equal(t, int32(123), out) - - require.True(t, v.Equal(VisibilityValueInt32(123))) - require.False(t, v.Equal(VisibilityValueInt32(124))) - require.False(t, v.Equal(VisibilityValueInt64(123))) - }) - t.Run("Int64", func(t *testing.T) { v := VisibilityValueInt64(9876543210) p := v.MustEncode() @@ -52,11 +21,10 @@ func TestVisibilityValue(t *testing.T) { require.True(t, v.Equal(VisibilityValueInt64(9876543210))) require.False(t, v.Equal(VisibilityValueInt64(9876543211))) - require.False(t, v.Equal(VisibilityValueInt(9876543210))) }) t.Run("String", func(t *testing.T) { - v := VisibilityValueString("hello, 世界") + v := VisibilityValueKeyword("hello, 世界") p := v.MustEncode() require.NotNil(t, p) @@ -65,8 +33,8 @@ func TestVisibilityValue(t *testing.T) { require.NoError(t, err) require.Equal(t, "hello, 世界", out) - require.True(t, v.Equal(VisibilityValueString("hello, 世界"))) - require.False(t, v.Equal(VisibilityValueString("hello"))) + require.True(t, v.Equal(VisibilityValueKeyword("hello, 世界"))) + require.False(t, v.Equal(VisibilityValueKeyword("hello"))) require.False(t, v.Equal(VisibilityValueBool(true))) }) @@ -82,7 +50,7 @@ func TestVisibilityValue(t *testing.T) { require.True(t, v.Equal(VisibilityValueBool(true))) require.False(t, v.Equal(VisibilityValueBool(false))) - require.False(t, v.Equal(VisibilityValueString("true"))) + require.False(t, v.Equal(VisibilityValueKeyword("true"))) }) t.Run("Float64", func(t *testing.T) { @@ -97,23 +65,6 @@ func TestVisibilityValue(t *testing.T) { require.True(t, v.Equal(VisibilityValueFloat64(3.14159))) require.False(t, v.Equal(VisibilityValueFloat64(2.71828))) - require.False(t, v.Equal(VisibilityValueInt(3))) - }) - - t.Run("ByteSlice", func(t *testing.T) { - v := VisibilityValueByteSlice([]byte{0x01, 0x02, 0x03}) - p := v.MustEncode() - require.NotNil(t, p) - - var out []byte - err := payload.Decode(p, &out) - require.NoError(t, err) - require.Equal(t, []byte{0x01, 0x02, 0x03}, out) - - require.True(t, v.Equal(VisibilityValueByteSlice([]byte{0x01, 0x02, 0x03}))) - require.False(t, v.Equal(VisibilityValueByteSlice([]byte{0x01, 0x02}))) - require.False(t, v.Equal(VisibilityValueByteSlice([]byte{0x01, 0x03, 0x02}))) - require.False(t, v.Equal(VisibilityValueString("\x01\x02\x03"))) }) t.Run("StringSlice", func(t *testing.T) { @@ -129,7 +80,7 @@ func TestVisibilityValue(t *testing.T) { require.True(t, v.Equal(VisibilityValueStringSlice([]string{"a", "b", "c"}))) require.False(t, v.Equal(VisibilityValueStringSlice([]string{"a", "c", "b"}))) require.False(t, v.Equal(VisibilityValueStringSlice([]string{"a", "b"}))) - require.False(t, v.Equal(VisibilityValueString("[a b c]"))) + require.False(t, v.Equal(VisibilityValueKeyword("[a b c]"))) }) // Time @@ -147,7 +98,7 @@ func TestVisibilityValue(t *testing.T) { require.True(t, v.Equal(VisibilityValueTime(base))) require.False(t, v.Equal(VisibilityValueTime(base.Add(time.Second)))) - require.False(t, v.Equal(VisibilityValueString(base.String()))) + require.False(t, v.Equal(VisibilityValueKeyword(base.String()))) }) } @@ -156,14 +107,14 @@ func TestIsVisibilityValueEqual(t *testing.T) { require.True(t, isVisibilityValueEqual(nil, nil)) // one nil - require.False(t, isVisibilityValueEqual(VisibilityValueInt(1), nil)) - require.False(t, isVisibilityValueEqual(nil, VisibilityValueInt(1))) + require.False(t, isVisibilityValueEqual(VisibilityValueInt64(1), nil)) + require.False(t, isVisibilityValueEqual(nil, VisibilityValueInt64(1))) // equal values - require.True(t, isVisibilityValueEqual(VisibilityValueString("x"), VisibilityValueString("x"))) + require.True(t, isVisibilityValueEqual(VisibilityValueKeyword("x"), VisibilityValueKeyword("x"))) require.True(t, isVisibilityValueEqual(VisibilityValueInt64(5), VisibilityValueInt64(5))) // not equal values - require.False(t, isVisibilityValueEqual(VisibilityValueInt(5), VisibilityValueInt(6))) - require.False(t, isVisibilityValueEqual(VisibilityValueInt(5), VisibilityValueInt64(5))) + require.False(t, isVisibilityValueEqual(VisibilityValueInt64(5), VisibilityValueInt64(6))) + require.False(t, isVisibilityValueEqual(VisibilityValueInt64(5), VisibilityValueFloat64(5))) } diff --git a/chasm/workflow.go b/chasm/workflow.go index aaa43d9ecc6..7b6a1e2be72 100644 --- a/chasm/workflow.go +++ b/chasm/workflow.go @@ -6,6 +6,6 @@ const ( ) var ( - WorkflowArchetype = Archetype(FullyQualifiedName(WorkflowLibraryName, WorkflowComponentName)) - WorkflowArchetypeID = ArchetypeID(GenerateTypeID(WorkflowArchetype)) + WorkflowArchetype = FullyQualifiedName(WorkflowLibraryName, WorkflowComponentName) + WorkflowArchetypeID = GenerateTypeID(WorkflowArchetype) ) diff --git a/client/clientfactory.go b/client/clientfactory.go index 00a9d122747..d0aa5eb10f7 100644 --- a/client/clientfactory.go +++ b/client/clientfactory.go @@ -129,7 +129,7 @@ func (cf *rpcClientFactory) NewMatchingClientWithTimeout( } keyResolver := newServiceKeyResolver(resolver) - clientProvider := func(clientKey string) (interface{}, error) { + clientProvider := func(clientKey string) (any, error) { connection := cf.rpcFactory.CreateMatchingGRPCConnection(clientKey) return matchingservice.NewMatchingServiceClient(connection), nil } diff --git a/client/frontend/client_gen.go b/client/frontend/client_gen.go index 7b3f3e3aa58..bcbcabdfc8c 100644 --- a/client/frontend/client_gen.go +++ b/client/frontend/client_gen.go @@ -49,6 +49,26 @@ func (c *clientImpl) CreateSchedule( return c.client.CreateSchedule(ctx, request, opts...) } +func (c *clientImpl) CreateWorkerDeployment( + ctx context.Context, + request *workflowservice.CreateWorkerDeploymentRequest, + opts ...grpc.CallOption, +) (*workflowservice.CreateWorkerDeploymentResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.CreateWorkerDeployment(ctx, request, opts...) +} + +func (c *clientImpl) CreateWorkerDeploymentVersion( + ctx context.Context, + request *workflowservice.CreateWorkerDeploymentVersionRequest, + opts ...grpc.CallOption, +) (*workflowservice.CreateWorkerDeploymentVersionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.CreateWorkerDeploymentVersion(ctx, request, opts...) +} + func (c *clientImpl) CreateWorkflowRule( ctx context.Context, request *workflowservice.CreateWorkflowRuleRequest, @@ -1019,6 +1039,16 @@ func (c *clientImpl) UpdateWorkerConfig( return c.client.UpdateWorkerConfig(ctx, request, opts...) } +func (c *clientImpl) UpdateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest, + opts ...grpc.CallOption, +) (*workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.UpdateWorkerDeploymentVersionComputeConfig(ctx, request, opts...) +} + func (c *clientImpl) UpdateWorkerDeploymentVersionMetadata( ctx context.Context, request *workflowservice.UpdateWorkerDeploymentVersionMetadataRequest, @@ -1058,3 +1088,13 @@ func (c *clientImpl) UpdateWorkflowExecutionOptions( defer cancel() return c.client.UpdateWorkflowExecutionOptions(ctx, request, opts...) } + +func (c *clientImpl) ValidateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest, + opts ...grpc.CallOption, +) (*workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.ValidateWorkerDeploymentVersionComputeConfig(ctx, request, opts...) +} diff --git a/client/frontend/metric_client_gen.go b/client/frontend/metric_client_gen.go index ad15ce5dc48..2115b2836c4 100644 --- a/client/frontend/metric_client_gen.go +++ b/client/frontend/metric_client_gen.go @@ -65,6 +65,34 @@ func (c *metricClient) CreateSchedule( return c.client.CreateSchedule(ctx, request, opts...) } +func (c *metricClient) CreateWorkerDeployment( + ctx context.Context, + request *workflowservice.CreateWorkerDeploymentRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.CreateWorkerDeploymentResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientCreateWorkerDeployment") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.CreateWorkerDeployment(ctx, request, opts...) +} + +func (c *metricClient) CreateWorkerDeploymentVersion( + ctx context.Context, + request *workflowservice.CreateWorkerDeploymentVersionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.CreateWorkerDeploymentVersionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientCreateWorkerDeploymentVersion") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.CreateWorkerDeploymentVersion(ctx, request, opts...) +} + func (c *metricClient) CreateWorkflowRule( ctx context.Context, request *workflowservice.CreateWorkflowRuleRequest, @@ -1423,6 +1451,20 @@ func (c *metricClient) UpdateWorkerConfig( return c.client.UpdateWorkerConfig(ctx, request, opts...) } +func (c *metricClient) UpdateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientUpdateWorkerDeploymentVersionComputeConfig") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.UpdateWorkerDeploymentVersionComputeConfig(ctx, request, opts...) +} + func (c *metricClient) UpdateWorkerDeploymentVersionMetadata( ctx context.Context, request *workflowservice.UpdateWorkerDeploymentVersionMetadataRequest, @@ -1478,3 +1520,17 @@ func (c *metricClient) UpdateWorkflowExecutionOptions( return c.client.UpdateWorkflowExecutionOptions(ctx, request, opts...) } + +func (c *metricClient) ValidateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientValidateWorkerDeploymentVersionComputeConfig") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.ValidateWorkerDeploymentVersionComputeConfig(ctx, request, opts...) +} diff --git a/client/frontend/retryable_client_gen.go b/client/frontend/retryable_client_gen.go index a46a3bbfd27..9915f8f84ed 100644 --- a/client/frontend/retryable_client_gen.go +++ b/client/frontend/retryable_client_gen.go @@ -71,6 +71,36 @@ func (c *retryableClient) CreateSchedule( return resp, err } +func (c *retryableClient) CreateWorkerDeployment( + ctx context.Context, + request *workflowservice.CreateWorkerDeploymentRequest, + opts ...grpc.CallOption, +) (*workflowservice.CreateWorkerDeploymentResponse, error) { + var resp *workflowservice.CreateWorkerDeploymentResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.CreateWorkerDeployment(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + +func (c *retryableClient) CreateWorkerDeploymentVersion( + ctx context.Context, + request *workflowservice.CreateWorkerDeploymentVersionRequest, + opts ...grpc.CallOption, +) (*workflowservice.CreateWorkerDeploymentVersionResponse, error) { + var resp *workflowservice.CreateWorkerDeploymentVersionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.CreateWorkerDeploymentVersion(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) CreateWorkflowRule( ctx context.Context, request *workflowservice.CreateWorkflowRuleRequest, @@ -1526,6 +1556,21 @@ func (c *retryableClient) UpdateWorkerConfig( return resp, err } +func (c *retryableClient) UpdateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest, + opts ...grpc.CallOption, +) (*workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse, error) { + var resp *workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.UpdateWorkerDeploymentVersionComputeConfig(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) UpdateWorkerDeploymentVersionMetadata( ctx context.Context, request *workflowservice.UpdateWorkerDeploymentVersionMetadataRequest, @@ -1585,3 +1630,18 @@ func (c *retryableClient) UpdateWorkflowExecutionOptions( err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) return resp, err } + +func (c *retryableClient) ValidateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest, + opts ...grpc.CallOption, +) (*workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse, error) { + var resp *workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.ValidateWorkerDeploymentVersionComputeConfig(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} diff --git a/client/history/caching_redirector_test.go b/client/history/caching_redirector_test.go index d6bd449663c..ccce1604251 100644 --- a/client/history/caching_redirector_test.go +++ b/client/history/caching_redirector_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.temporal.io/api/serviceerror" @@ -98,7 +99,7 @@ func cacheRetainingTest(s *cachingRedirectorSuite, opErr error, verify func(erro r := NewCachingRedirector(s.connections, s.resolver, s.logger, dynamicconfig.GetDurationPropertyFn(0)) defer r.stop() - for i := 0; i < 3; i++ { + for range 3 { err := r.Execute( context.Background(), shardID, @@ -315,15 +316,18 @@ func (s *cachingRedirectorSuite) TestStaleTTL() { defer r.mu.RUnlock() entry := r.mu.cache[shardID] return !entry.staleAt.IsZero() - }, 4*staleTTL, staleTTL) + }, 4*staleTTL, 10*time.Millisecond) + // Wait for the stale TTL to expire so clientForShardID re-resolves the shard owner. s.resolver.EXPECT(). Lookup(convert.Int32ToString(shardID)). Return(membership.NewHostInfoFromAddress(string(testAddr2)), nil). Times(1) - cli, err = r.clientForShardID(shardID) - s.NoError(err) - s.Equal(mockClient, cli) - s.Equal(2, s.connections.resetCalls) + s.EventuallyWithT(func(t *assert.CollectT) { + cli, err = r.clientForShardID(shardID) + assert.NoError(t, err) + assert.Equal(t, mockClient, cli) + assert.Equal(t, 2, s.connections.resetCalls) + }, 4*staleTTL, 10*time.Millisecond) } diff --git a/client/history/client_test.go b/client/history/client_test.go index 128e0427ed3..be8ee24891f 100644 --- a/client/history/client_test.go +++ b/client/history/client_test.go @@ -131,7 +131,7 @@ func TestShardAgnosticConnectionStrategy(t *testing.T) { rpcFactory, time.Second, ) - for i := 0; i < 3; i++ { + for range 3 { err := tc.fn(client) require.NoError(t, err) } diff --git a/client/history/historytest/clienttest.go b/client/history/historytest/clienttest.go index c47c1a9eaee..de9cb8990a3 100644 --- a/client/history/historytest/clienttest.go +++ b/client/history/historytest/clienttest.go @@ -117,7 +117,7 @@ func readTasks( // We want to run a test where the client makes multiple requests to the server because the client is stateful. In // particular, the first request here should establish a connection, and the next one should reuse that connection. - for i := 0; i < numTasks; i++ { + for i := range numTasks { res, err := client.GetDLQTasks(context.Background(), &historyservice.GetDLQTasksRequest{ DlqKey: &commonspb.HistoryDLQKey{ TaskCategory: int32(tasks.CategoryTransfer.ID()), @@ -181,7 +181,7 @@ func enqueueTasks( task := &tasks.WorkflowTask{ TaskID: 42, } - for i := 0; i < numTasks; i++ { + for range numTasks { _, err := historyTaskQueueManager.EnqueueTask(context.Background(), &persistence.EnqueueTaskRequest{ QueueType: persistence.QueueTypeHistoryDLQ, SourceCluster: sourceCluster, diff --git a/client/matching/loadbalancer.go b/client/matching/loadbalancer.go index b4d004568da..6c763eb2409 100644 --- a/client/matching/loadbalancer.go +++ b/client/matching/loadbalancer.go @@ -76,7 +76,7 @@ func NewLoadBalancer( func (lb *defaultLoadBalancer) PickWritePartition( taskQueue *tqid.TaskQueue, ) *tqid.NormalPartition { - if n, ok := testhooks.Get[int](lb.testHooks, testhooks.MatchingLBForceWritePartition); ok { + if n, ok := testhooks.Get(lb.testHooks, testhooks.MatchingLBForceWritePartition, namespace.ID(taskQueue.NamespaceId())); ok { return taskQueue.NormalPartition(n) } @@ -105,7 +105,7 @@ func (lb *defaultLoadBalancer) PickReadPartition( partitionCount = lb.nReadPartitions(string(namespaceName), taskQueue.Name(), taskQueue.TaskType()) } - if n, ok := testhooks.Get[int](lb.testHooks, testhooks.MatchingLBForceReadPartition); ok { + if n, ok := testhooks.Get(lb.testHooks, testhooks.MatchingLBForceReadPartition, namespace.ID(taskQueue.NamespaceId())); ok { return tqlb.forceReadPartition(partitionCount, n) } diff --git a/client/matching/loadbalancer_test.go b/client/matching/loadbalancer_test.go index a6d9993887a..ac73864130a 100644 --- a/client/matching/loadbalancer_test.go +++ b/client/matching/loadbalancer_test.go @@ -98,7 +98,7 @@ func TestLoadBalancerConcurrent(t *testing.T) { concurrentCount := 10 * partitionCount wg.Add(concurrentCount) - for i := 0; i < concurrentCount; i++ { + for range concurrentCount { go func() { defer wg.Done() tqlb.pickReadPartition(partitionCount) diff --git a/cmd/server/main.go b/cmd/server/main.go index 7ff18afdc8e..a1f8e8de277 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -194,17 +194,6 @@ func buildCLI() *cli.App { tag.Bool("debug-mode", debug.Enabled), ) - var dynamicConfigClient dynamicconfig.Client - if cfg.DynamicConfigClient != nil { - dynamicConfigClient, err = dynamicconfig.NewFileBasedClient(cfg.DynamicConfigClient, logger, temporal.InterruptCh()) - if err != nil { - return cli.Exit(fmt.Sprintf("Unable to create dynamic config client. Error: %v", err), 1) - } - } else { - dynamicConfigClient = dynamicconfig.NewNoopClient() - logger.Info("Dynamic config client is not configured. Using noop client.") - } - authorizer, err := authorization.GetAuthorizerFromConfig( &cfg.Global.Authorization, ) @@ -233,7 +222,6 @@ func buildCLI() *cli.App { s, err := temporal.NewServer( temporal.ForServices(services), temporal.WithConfig(cfg), - temporal.WithDynamicConfigClient(dynamicConfigClient), temporal.WithLogger(logger), temporal.InterruptOn(temporal.InterruptCh()), temporal.WithAuthorizer(authorizer), diff --git a/cmd/tools/fairsim/main.go b/cmd/tools/fairsim/main.go new file mode 100644 index 00000000000..df64e00157b --- /dev/null +++ b/cmd/tools/fairsim/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "go.temporal.io/server/tools/fairsim" +) + +func main() { + if err := fairsim.RunTool(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/tools/flakereport/main.go b/cmd/tools/flakereport/main.go new file mode 100644 index 00000000000..874d16589b8 --- /dev/null +++ b/cmd/tools/flakereport/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "go.temporal.io/server/tools/flakereport" +) + +func main() { + if err := flakereport.NewCliApp().Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "Error running flakereport: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/tools/gendynamicconfig/main.go b/cmd/tools/gendynamicconfig/main.go index 88655d1c3ba..ef4c127bb58 100644 --- a/cmd/tools/gendynamicconfig/main.go +++ b/cmd/tools/gendynamicconfig/main.go @@ -110,6 +110,11 @@ var ( {}, }`, }, + { + Name: "ChasmTaskType", + GoArgs: "chasmTaskType string", + Expr: "[]Constraints{{ChasmTaskType: chasmTaskType}, {}}", + }, }} ) diff --git a/cmd/tools/genrpcserverinterceptors/main.go b/cmd/tools/genrpcserverinterceptors/main.go index b6218645c5f..5b8cf40d8f9 100644 --- a/cmd/tools/genrpcserverinterceptors/main.go +++ b/cmd/tools/genrpcserverinterceptors/main.go @@ -1,6 +1,7 @@ package main import ( + "cmp" _ "embed" "flag" "fmt" @@ -21,9 +22,12 @@ type ( messageData struct { Type string - WorkflowIdGetter string - RunIdGetter string - TaskTokenGetter string + WorkflowIDGetter string + RunIDGetter string + TaskTokenGetter string + ActivityIDGetter string + OperationIDGetter string + ChasmRunIDGetter string } grpcServerData struct { @@ -67,13 +71,21 @@ var ( GetTaskToken() []byte })(nil)).Elem() - workflowIdGetterT = reflect.TypeOf((*interface { + workflowIDGetterT = reflect.TypeOf((*interface { GetWorkflowId() string })(nil)).Elem() - runIdGetterT = reflect.TypeOf((*interface { + runIDGetterT = reflect.TypeOf((*interface { GetRunId() string })(nil)).Elem() + + activityIDGetterT = reflect.TypeOf((*interface { + GetActivityId() string + })(nil)).Elem() + + operationIDGetterT = reflect.TypeOf((*interface { + GetOperationId() string + })(nil)).Elem() ) func main() { @@ -120,11 +132,11 @@ func workflowTagGetters(messageType reflect.Type, depth int) messageData { switch { case messageType.AssignableTo(executionGetterT): - pd.WorkflowIdGetter = "GetExecution().GetWorkflowId()" - pd.RunIdGetter = "GetExecution().GetRunId()" + pd.WorkflowIDGetter = "GetExecution().GetWorkflowId()" + pd.RunIDGetter = "GetExecution().GetRunId()" case messageType.AssignableTo(workflowExecutionGetterT): - pd.WorkflowIdGetter = "GetWorkflowExecution().GetWorkflowId()" - pd.RunIdGetter = "GetWorkflowExecution().GetRunId()" + pd.WorkflowIDGetter = "GetWorkflowExecution().GetWorkflowId()" + pd.RunIDGetter = "GetWorkflowExecution().GetRunId()" case messageType.AssignableTo(taskTokenGetterT): for _, ert := range excludeTaskTokenTypes { if messageType.AssignableTo(ert) { @@ -133,22 +145,24 @@ func workflowTagGetters(messageType reflect.Type, depth int) messageData { } pd.TaskTokenGetter = "GetTaskToken()" default: - // Might be one of these, both, or neither. - if messageType.AssignableTo(workflowIdGetterT) { - pd.WorkflowIdGetter = "GetWorkflowId()" + // Might have any combination of these, or none. + if messageType.AssignableTo(workflowIDGetterT) { + pd.WorkflowIDGetter = "GetWorkflowId()" } - if messageType.AssignableTo(runIdGetterT) { - pd.RunIdGetter = "GetRunId()" + if messageType.AssignableTo(runIDGetterT) { + pd.RunIDGetter = "GetRunId()" + } + if messageType.AssignableTo(activityIDGetterT) { + pd.ActivityIDGetter = "GetActivityId()" + } + if messageType.AssignableTo(operationIDGetterT) { + pd.OperationIDGetter = "GetOperationId()" } } // Iterates over fields in order they defined in proto file, not proto index. // Order is important because the first match wins. for fieldNum := 0; fieldNum < messageType.Elem().NumField(); fieldNum++ { - if (pd.WorkflowIdGetter != "" && pd.RunIdGetter != "") || pd.TaskTokenGetter != "" { - break - } - nestedRequest := messageType.Elem().Field(fieldNum) if nestedRequest.Type.Kind() != reflect.Ptr { continue @@ -162,15 +176,32 @@ func workflowTagGetters(messageType reflect.Type, depth int) messageData { nestedRd := workflowTagGetters(nestedRequest.Type, depth+1) // First match wins: if getter is already set, it won't be overwritten. - if pd.WorkflowIdGetter == "" && nestedRd.WorkflowIdGetter != "" { - pd.WorkflowIdGetter = fmt.Sprintf("Get%s().%s", nestedRequest.Name, nestedRd.WorkflowIdGetter) + if pd.WorkflowIDGetter == "" && nestedRd.WorkflowIDGetter != "" { + pd.WorkflowIDGetter = fmt.Sprintf("Get%s().%s", nestedRequest.Name, nestedRd.WorkflowIDGetter) } - if pd.RunIdGetter == "" && nestedRd.RunIdGetter != "" { - pd.RunIdGetter = fmt.Sprintf("Get%s().%s", nestedRequest.Name, nestedRd.RunIdGetter) + if pd.RunIDGetter == "" && nestedRd.RunIDGetter != "" { + pd.RunIDGetter = fmt.Sprintf("Get%s().%s", nestedRequest.Name, nestedRd.RunIDGetter) } if pd.TaskTokenGetter == "" && nestedRd.TaskTokenGetter != "" { pd.TaskTokenGetter = fmt.Sprintf("Get%s().%s", nestedRequest.Name, nestedRd.TaskTokenGetter) } + if pd.ActivityIDGetter == "" && nestedRd.ActivityIDGetter != "" { + pd.ActivityIDGetter = fmt.Sprintf("Get%s().%s", nestedRequest.Name, nestedRd.ActivityIDGetter) + } + if pd.OperationIDGetter == "" && nestedRd.OperationIDGetter != "" { + pd.OperationIDGetter = fmt.Sprintf("Get%s().%s", nestedRequest.Name, nestedRd.OperationIDGetter) + } } + + // When a business ID (activity or operation) is present without a workflow ID, + // the run_id is not a workflow run ID. Only apply at the top level. + if depth == 0 { + hasChasmBusinessID := pd.WorkflowIDGetter == "" && cmp.Or(pd.ActivityIDGetter, pd.OperationIDGetter) != "" + if hasChasmBusinessID && pd.RunIDGetter != "" { + pd.ChasmRunIDGetter = pd.RunIDGetter + pd.RunIDGetter = "" + } + } + return pd } diff --git a/cmd/tools/genrpcserverinterceptors/main_test.go b/cmd/tools/genrpcserverinterceptors/main_test.go index c115a1af98c..bfd43b3880e 100644 --- a/cmd/tools/genrpcserverinterceptors/main_test.go +++ b/cmd/tools/genrpcserverinterceptors/main_test.go @@ -12,11 +12,14 @@ import ( func TestWorkflowTagGetters(t *testing.T) { testCases := []struct { - name string - reqT reflect.Type - workflowIDGetter string - runIDGetter string - taskTokenGetter string + name string + reqT reflect.Type + workflowIDGetter string + runIDGetter string + taskTokenGetter string + activityIDGetter string + operationIDGetter string + chasmRunIDGetter string }{ { name: "Request with only workflowID", @@ -28,6 +31,7 @@ func TestWorkflowTagGetters(t *testing.T) { reqT: reflect.TypeOf(&workflowservice.RecordActivityTaskHeartbeatByIdRequest{}), workflowIDGetter: "GetWorkflowId()", runIDGetter: "GetRunId()", + activityIDGetter: "GetActivityId()", }, { name: "Request with execution", @@ -68,20 +72,33 @@ func TestWorkflowTagGetters(t *testing.T) { workflowIDGetter: "GetWorkflowState().GetExecutionInfo().GetWorkflowId()", runIDGetter: "GetWorkflowState().GetExecutionState().GetRunId()", }, + { + name: "Chasm activity request with activity_id and run_id", + reqT: reflect.TypeOf(&workflowservice.DescribeActivityExecutionRequest{}), + activityIDGetter: "GetActivityId()", + chasmRunIDGetter: "GetRunId()", + }, + { + name: "Chasm activity request with only activity_id", + reqT: reflect.TypeOf(&workflowservice.StartActivityExecutionRequest{}), + activityIDGetter: "GetActivityId()", + }, + { + name: "History request with nested operation_id", + reqT: reflect.TypeOf(&historyservice.CancelNexusOperationRequest{}), + operationIDGetter: "GetRequest().GetOperationId()", + }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { rd := workflowTagGetters(tt.reqT, 0) - if tt.workflowIDGetter != "" { - assert.Equal(t, tt.workflowIDGetter, rd.WorkflowIdGetter) - } - if tt.runIDGetter != "" { - assert.Equal(t, tt.runIDGetter, rd.RunIdGetter) - } - if tt.taskTokenGetter != "" { - assert.Equal(t, tt.taskTokenGetter, rd.TaskTokenGetter) - } + assert.Equal(t, tt.workflowIDGetter, rd.WorkflowIDGetter, "WorkflowIDGetter") + assert.Equal(t, tt.runIDGetter, rd.RunIDGetter, "RunIDGetter") + assert.Equal(t, tt.taskTokenGetter, rd.TaskTokenGetter, "TaskTokenGetter") + assert.Equal(t, tt.activityIDGetter, rd.ActivityIDGetter, "ActivityIDGetter") + assert.Equal(t, tt.operationIDGetter, rd.OperationIDGetter, "OperationIDGetter") + assert.Equal(t, tt.chasmRunIDGetter, rd.ChasmRunIDGetter, "ChasmRunIDGetter") }) } } diff --git a/cmd/tools/genrpcserverinterceptors/server_interceptors.tmpl b/cmd/tools/genrpcserverinterceptors/server_interceptors.tmpl index 94e060982cc..0c99a67e315 100644 --- a/cmd/tools/genrpcserverinterceptors/server_interceptors.tmpl +++ b/cmd/tools/genrpcserverinterceptors/server_interceptors.tmpl @@ -14,14 +14,20 @@ func (wt *WorkflowTags) extractFrom{{.Server}}Message(message any) []tag.Tag { switch r := message.(type) { {{- range .Messages}} case {{.Type}}: - {{- if or .TaskTokenGetter .WorkflowIdGetter .RunIdGetter}} + {{- if or .TaskTokenGetter .WorkflowIDGetter .RunIDGetter .ActivityIDGetter .OperationIDGetter .ChasmRunIDGetter}} {{- if .TaskTokenGetter}} return wt.fromTaskToken(r.{{ .TaskTokenGetter}}) {{- else}} return []tag.Tag{ - {{if .WorkflowIdGetter}} tag.WorkflowID(r.{{.WorkflowIdGetter}}), + {{if .WorkflowIDGetter}} tag.WorkflowID(r.{{.WorkflowIDGetter}}), {{end -}} - {{if .RunIdGetter}} tag.WorkflowRunID(r.{{.RunIdGetter}}), + {{if .ActivityIDGetter}} tag.ActivityID(r.{{.ActivityIDGetter}}), + {{end -}} + {{if .OperationIDGetter}} tag.OperationID(r.{{.OperationIDGetter}}), + {{end -}} + {{if .RunIDGetter}} tag.WorkflowRunID(r.{{.RunIDGetter}}), + {{end -}} + {{if .ChasmRunIDGetter}} tag.ChasmRunID(r.{{.ChasmRunIDGetter}}), {{end -}} } {{- end}} diff --git a/cmd/tools/getproto/files.go b/cmd/tools/getproto/files.go index 5a269db0083..6ef97ed180e 100644 --- a/cmd/tools/getproto/files.go +++ b/cmd/tools/getproto/files.go @@ -1,3 +1,4 @@ + // Code generated by getproto. DO NOT EDIT. // If you get build errors in this file, just delete it. It will be regenerated. @@ -10,6 +11,7 @@ import ( batch "go.temporal.io/api/batch/v1" command "go.temporal.io/api/command/v1" common "go.temporal.io/api/common/v1" + compute "go.temporal.io/api/compute/v1" deployment "go.temporal.io/api/deployment/v1" enums "go.temporal.io/api/enums/v1" failure "go.temporal.io/api/failure/v1" @@ -49,6 +51,9 @@ func init() { importMap["temporal/api/batch/v1/message.proto"] = batch.File_temporal_api_batch_v1_message_proto importMap["temporal/api/command/v1/message.proto"] = command.File_temporal_api_command_v1_message_proto importMap["temporal/api/common/v1/message.proto"] = common.File_temporal_api_common_v1_message_proto + importMap["temporal/api/compute/v1/config.proto"] = compute.File_temporal_api_compute_v1_config_proto + importMap["temporal/api/compute/v1/provider.proto"] = compute.File_temporal_api_compute_v1_provider_proto + importMap["temporal/api/compute/v1/scaler.proto"] = compute.File_temporal_api_compute_v1_scaler_proto importMap["temporal/api/deployment/v1/message.proto"] = deployment.File_temporal_api_deployment_v1_message_proto importMap["temporal/api/enums/v1/activity.proto"] = enums.File_temporal_api_enums_v1_activity_proto importMap["temporal/api/enums/v1/batch_operation.proto"] = enums.File_temporal_api_enums_v1_batch_operation_proto diff --git a/cmd/tools/getproto/main.go b/cmd/tools/getproto/main.go index 26bfc621c2e..f1e7113ca40 100644 --- a/cmd/tools/getproto/main.go +++ b/cmd/tools/getproto/main.go @@ -144,7 +144,7 @@ func checkImports(files map[string]protoreflect.FileDescriptor) { for _, fd := range files { imports := fd.Imports() num := imports.Len() - for i := 0; i < num; i++ { + for i := range num { imp := imports.Get(i).Path() if strings.HasPrefix(imp, "temporal/api/") || strings.HasPrefix(imp, "google/") { if _, ok := files[imp]; !ok { diff --git a/cmd/tools/parallelize/main.go b/cmd/tools/parallelize/main.go new file mode 100644 index 00000000000..2dc661d68c2 --- /dev/null +++ b/cmd/tools/parallelize/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "go.temporal.io/server/tools/parallelize" +) + +func main() { + if err := parallelize.Main(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/tools/protogen/main.go b/cmd/tools/protogen/main.go index 0027c721a2b..cf2c00b2e6e 100644 --- a/cmd/tools/protogen/main.go +++ b/cmd/tools/protogen/main.go @@ -17,7 +17,7 @@ import ( var cyan = color.New(color.FgHiCyan, color.Bold) -func info(format string, args ...interface{}) { +func info(format string, args ...any) { log.Println(cyan.Sprintf(format, args...)) } diff --git a/common/api/metadata.go b/common/api/metadata.go index 669f558b29b..9cca5ad0e04 100644 --- a/common/api/metadata.go +++ b/common/api/metadata.go @@ -68,111 +68,115 @@ const ( var ( workflowServiceMetadata = map[string]MethodMetadata{ - "RegisterNamespace": {Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone}, - "DescribeNamespace": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "ListNamespaces": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateNamespace": {Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone}, - "DeprecateNamespace": {Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone}, - "StartWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "GetWorkflowExecutionHistory": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, - "GetWorkflowExecutionHistoryReverse": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "PollWorkflowTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingAlways}, - "RespondWorkflowTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondWorkflowTaskFailed": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "PollActivityTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingAlways}, - "RecordActivityTaskHeartbeat": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RecordActivityTaskHeartbeatById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondActivityTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondActivityTaskCompletedById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondActivityTaskFailed": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondActivityTaskFailedById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondActivityTaskCanceled": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondActivityTaskCanceledById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "CountActivityExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "DeleteActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DescribeActivityExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, - "PollActivityExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, - "ListActivityExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "RequestCancelActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "StartActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "TerminateActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "PollNexusTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingAlways}, - "RespondNexusTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RespondNexusTaskFailed": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RequestCancelWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "SignalWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "SignalWithStartWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ResetWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "TerminateWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DeleteWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ListOpenWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "ListClosedWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "ListWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "ListArchivedWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "ScanWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "CountWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "GetSearchAttributes": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, - "RespondQueryTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ResetStickyTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ShutdownWorker": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ExecuteMultiOperation": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "QueryWorkflow": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "DescribeWorkflowExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "DescribeTaskQueue": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "GetClusterInfo": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, - "GetSystemInfo": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, - "ListTaskQueuePartitions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "CreateSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DescribeSchedule": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "PatchSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ListScheduleMatchingTimes": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "DeleteSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ListSchedules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "CountSchedules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateWorkerBuildIdCompatibility": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "GetWorkerBuildIdCompatibility": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateWorkerVersioningRules": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "GetWorkerVersioningRules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "GetWorkerTaskReachability": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "PollWorkflowExecutionUpdate": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, - "StartBatchOperation": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "StopBatchOperation": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DescribeBatchOperation": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "ListBatchOperations": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateActivityOptions": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "PauseActivity": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "UnpauseActivity": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ResetActivity": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "UpdateWorkflowExecutionOptions": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DescribeDeployment": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] - "ListDeployments": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] - "GetDeploymentReachability": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] - "GetCurrentDeployment": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] - "SetCurrentDeployment": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, // [cleanup-wv-pre-release] - "DescribeWorkerDeploymentVersion": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "DescribeWorkerDeployment": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "SetWorkerDeploymentCurrentVersion": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "SetWorkerDeploymentRampingVersion": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "SetWorkerDeploymentManager": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DeleteWorkerDeployment": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DeleteWorkerDeploymentVersion": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "UpdateWorkerDeploymentVersionMetadata": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ListWorkerDeployments": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "CreateWorkflowRule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "DescribeWorkflowRule": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "DeleteWorkflowRule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ListWorkflowRules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "TriggerWorkflowRule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "RecordWorkerHeartbeat": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "ListWorkers": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "DescribeWorker": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateTaskQueueConfig": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "FetchWorkerConfig": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, - "UpdateWorkerConfig": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "PauseWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, - "UnpauseWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RegisterNamespace": {Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone}, + "DescribeNamespace": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "ListNamespaces": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateNamespace": {Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone}, + "DeprecateNamespace": {Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone}, + "StartWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "GetWorkflowExecutionHistory": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, + "GetWorkflowExecutionHistoryReverse": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "PollWorkflowTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingAlways}, + "RespondWorkflowTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondWorkflowTaskFailed": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "PollActivityTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingAlways}, + "RecordActivityTaskHeartbeat": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RecordActivityTaskHeartbeatById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondActivityTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondActivityTaskCompletedById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondActivityTaskFailed": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondActivityTaskFailedById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondActivityTaskCanceled": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondActivityTaskCanceledById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "CountActivityExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DeleteActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DescribeActivityExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, + "PollActivityExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, + "ListActivityExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "RequestCancelActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "StartActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "TerminateActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "PollNexusTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingAlways}, + "RespondNexusTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RespondNexusTaskFailed": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RequestCancelWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "SignalWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "SignalWithStartWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ResetWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "TerminateWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DeleteWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ListOpenWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "ListClosedWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "ListWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "ListArchivedWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "ScanWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "CountWorkflowExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "GetSearchAttributes": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, + "RespondQueryTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ResetStickyTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ShutdownWorker": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ExecuteMultiOperation": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "QueryWorkflow": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DescribeWorkflowExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DescribeTaskQueue": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "GetClusterInfo": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, + "GetSystemInfo": {Scope: ScopeCluster, Access: AccessReadOnly, Polling: PollingNone}, + "ListTaskQueuePartitions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "CreateSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DescribeSchedule": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "PatchSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ListScheduleMatchingTimes": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DeleteSchedule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ListSchedules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "CountSchedules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateWorkerBuildIdCompatibility": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "GetWorkerBuildIdCompatibility": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateWorkerVersioningRules": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "GetWorkerVersioningRules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "GetWorkerTaskReachability": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "PollWorkflowExecutionUpdate": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, + "StartBatchOperation": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "StopBatchOperation": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DescribeBatchOperation": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "ListBatchOperations": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateActivityOptions": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "PauseActivity": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "UnpauseActivity": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ResetActivity": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "UpdateWorkflowExecutionOptions": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DescribeDeployment": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] + "ListDeployments": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] + "GetDeploymentReachability": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] + "GetCurrentDeployment": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, // [cleanup-wv-pre-release] + "SetCurrentDeployment": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, // [cleanup-wv-pre-release] + "DescribeWorkerDeploymentVersion": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DescribeWorkerDeployment": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "SetWorkerDeploymentCurrentVersion": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "SetWorkerDeploymentRampingVersion": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "SetWorkerDeploymentManager": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "CreateWorkerDeployment": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DeleteWorkerDeployment": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "CreateWorkerDeploymentVersion": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "UpdateWorkerDeploymentVersionComputeConfig": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ValidateWorkerDeploymentVersionComputeConfig": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DeleteWorkerDeploymentVersion": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "UpdateWorkerDeploymentVersionMetadata": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ListWorkerDeployments": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "CreateWorkflowRule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DescribeWorkflowRule": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DeleteWorkflowRule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ListWorkflowRules": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "TriggerWorkflowRule": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "RecordWorkerHeartbeat": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "ListWorkers": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "DescribeWorker": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateTaskQueueConfig": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "FetchWorkerConfig": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "UpdateWorkerConfig": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "PauseWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "UnpauseWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, } operatorServiceMetadata = map[string]MethodMetadata{ "AddSearchAttributes": {Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone}, diff --git a/common/archiver/README.md b/common/archiver/README.md index 5ddc8e810fb..be0a6fdea14 100644 --- a/common/archiver/README.md +++ b/common/archiver/README.md @@ -2,7 +2,15 @@ This README explains how to add new Archiver implementations. -## Steps +There are two approaches: + +1. **Built-in implementation** — add the archiver directly to this repository (e.g., `filestore`, `gcloud`, `s3store`). **We are not currently accepting contributions for new built-in archiver implementations.** Maintaining a growing set of built-in implementations places an ongoing maintenance burden on the team, so new implementations should use Option 2 instead. + +2. **Custom implementation via server option** — implement the archiver in an external package and inject it into the server at startup using `WithCustomHistoryArchiverFactory` / `WithCustomVisibilityArchiverFactory`. This is the recommended approach for all new archiver implementations. + +--- + +## Option 1: Built-in implementation (in-repo) **Step 1: Create a new package for your implementation** @@ -73,6 +81,120 @@ Modify the `./provider/provider.go` file so that the `ArchiverProvider` knows ho Also, add configs for you archiver to static yaml config files and modify the `HistoryArchiverProvider` and `VisibilityArchiverProvider` struct in the `../common/service/config.go` accordingly. +--- + +## Option 2: Custom implementation via server option (external package) + +This approach lets you define archiver implementations in your own codebase and inject them into the Temporal server at startup, without modifying the server source. + +**Step 1: Implement the HistoryArchiver and VisibilityArchiver interfaces** + +Same interfaces as Steps 2 and 3 above. + +**Step 2: Implement CustomHistoryArchiverFactory and/or CustomVisibilityArchiverFactory** + +```go +// CustomHistoryArchiverFactory constructs a history archiver for a given URI scheme. +// Return provider.ErrUnknownScheme to fall back to the built-in implementation for that scheme. +// If a non-nil archiver is returned, it takes precedence over built-in archiver implementations. +type CustomHistoryArchiverFactory interface { + NewCustomHistoryArchiver(provider.NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) +} + +// CustomVisibilityArchiverFactory constructs a visibility archiver for a given URI scheme. +// Return provider.ErrUnknownScheme to fall back to the built-in implementation for that scheme. +// If a non-nil archiver is returned, it takes precedence over built-in archiver implementations. +type CustomVisibilityArchiverFactory interface { + NewCustomVisibilityArchiver(provider.NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) +} +``` + +The params structs provide everything your factory needs to construct an archiver: + +```go +type NewCustomHistoryArchiverParams struct { + Scheme string + ExecutionManager persistence.ExecutionManager + Logger log.Logger + MetricsHandler metrics.Handler + Configs map[string]any // from archival.history.provider.customStores. in config yaml +} + +type NewCustomVisibilityArchiverParams struct { + Scheme string + Logger log.Logger + MetricsHandler metrics.Handler + Configs map[string]any // from archival.visibility.provider.customStores. in config yaml +} +``` + +Example factory implementation using the functional adapter types: + +```go +historyFactory := provider.CustomHistoryArchiverFactoryFunc(func(params provider.NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) { + if params.Scheme != "myscheme" { + return nil, provider.ErrUnknownScheme + } + return mypackage.NewHistoryArchiver(params.ExecutionManager, params.Logger, params.MetricsHandler, params.Configs) +}) + +visibilityFactory := provider.CustomVisibilityArchiverFactoryFunc(func(params provider.NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) { + if params.Scheme != "myscheme" { + return nil, provider.ErrUnknownScheme + } + return mypackage.NewVisibilityArchiver(params.Logger, params.MetricsHandler, params.Configs) +}) +``` + +**Step 3: Register the factories with the server** + +Pass the factories as server options when constructing the Temporal server: + +```go +s, err := temporal.NewServer( + temporal.WithConfig(cfg), + temporal.WithCustomHistoryArchiverFactory(historyFactory), + temporal.WithCustomVisibilityArchiverFactory(visibilityFactory), + // ... other options +) +``` + +**Step 4: Configure archival in your YAML config** + +Enable archival and configure the URI scheme for your implementation. Use `customStores` to pass arbitrary config key-values to your factory: + +```yaml +archival: + history: + state: "enabled" + enableRead: true + provider: + customStores: + myscheme: # must match the scheme in your URIs + endpoint: "https://my-storage.example.com" + bucketName: "temporal-history" + visibility: + state: "enabled" + enableRead: true + provider: + customStores: + myscheme: + endpoint: "https://my-storage.example.com" + bucketName: "temporal-visibility" + +namespaceDefaults: + archival: + history: + state: "enabled" + URI: "myscheme://temporal-history" + visibility: + state: "enabled" + URI: "myscheme://temporal-visibility" +``` + +The `customStores.` map is passed as `Configs` in the params to your factory. Built-in schemes (`filestore`, `gstorage`, `s3store`) continue to use their own config sections unless your factory handles them (see FAQ below). + +--- ## FAQ **If my Archive method can automatically be retried by caller how can I record and access progress between retries?** @@ -134,6 +256,12 @@ Its usage is simpler than `ExecutionManager`, so archiver implementations can ch See the `historyIterator.go` file for more details. Sample usage can be found in the filestore historyArchiver implementation. +**Can a custom factory override a built-in scheme like `filestore`?** + +Yes. The custom factory is always consulted first. If your factory returns a non-nil archiver for a scheme that is also built-in (e.g., `filestore`), your implementation takes precedence and the built-in one is never used. Only return `ErrUnknownScheme` for schemes you want to delegate to the built-in implementations. + +Note that when overriding a built-in scheme, the `Configs` field in the params is populated from `customStores.` — not from the built-in config section (e.g., `filestore:`). If you need those config values, read them from `customStores` instead. + **Should my archiver define all its own error types?** Each archiver is free to define and return any errors it wants. However many common errors which diff --git a/common/archiver/filestore/util.go b/common/archiver/filestore/util.go index 64ef3c4ce99..c73b0429ec3 100644 --- a/common/archiver/filestore/util.go +++ b/common/archiver/filestore/util.go @@ -139,7 +139,7 @@ func decodeVisibilityRecord(data []byte) (*archiverspb.VisibilityRecord, error) return record, nil } -func serializeToken(token interface{}) ([]byte, error) { +func serializeToken(token any) ([]byte, error) { if token == nil { return nil, nil } diff --git a/common/archiver/gcloud/connector/client.go b/common/archiver/gcloud/connector/client.go index 2f2aaaa74b0..f6f8dbfe78b 100644 --- a/common/archiver/gcloud/connector/client.go +++ b/common/archiver/gcloud/connector/client.go @@ -25,7 +25,7 @@ var ( type ( // Precondition is a function that allow you to filter a query result. // If subject match params conditions then return true, else return false. - Precondition func(subject interface{}) bool + Precondition func(subject any) bool // Client is a wrapper around Google cloud storages client library. Client interface { @@ -73,11 +73,10 @@ func NewClientWithParams(clientD GcloudStorageClient) (Client, error) { func (s *storageWrapper) Upload(ctx context.Context, URI archiver.URI, fileName string, file []byte) (err error) { bucket := s.client.Bucket(URI.Hostname()) writer := bucket.Object(formatSinkPath(URI.Path()) + "/" + fileName).NewWriter(ctx) + defer func() { + err = multierr.Combine(err, writer.Close()) + }() _, err = io.Copy(writer, bytes.NewReader(file)) - if err == nil { - err = writer.Close() - } - return err } diff --git a/common/archiver/gcloud/connector/client_test.go b/common/archiver/gcloud/connector/client_test.go index 6ee2ff8930b..10c32a9dfc4 100644 --- a/common/archiver/gcloud/connector/client_test.go +++ b/common/archiver/gcloud/connector/client_test.go @@ -281,7 +281,7 @@ func (s *clientSuite) TestQueryWithFilter() { } func newWorkflowIDPrecondition(workflowID string) connector.Precondition { - return func(subject interface{}) bool { + return func(subject any) bool { if workflowID == "" { return true diff --git a/common/archiver/gcloud/util.go b/common/archiver/gcloud/util.go index 3396abff2cf..a7f94f1ba5c 100644 --- a/common/archiver/gcloud/util.go +++ b/common/archiver/gcloud/util.go @@ -86,7 +86,7 @@ func extractCloseFailoverVersion(filename string) (int64, int, error) { return failoverVersion, highestPart, err } -func serializeToken(token interface{}) ([]byte, error) { +func serializeToken(token any) ([]byte, error) { if token == nil { return nil, nil } @@ -140,7 +140,7 @@ func convertToExecutionInfo(record *archiverspb.VisibilityRecord, saTypeMap sear } func newRunIDPrecondition(runID string) connector.Precondition { - return func(subject interface{}) bool { + return func(subject any) bool { if runID == "" { return true @@ -164,7 +164,7 @@ func newRunIDPrecondition(runID string) connector.Precondition { } func newWorkflowIDPrecondition(workflowID string) connector.Precondition { - return func(subject interface{}) bool { + return func(subject any) bool { if workflowID == "" { return true @@ -188,7 +188,7 @@ func newWorkflowIDPrecondition(workflowID string) connector.Precondition { } func newWorkflowTypeNamePrecondition(workflowTypeName string) connector.Precondition { - return func(subject interface{}) bool { + return func(subject any) bool { if workflowTypeName == "" { return true diff --git a/common/archiver/gcloud/visibility_archiver.go b/common/archiver/gcloud/visibility_archiver.go index fd73ab54e54..6db106419e5 100644 --- a/common/archiver/gcloud/visibility_archiver.go +++ b/common/archiver/gcloud/visibility_archiver.go @@ -214,7 +214,7 @@ func (v *visibilityArchiver) queryAll( pageSize: request.PageSize, nextPageToken: request.NextPageToken, parsedQuery: &parsedQuery{}, - }, saTypeMap, request.NamespaceID) + }, saTypeMap, constructVisibilityFilenamePrefix(request.NamespaceID, indexKeyCloseTimeout)) } func (v *visibilityArchiver) queryPrefix(ctx context.Context, uri archiver.URI, request *queryVisibilityRequest, saTypeMap searchattribute.NameTypeMap, prefix string) (*archiver.QueryVisibilityResponse, error) { diff --git a/common/archiver/gcloud/visibility_archiver_test.go b/common/archiver/gcloud/visibility_archiver_test.go index 456a17846ca..3cdbeab59a3 100644 --- a/common/archiver/gcloud/visibility_archiver_test.go +++ b/common/archiver/gcloud/visibility_archiver_test.go @@ -368,7 +368,7 @@ func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_Pagination() { storageWrapper.EXPECT().QueryWithFilters( gomock.Any(), URI, - gomock.Any(), + constructVisibilityFilenamePrefix(testNamespaceID, indexKeyCloseTimeout), 1, 0, gomock.Any(), @@ -381,7 +381,7 @@ func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_Pagination() { storageWrapper.EXPECT().QueryWithFilters( gomock.Any(), URI, - gomock.Any(), + constructVisibilityFilenamePrefix(testNamespaceID, indexKeyCloseTimeout), 1, 1, gomock.Any(), @@ -412,7 +412,7 @@ func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_Pagination() { executions := make(map[string]*workflowpb.WorkflowExecutionInfo, limit) numPages := 2 - for i := 0; i < numPages; i++ { + for i := range numPages { req := &archiver.QueryVisibilityRequest{ NamespaceID: testNamespaceID, PageSize: 1, diff --git a/common/archiver/history_iterator.go b/common/archiver/history_iterator.go index 37257079032..97ea9aee597 100644 --- a/common/archiver/history_iterator.go +++ b/common/archiver/history_iterator.go @@ -218,14 +218,14 @@ func (i *historyIterator) reset(stateToken []byte) error { type ( // SizeEstimator is used to estimate the size of any object SizeEstimator interface { - EstimateSize(v interface{}) (int, error) + EstimateSize(v any) (int, error) } jsonSizeEstimator struct { } ) -func (e *jsonSizeEstimator) EstimateSize(v interface{}) (int, error) { +func (e *jsonSizeEstimator) EstimateSize(v any) (int, error) { // protojson must be used for proto structs. if protoMessage, ok := v.(proto.Message); ok { bs, err := protojson.Marshal(protoMessage) diff --git a/common/archiver/history_iterator_test.go b/common/archiver/history_iterator_test.go index 7463288f544..be1e86f1ab6 100644 --- a/common/archiver/history_iterator_test.go +++ b/common/archiver/history_iterator_test.go @@ -56,7 +56,7 @@ type ( testSizeEstimator struct{} ) -func (e *testSizeEstimator) EstimateSize(v interface{}) (int, error) { +func (e *testSizeEstimator) EstimateSize(v any) (int, error) { historyBatch, ok := v.(*historypb.History) if !ok { return -1, errors.New("test size estimator only estimate the size of history batches") @@ -420,11 +420,11 @@ func (s *HistoryIteratorSuite) TestNext_Fail_ReturnErrOnSecondCallToNext() { func (s *HistoryIteratorSuite) TestNext_Success_TenCallsToNext() { var batchInfo []int - for i := 0; i < 100; i++ { + for range 100 { batchInfo = append(batchInfo, []int{1, 2, 3, 4, 4, 3, 2, 1}...) } var pages []page - for i := 0; i < 100; i++ { + for i := range 100 { p := page{ firstbatchIdx: i * 8, numBatches: 8, @@ -440,7 +440,7 @@ func (s *HistoryIteratorSuite) TestNext_Success_TenCallsToNext() { FinishedIteration: false, NextEventID: common.FirstEventID, } - for i := 0; i < 10; i++ { + for i := range 10 { s.assertStateMatches(expectedIteratorState, itr) s.True(itr.HasNext()) blob, err := itr.Next(context.Background()) @@ -641,7 +641,7 @@ func (s *HistoryIteratorSuite) constructHistoryBatches(batchInfo []int, page pag eventsID := firstEventID for batchIdx, numEvents := range batchInfo[page.firstbatchIdx : page.firstbatchIdx+page.numBatches] { var events []*historypb.HistoryEvent - for i := 0; i < numEvents; i++ { + for range numEvents { event := &historypb.HistoryEvent{ EventId: eventsID, Version: page.firstEventFailoverVersion, diff --git a/common/archiver/options.go b/common/archiver/options.go index a54661fd542..f7146236552 100644 --- a/common/archiver/options.go +++ b/common/archiver/options.go @@ -24,8 +24,8 @@ type ( // ProgressManager is used to record and load archive progress ProgressManager interface { - RecordProgress(ctx context.Context, progress interface{}) error - LoadProgress(ctx context.Context, valuePtr interface{}) error + RecordProgress(ctx context.Context, progress any) error + LoadProgress(ctx context.Context, valuePtr any) error HasProgress(ctx context.Context) bool } ) @@ -50,12 +50,12 @@ func GetHeartbeatArchiveOption() ArchiveOption { type heartbeatProgressManager struct{} -func (h *heartbeatProgressManager) RecordProgress(ctx context.Context, progress interface{}) error { +func (h *heartbeatProgressManager) RecordProgress(ctx context.Context, progress any) error { activity.RecordHeartbeat(ctx, progress) return nil } -func (h *heartbeatProgressManager) LoadProgress(ctx context.Context, valuePtr interface{}) error { +func (h *heartbeatProgressManager) LoadProgress(ctx context.Context, valuePtr any) error { if !h.HasProgress(ctx) { return errors.New("no progress information in the context") } diff --git a/common/archiver/provider/provider.go b/common/archiver/provider/provider.go index a65eea90fef..8b53125a0cf 100644 --- a/common/archiver/provider/provider.go +++ b/common/archiver/provider/provider.go @@ -31,12 +31,50 @@ type ( GetVisibilityArchiver(scheme string) (archiver.VisibilityArchiver, error) } + // NewCustomHistoryArchiverParams provides dependencies for constructing a history archiver. + NewCustomHistoryArchiverParams struct { + Scheme string + ExecutionManager persistence.ExecutionManager + Logger log.Logger + MetricsHandler metrics.Handler + Configs map[string]any + } + + // NewCustomVisibilityArchiverParams provides dependencies for constructing a visibility archiver. + NewCustomVisibilityArchiverParams struct { + Scheme string + Logger log.Logger + MetricsHandler metrics.Handler + Configs map[string]any + } + + // CustomHistoryArchiverFactory constructs a history archiver for the given scheme. + // Return ErrUnknownScheme to fall back to the default implementation. + // If a non-nil archiver is returned, it takes precedence over built-in archiver implementations. + CustomHistoryArchiverFactory interface { + NewCustomHistoryArchiver(NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) + } + + // CustomVisibilityArchiverFactory constructs a visibility archiver for the given scheme. + // Return ErrUnknownScheme to fall back to the default implementation. + // If a non-nil archiver is returned, it takes precedence over built-in archiver implementations. + CustomVisibilityArchiverFactory interface { + NewCustomVisibilityArchiver(NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) + } + + CustomHistoryArchiverFactoryFunc func(NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) + + CustomVisibilityArchiverFactoryFunc func(NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) + archiverProvider struct { sync.RWMutex historyArchiverConfigs *config.HistoryArchiverProvider visibilityArchiverConfigs *config.VisibilityArchiverProvider + customHistoryArchiverFactory CustomHistoryArchiverFactory + customVisibilityArchiverFactory CustomVisibilityArchiverFactory + executionManager persistence.ExecutionManager logger log.Logger metricsHandler metrics.Handler @@ -47,22 +85,38 @@ type ( } ) +func (f CustomHistoryArchiverFactoryFunc) NewCustomHistoryArchiver( + params NewCustomHistoryArchiverParams, +) (archiver.HistoryArchiver, error) { + return f(params) +} + +func (f CustomVisibilityArchiverFactoryFunc) NewCustomVisibilityArchiver( + params NewCustomVisibilityArchiverParams, +) (archiver.VisibilityArchiver, error) { + return f(params) +} + // NewArchiverProvider returns a new Archiver provider func NewArchiverProvider( historyArchiverConfigs *config.HistoryArchiverProvider, visibilityArchiverConfigs *config.VisibilityArchiverProvider, + customHistoryArchiverFactory CustomHistoryArchiverFactory, + customVisibilityArchiverFactory CustomVisibilityArchiverFactory, executionManager persistence.ExecutionManager, logger log.Logger, metricsHandler metrics.Handler, ) ArchiverProvider { return &archiverProvider{ - historyArchiverConfigs: historyArchiverConfigs, - visibilityArchiverConfigs: visibilityArchiverConfigs, - executionManager: executionManager, - logger: logger, - metricsHandler: metricsHandler, - historyArchivers: make(map[string]archiver.HistoryArchiver), - visibilityArchivers: make(map[string]archiver.VisibilityArchiver), + historyArchiverConfigs: historyArchiverConfigs, + visibilityArchiverConfigs: visibilityArchiverConfigs, + executionManager: executionManager, + logger: logger, + metricsHandler: metricsHandler, + customHistoryArchiverFactory: customHistoryArchiverFactory, + customVisibilityArchiverFactory: customVisibilityArchiverFactory, + historyArchivers: make(map[string]archiver.HistoryArchiver), + visibilityArchivers: make(map[string]archiver.VisibilityArchiver), } } @@ -74,27 +128,46 @@ func (p *archiverProvider) GetHistoryArchiver(scheme string) (historyArchiver ar } p.RUnlock() - switch scheme { - case filestore.URIScheme: - if p.historyArchiverConfigs.Filestore == nil { - return nil, ErrArchiverConfigNotFound + if p.customHistoryArchiverFactory != nil { + var customConfigs map[string]any + if p.historyArchiverConfigs != nil { + customConfigs = p.historyArchiverConfigs.CustomStores[scheme] } - historyArchiver, err = filestore.NewHistoryArchiver(p.executionManager, p.logger, p.metricsHandler, p.historyArchiverConfigs.Filestore) - - case gcloud.URIScheme: - if p.historyArchiverConfigs.Gstorage == nil { - return nil, ErrArchiverConfigNotFound + historyArchiver, err = p.customHistoryArchiverFactory.NewCustomHistoryArchiver(NewCustomHistoryArchiverParams{ + Scheme: scheme, + ExecutionManager: p.executionManager, + Logger: p.logger, + MetricsHandler: p.metricsHandler, + Configs: customConfigs, + }) + if err != nil && !errors.Is(err, ErrUnknownScheme) { + return nil, err } + } - historyArchiver, err = gcloud.NewHistoryArchiver(p.executionManager, p.logger, p.metricsHandler, p.historyArchiverConfigs.Gstorage) - - case s3store.URIScheme: - if p.historyArchiverConfigs.S3store == nil { - return nil, ErrArchiverConfigNotFound + if historyArchiver == nil { + switch scheme { + case filestore.URIScheme: + if p.historyArchiverConfigs.Filestore == nil { + return nil, ErrArchiverConfigNotFound + } + historyArchiver, err = filestore.NewHistoryArchiver(p.executionManager, p.logger, p.metricsHandler, p.historyArchiverConfigs.Filestore) + + case gcloud.URIScheme: + if p.historyArchiverConfigs.Gstorage == nil { + return nil, ErrArchiverConfigNotFound + } + + historyArchiver, err = gcloud.NewHistoryArchiver(p.executionManager, p.logger, p.metricsHandler, p.historyArchiverConfigs.Gstorage) + + case s3store.URIScheme: + if p.historyArchiverConfigs.S3store == nil { + return nil, ErrArchiverConfigNotFound + } + historyArchiver, err = s3store.NewHistoryArchiver(p.executionManager, p.logger, p.metricsHandler, p.historyArchiverConfigs.S3store) + default: + return nil, ErrUnknownScheme } - historyArchiver, err = s3store.NewHistoryArchiver(p.executionManager, p.logger, p.metricsHandler, p.historyArchiverConfigs.S3store) - default: - return nil, ErrUnknownScheme } if err != nil { @@ -121,25 +194,43 @@ func (p *archiverProvider) GetVisibilityArchiver(scheme string) (archiver.Visibi var visibilityArchiver archiver.VisibilityArchiver var err error - switch scheme { - case filestore.URIScheme: - if p.visibilityArchiverConfigs.Filestore == nil { - return nil, ErrArchiverConfigNotFound - } - visibilityArchiver, err = filestore.NewVisibilityArchiver(p.logger, p.metricsHandler, p.visibilityArchiverConfigs.Filestore) - case s3store.URIScheme: - if p.visibilityArchiverConfigs.S3store == nil { - return nil, ErrArchiverConfigNotFound + if p.customVisibilityArchiverFactory != nil { + var customConfigs map[string]any + if p.visibilityArchiverConfigs != nil { + customConfigs = p.visibilityArchiverConfigs.CustomStores[scheme] } - visibilityArchiver, err = s3store.NewVisibilityArchiver(p.logger, p.metricsHandler, p.visibilityArchiverConfigs.S3store) - case gcloud.URIScheme: - if p.visibilityArchiverConfigs.Gstorage == nil { - return nil, ErrArchiverConfigNotFound + visibilityArchiver, err = p.customVisibilityArchiverFactory.NewCustomVisibilityArchiver(NewCustomVisibilityArchiverParams{ + Scheme: scheme, + Logger: p.logger, + MetricsHandler: p.metricsHandler, + Configs: customConfigs, + }) + if err != nil && !errors.Is(err, ErrUnknownScheme) { + return nil, err } - visibilityArchiver, err = gcloud.NewVisibilityArchiver(p.logger, p.metricsHandler, p.visibilityArchiverConfigs.Gstorage) + } - default: - return nil, ErrUnknownScheme + if visibilityArchiver == nil { + switch scheme { + case filestore.URIScheme: + if p.visibilityArchiverConfigs.Filestore == nil { + return nil, ErrArchiverConfigNotFound + } + visibilityArchiver, err = filestore.NewVisibilityArchiver(p.logger, p.metricsHandler, p.visibilityArchiverConfigs.Filestore) + case s3store.URIScheme: + if p.visibilityArchiverConfigs.S3store == nil { + return nil, ErrArchiverConfigNotFound + } + visibilityArchiver, err = s3store.NewVisibilityArchiver(p.logger, p.metricsHandler, p.visibilityArchiverConfigs.S3store) + case gcloud.URIScheme: + if p.visibilityArchiverConfigs.Gstorage == nil { + return nil, ErrArchiverConfigNotFound + } + visibilityArchiver, err = gcloud.NewVisibilityArchiver(p.logger, p.metricsHandler, p.visibilityArchiverConfigs.Gstorage) + + default: + return nil, ErrUnknownScheme + } } if err != nil { return nil, err diff --git a/common/archiver/provider/provider_mock.go b/common/archiver/provider/provider_mock.go index 379d63e11c5..573b90f5a2b 100644 --- a/common/archiver/provider/provider_mock.go +++ b/common/archiver/provider/provider_mock.go @@ -69,3 +69,81 @@ func (mr *MockArchiverProviderMockRecorder) GetVisibilityArchiver(scheme any) *g mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVisibilityArchiver", reflect.TypeOf((*MockArchiverProvider)(nil).GetVisibilityArchiver), scheme) } + +// MockCustomHistoryArchiverFactory is a mock of CustomHistoryArchiverFactory interface. +type MockCustomHistoryArchiverFactory struct { + ctrl *gomock.Controller + recorder *MockCustomHistoryArchiverFactoryMockRecorder + isgomock struct{} +} + +// MockCustomHistoryArchiverFactoryMockRecorder is the mock recorder for MockCustomHistoryArchiverFactory. +type MockCustomHistoryArchiverFactoryMockRecorder struct { + mock *MockCustomHistoryArchiverFactory +} + +// NewMockCustomHistoryArchiverFactory creates a new mock instance. +func NewMockCustomHistoryArchiverFactory(ctrl *gomock.Controller) *MockCustomHistoryArchiverFactory { + mock := &MockCustomHistoryArchiverFactory{ctrl: ctrl} + mock.recorder = &MockCustomHistoryArchiverFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCustomHistoryArchiverFactory) EXPECT() *MockCustomHistoryArchiverFactoryMockRecorder { + return m.recorder +} + +// NewCustomHistoryArchiver mocks base method. +func (m *MockCustomHistoryArchiverFactory) NewCustomHistoryArchiver(arg0 NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewCustomHistoryArchiver", arg0) + ret0, _ := ret[0].(archiver.HistoryArchiver) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewCustomHistoryArchiver indicates an expected call of NewCustomHistoryArchiver. +func (mr *MockCustomHistoryArchiverFactoryMockRecorder) NewCustomHistoryArchiver(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCustomHistoryArchiver", reflect.TypeOf((*MockCustomHistoryArchiverFactory)(nil).NewCustomHistoryArchiver), arg0) +} + +// MockCustomVisibilityArchiverFactory is a mock of CustomVisibilityArchiverFactory interface. +type MockCustomVisibilityArchiverFactory struct { + ctrl *gomock.Controller + recorder *MockCustomVisibilityArchiverFactoryMockRecorder + isgomock struct{} +} + +// MockCustomVisibilityArchiverFactoryMockRecorder is the mock recorder for MockCustomVisibilityArchiverFactory. +type MockCustomVisibilityArchiverFactoryMockRecorder struct { + mock *MockCustomVisibilityArchiverFactory +} + +// NewMockCustomVisibilityArchiverFactory creates a new mock instance. +func NewMockCustomVisibilityArchiverFactory(ctrl *gomock.Controller) *MockCustomVisibilityArchiverFactory { + mock := &MockCustomVisibilityArchiverFactory{ctrl: ctrl} + mock.recorder = &MockCustomVisibilityArchiverFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCustomVisibilityArchiverFactory) EXPECT() *MockCustomVisibilityArchiverFactoryMockRecorder { + return m.recorder +} + +// NewCustomVisibilityArchiver mocks base method. +func (m *MockCustomVisibilityArchiverFactory) NewCustomVisibilityArchiver(arg0 NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewCustomVisibilityArchiver", arg0) + ret0, _ := ret[0].(archiver.VisibilityArchiver) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewCustomVisibilityArchiver indicates an expected call of NewCustomVisibilityArchiver. +func (mr *MockCustomVisibilityArchiverFactoryMockRecorder) NewCustomVisibilityArchiver(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCustomVisibilityArchiver", reflect.TypeOf((*MockCustomVisibilityArchiverFactory)(nil).NewCustomVisibilityArchiver), arg0) +} diff --git a/common/archiver/provider/provider_test.go b/common/archiver/provider/provider_test.go new file mode 100644 index 00000000000..6593f72eeae --- /dev/null +++ b/common/archiver/provider/provider_test.go @@ -0,0 +1,618 @@ +package provider + +import ( + "errors" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.temporal.io/server/common/archiver" + "go.temporal.io/server/common/archiver/filestore" + "go.temporal.io/server/common/archiver/gcloud" + "go.temporal.io/server/common/archiver/s3store" + "go.temporal.io/server/common/config" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/persistence" + "go.uber.org/mock/gomock" +) + +type ( + ProviderSuite struct { + *require.Assertions + suite.Suite + + controller *gomock.Controller + mockExecutionManager *persistence.MockExecutionManager + mockHistoryArchiver *archiver.MockHistoryArchiver + mockVisibilityArchiver *archiver.MockVisibilityArchiver + mockCustomHistoryFactory *MockCustomHistoryArchiverFactory + mockCustomVisibilityFactory *MockCustomVisibilityArchiverFactory + + logger log.Logger + metricsHandler metrics.Handler + } +) + +func TestProviderSuite(t *testing.T) { + suite.Run(t, new(ProviderSuite)) +} + +func (s *ProviderSuite) SetupTest() { + s.Assertions = require.New(s.T()) + s.controller = gomock.NewController(s.T()) + + s.mockExecutionManager = persistence.NewMockExecutionManager(s.controller) + s.mockHistoryArchiver = archiver.NewMockHistoryArchiver(s.controller) + s.mockVisibilityArchiver = archiver.NewMockVisibilityArchiver(s.controller) + s.mockCustomHistoryFactory = NewMockCustomHistoryArchiverFactory(s.controller) + s.mockCustomVisibilityFactory = NewMockCustomVisibilityArchiverFactory(s.controller) + + s.logger = log.NewNoopLogger() + s.metricsHandler = metrics.NoopMetricsHandler +} + +func (s *ProviderSuite) TearDownTest() { + s.controller.Finish() +} + +func (s *ProviderSuite) TestNewArchiverProvider() { + historyConfig := &config.HistoryArchiverProvider{} + visibilityConfig := &config.VisibilityArchiverProvider{} + + provider := NewArchiverProvider( + historyConfig, + visibilityConfig, + s.mockCustomHistoryFactory, + s.mockCustomVisibilityFactory, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + s.NotNil(provider) + + // Verify internal state + p := provider.(*archiverProvider) + s.Equal(historyConfig, p.historyArchiverConfigs) + s.Equal(visibilityConfig, p.visibilityArchiverConfigs) + s.Equal(s.mockCustomHistoryFactory, p.customHistoryArchiverFactory) + s.Equal(s.mockCustomVisibilityFactory, p.customVisibilityArchiverFactory) + s.Equal(s.mockExecutionManager, p.executionManager) + s.Equal(s.logger, p.logger) + s.Equal(s.metricsHandler, p.metricsHandler) + s.NotNil(p.historyArchivers) + s.NotNil(p.visibilityArchivers) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_CustomFactory_Success() { + scheme := "custom" + + provider := NewArchiverProvider( + &config.HistoryArchiverProvider{ + CustomStores: map[string]map[string]any{ + scheme: {"key": "value"}, + }, + }, + nil, + s.mockCustomHistoryFactory, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + s.mockCustomHistoryFactory.EXPECT(). + NewCustomHistoryArchiver(gomock.Any()). + DoAndReturn(func(params NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) { + s.Equal(scheme, params.Scheme) + s.Equal(s.mockExecutionManager, params.ExecutionManager) + s.Equal(s.logger, params.Logger) + s.Equal(s.metricsHandler, params.MetricsHandler) + s.Equal(map[string]any{"key": "value"}, params.Configs) + return s.mockHistoryArchiver, nil + }) + + result, err := provider.GetHistoryArchiver(scheme) + s.NoError(err) + s.Equal(s.mockHistoryArchiver, result) + + // Test caching - should not call factory again + result2, err := provider.GetHistoryArchiver(scheme) + s.NoError(err) + s.Equal(result, result2) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_CustomFactory_ReturnsError() { + scheme := "custom" + expectedErr := errors.New("custom factory error") + + provider := NewArchiverProvider( + nil, + nil, + s.mockCustomHistoryFactory, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + s.mockCustomHistoryFactory.EXPECT(). + NewCustomHistoryArchiver(gomock.Any()). + Return(nil, expectedErr) + + result, err := provider.GetHistoryArchiver(scheme) + s.Error(err) + s.Equal(expectedErr, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_CustomFactory_FallbackToBuiltIn() { + scheme := filestore.URIScheme + + provider := NewArchiverProvider( + &config.HistoryArchiverProvider{ + Filestore: &config.FilestoreArchiver{ + FileMode: "0600", + DirMode: "0700", + }, + }, + nil, + s.mockCustomHistoryFactory, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + // Custom factory returns ErrUnknownScheme, should fallback to built-in + s.mockCustomHistoryFactory.EXPECT(). + NewCustomHistoryArchiver(gomock.Any()). + Return(nil, ErrUnknownScheme) + + result, err := provider.GetHistoryArchiver(scheme) + s.NoError(err) + s.NotNil(result) + + // Verify it's cached + result2, err := provider.GetHistoryArchiver(scheme) + s.NoError(err) + s.Equal(result, result2) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_UnknownScheme() { + scheme := "unknown" + + provider := NewArchiverProvider( + nil, + nil, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetHistoryArchiver(scheme) + s.Error(err) + s.Equal(ErrUnknownScheme, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_ConfigNotFound_Filestore() { + scheme := filestore.URIScheme + + provider := NewArchiverProvider( + &config.HistoryArchiverProvider{ + Filestore: nil, // Config not set + }, + nil, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetHistoryArchiver(scheme) + s.Error(err) + s.Equal(ErrArchiverConfigNotFound, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_ConfigNotFound_GCloud() { + scheme := gcloud.URIScheme + + provider := NewArchiverProvider( + &config.HistoryArchiverProvider{ + Gstorage: nil, // Config not set + }, + nil, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetHistoryArchiver(scheme) + s.Error(err) + s.Equal(ErrArchiverConfigNotFound, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_ConfigNotFound_S3() { + scheme := s3store.URIScheme + + provider := NewArchiverProvider( + &config.HistoryArchiverProvider{ + S3store: nil, // Config not set + }, + nil, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetHistoryArchiver(scheme) + s.Error(err) + s.Equal(ErrArchiverConfigNotFound, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_ConcurrentAccess() { + scheme := "custom" + + provider := NewArchiverProvider( + nil, + nil, + s.mockCustomHistoryFactory, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + // The factory may be called multiple times before caching occurs, + // but all calls should return the same archiver. + s.mockCustomHistoryFactory.EXPECT(). + NewCustomHistoryArchiver(gomock.Any()). + Return(s.mockHistoryArchiver, nil). + AnyTimes() + + var wg sync.WaitGroup + numGoroutines := 10 + results := make([]archiver.HistoryArchiver, numGoroutines) + archiverErrors := make([]error, numGoroutines) + + for i := range numGoroutines { + wg.Go(func() { + results[i], archiverErrors[i] = provider.GetHistoryArchiver(scheme) + }) + } + + wg.Wait() + + // All should succeed and return a valid archiver + for i := range numGoroutines { + s.NoError(archiverErrors[i]) + s.NotNil(results[i]) + } + + // Verify caching works after concurrent initialization + cachedResult, err := provider.GetHistoryArchiver(scheme) + s.NoError(err) + s.NotNil(cachedResult) +} + +func (s *ProviderSuite) TestGetHistoryArchiver_NilConfigs() { + scheme := "custom" + + provider := NewArchiverProvider( + nil, // nil history config + nil, + s.mockCustomHistoryFactory, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + s.mockCustomHistoryFactory.EXPECT(). + NewCustomHistoryArchiver(gomock.Any()). + DoAndReturn(func(params NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) { + s.Nil(params.Configs) // Should be nil when configs are not provided + return s.mockHistoryArchiver, nil + }) + + result, err := provider.GetHistoryArchiver(scheme) + s.NoError(err) + s.Equal(s.mockHistoryArchiver, result) +} + +func (s *ProviderSuite) TestCustomHistoryArchiverFactoryFunc() { + called := false + expectedArchiver := s.mockHistoryArchiver + + factoryFunc := CustomHistoryArchiverFactoryFunc(func(params NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) { + called = true + s.Equal("test-scheme", params.Scheme) + return expectedArchiver, nil + }) + + result, err := factoryFunc.NewCustomHistoryArchiver(NewCustomHistoryArchiverParams{ + Scheme: "test-scheme", + }) + + s.NoError(err) + s.True(called) + s.Equal(expectedArchiver, result) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_CustomFactory_Success() { + scheme := "custom" + + provider := NewArchiverProvider( + nil, + &config.VisibilityArchiverProvider{ + CustomStores: map[string]map[string]any{ + scheme: {"key": "value"}, + }, + }, + nil, + s.mockCustomVisibilityFactory, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + s.mockCustomVisibilityFactory.EXPECT(). + NewCustomVisibilityArchiver(gomock.Any()). + DoAndReturn(func(params NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) { + s.Equal(scheme, params.Scheme) + s.Equal(s.logger, params.Logger) + s.Equal(s.metricsHandler, params.MetricsHandler) + s.Equal(map[string]any{"key": "value"}, params.Configs) + return s.mockVisibilityArchiver, nil + }) + + result, err := provider.GetVisibilityArchiver(scheme) + s.NoError(err) + s.Equal(s.mockVisibilityArchiver, result) + + // Test caching - should not call factory again + result2, err := provider.GetVisibilityArchiver(scheme) + s.NoError(err) + s.Equal(result, result2) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_CustomFactory_ReturnsError() { + scheme := "custom" + expectedErr := errors.New("custom factory error") + + provider := NewArchiverProvider( + nil, + nil, + nil, + s.mockCustomVisibilityFactory, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + s.mockCustomVisibilityFactory.EXPECT(). + NewCustomVisibilityArchiver(gomock.Any()). + Return(nil, expectedErr) + + result, err := provider.GetVisibilityArchiver(scheme) + s.Error(err) + s.Equal(expectedErr, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_CustomFactory_FallbackToBuiltIn() { + scheme := filestore.URIScheme + + provider := NewArchiverProvider( + nil, + &config.VisibilityArchiverProvider{ + Filestore: &config.FilestoreArchiver{ + FileMode: "0600", + DirMode: "0700", + }, + }, + nil, + s.mockCustomVisibilityFactory, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + // Custom factory returns ErrUnknownScheme, should fallback to built-in + s.mockCustomVisibilityFactory.EXPECT(). + NewCustomVisibilityArchiver(gomock.Any()). + Return(nil, ErrUnknownScheme) + + result, err := provider.GetVisibilityArchiver(scheme) + s.NoError(err) + s.NotNil(result) + + // Verify it's cached + result2, err := provider.GetVisibilityArchiver(scheme) + s.NoError(err) + s.Equal(result, result2) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_UnknownScheme() { + scheme := "unknown" + + provider := NewArchiverProvider( + nil, + nil, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetVisibilityArchiver(scheme) + s.Error(err) + s.Equal(ErrUnknownScheme, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_ConfigNotFound_Filestore() { + scheme := filestore.URIScheme + + provider := NewArchiverProvider( + nil, + &config.VisibilityArchiverProvider{ + Filestore: nil, // Config not set + }, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetVisibilityArchiver(scheme) + s.Error(err) + s.Equal(ErrArchiverConfigNotFound, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_ConfigNotFound_GCloud() { + scheme := gcloud.URIScheme + + provider := NewArchiverProvider( + nil, + &config.VisibilityArchiverProvider{ + Gstorage: nil, // Config not set + }, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetVisibilityArchiver(scheme) + s.Error(err) + s.Equal(ErrArchiverConfigNotFound, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_ConfigNotFound_S3() { + scheme := s3store.URIScheme + + provider := NewArchiverProvider( + nil, + &config.VisibilityArchiverProvider{ + S3store: nil, // Config not set + }, + nil, + nil, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + result, err := provider.GetVisibilityArchiver(scheme) + s.Error(err) + s.Equal(ErrArchiverConfigNotFound, err) + s.Nil(result) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_ConcurrentAccess() { + scheme := "custom" + + provider := NewArchiverProvider( + nil, + nil, + nil, + s.mockCustomVisibilityFactory, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + // The factory may be called multiple times before caching occurs, + // but all calls should return the same archiver. + s.mockCustomVisibilityFactory.EXPECT(). + NewCustomVisibilityArchiver(gomock.Any()). + Return(s.mockVisibilityArchiver, nil). + AnyTimes() + + var wg sync.WaitGroup + numGoroutines := 10 + results := make([]archiver.VisibilityArchiver, numGoroutines) + archiverErrors := make([]error, numGoroutines) + + for i := range numGoroutines { + wg.Go(func() { + results[i], archiverErrors[i] = provider.GetVisibilityArchiver(scheme) + }) + } + + wg.Wait() + + // All should succeed and return a valid archiver + for i := range numGoroutines { + s.NoError(archiverErrors[i]) + s.NotNil(results[i]) + } + + // Verify caching works after concurrent initialization + cachedResult, err := provider.GetVisibilityArchiver(scheme) + s.NoError(err) + s.NotNil(cachedResult) +} + +func (s *ProviderSuite) TestGetVisibilityArchiver_NilConfigs() { + scheme := "custom" + + provider := NewArchiverProvider( + nil, + nil, // nil visibility config + nil, + s.mockCustomVisibilityFactory, + s.mockExecutionManager, + s.logger, + s.metricsHandler, + ) + + s.mockCustomVisibilityFactory.EXPECT(). + NewCustomVisibilityArchiver(gomock.Any()). + DoAndReturn(func(params NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) { + s.Nil(params.Configs) // Should be nil when configs are not provided + return s.mockVisibilityArchiver, nil + }) + + result, err := provider.GetVisibilityArchiver(scheme) + s.NoError(err) + s.Equal(s.mockVisibilityArchiver, result) +} + +func (s *ProviderSuite) TestCustomVisibilityArchiverFactoryFunc() { + called := false + expectedArchiver := s.mockVisibilityArchiver + + factoryFunc := CustomVisibilityArchiverFactoryFunc(func(params NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) { + called = true + s.Equal("test-scheme", params.Scheme) + return expectedArchiver, nil + }) + + result, err := factoryFunc.NewCustomVisibilityArchiver(NewCustomVisibilityArchiverParams{ + Scheme: "test-scheme", + }) + + s.NoError(err) + s.True(called) + s.Equal(expectedArchiver, result) +} diff --git a/common/archiver/s3store/util.go b/common/archiver/s3store/util.go index 1e7cc3c452c..c082c5e4695 100644 --- a/common/archiver/s3store/util.go +++ b/common/archiver/s3store/util.go @@ -42,7 +42,7 @@ func decodeVisibilityRecord(data []byte) (*archiverspb.VisibilityRecord, error) return record, nil } -func SerializeToken(token interface{}) ([]byte, error) { +func SerializeToken(token any) ([]byte, error) { if token == nil { return nil, nil } diff --git a/common/authorization/audience_mapper.go b/common/authorization/audience_mapper.go index c0c37e83d1b..b4d6c89b610 100644 --- a/common/authorization/audience_mapper.go +++ b/common/authorization/audience_mapper.go @@ -13,7 +13,7 @@ type AudienceMapper struct { } // Audience returns the configured audience string. -func (m *AudienceMapper) Audience(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo) string { +func (m *AudienceMapper) Audience(ctx context.Context, req any, info *grpc.UnaryServerInfo) string { return m.JwtAudience } diff --git a/common/authorization/authorizer.go b/common/authorization/authorizer.go index 191fb863a94..ac79cc837e0 100644 --- a/common/authorization/authorizer.go +++ b/common/authorization/authorizer.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + commonpb "go.temporal.io/api/common/v1" "go.temporal.io/server/common/config" ) @@ -29,7 +30,7 @@ type CallTarget struct { // The nexus endpoint name being targeted (if any). NexusEndpointName string // Request contains a deserialized copy of the API request object - Request interface{} + Request any } // @@@SNIPEND @@ -40,6 +41,8 @@ type ( Decision Decision // Reason may contain a message explaining the value of the Decision field. Reason string + // Principal is the server-computed identity of the caller. Can be nil when not computed. + Principal *commonpb.Principal } // Decision is enum type for auth decision diff --git a/common/authorization/claim_mapper.go b/common/authorization/claim_mapper.go index 61aaf6110cd..3d05f8448f8 100644 --- a/common/authorization/claim_mapper.go +++ b/common/authorization/claim_mapper.go @@ -58,6 +58,25 @@ func (*noopClaimMapper) AuthInfoRequired() bool { return false } +// internalClaimMapper is used by the internal frontend to identify requests as +// coming from the Temporal server itself. +type internalClaimMapper struct{} + +var _ ClaimMapper = (*internalClaimMapper)(nil) +var _ ClaimMapperWithAuthInfoRequired = (*internalClaimMapper)(nil) + +func NewInternalClaimMapper() ClaimMapper { + return &internalClaimMapper{} +} + +func (*internalClaimMapper) GetClaims(_ *AuthInfo) (*Claims, error) { + return &Claims{System: RoleAdmin, AuthType: "temporal", Subject: "internal"}, nil +} + +func (*internalClaimMapper) AuthInfoRequired() bool { + return false +} + func GetClaimMapperFromConfig(config *config.Authorization, logger log.Logger) (ClaimMapper, error) { switch strings.ToLower(config.ClaimMapper) { diff --git a/common/authorization/default_authorizer.go b/common/authorization/default_authorizer.go index dbc1d13dfe6..03ddd31cf92 100644 --- a/common/authorization/default_authorizer.go +++ b/common/authorization/default_authorizer.go @@ -3,6 +3,7 @@ package authorization import ( "context" + commonpb "go.temporal.io/api/common/v1" "go.temporal.io/server/common/api" ) @@ -56,7 +57,9 @@ func (a *defaultAuthorizer) Authorize(_ context.Context, claims *Claims, target } if hasRole >= getRequiredRole(metadata.Access) { - return resultAllow, nil + result := Result{Decision: DecisionAllow} + result.Principal = &commonpb.Principal{Type: claims.AuthType, Name: claims.Subject} + return result, nil } return resultDeny, nil } diff --git a/common/authorization/default_jwt_claim_mapper.go b/common/authorization/default_jwt_claim_mapper.go index 9a15d5b347f..a4e58d4e72a 100644 --- a/common/authorization/default_jwt_claim_mapper.go +++ b/common/authorization/default_jwt_claim_mapper.go @@ -75,7 +75,7 @@ var _ ClaimMapper = (*defaultJWTClaimMapper)(nil) func (a *defaultJWTClaimMapper) GetClaims(authInfo *AuthInfo) (*Claims, error) { - claims := Claims{} + claims := Claims{AuthType: "jwt"} if authInfo.AuthToken == "" { return &claims, nil @@ -99,7 +99,7 @@ func (a *defaultJWTClaimMapper) GetClaims(authInfo *AuthInfo) (*Claims, error) { return nil, serviceerror.NewPermissionDenied("unexpected value type of \"sub\" claim", "") } claims.Subject = subject - permissions, ok := jwtClaims[a.permissionsClaimName].([]interface{}) + permissions, ok := jwtClaims[a.permissionsClaimName].([]any) if ok { err := a.extractPermissions(permissions, &claims) if err != nil { @@ -109,7 +109,7 @@ func (a *defaultJWTClaimMapper) GetClaims(authInfo *AuthInfo) (*Claims, error) { return &claims, nil } -func (a *defaultJWTClaimMapper) extractPermissions(permissions []interface{}, claims *Claims) error { +func (a *defaultJWTClaimMapper) extractPermissions(permissions []any, claims *Claims) error { for _, permission := range permissions { p, ok := permission.(string) if !ok { @@ -156,13 +156,13 @@ func parseJWTWithAudience(tokenString string, keyProvider TokenKeyProvider, audi var keyFunc jwt.Keyfunc if provider, _ := keyProvider.(RawTokenKeyProvider); provider != nil { - keyFunc = func(token *jwt.Token) (interface{}, error) { + keyFunc = func(token *jwt.Token) (any, error) { // reserve context // impl may introduce network request to get public key return provider.GetKey(context.Background(), token) } } else { - keyFunc = func(token *jwt.Token) (interface{}, error) { + keyFunc = func(token *jwt.Token) (any, error) { kid, ok := token.Header["kid"].(string) if !ok { return nil, fmt.Errorf("malformed token - no \"kid\" header") diff --git a/common/authorization/interceptor.go b/common/authorization/interceptor.go index cab24bbb33a..06a3d267459 100644 --- a/common/authorization/interceptor.go +++ b/common/authorization/interceptor.go @@ -7,6 +7,7 @@ import ( "crypto/x509/pkix" "time" + commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" @@ -30,7 +31,7 @@ type ( type ( // JWTAudienceMapper returns JWT audience for a given request JWTAudienceMapper interface { - Audience(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo) string + Audience(ctx context.Context, req any, info *grpc.UnaryServerInfo) string } NamespaceChecker interface { @@ -90,6 +91,7 @@ type Interceptor struct { authExtraHeaderName string exposeAuthorizerErrors dynamicconfig.BoolPropertyFn enableCrossNamespaceCommands dynamicconfig.BoolPropertyFn + enablePrincipalPropagation dynamicconfig.BoolPropertyFnWithNamespaceFilter } // NewInterceptor creates an authorization interceptor. @@ -104,6 +106,7 @@ func NewInterceptor( authExtraHeaderName string, exposeAuthorizerErrors dynamicconfig.BoolPropertyFn, enableCrossNamespaceCommands dynamicconfig.BoolPropertyFn, + enablePrincipalPropagation dynamicconfig.BoolPropertyFnWithNamespaceFilter, ) *Interceptor { return &Interceptor{ claimMapper: claimMapper, @@ -116,15 +119,16 @@ func NewInterceptor( audienceGetter: audienceGetter, exposeAuthorizerErrors: exposeAuthorizerErrors, enableCrossNamespaceCommands: enableCrossNamespaceCommands, + enablePrincipalPropagation: enablePrincipalPropagation, } } func (a *Interceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { tlsConnection := TLSInfoFromContext(ctx) authInfo := a.GetAuthInfo(tlsConnection, headers.NewGRPCHeaderGetter(ctx), func() string { @@ -146,6 +150,10 @@ func (a *Interceptor) Intercept( ctx = a.EnhanceContext(ctx, authInfo, claims) } + // Always strip inbound principal headers to prevent external callers from + // spoofing principal identity, regardless of whether the authorizer is enabled. + ctx = headers.StripPrincipal(ctx) + if a.authorizer != nil { var namespace string requestWithNamespace, ok := req.(hasNamespace) @@ -157,9 +165,13 @@ func (a *Interceptor) Intercept( APIName: info.FullMethod, Request: req, } - if err := a.Authorize(ctx, claims, ct); err != nil { + principal, err := a.Authorize(ctx, claims, ct) + if err != nil { return nil, err } + if a.enablePrincipalPropagation != nil && a.enablePrincipalPropagation(namespace) && principal != nil { + ctx = headers.SetPrincipal(ctx, principal) + } // Authorize target namespaces in cross-namespace commands if err := a.authorizeTargetNamespaces(ctx, claims, namespace, req); err != nil { @@ -224,9 +236,10 @@ func (a *Interceptor) EnhanceContext(ctx context.Context, authInfo *AuthInfo, cl // Authorize uses the policy's authorizer to authorize a request based on provided claims and call target. // Logs and emits metrics when unauthorized. -func (a *Interceptor) Authorize(ctx context.Context, claims *Claims, ct *CallTarget) error { +// Returns the principal identity and any authorization error. +func (a *Interceptor) Authorize(ctx context.Context, claims *Claims, ct *CallTarget) (*commonpb.Principal, error) { if a.authorizer == nil { - return nil + return nil, nil } mh := a.getMetricsHandler(ct.Namespace) @@ -238,19 +251,19 @@ func (a *Interceptor) Authorize(ctx context.Context, claims *Claims, ct *CallTar metrics.ServiceErrAuthorizeFailedCounter.With(mh).Record(1) a.logger.Error("Authorization error", tag.Error(err)) if a.exposeAuthorizerErrors() { - return err + return nil, err } - return errUnauthorized // return a generic error to the caller without disclosing details + return nil, errUnauthorized // return a generic error to the caller without disclosing details } if result.Decision != DecisionAllow { metrics.ServiceErrUnauthorizedCounter.With(mh).Record(1) // if a reason is included in the result, include it in the error message if result.Reason != "" { - return serviceerror.NewPermissionDenied(RequestUnauthorized, result.Reason) + return nil, serviceerror.NewPermissionDenied(RequestUnauthorized, result.Reason) } - return errUnauthorized // return a generic error to the caller without disclosing details + return nil, errUnauthorized // return a generic error to the caller without disclosing details } - return nil + return result.Principal, nil } // getMetricsHandler returns a metrics handler with a namespace tag @@ -275,7 +288,7 @@ func (a *Interceptor) authorizeTargetNamespaces( ctx context.Context, claims *Claims, sourceNamespace string, - req interface{}, + req any, ) error { // Skip if cross-namespace commands are not enabled if !a.enableCrossNamespaceCommands() { @@ -327,7 +340,7 @@ func (a *Interceptor) authorizeTargetNamespaces( } // Authorize access to target namespace for this specific API - if err := a.Authorize(ctx, claims, &CallTarget{ + if _, err := a.Authorize(ctx, claims, &CallTarget{ APIName: api.WorkflowServicePrefix + apiName, Namespace: targetNamespace, Request: req, diff --git a/common/authorization/interceptor_test.go b/common/authorization/interceptor_test.go index 5cfebd4e704..965328f4a86 100644 --- a/common/authorization/interceptor_test.go +++ b/common/authorization/interceptor_test.go @@ -8,11 +8,13 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" + commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/common/api" "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/headers" "go.temporal.io/server/common/log" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" @@ -84,8 +86,9 @@ func (s *authorizerInterceptorSuite) SetupTest() { "", dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), // enablePrincipalPropagation ) - s.handler = func(ctx context.Context, req interface{}) (interface{}, error) { return true, nil } + s.handler = func(ctx context.Context, req any) (any, error) { return true, nil } } func (s *authorizerInterceptorSuite) TearDownTest() { @@ -159,6 +162,7 @@ func (s *authorizerInterceptorSuite) TestAuthorizationFailedExposed() { "", dynamicconfig.GetBoolPropertyFn(true), // exposeAuthorizerErrors dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), // enablePrincipalPropagation ) authErr := serviceerror.NewInternal("intentional test failure") @@ -192,6 +196,7 @@ func (s *authorizerInterceptorSuite) TestNoopClaimMapperWithoutTLS() { "", dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), // enablePrincipalPropagation ) _, err := interceptor.Intercept(ctx, describeNamespaceRequest, describeNamespaceInfo, s.handler) s.NoError(err) @@ -209,6 +214,7 @@ func (s *authorizerInterceptorSuite) TestAlternateHeaders() { "custom-extra-header", dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), // enablePrincipalPropagation ) cases := []struct { @@ -318,6 +324,7 @@ func (s *authorizerInterceptorSuite) newCrossNamespaceInterceptor(namespaces ... "", dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors dynamicconfig.GetBoolPropertyFn(true), // enableCrossNamespaceCommands + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), // enablePrincipalPropagation ) } @@ -521,6 +528,78 @@ func (s *authorizerInterceptorSuite) TestMultipleCommands_AuthDeduplication() { s.NoError(err) } +func (s *authorizerInterceptorSuite) TestPrincipalPropagation_Enabled() { + principal := &commonpb.Principal{Type: "user", Name: "alice"} + s.mockAuthorizer.EXPECT().Authorize(gomock.Any(), nil, describeNamespaceTarget). + Return(Result{Decision: DecisionAllow, Principal: principal}, nil) + + interceptor := NewInterceptor( + s.mockClaimMapper, + s.mockAuthorizer, + s.mockMetricsHandler, + log.NewNoopLogger(), + mockNamespaceChecker(testNamespace), + nil, + "", + "", + dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors + dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true), // enablePrincipalPropagation + ) + + inCtx := metadata.NewIncomingContext(ctx, metadata.MD{}) + var gotPrincipal *commonpb.Principal + handler := func(ctx context.Context, req any) (any, error) { + gotPrincipal = headers.GetPrincipal(ctx) + return true, nil + } + + res, err := interceptor.Intercept(inCtx, describeNamespaceRequest, describeNamespaceInfo, handler) + s.True(res.(bool)) + s.NoError(err) + s.Equal(principal, gotPrincipal) +} + +func (s *authorizerInterceptorSuite) TestPrincipalPropagation_Disabled() { + s.mockAuthorizer.EXPECT().Authorize(gomock.Any(), nil, describeNamespaceTarget). + Return(Result{Decision: DecisionAllow, Principal: &commonpb.Principal{Type: "user", Name: "alice"}}, nil) + + inCtx := metadata.NewIncomingContext(ctx, metadata.MD{}) + var gotPrincipal *commonpb.Principal + handler := func(ctx context.Context, req any) (any, error) { + gotPrincipal = headers.GetPrincipal(ctx) + return true, nil + } + + // s.interceptor has enablePrincipalPropagation=false + res, err := s.interceptor.Intercept(inCtx, describeNamespaceRequest, describeNamespaceInfo, handler) + s.True(res.(bool)) + s.NoError(err) + s.Nil(gotPrincipal) +} + +func (s *authorizerInterceptorSuite) TestPrincipalPropagation_SpoofedHeadersStripped() { + s.mockAuthorizer.EXPECT().Authorize(gomock.Any(), nil, describeNamespaceTarget). + Return(Result{Decision: DecisionAllow}, nil) // no principal returned + + // Inject spoofed principal headers in the incoming context. + inCtx := metadata.NewIncomingContext(ctx, metadata.Pairs( + headers.PrincipalTypeHeaderName, "spoofed-type", + headers.PrincipalNameHeaderName, "spoofed-name", + )) + var gotPrincipal *commonpb.Principal + handler := func(ctx context.Context, req any) (any, error) { + gotPrincipal = headers.GetPrincipal(ctx) + return true, nil + } + + // s.interceptor has enablePrincipalPropagation=false + res, err := s.interceptor.Intercept(inCtx, describeNamespaceRequest, describeNamespaceInfo, handler) + s.True(res.(bool)) + s.NoError(err) + s.Nil(gotPrincipal, "spoofed principal headers should be stripped") +} + func (s *authorizerInterceptorSuite) TestMultipleTargetNamespaces() { // Test commands targeting different namespaces request := &workflowservice.RespondWorkflowTaskCompletedRequest{ diff --git a/common/authorization/roles.go b/common/authorization/roles.go index cae2d5ed8cb..da424844ca9 100644 --- a/common/authorization/roles.go +++ b/common/authorization/roles.go @@ -30,7 +30,9 @@ type Claims struct { // Roles within specific namespaces Namespaces map[string]Role // Free form bucket for extra data - Extensions interface{} + Extensions any + // AuthType identifies the authentication method that produced these claims (e.g., "jwt", "mtls"). + AuthType string } // @@@SNIPEND diff --git a/common/authorization/token_key_provider.go b/common/authorization/token_key_provider.go index bcbd7afa9c4..a5592fcee32 100644 --- a/common/authorization/token_key_provider.go +++ b/common/authorization/token_key_provider.go @@ -20,7 +20,7 @@ type TokenKeyProvider interface { // RawTokenKeyProvider is a TokenKeyProvider that provides keys for validating JWT tokens type RawTokenKeyProvider interface { - GetKey(ctx context.Context, token *jwt.Token) (interface{}, error) + GetKey(ctx context.Context, token *jwt.Token) (any, error) SupportedMethods() []string Close() } diff --git a/common/backoff/jitter_test.go b/common/backoff/jitter_test.go index ade3dd672f2..9b3f5bf62fd 100644 --- a/common/backoff/jitter_test.go +++ b/common/backoff/jitter_test.go @@ -28,7 +28,7 @@ func (s *jitterSuite) TestJitter_Int64() { lowerBound := int64(float64(input) * (1 - coefficient)) upperBound := int64(float64(input) * (1 + coefficient)) - for i := 0; i < 1048576; i++ { + for range 1048576 { result := Jitter(input, coefficient) s.True(result >= lowerBound) s.True(result < upperBound) @@ -45,7 +45,7 @@ func (s *jitterSuite) TestJitter_Float64() { lowerBound := float64(input) * (1 - coefficient) upperBound := float64(input) * (1 + coefficient) - for i := 0; i < 1048576; i++ { + for range 1048576 { result := Jitter(input, coefficient) s.True(result >= lowerBound) s.True(result < upperBound) @@ -62,7 +62,7 @@ func (s *jitterSuite) TestJitter_Duration() { lowerBound := time.Duration(int64(float64(input.Nanoseconds()) * (1 - coefficient))) upperBound := time.Duration(int64(float64(input.Nanoseconds()) * (1 + coefficient))) - for i := 0; i < 1048576; i++ { + for range 1048576 { result := Jitter(input, coefficient) s.True(result >= lowerBound) s.True(result < upperBound) diff --git a/common/backoff/retrypolicy_test.go b/common/backoff/retrypolicy_test.go index 321c304274f..78ee024a282 100644 --- a/common/backoff/retrypolicy_test.go +++ b/common/backoff/retrypolicy_test.go @@ -35,7 +35,7 @@ func ExampleExponentialRetryPolicy_WithMaximumInterval() { p2 = p2.WithMaximumInterval(time.Second * 10) var e1, e2 time.Duration fmt.Printf("%-10s| %15s| %15s\n", "Attempt", "Delay", "Capped Delay") - for attempts := 0; attempts < 10; attempts++ { + for attempts := range 10 { d1 := p1.ComputeNextDelay(e1, attempts, nil) d2 := p2.ComputeNextDelay(e2, attempts, nil) e1 += d1 @@ -127,7 +127,7 @@ func (s *RetryPolicySuite) TestBackoffCoefficient() { r, _ := createRetrier(policy) min, max := getNextBackoffRange(2 * time.Second) - for i := 0; i < 10; i++ { + for range 10 { next := r.NextBackOff(nil) s.True(next >= min, "NextBackoff too low") s.True(next < max, "NextBackoff too high") @@ -205,7 +205,7 @@ func (s *RetryPolicySuite) TestNoMaxAttempts() { WithMaximumInterval(10 * time.Second) r, ts := createRetrier(policy) - for i := 0; i < 100; i++ { + for range 100 { next := r.NextBackOff(nil) s.True(next > 0 || next == done, "Unexpected value for next retry duration: %v", next) ts.Advance(next) @@ -216,7 +216,7 @@ func (s *RetryPolicySuite) TestUnbounded() { policy := createPolicy(50 * time.Millisecond) r, ts := createRetrier(policy) - for i := 0; i < 100; i++ { + for range 100 { next := r.NextBackOff(nil) s.True(next > 0 || next == done, "Unexpected value for next retry duration: %v", next) ts.Advance(next) diff --git a/common/cache/cache.go b/common/cache/cache.go index 9590cfac0d7..757171dcba9 100644 --- a/common/cache/cache.go +++ b/common/cache/cache.go @@ -12,20 +12,20 @@ import ( type Cache interface { // Get retrieves an element based on a key, returning nil if the element // does not exist - Get(key interface{}) interface{} + Get(key any) any // Put adds an element to the cache, returning the previous element - Put(key interface{}, value interface{}) interface{} + Put(key any, value any) any // PutIfNotExist puts a value associated with a given key if it does not exist - PutIfNotExist(key interface{}, value interface{}) (interface{}, error) + PutIfNotExist(key any, value any) (any, error) // Delete deletes an element in the cache - Delete(key interface{}) + Delete(key any) // Release decrements the ref count of a pinned element. If the ref count // drops to 0, the element can be evicted from the cache. - Release(key interface{}) + Release(key any) // Iterator returns the iterator of the cache Iterator() Iterator @@ -73,7 +73,7 @@ type SimpleOptions struct { // scheduled for removal from the Cache. If f is a function with the // appropriate signature and i is the interface{} scheduled for // deletion, Cache calls go f(i) -type RemovedFunc func(interface{}) +type RemovedFunc func(any) // Iterator represents the interface for cache iterators. type Iterator interface { @@ -89,9 +89,9 @@ type Iterator interface { // Entry represents a key-value entry within the map. type Entry interface { // Key represents the key - Key() interface{} + Key() any // Value represents the value - Value() interface{} + Value() any // CreateTime represents the time when the entry is created CreateTime() time.Time } diff --git a/common/cache/lru.go b/common/cache/lru.go index 19620819577..c44353e4e12 100644 --- a/common/cache/lru.go +++ b/common/cache/lru.go @@ -32,7 +32,7 @@ type ( lru struct { mut sync.Mutex byAccess *list.List - byKey map[interface{}]*list.Element + byKey map[any]*list.Element maxSize int currSize int pinnedSize int @@ -53,9 +53,9 @@ type ( } entryImpl struct { - key interface{} + key any createTime time.Time - value interface{} + value any refCount int size int } @@ -117,11 +117,11 @@ func (c *lru) Iterator() Iterator { return iterator } -func (entry *entryImpl) Key() interface{} { +func (entry *entryImpl) Key() any { return entry.key } -func (entry *entryImpl) Value() interface{} { +func (entry *entryImpl) Value() any { return entry.value } @@ -163,7 +163,7 @@ func NewWithMetrics(maxSize int, opts *Options, handler metrics.Handler) Stoppab metrics.CacheTtl.With(handler).Record(opts.TTL) c := &lru{ byAccess: list.New(), - byKey: make(map[interface{}]*list.Element), + byKey: make(map[any]*list.Element), ttl: opts.TTL, maxSize: maxSize, currSize: 0, @@ -187,7 +187,7 @@ func NewLRU(maxSize int, handler metrics.Handler) StoppableCache { } // Get retrieves the value stored under the given key -func (c *lru) Get(key interface{}) interface{} { +func (c *lru) Get(key any) any { if c.maxSize == 0 { // return nil } @@ -215,7 +215,7 @@ func (c *lru) Get(key interface{}) interface{} { } // Put puts a new value associated with a given key, returning the existing value (if present) -func (c *lru) Put(key interface{}, value interface{}) interface{} { +func (c *lru) Put(key any, value any) any { if c.pin { panic("Cannot use Put API in Pin mode. Use Delete and PutIfNotExist if necessary") } @@ -224,7 +224,7 @@ func (c *lru) Put(key interface{}, value interface{}) interface{} { } // PutIfNotExist puts a value associated with a given key if it does not exist -func (c *lru) PutIfNotExist(key interface{}, value interface{}) (interface{}, error) { +func (c *lru) PutIfNotExist(key any, value any) (any, error) { existing, err := c.putInternal(key, value, false) if err != nil { return nil, err @@ -239,7 +239,7 @@ func (c *lru) PutIfNotExist(key interface{}, value interface{}) (interface{}, er } // Delete deletes a key, value pair associated with a key -func (c *lru) Delete(key interface{}) { +func (c *lru) Delete(key any) { if c.maxSize == 0 { return } @@ -253,7 +253,7 @@ func (c *lru) Delete(key interface{}) { } // Release decrements the ref count of a pinned element. -func (c *lru) Release(key interface{}) { +func (c *lru) Release(key any) { if c.maxSize == 0 || !c.pin { return } @@ -293,7 +293,7 @@ func (c *lru) Size() int { // Put puts a new value associated with a given key, returning the existing value (if present) // allowUpdate flag is used to control overwrite behavior if the value exists. -func (c *lru) putInternal(key interface{}, value interface{}, allowUpdate bool) (interface{}, error) { +func (c *lru) putInternal(key any, value any, allowUpdate bool) (any, error) { if c.maxSize == 0 { return nil, nil } diff --git a/common/cache/lru_test.go b/common/cache/lru_test.go index b960312e47b..bb73bc38ab4 100644 --- a/common/cache/lru_test.go +++ b/common/cache/lru_test.go @@ -161,7 +161,7 @@ func TestLRUCacheConcurrentAccess(t *testing.T) { start := make(chan struct{}) var wg sync.WaitGroup - for i := 0; i < 20; i++ { + for range 20 { wg.Add(2) // concurrent get and put @@ -170,7 +170,7 @@ func TestLRUCacheConcurrentAccess(t *testing.T) { <-start - for j := 0; j < 1000; j++ { + for range 1000 { cache.Get("A") cache.Put("A", "fooo") } @@ -182,7 +182,7 @@ func TestLRUCacheConcurrentAccess(t *testing.T) { <-start - for j := 0; j < 50; j++ { + for range 50 { it := cache.Iterator() for it.HasNext() { _ = it.Next() @@ -437,7 +437,7 @@ func TestCache_ItemHasCacheSizeDefined(t *testing.T) { startWG.Wait() assert.True(t, cache.Size() < maxTotalBytes) }() - for i := 0; i < numPuts; i++ { + for range numPuts { go func() { defer endWG.Done() diff --git a/common/cache/simple.go b/common/cache/simple.go index 5ce13108fb9..e08cbf1a71c 100644 --- a/common/cache/simple.go +++ b/common/cache/simple.go @@ -14,7 +14,7 @@ var ( type ( simple struct { sync.RWMutex - accessMap map[interface{}]*list.Element + accessMap map[any]*list.Element iterateList *list.List rmFunc RemovedFunc } @@ -25,8 +25,8 @@ type ( } simpleEntry struct { - key interface{} - value interface{} + key any + value any } ) @@ -57,11 +57,11 @@ func (it *simpleItr) Next() Entry { return entry } -func (e *simpleEntry) Key() interface{} { +func (e *simpleEntry) Key() any { return e.key } -func (e *simpleEntry) Value() interface{} { +func (e *simpleEntry) Value() any { return e.value } @@ -81,13 +81,13 @@ func NewSimple(opts *SimpleOptions) Cache { } return &simple{ iterateList: list.New(), - accessMap: make(map[interface{}]*list.Element), + accessMap: make(map[any]*list.Element), rmFunc: opts.RemovedFunc, } } // Get retrieves the value stored under the given key -func (c *simple) Get(key interface{}) interface{} { +func (c *simple) Get(key any) any { c.RLock() defer c.RUnlock() @@ -99,7 +99,7 @@ func (c *simple) Get(key interface{}) interface{} { } // Put puts a new value associated with a given key, returning the existing value (if present). -func (c *simple) Put(key interface{}, value interface{}) interface{} { +func (c *simple) Put(key any, value any) any { c.Lock() defer c.Unlock() existing := c.putInternal(key, value, true) @@ -107,7 +107,7 @@ func (c *simple) Put(key interface{}, value interface{}) interface{} { } // PutIfNotExist puts a value associated with a given key if it does not exist -func (c *simple) PutIfNotExist(key interface{}, value interface{}) (interface{}, error) { +func (c *simple) PutIfNotExist(key any, value any) (any, error) { c.Lock() defer c.Unlock() existing := c.putInternal(key, value, false) @@ -119,7 +119,7 @@ func (c *simple) PutIfNotExist(key interface{}, value interface{}) (interface{}, } // Delete deletes a key, value pair associated with a key -func (c *simple) Delete(key interface{}) { +func (c *simple) Delete(key any) { c.Lock() defer c.Unlock() @@ -136,7 +136,7 @@ func (c *simple) Delete(key interface{}) { } // Release does nothing for simple cache -func (c *simple) Release(_ interface{}) {} +func (c *simple) Release(_ any) {} // Size returns the number of entries currently in the cache func (c *simple) Size() int { @@ -155,7 +155,7 @@ func (c *simple) Iterator() Iterator { return iterator } -func (c *simple) putInternal(key interface{}, value interface{}, allowUpdate bool) interface{} { +func (c *simple) putInternal(key any, value any, allowUpdate bool) any { elt := c.accessMap[key] if elt != nil { // nolint:revive diff --git a/common/cache/simple_test.go b/common/cache/simple_test.go index 1becc341ed3..7c89e3bf8da 100644 --- a/common/cache/simple_test.go +++ b/common/cache/simple_test.go @@ -73,7 +73,7 @@ func TestSimpleCacheConcurrentAccess(t *testing.T) { start := make(chan struct{}) var wg sync.WaitGroup - for i := 0; i < 20; i++ { + for range 20 { wg.Add(2) // concurrent get and put @@ -82,7 +82,7 @@ func TestSimpleCacheConcurrentAccess(t *testing.T) { <-start - for j := 0; j < 1000; j++ { + for range 1000 { cache.Get("A") cache.Put("A", "fooo") } @@ -94,7 +94,7 @@ func TestSimpleCacheConcurrentAccess(t *testing.T) { <-start - for j := 0; j < 50; j++ { + for range 50 { it := cache.Iterator() for it.HasNext() { _ = it.Next() @@ -111,7 +111,7 @@ func TestSimpleCacheConcurrentAccess(t *testing.T) { func TestSimpleRemoveFunc(t *testing.T) { ch := make(chan bool) cache := NewSimple(&SimpleOptions{ - RemovedFunc: func(i interface{}) { + RemovedFunc: func(i any) { _, ok := i.(*testing.T) assert.True(t, ok) ch <- true diff --git a/common/cache/size_getter.go b/common/cache/size_getter.go index 1eab5d8df16..2d49fd186d9 100644 --- a/common/cache/size_getter.go +++ b/common/cache/size_getter.go @@ -13,7 +13,7 @@ type ( } ) -func getSize(value interface{}) int { +func getSize(value any) int { if v, ok := value.(SizeGetter); ok { return v.CacheSize() } diff --git a/common/checksum/crc_test.go b/common/checksum/crc_test.go index fe6749edec8..0955ebcaa3e 100644 --- a/common/checksum/crc_test.go +++ b/common/checksum/crc_test.go @@ -36,11 +36,11 @@ func TestCRC32OverProto(t *testing.T) { doneWG := sync.WaitGroup{} doneWG.Add(parallism) - for i := 0; i < parallism; i++ { + for range parallism { go func() { defer doneWG.Done() <-startC - for count := 0; count < loopCount; count++ { + for range loopCount { csum, err := GenerateCRC32(obj, 1) if err != nil { return diff --git a/common/client_cache.go b/common/client_cache.go index 0149a65d6a8..a4e32329f5f 100644 --- a/common/client_cache.go +++ b/common/client_cache.go @@ -8,9 +8,9 @@ type ( // ClientCache store initialized clients ClientCache interface { Lookup(key string, index int) (string, error) // pass through to keyResolver - GetClientForKey(key string, index int) (interface{}, error) - GetClientForClientKey(clientKey string) (interface{}, error) - GetAllClients() ([]interface{}, error) + GetClientForKey(key string, index int) (any, error) + GetClientForClientKey(clientKey string) (any, error) + GetAllClients() ([]any, error) } keyResolver interface { @@ -18,14 +18,14 @@ type ( GetAllAddresses() ([]string, error) } - clientProvider func(string) (interface{}, error) + clientProvider func(string) (any, error) clientCacheImpl struct { keyResolver keyResolver clientProvider clientProvider cacheLock sync.RWMutex - clients map[string]interface{} + clients map[string]any } ) @@ -39,7 +39,7 @@ func NewClientCache( keyResolver: keyResolver, clientProvider: clientProvider, - clients: make(map[string]interface{}), + clients: make(map[string]any), } } @@ -47,7 +47,7 @@ func (c *clientCacheImpl) Lookup(key string, index int) (string, error) { return c.keyResolver.Lookup(key, index) } -func (c *clientCacheImpl) GetClientForKey(key string, index int) (interface{}, error) { +func (c *clientCacheImpl) GetClientForKey(key string, index int) (any, error) { clientKey, err := c.Lookup(key, index) if err != nil { return nil, err @@ -55,7 +55,7 @@ func (c *clientCacheImpl) GetClientForKey(key string, index int) (interface{}, e return c.GetClientForClientKey(clientKey) } -func (c *clientCacheImpl) GetClientForClientKey(clientKey string) (interface{}, error) { +func (c *clientCacheImpl) GetClientForClientKey(clientKey string) (any, error) { c.cacheLock.RLock() client, ok := c.clients[clientKey] c.cacheLock.RUnlock() @@ -79,8 +79,8 @@ func (c *clientCacheImpl) GetClientForClientKey(clientKey string) (interface{}, return client, nil } -func (c *clientCacheImpl) GetAllClients() ([]interface{}, error) { - var result []interface{} +func (c *clientCacheImpl) GetAllClients() ([]any, error) { + var result []any allAddresses, err := c.keyResolver.GetAllAddresses() if err != nil { return nil, err diff --git a/common/codec/jsonpb.go b/common/codec/jsonpb.go index 63d3dd2b070..79e04552c08 100644 --- a/common/codec/jsonpb.go +++ b/common/codec/jsonpb.go @@ -94,7 +94,7 @@ func (e *JSONPBEncoder) encodeSlice( ) ([]byte, error) { var buf bytes.Buffer buf.WriteString("[") - for i := 0; i < len; i++ { + for i := range len { pb := item(i) bs, err := e.marshaler.Marshal(pb) if err != nil { diff --git a/common/collection/concurrent_tx_map.go b/common/collection/concurrent_tx_map.go index 9ba1fe28370..2cc7324f8a2 100644 --- a/common/collection/concurrent_tx_map.go +++ b/common/collection/concurrent_tx_map.go @@ -36,7 +36,7 @@ type ( // of thread safe map mapShard struct { sync.RWMutex - items map[interface{}]interface{} + items map[any]any } ) @@ -62,10 +62,10 @@ func NewShardedConcurrentTxMap(initialCap int, hashfn HashFunc) ConcurrentTxMap } // Get returns the value corresponding to the key, if it exist -func (cmap *ShardedConcurrentTxMap) Get(key interface{}) (interface{}, bool) { +func (cmap *ShardedConcurrentTxMap) Get(key any) (any, bool) { shard := cmap.getShard(key) var ok bool - var value interface{} + var value any shard.RLock() if shard.items != nil { value, ok = shard.items[key] @@ -75,13 +75,13 @@ func (cmap *ShardedConcurrentTxMap) Get(key interface{}) (interface{}, bool) { } // Contains returns true if the key exist and false otherwise -func (cmap *ShardedConcurrentTxMap) Contains(key interface{}) bool { +func (cmap *ShardedConcurrentTxMap) Contains(key any) bool { _, ok := cmap.Get(key) return ok } // Put records the given key value mapping. Overwrites previous values -func (cmap *ShardedConcurrentTxMap) Put(key interface{}, value interface{}) { +func (cmap *ShardedConcurrentTxMap) Put(key any, value any) { shard := cmap.getShard(key) shard.Lock() cmap.lazyInitShard(shard) @@ -95,7 +95,7 @@ func (cmap *ShardedConcurrentTxMap) Put(key interface{}, value interface{}) { // PutIfNotExist records the mapping, if there is no mapping for this key already // Returns true if the mapping was recorded, false otherwise -func (cmap *ShardedConcurrentTxMap) PutIfNotExist(key interface{}, value interface{}) bool { +func (cmap *ShardedConcurrentTxMap) PutIfNotExist(key any, value any) bool { shard := cmap.getShard(key) var ok bool shard.Lock() @@ -110,7 +110,7 @@ func (cmap *ShardedConcurrentTxMap) PutIfNotExist(key interface{}, value interfa } // Remove deletes the given key from the map -func (cmap *ShardedConcurrentTxMap) Remove(key interface{}) { +func (cmap *ShardedConcurrentTxMap) Remove(key any) { shard := cmap.getShard(key) shard.Lock() cmap.lazyInitShard(shard) @@ -124,9 +124,9 @@ func (cmap *ShardedConcurrentTxMap) Remove(key interface{}) { // GetAndDo returns the value corresponding to the key, and apply fn to key value before return value // return (value, value exist or not, error when evaluation fn) -func (cmap *ShardedConcurrentTxMap) GetAndDo(key interface{}, fn ActionFunc) (interface{}, bool, error) { +func (cmap *ShardedConcurrentTxMap) GetAndDo(key any, fn ActionFunc) (any, bool, error) { shard := cmap.getShard(key) - var value interface{} + var value any var ok bool var err error shard.Lock() @@ -142,7 +142,7 @@ func (cmap *ShardedConcurrentTxMap) GetAndDo(key interface{}, fn ActionFunc) (in // PutOrDo put the key value in the map, if key does not exists, otherwise, call fn with existing key and value // return (value, fn evaluated or not, error when evaluation fn) -func (cmap *ShardedConcurrentTxMap) PutOrDo(key interface{}, value interface{}, fn ActionFunc) (interface{}, bool, error) { +func (cmap *ShardedConcurrentTxMap) PutOrDo(key any, value any, fn ActionFunc) (any, bool, error) { shard := cmap.getShard(key) var err error shard.Lock() @@ -160,7 +160,7 @@ func (cmap *ShardedConcurrentTxMap) PutOrDo(key interface{}, value interface{}, } // RemoveIf deletes the given key from the map if fn return true -func (cmap *ShardedConcurrentTxMap) RemoveIf(key interface{}, fn PredicateFunc) bool { +func (cmap *ShardedConcurrentTxMap) RemoveIf(key any, fn PredicateFunc) bool { shard := cmap.getShard(key) var removed bool shard.Lock() @@ -196,7 +196,7 @@ func (cmap *ShardedConcurrentTxMap) Iter() MapIterator { iterator.stopCh = make(chan struct{}) go func(iterator *mapIteratorImpl) { - for i := 0; i < nShards; i++ { + for i := range nShards { cmap.shards[i].RLock() for k, v := range cmap.shards[i].items { entry := &MapEntry{Key: k, Value: v} @@ -221,13 +221,13 @@ func (cmap *ShardedConcurrentTxMap) Len() int { return int(atomic.LoadInt32(&cmap.size)) } -func (cmap *ShardedConcurrentTxMap) getShard(key interface{}) *mapShard { +func (cmap *ShardedConcurrentTxMap) getShard(key any) *mapShard { shardIdx := cmap.hashfn(key) % nShards return &cmap.shards[shardIdx] } func (cmap *ShardedConcurrentTxMap) lazyInitShard(shard *mapShard) { if shard.items == nil { - shard.items = make(map[interface{}]interface{}, cmap.initialCap) + shard.items = make(map[any]any, cmap.initialCap) } } diff --git a/common/collection/concurrent_tx_map_test.go b/common/collection/concurrent_tx_map_test.go index 6838ccdcadd..026003ea2f4 100644 --- a/common/collection/concurrent_tx_map_test.go +++ b/common/collection/concurrent_tx_map_test.go @@ -59,7 +59,7 @@ func (s *ConcurrentTxMapSuite) TestGetAndDo() { var value intType fnApplied := false - interf, ok, err := testMap.GetAndDo(key, func(key interface{}, value interface{}) error { + interf, ok, err := testMap.GetAndDo(key, func(key any, value any) error { fnApplied = true return nil }) @@ -70,7 +70,7 @@ func (s *ConcurrentTxMapSuite) TestGetAndDo() { value = intType(1) testMap.Put(key, &value) - interf, ok, err = testMap.GetAndDo(key, func(key interface{}, value interface{}) error { + interf, ok, err = testMap.GetAndDo(key, func(key any, value any) error { fnApplied = true intValue := value.(*intType) *intValue++ @@ -91,7 +91,7 @@ func (s *ConcurrentTxMapSuite) TestPutOrDo() { fnApplied := false value = intType(1) - interf, ok, err := testMap.PutOrDo(key, &value, func(key interface{}, value interface{}) error { + interf, ok, err := testMap.PutOrDo(key, &value, func(key any, value any) error { fnApplied = true return errors.New("some err") }) @@ -102,7 +102,7 @@ func (s *ConcurrentTxMapSuite) TestPutOrDo() { s.False(fnApplied, "PutOrDo should not apply function when key not exixts") anotherValue := intType(111) - interf, ok, err = testMap.PutOrDo(key, &anotherValue, func(key interface{}, value interface{}) error { + interf, ok, err = testMap.PutOrDo(key, &anotherValue, func(key any, value any) error { fnApplied = true intValue := value.(*intType) *intValue++ @@ -121,14 +121,14 @@ func (s *ConcurrentTxMapSuite) TestRemoveIf() { value := intType(1) testMap.Put(key, &value) - removed := testMap.RemoveIf(key, func(key interface{}, value interface{}) bool { + removed := testMap.RemoveIf(key, func(key any, value any) bool { intValue := value.(*intType) return *intValue == intType(2) }) s.Equal(1, testMap.Len(), "TestRemoveIf should only entry if condition is met") s.False(removed, "TestRemoveIf should return false if key is not deleted") - removed = testMap.RemoveIf(key, func(key interface{}, value interface{}) bool { + removed = testMap.RemoveIf(key, func(key any, value any) bool { intValue := value.(*intType) return *intValue == intType(1) }) @@ -141,7 +141,7 @@ func (s *ConcurrentTxMapSuite) TestGetAfterPut() { countMap := make(map[string]int) testMap := NewShardedConcurrentTxMap(1, UUIDHashCode) - for i := 0; i < 1024; i++ { + for range 1024 { key := uuid.NewString() countMap[key] = 0 testMap.Put(key, boolType(true)) @@ -185,7 +185,7 @@ func (s *ConcurrentTxMapSuite) TestPutIfNotExist() { func (s *ConcurrentTxMapSuite) TestMapConcurrency() { nKeys := 1024 keys := make([]string, nKeys) - for i := 0; i < nKeys; i++ { + for i := range nKeys { keys[i] = uuid.NewString() } @@ -196,13 +196,13 @@ func (s *ConcurrentTxMapSuite) TestMapConcurrency() { startWG.Add(1) - for i := 0; i < 10; i++ { + for range 10 { doneWG.Add(1) go func() { startWG.Wait() - for n := 0; n < nKeys; n++ { + for n := range nKeys { val := intType(rand.Int()) if testMap.PutIfNotExist(keys[n], val) { atomic.AddInt32(&total, int32(val)) @@ -220,7 +220,7 @@ func (s *ConcurrentTxMapSuite) TestMapConcurrency() { s.Equal(nKeys, testMap.Len(), "Wrong concurrent map size") var gotTotal int32 - for i := 0; i < nKeys; i++ { + for i := range nKeys { v, ok := testMap.Get(keys[i]) s.True(ok, "Get failed to find previously inserted key") intVal := v.(intType) diff --git a/common/collection/interface.go b/common/collection/interface.go index 0046fce2752..f07b90e2e19 100644 --- a/common/collection/interface.go +++ b/common/collection/interface.go @@ -16,37 +16,37 @@ type ( } // HashFunc represents a hash function for string - HashFunc func(interface{}) uint32 + HashFunc func(any) uint32 // ActionFunc take a key and value, do calculation and return err - ActionFunc func(key interface{}, value interface{}) error + ActionFunc func(key any, value any) error // PredicateFunc take a key and value, do calculation and return boolean - PredicateFunc func(key interface{}, value interface{}) bool + PredicateFunc func(key any, value any) bool // ConcurrentTxMap is a generic interface for any implementation of a dictionary // or a key value lookup table that is thread safe, and providing functionality // to modify key / value pair inside within a transaction ConcurrentTxMap interface { // Get returns the value for the given key - Get(key interface{}) (interface{}, bool) + Get(key any) (any, bool) // Contains returns true if the key exist and false otherwise - Contains(key interface{}) bool + Contains(key any) bool // Put records the mapping from given key to value - Put(key interface{}, value interface{}) + Put(key any, value any) // PutIfNotExist records the key value mapping only // if the mapping does not already exist - PutIfNotExist(key interface{}, value interface{}) bool + PutIfNotExist(key any, value any) bool // Remove deletes the key from the map - Remove(key interface{}) + Remove(key any) // GetAndDo returns the value corresponding to the key, and apply fn to key value before return value // return (value, value exist or not, error when evaluation fn) - GetAndDo(key interface{}, fn ActionFunc) (interface{}, bool, error) + GetAndDo(key any, fn ActionFunc) (any, bool, error) // PutOrDo put the key value in the map, if key does not exists, otherwise, call fn with existing key and value // return (value, fn evaluated or not, error when evaluation fn) - PutOrDo(key interface{}, value interface{}, fn ActionFunc) (interface{}, bool, error) + PutOrDo(key any, value any, fn ActionFunc) (any, bool, error) // RemoveIf deletes the given key from the map if fn return true // return whether the key is removed or not - RemoveIf(key interface{}, fn PredicateFunc) bool + RemoveIf(key any, fn PredicateFunc) bool // Iter returns an iterator to the map Iter() MapIterator // Len returns the number of items in the map @@ -66,9 +66,9 @@ type ( // MapEntry represents a key-value entry within the map MapEntry struct { // Key represents the key - Key interface{} + Key any // Value represents the value - Value interface{} + Value any } ) diff --git a/common/collection/paging_iterator_test.go b/common/collection/paging_iterator_test.go index 7dea8144971..e788af230f0 100644 --- a/common/collection/paging_iterator_test.go +++ b/common/collection/paging_iterator_test.go @@ -82,7 +82,7 @@ func (s *pagingIteratorSuite) TestIteration_NoErr() { func (s *pagingIteratorSuite) TestIteration_Err_Beginging() { phase := 0 - ite := NewPagingIterator(func(token []byte) ([]interface{}, []byte, error) { + ite := NewPagingIterator(func(token []byte) ([]any, []byte, error) { switch phase { case 0: defer func() { phase++ }() @@ -102,13 +102,13 @@ func (s *pagingIteratorSuite) TestIteration_Err_Beginging() { func (s *pagingIteratorSuite) TestIteration_Err_NotBegining() { phase := 0 - outputs := [][]interface{}{ + outputs := [][]any{ {1, 2, 3, 4, 5}, } tokens := [][]byte{ []byte("some random token 1"), } - pagingFn := func(token []byte) ([]interface{}, []byte, error) { + pagingFn := func(token []byte) ([]any, []byte, error) { switch phase { case 0: s.Equal(0, len(token)) diff --git a/common/collection/priority_queue.go b/common/collection/priority_queue.go index c636093a12e..f499bbcf813 100644 --- a/common/collection/priority_queue.go +++ b/common/collection/priority_queue.go @@ -78,12 +78,12 @@ func (pq *priorityQueueImpl[T]) Swap(i, j int) { } // Push push an item to priority queue, used by go internal heap implementation -func (pq *priorityQueueImpl[T]) Push(item interface{}) { +func (pq *priorityQueueImpl[T]) Push(item any) { pq.items = append(pq.items, item.(T)) } // Pop pop an item from priority queue, used by go internal heap implementation -func (pq *priorityQueueImpl[T]) Pop() interface{} { +func (pq *priorityQueueImpl[T]) Pop() any { pqItem := pq.items[pq.Len()-1] pq.items = pq.items[0 : pq.Len()-1] return pqItem diff --git a/common/collection/priority_queue_test.go b/common/collection/priority_queue_test.go index d94f5a23351..87757263e78 100644 --- a/common/collection/priority_queue_test.go +++ b/common/collection/priority_queue_test.go @@ -94,11 +94,11 @@ func (s *PriorityQueueSuite) TestInsertAndPop() { } func (s *PriorityQueueSuite) TestRandomNumber() { - for round := 0; round < 1000; round++ { + for range 1000 { expected := []int{} result := []int{} - for i := 0; i < 1000; i++ { + for range 1000 { num := rand.Int() s.pq.Add(&testPriorityQueueItem{num}) expected = append(expected, num) diff --git a/common/collection/sync_map_test.go b/common/collection/sync_map_test.go index d867d9d31b8..87521542f79 100644 --- a/common/collection/sync_map_test.go +++ b/common/collection/sync_map_test.go @@ -17,35 +17,35 @@ func TestMap_MultiThreaded(t *testing.T) { go func() { defer wg.Done() <-barrier - for i := 0; i < 1000; i++ { + for i := range 1000 { m.Set(i, i) } }() go func() { <-barrier defer wg.Done() - for i := 0; i < 1000; i++ { + for i := range 1000 { m.Get(i) } }() go func() { <-barrier defer wg.Done() - for i := 0; i < 1000; i++ { + for i := range 1000 { m.GetOrSet(i, i) } }() go func() { <-barrier defer wg.Done() - for i := 0; i < 1000; i++ { + for i := range 1000 { m.Pop(i) } }() go func() { <-barrier defer wg.Done() - for i := 0; i < 1000; i++ { + for range 1000 { m.PopAll() } }() diff --git a/common/collection/util.go b/common/collection/util.go index ab695a67415..d49681d8ea7 100644 --- a/common/collection/util.go +++ b/common/collection/util.go @@ -8,7 +8,7 @@ import ( // UUIDHashCode is a hash function for hashing string uuid // if the uuid is malformed, then the hash function always // returns 0 as the hash value -func UUIDHashCode(input interface{}) uint32 { +func UUIDHashCode(input any) uint32 { key, ok := input.(string) if !ok { return 0 diff --git a/common/config/config.go b/common/config/config.go index 0ca4baaa6b6..3d4b47be22c 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -484,6 +484,9 @@ type ( Filestore *FilestoreArchiver `yaml:"filestore"` Gstorage *GstorageArchiver `yaml:"gstorage"` S3store *S3Archiver `yaml:"s3store"` + // CustomStores contains the config for all custom history archivers + // The structure is a map of archiver name (scheme) to a map of config key-values + CustomStores map[string]map[string]any `yaml:"customStores"` } // VisibilityArchival contains the config for visibility archival @@ -501,6 +504,9 @@ type ( Filestore *FilestoreArchiver `yaml:"filestore"` S3store *S3Archiver `yaml:"s3store"` Gstorage *GstorageArchiver `yaml:"gstorage"` + // CustomStores contains the config for all custom visibility archivers + // The structure is a map of archiver name (scheme) to a map of config key-values + CustomStores map[string]map[string]any `yaml:"customStores"` } // FilestoreArchiver contain the config for filestore archiver diff --git a/common/dynamicconfig/client.go b/common/dynamicconfig/client.go index 758efcf2974..5c903b2270f 100644 --- a/common/dynamicconfig/client.go +++ b/common/dynamicconfig/client.go @@ -89,6 +89,7 @@ type ( NamespaceID string TaskQueueName string Destination string + ChasmTaskType string TaskQueueType enumspb.TaskQueueType ShardID int32 TaskType enumsspb.TaskType diff --git a/common/dynamicconfig/collection_test.go b/common/dynamicconfig/collection_test.go index eced01b52d9..7ae80fdbd63 100644 --- a/common/dynamicconfig/collection_test.go +++ b/common/dynamicconfig/collection_test.go @@ -38,8 +38,10 @@ const ( testGetDurationPropertyStructuredDefaults = "testGetDurationPropertyStructuredDefaults" testGetBoolPropertyFilteredByNamespaceIDKey = "testGetBoolPropertyFilteredByNamespaceIDKey" testGetBoolPropertyFilteredByTaskQueueInfoKey = "testGetBoolPropertyFilteredByTaskQueueInfoKey" + testGetStringPropertyFilteredByNamespaceKey = "testGetStringPropertyFilteredByNamespaceKey" testGetStringPropertyFilteredByNamespaceIDKey = "testGetStringPropertyFilteredByNamespaceIDKey" testGetIntPropertyFilteredByDestinationKey = "testGetIntPropertyFilteredByDestinationKey" + testGetDurationPropertyFilteredByChasmTaskTypeKey = "testGetDurationPropertyFilteredByChasmTaskTypeKey" ) // Note: fileBasedClientSuite also heavily tests Collection, since some tests are easier with data @@ -87,17 +89,17 @@ func (s *collectionSuite) TestGetIntPropertyFilteredByNamespace() { } func (s *collectionSuite) TestGetStringPropertyFnFilteredByNamespace() { - namespace := "testNamespace" - value := dynamicconfig.DefaultEventEncoding.Get(s.cln) - // copied default value, change this if it changes - s.Equal(enumspb.ENCODING_TYPE_PROTO3.String(), value(namespace)) - s.client.SetValue(dynamicconfig.DefaultEventEncoding.Key().String(), "efg") - s.Equal("efg", value(namespace)) + ns := "testNamespace" + setting := dynamicconfig.NewNamespaceStringSetting(testGetStringPropertyFilteredByNamespaceKey, "abc", "") + value := setting.Get(s.cln) + s.Equal("abc", value(ns)) + s.client.SetValue(testGetStringPropertyFilteredByNamespaceKey, "efg") + s.Equal("efg", value(ns)) } func (s *collectionSuite) TestGetStringPropertyFnFilteredByNamespaceID() { - setting := dynamicconfig.NewNamespaceIDStringSetting(testGetStringPropertyFilteredByNamespaceIDKey, "abc", "") namespaceID := namespace.ID("testNamespaceID") + setting := dynamicconfig.NewNamespaceIDStringSetting(testGetStringPropertyFilteredByNamespaceIDKey, "abc", "") value := setting.Get(s.cln) s.Equal("abc", value(namespaceID)) s.client.SetValue(testGetStringPropertyFilteredByNamespaceIDKey, "efg") @@ -199,6 +201,15 @@ func (s *collectionSuite) TestGetDurationPropertyFilteredByTaskType() { s.Equal(time.Minute, value(taskType)) } +func (s *collectionSuite) TestGetDurationPropertyFilteredByChasmTaskType() { + setting := dynamicconfig.NewChasmTaskTypeDurationSetting(testGetDurationPropertyFilteredByChasmTaskTypeKey, time.Second, "") + chasmTaskType := "activity.dispatch" + value := setting.Get(s.cln) + s.Equal(time.Second, value(chasmTaskType)) + s.client.SetValue(testGetDurationPropertyFilteredByChasmTaskTypeKey, time.Minute) + s.Equal(time.Minute, value(chasmTaskType)) +} + func (s *collectionSuite) TestGetDurationPropertyStructuredDefaults() { setting := dynamicconfig.NewTaskQueueDurationSettingWithConstrainedDefault( testGetDurationPropertyStructuredDefaults, @@ -272,7 +283,7 @@ func (s *collectionSuite) TestGetDurationPropertyStructuredDefaults() { } func (s *collectionSuite) TestGetMapProperty() { - def := map[string]interface{}{"testKey": 123} + def := map[string]any{"testKey": 123} setting := dynamicconfig.NewGlobalMapSetting( testGetMapPropertyKey, def, diff --git a/common/dynamicconfig/config/testConfig.yaml b/common/dynamicconfig/config/testConfig.yaml index f8eefdbc2a4..258b8ee08c7 100644 --- a/common/dynamicconfig/config/testConfig.yaml +++ b/common/dynamicconfig/config/testConfig.yaml @@ -91,6 +91,12 @@ testGetDurationPropertyFilteredByTaskTypeKey: - value: 10s constraints: historytasktype: 1 +testGetDurationPropertyFilteredByChasmTaskTypeKey: + - value: 30s + constraints: + chasmtasktype: "activity.dispatch" + - value: 24h + constraints: {} testGetIntPropertyFilteredByDestinationKey: - value: 10 constraints: {} diff --git a/common/dynamicconfig/constants.go b/common/dynamicconfig/constants.go index 2fd1b474e33..c9e6c4eb4c0 100644 --- a/common/dynamicconfig/constants.go +++ b/common/dynamicconfig/constants.go @@ -5,11 +5,11 @@ import ( "os" "time" - enumspb "go.temporal.io/api/enums/v1" sdkworker "go.temporal.io/sdk/worker" "go.temporal.io/server/common/debug" "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/retrypolicy" + "go.temporal.io/server/common/util" "go.temporal.io/server/service/matching/counter" ) @@ -173,6 +173,12 @@ config as the other services.`, 3*time.Second, `RingpopApproximateMaxPropagationTime is used for timing certain startup and shutdown processes. (It is not and doesn't have to be a guarantee.)`, + ) + RingpopReplicaPoints = NewGlobalIntSetting( + "system.ringpopReplicaPoints", + 100, + `RingpopReplicaPoints is the number of virtual nodes (replica points) per physical host +in the consistent hash ring used by ringpop. Changing it may cause service disruption during deployment.`, ) EnableParentClosePolicyWorker = NewGlobalBoolSetting( "system.enableParentClosePolicyWorker", @@ -686,6 +692,15 @@ exceeded, not when it is only reached.`, instances in the cluster, for a given namespace, per-API method. If this is set to 0 (the default), then it is ignored. The name 'frontend.globalNamespaceCount' is kept for consistency with the per-instance limit name, 'frontend.namespaceCount'.`, + ) + FrontendPollWaitForNamespaceRateLimitToken = NewNamespaceBoolSetting( + "frontend.pollWaitForNamespaceRateLimitToken", + false, + `FrontendPollWaitForNamespaceRateLimitToken controls whether poll requests wait for +a namespace RPS rate limit token to become available instead of immediately rejecting +with ResourceExhausted. When enabled, poll requests block until a token is available +or the request context deadline is reached. The concurrent request rate limiter fires +before this limiter and will still reject requests that exceed the concurrent limit.`, ) FrontendMaxNamespaceVisibilityRPSPerInstance = NewNamespaceIntSetting( "frontend.namespaceRPS.visibility", @@ -834,6 +849,12 @@ This config is EXPERIMENTAL and may be changed or removed in a later release.`, false, `ExposeAuthorizerErrors controls whether the frontend authorization interceptor will pass through errors returned by the Authorizer component. If false, a generic PermissionDenied error without details will be returned. Default false.`, + ) + EnablePrincipalPropagation = NewNamespaceBoolSetting( + "frontend.enablePrincipalPropagation", + false, + `EnablePrincipalPropagation controls whether the authorization interceptor propagates the authenticated +principal identity as gRPC headers.`, ) KeepAliveMinTime = NewGlobalDurationSetting( "frontend.keepAliveMinTime", @@ -904,7 +925,7 @@ and deployment interaction in matching and history.`, ) UseRevisionNumberForWorkerVersioning = NewNamespaceBoolSetting( "system.useRevisionNumberForWorkerVersioning", - false, + true, `UseRevisionNumberForWorkerVersioning enables the use of revision number to resolve consistency problems that may arise during task dispatch time.`, ) EnableSuggestCaNOnNewTargetVersion = NewNamespaceBoolSetting( @@ -917,13 +938,6 @@ and deployment interaction in matching and history.`, true, `EnableSendTargetVersionChanged lets Pinned workflows receive TargetWorkerDeploymentVersionChanged=true when a new target version is available for that workflow.`, ) - EnableNexus = NewGlobalBoolSetting( - "system.enableNexus", - true, - `Toggles all Nexus functionality on the server. Note that toggling this requires restarting server hosts for it - to take effect.`, - ) - AllowDeleteNamespaceIfNexusEndpointTarget = NewGlobalBoolSetting( "frontend.allowDeleteNamespaceIfNexusEndpointTarget", false, @@ -955,10 +969,17 @@ used when the first cache layer has a miss. Requires server restart for change t FrontendNexusRequestHeadersBlacklist = NewGlobalTypedSettingWithConverter( "frontend.nexusRequestHeadersBlacklist", ConvertWildcardStringListToRegexp, - MatchNothingRE, - `Nexus request headers to be removed before being sent to a user handler. -Wildcards (*) are expanded to allow any substring. By default blacklist is empty. -Concrete type should be list of strings.`, + // Failure support is an internal implementation detail that shouldn't propagate to the user. + util.MustWildCardStringsToRegexp([]string{ + "accept-encoding", + "x-forwarded-for", + "xdc-redirection", + "xdc-redirection-api", + "temporal-nexus-failure-support", + }), + `Nexus request headers to be removed before being sent to a user handler. Wildcards (*) are expanded to +allow any substring. By default headers that are meant for internal use are disallowed. Concrete type should be list of +strings.`, ) FrontendNexusForwardRequestUseEndpointDispatch = NewGlobalBoolSetting( "frontend.nexusForwardRequestUseEndpointDispatch", @@ -1313,6 +1334,12 @@ duration since last poll exceeds this threshold.`, 20*time.Second, `QueryPollerUnavailableWindow WF Queries are rejected after a while if no poller has been seen within the window`, ) + MatchingEmitTaskDispatchLatencyAtPoll = NewTaskQueueBoolSetting( + "matching.emitTaskDispatchLatencyAtPoll", + true, + `When enabled, TaskDispatchLatencyPerTaskQueue is emitted when responding to poll requests (with extra tags +like partition and worker-version) instead of being emitted at the matcher level.`, + ) MatchingListNexusEndpointsLongPollTimeout = NewGlobalDurationSetting( "matching.listNexusEndpointsLongPollTimeout", 5*time.Minute-10*time.Second, @@ -1342,7 +1369,7 @@ these log lines can be noisy, we want to be able to turn on and sample selective ) MatchingDeploymentWorkflowVersion = NewNamespaceIntSetting( "matching.deploymentWorkflowVersion", - 0, + 2, `MatchingDeploymentWorkflowVersion controls what version of the logic should the manager workflows use.`, ) MatchingMaxTaskQueuesInDeployment = NewNamespaceIntSetting( @@ -1391,8 +1418,8 @@ second per poller by one physical queue manager`, ) MatchingUseNewMatcher = NewTaskQueueTypedSettingWithConverter( "matching.useNewMatcher", - ConvertGradualChange(false), - StaticGradualChange(false), + ConvertGradualChange(true), + StaticGradualChange(true), `Use priority-enabled TaskMatcher`, ) MatchingEnableFairness = NewTaskQueueTypedSettingWithConverter( @@ -1403,7 +1430,7 @@ second per poller by one physical queue manager`, ) MatchingEnableMigration = NewTaskQueueBoolSetting( "matching.enableMigration", - false, + true, `Allows migration between v1 and v2 (fairness) task backlogs.`, ) MatchingPriorityLevels = NewTaskQueueIntSetting( @@ -1437,6 +1464,13 @@ second per poller by one physical queue manager`, `MatchingEnableWorkerPluginMetrics controls whether to export worker plugin metrics. The metric has 2 dimensions: namespace_id and plugin_name. Disabled by default as this is an optional feature and also requires a metrics collection system that can handle higher cardinalities.`, + ) + MatchingEnablePollerAutoscalingMetrics = NewGlobalBoolSetting( + "matching.enablePollerAutoscalingMetrics", + false, + `MatchingEnablePollerAutoscalingMetrics controls whether to export poller autoscaling metrics. +The metric has dimensions: namespace, taskqueue, and task_type (Workflow, Activity, Nexus). Disabled by +default as namespace cardinality can be high and this requires a metrics collection system that can handle it.`, ) MatchingAutoEnableV2 = NewTaskQueueBoolSetting( "matching.autoEnableV2", @@ -1505,6 +1539,13 @@ Don't change this on a live cluster without using the gradual change mechanism. [go.temporal.io/server/common/persistence.QueueV2]`, ) + EnableDeleteWorkflowExecutionReplication = NewGlobalBoolSetting( + "history.enableDeleteWorkflowExecutionReplication", + false, + `EnableDeleteWorkflowExecutionReplication controls whether a replication task is generated when a workflow +execution is deleted. When enabled, workflow deletions on the active cluster will be replicated to passive clusters.`, + ) + HistoryRPS = NewGlobalIntSetting( "history.rps", 3000, @@ -1719,6 +1760,15 @@ before calling remote for missing events`, 15*time.Minute, `StandbyTaskMissingEventsDiscardDelay is the amount of time standby cluster's will wait (if events are missing) before discarding the task`, + ) + ChasmStandbyTaskDiscardDelay = NewChasmTaskTypeDurationSetting( + "history.ChasmStandbyTaskDiscardDelay", + 24*time.Hour, + `ChasmStandbyTaskDiscardDelay is the amount of time standby cluster will wait +before discarding a CHASM task. Configurable per RegistrableTask type (e.g. "activity.dispatch"). +The default is intentionally much higher than the non CHASM standby discard delay because +discarding a CHASM task can leave the execution in a stuck state after failover. Task types +that can be safely offloaded should be configured with a shorter delay.`, ) QueuePendingTaskCriticalCount = NewGlobalIntSetting( "history.queuePendingTaskCriticalCount", @@ -1816,6 +1866,30 @@ If value less or equal to 0, will fall back to HistoryPersistenceNamespaceMaxQPS time.Hour, `TaskSchedulerInactiveChannelDeletionDelay the time delay before a namespace's' channel is removed from the scheduler`, ) + TaskSchedulerEnableExecutionQueueScheduler = NewGlobalBoolSetting( + "history.taskSchedulerEnableExecutionQueueScheduler", + false, + `TaskSchedulerEnableExecutionQueueScheduler enables the execution queue scheduler +that processes tasks for contended workflows sequentially to avoid busy workflow errors`, + ) + TaskSchedulerExecutionQueueSchedulerMaxQueues = NewGlobalIntSetting( + "history.taskSchedulerExecutionQueueSchedulerMaxQueues", + 500, + `TaskSchedulerExecutionQueueSchedulerMaxQueues is the maximum number of concurrent per-workflow queues in the execution queue scheduler. +When this limit is reached, new workflows will fall back to the base FIFO scheduler.`, + ) + TaskSchedulerExecutionQueueSchedulerQueueTTL = NewGlobalDurationSetting( + "history.taskSchedulerExecutionQueueSchedulerQueueTTL", + 5*time.Second, + `TaskSchedulerExecutionQueueSchedulerQueueTTL is how long a per-workflow queue goroutine waits idle before exiting.`, + ) + + TaskSchedulerExecutionQueueSchedulerQueueConcurrency = NewGlobalIntSetting( + "history.taskSchedulerExecutionQueueSchedulerQueueConcurrency", + 2, + `TaskSchedulerExecutionQueueSchedulerQueueConcurrency is the max number of worker goroutines per workflow queue. +Higher values allow limited parallelism per workflow. Values <= 0 are capped to 1.`, + ) TimerTaskBatchSize = NewGlobalIntSetting( "history.timerTaskBatchSize", @@ -2312,11 +2386,6 @@ When the this config is zero or lower we will only update shard info at most onc false, `EmitShardLagLog whether emit the shard lag log`, ) - DefaultEventEncoding = NewNamespaceStringSetting( - "history.defaultEventEncoding", - enumspb.ENCODING_TYPE_PROTO3.String(), - `DefaultEventEncoding is the encoding type for history events`, - ) DefaultActivityRetryPolicy = NewNamespaceTypedSetting( "history.defaultActivityRetryPolicy", retrypolicy.DefaultDefaultRetrySettings, @@ -2390,6 +2459,11 @@ the number of children greater than or equal to this threshold`, false, `EnableDropRepeatedWorkflowTaskFailures whether to silently drop repeated workflow task failures`, ) + SendTransientOrSpeculativeWorkflowTaskEvents = NewNamespaceBoolSetting( + "history.sendTransientOrSpeculativeWorkflowTaskEvents", + true, + `SendTransientOrSpeculativeWorkflowTaskEvents controls whether GetWorkflowExecutionHistory returns non-durable transient or speculative workflow task events. Enabled by default but can be disabled per namespace if it causes compatibility problems.`, + ) DefaultWorkflowTaskTimeout = NewNamespaceDurationSetting( "history.defaultWorkflowTaskTimeout", primitives.DefaultWorkflowTaskTimeout, @@ -2778,6 +2852,13 @@ that task will be sent to DLQ.`, instead of the existing (V1) implementation.`, ) + EnableCHASMSchedulerRouting = NewNamespaceBoolSetting( + "history.enableCHASMSchedulerRouting", + false, + `EnableCHASMSchedulerRouting controls whether schedule RPCs are routed to the CHASM (V2) implementation +first (with fallback to V1), excluding CreateSchedule.`, + ) + EnableCHASMSchedulerMigration = NewNamespaceBoolSetting( "history.enableCHASMSchedulerMigration", false, @@ -3130,10 +3211,12 @@ WorkerActivitiesPerSecond, MaxConcurrentActivityTaskPollers. preventing task orphaning that can occur if tasks are dispatched to a shutting-down worker.`, ) + // Deprecated: ListWorkersEnabled is no longer honored. ListWorkers and DescribeWorker APIs are + // always enabled. The write path is gated by WorkerHeartbeatsEnabled. ListWorkersEnabled = NewNamespaceBoolSetting( "frontend.ListWorkersEnabled", true, - `ListWorkersEnabled is a "feature enable" flag. It allows clients to get workers heartbeat information.`, + `Deprecated: no longer honored. ListWorkers and DescribeWorker are always enabled.`, ) WorkerCommandsEnabled = NewNamespaceBoolSetting( diff --git a/common/dynamicconfig/file_based_client.go b/common/dynamicconfig/file_based_client.go index 82eb8bd37b5..4af3c2b4abd 100644 --- a/common/dynamicconfig/file_based_client.go +++ b/common/dynamicconfig/file_based_client.go @@ -10,10 +10,11 @@ import ( "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/metrics" ) -var _ Client = (*fileBasedClient)(nil) -var _ NotifyingClient = (*fileBasedClient)(nil) +var _ Client = (*FileBasedClient)(nil) +var _ NotifyingClient = (*FileBasedClient)(nil) const ( minPollInterval = time.Second * 5 @@ -33,13 +34,14 @@ type ( PollInterval time.Duration `yaml:"pollInterval"` } - fileBasedClient struct { + FileBasedClient struct { values atomic.Value // ConfigValueMap logger log.Logger reader FileReader - lastUpdatedTime time.Time + lastCheckedTime time.Time config *FileBasedClientConfig - doneCh <-chan interface{} + doneCh <-chan any + metricsHandler atomic.Pointer[metrics.Handler] NotifyingClientImpl } @@ -50,37 +52,76 @@ type ( ) // NewFileBasedClient creates a file based client. -func NewFileBasedClient(config *FileBasedClientConfig, logger log.Logger, doneCh <-chan interface{}) (*fileBasedClient, error) { +// use the NewFileBasedClientWithMetrics instead if you want to collect metrics. +// This method is retained mainly for backward compatibility. +func NewFileBasedClient(config *FileBasedClientConfig, logger log.Logger, doneCh <-chan any) (*FileBasedClient, error) { + return NewFileBasedClientWithMetrics(config, logger, doneCh, metrics.NoopMetricsHandler) +} + +func NewFileBasedClientWithMetrics(config *FileBasedClientConfig, logger log.Logger, doneCh <-chan any, metricsHandler metrics.Handler) (*FileBasedClient, error) { if config == nil { return nil, errors.New("configuration for dynamic config client is nil") } reader := &osReader{path: config.Filepath} - return NewFileBasedClientWithReader(reader, config, logger, doneCh) + return NewFileBasedClientWithReader(reader, config, logger, doneCh, metricsHandler) } -func NewFileBasedClientWithReader(reader FileReader, config *FileBasedClientConfig, logger log.Logger, doneCh <-chan interface{}) (*fileBasedClient, error) { - client := &fileBasedClient{ +func NewFileBasedClientWithReader(reader FileReader, config *FileBasedClientConfig, logger log.Logger, doneCh <-chan any, metricsHandler metrics.Handler) (*FileBasedClient, error) { + if config == nil { + return nil, errors.New("configuration for dynamic config client is nil") + } + if reader == nil { + return nil, errors.New("file reader for dynamic config client is nil") + } + if logger == nil { + return nil, errors.New("logger for dynamic config client is nil") + } + if metricsHandler == nil { + metricsHandler = metrics.NoopMetricsHandler + logger.Warn("metrics handler is nil, using noop metrics handler") + } + + client := &FileBasedClient{ logger: logger, reader: reader, config: config, doneCh: doneCh, NotifyingClientImpl: NewNotifyingClientImpl(), } - + client.metricsHandler.Store(&metricsHandler) err := client.init() if err != nil { return nil, err } - return client, nil } -func (fc *fileBasedClient) GetValue(key Key) []ConstrainedValue { +// SetMetricsHandler sets the metrics handler for the client. This is useful when the +// metricsHandler is not available at the time of initialization due to circular dependencies. +func (fc *FileBasedClient) SetMetricsHandler(metricsHandler metrics.Handler) { + if metricsHandler == nil { + fc.logger.Warn("metrics handler is nil, using noop metrics handler") + metricsHandler = metrics.NoopMetricsHandler + } + fc.metricsHandler.Store(&metricsHandler) +} + +func (fc *FileBasedClient) getMetricsHandler() metrics.Handler { + h := fc.metricsHandler.Load() // nolint:revive // unchecked-type-assertion + if h == nil { + // this should never happen, but we'll log a warning if it does + fc.logger.Warn("dynamic config is missing correct metrics handler, using noop metrics handler") + return metrics.NoopMetricsHandler + } + return *h +} + +func (fc *FileBasedClient) GetValue(key Key) []ConstrainedValue { values := fc.values.Load().(ConfigValueMap) // nolint:revive // unchecked-type-assertion return values[key] } -func (fc *fileBasedClient) init() error { +func (fc *FileBasedClient) init() error { if err := fc.validateStaticConfig(fc.config); err != nil { return fmt.Errorf("unable to validate dynamic config: %w", err) } @@ -110,15 +151,27 @@ func (fc *fileBasedClient) init() error { // This is public mainly for testing. The update loop will call this periodically, you don't // have to call it explicitly. -func (fc *fileBasedClient) Update() error { - modtime, err := fc.reader.GetModTime() - if err != nil { - return fmt.Errorf("dynamic config file: %s: %w", fc.config.Filepath, err) +func (fc *FileBasedClient) Update() (updateErr error) { + modtime, updateErr := fc.reader.GetModTime() + retryOnErr := true + defer func() { + h := fc.getMetricsHandler() + // gauge value 1 refers to a failed update state, and should trigger alerts + if updateErr != nil { + metrics.DynamicConfigUpdateFailure.With(h).Record(1) + } else { + metrics.DynamicConfigUpdateFailure.With(h).Record(0) + } + if updateErr == nil || !retryOnErr { + fc.lastCheckedTime = modtime + } + }() + if updateErr != nil { + return fmt.Errorf("dynamic config file: %s: %w", fc.config.Filepath, updateErr) } - if !modtime.After(fc.lastUpdatedTime) { + if !modtime.After(fc.lastCheckedTime) { return nil } - fc.lastUpdatedTime = modtime contents, err := fc.reader.ReadFile() if err != nil { @@ -127,12 +180,14 @@ func (fc *fileBasedClient) Update() error { lr := LoadYamlFile(contents) for _, e := range lr.Errors { - fc.logger.Warn("dynamic config error", tag.Error(e)) + fc.logger.Error("dynamic config error", tag.Error(e)) } for _, w := range lr.Warnings { fc.logger.Warn("dynamic config warning", tag.Error(w)) } if len(lr.Errors) > 0 { + // we don't retry on parsing errors which will fail deterministically until the file is fixed + retryOnErr = false return fmt.Errorf("loading dynamic config failed: %d errors, %d warnings", len(lr.Errors), len(lr.Warnings)) } @@ -146,7 +201,7 @@ func (fc *fileBasedClient) Update() error { return nil } -func (fc *fileBasedClient) validateStaticConfig(config *FileBasedClientConfig) error { +func (fc *FileBasedClient) validateStaticConfig(config *FileBasedClientConfig) error { if config == nil { return errors.New("configuration for dynamic config client is nil") } diff --git a/common/dynamicconfig/file_based_client_test.go b/common/dynamicconfig/file_based_client_test.go index b5e3e7c34f2..5ae15d8c50d 100644 --- a/common/dynamicconfig/file_based_client_test.go +++ b/common/dynamicconfig/file_based_client_test.go @@ -1,6 +1,7 @@ package dynamicconfig_test import ( + "errors" "strings" "testing" "time" @@ -11,6 +12,8 @@ import ( enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/metrics/metricstest" "go.temporal.io/server/common/retrypolicy" "go.uber.org/mock/gomock" ) @@ -22,7 +25,7 @@ type fileBasedClientSuite struct { *require.Assertions client dynamicconfig.Client collection *dynamicconfig.Collection - doneCh chan interface{} + doneCh chan any } func TestFileBasedClientSuite(t *testing.T) { @@ -32,12 +35,12 @@ func TestFileBasedClientSuite(t *testing.T) { func (s *fileBasedClientSuite) SetupSuite() { var err error - s.doneCh = make(chan interface{}) + s.doneCh = make(chan any) logger := log.NewNoopLogger() - s.client, err = dynamicconfig.NewFileBasedClient(&dynamicconfig.FileBasedClientConfig{ + s.client, err = dynamicconfig.NewFileBasedClientWithMetrics(&dynamicconfig.FileBasedClientConfig{ Filepath: "config/testConfig.yaml", PollInterval: time.Second * 5, - }, logger, s.doneCh) + }, logger, s.doneCh, metrics.NoopMetricsHandler) s.Require().NoError(err) s.collection = dynamicconfig.NewCollection(s.client, logger) s.collection.Start() @@ -72,6 +75,20 @@ func (s *fileBasedClientSuite) TestGetValue_NonExistKey() { s.Equal(defaultValue, v) } +func (s *fileBasedClientSuite) TestNewFileBasedClientWithoutMetrics() { + dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "") + logger := log.NewNoopLogger() + doneCh := make(chan any) + defer close(doneCh) + + _, err := dynamicconfig.NewFileBasedClient( + &dynamicconfig.FileBasedClientConfig{ + Filepath: "config/testConfig.yaml", + PollInterval: time.Minute * 5, + }, logger, doneCh) + s.NoError(err) +} + func (s *fileBasedClientSuite) TestGetValue_CaseInsensitie() { cvs := s.client.GetValue(dynamicconfig.MakeKey(testCaseInsensitivePropertyKey)) s.Equal(1, len(cvs)) @@ -177,14 +194,14 @@ func (s *fileBasedClientSuite) TestGetStringValue() { } func (s *fileBasedClientSuite) TestGetMapValue() { - var defaultVal map[string]interface{} + var defaultVal map[string]any v := dynamicconfig.NewGlobalMapSetting(testGetMapPropertyKey, defaultVal, "").Get(s.collection)() - expectedVal := map[string]interface{}{ + expectedVal := map[string]any{ "key1": "1", "key2": 1, - "key3": []interface{}{ + "key3": []any{ false, - map[string]interface{}{ + map[string]any{ "key4": true, "key5": 2.1, }, @@ -220,7 +237,7 @@ func (s *fileBasedClientSuite) TestGetTypedValue() { } func (s *fileBasedClientSuite) TestGetMapValue_WrongType() { - var defaultVal map[string]interface{} + var defaultVal map[string]any v := dynamicconfig.NewNamespaceMapSetting(testGetMapPropertyKey, defaultVal, "").Get(s.collection)("random-namespace") s.Equal(defaultVal, v) } @@ -255,25 +272,53 @@ func (s *fileBasedClientSuite) TestGetDurationValue_FilteredByTaskTypeQueue() { s.Equal(expectedValue, v) } -func (s *fileBasedClientSuite) TestValidateConfig_ConfigNotExist() { - _, err := dynamicconfig.NewFileBasedClient(nil, nil, nil) - s.Error(err) +func (s *fileBasedClientSuite) TestGetDurationValue_FilteredByChasmTaskType() { + setting := dynamicconfig.NewChasmTaskTypeDurationSetting(testGetDurationPropertyFilteredByChasmTaskTypeKey, 0, "") + v := setting.Get(s.collection)("activity.dispatch") + s.Equal(30*time.Second, v) + v = setting.Get(s.collection)("callback.invoke") + s.Equal(24*time.Hour, v) +} + +func (s *fileBasedClientSuite) TestValidateConfig_NilLogger() { + doneCh := make(chan any) + defer close(doneCh) + reader := dynamicconfig.NewMockFileReader(gomock.NewController(s.T())) + _, err := dynamicconfig.NewFileBasedClientWithReader(reader, &dynamicconfig.FileBasedClientConfig{ + Filepath: "config/testConfig.yaml", + PollInterval: time.Second * 5, + }, nil, doneCh, metrics.NoopMetricsHandler) + s.Error(err, "logger for dynamic config client is nil") } -func (s *fileBasedClientSuite) TestValidateConfig_FileNotExist() { - _, err := dynamicconfig.NewFileBasedClient(&dynamicconfig.FileBasedClientConfig{ - Filepath: "file/not/exist.yaml", - PollInterval: time.Second * 10, - }, nil, nil) - s.Error(err) +func (s *fileBasedClientSuite) TestValidateConfig_NilConfig() { + logger := log.NewNoopLogger() + doneCh := make(chan any) + defer close(doneCh) + _, err := dynamicconfig.NewFileBasedClientWithMetrics(nil, logger, doneCh, metrics.NoopMetricsHandler) + s.Error(err, "configuration for dynamic config client is nil") +} + +func (s *fileBasedClientSuite) TestValidateConfig_NilReader() { + logger := log.NewNoopLogger() + doneCh := make(chan any) + defer close(doneCh) + _, err := dynamicconfig.NewFileBasedClientWithReader(nil, &dynamicconfig.FileBasedClientConfig{ + Filepath: "config/testConfig.yaml", + PollInterval: time.Second * 5, + }, logger, doneCh, metrics.NoopMetricsHandler) + s.Error(err, "file reader for dynamic config client is nil") } func (s *fileBasedClientSuite) TestValidateConfig_ShortPollInterval() { - _, err := dynamicconfig.NewFileBasedClient(&dynamicconfig.FileBasedClientConfig{ + logger := log.NewNoopLogger() + doneCh := make(chan any) + defer close(doneCh) + _, err := dynamicconfig.NewFileBasedClientWithMetrics(&dynamicconfig.FileBasedClientConfig{ Filepath: "config/testConfig.yaml", PollInterval: time.Second, - }, nil, nil) - s.Error(err) + }, logger, doneCh, metrics.NoopMetricsHandler) + s.Contains(err.Error(), "poll interval should be at least") } func (s *fileBasedClientSuite) TestUpdate_ChangedValue() { @@ -284,7 +329,7 @@ func (s *fileBasedClientSuite) TestUpdate_ChangedValue() { ctrl := gomock.NewController(s.T()) defer ctrl.Finish() - doneCh := make(chan interface{}) + doneCh := make(chan any) reader := dynamicconfig.NewMockFileReader(ctrl) mockLogger := log.NewMockLogger(ctrl) @@ -339,7 +384,7 @@ testGetBoolPropertyKey: &dynamicconfig.FileBasedClientConfig{ Filepath: "anyValue", PollInterval: updateInterval, - }, mockLogger, doneCh) + }, mockLogger, doneCh, metrics.NoopMetricsHandler) s.NoError(err) c := dynamicconfig.NewCollection(client, mockLogger) @@ -372,13 +417,53 @@ testGetBoolPropertyKey: close(doneCh) } +func (s *fileBasedClientSuite) TestNewFileBasedClientWithNilMetricsHandler() { + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + originFileData := []byte(` +testGetFloat64PropertyKey: +- value: 12.22 + constraints: {} +`) + + doneCh := make(chan any) + reader := dynamicconfig.NewMockFileReader(ctrl) + mockLogger := log.NewMockLogger(ctrl) + fileModTime := time.Now().Add(-time.Minute * 5) + pollInterval := time.Minute * 5 + mockLogger.EXPECT().Info(gomock.Any()).Times(2) + // warnings expected with nil metrics and unregistered key + mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() + + reader.EXPECT().GetModTime().Return(fileModTime, nil).Times(3) + reader.EXPECT().ReadFile().Return([]byte(originFileData), nil) + + client, err := dynamicconfig.NewFileBasedClientWithReader(reader, + &dynamicconfig.FileBasedClientConfig{ + Filepath: "anyValue", + PollInterval: pollInterval, + }, mockLogger, doneCh, nil) + s.NoError(err) + + // set the metrics handler and shoul expect no warnings + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + client.SetMetricsHandler(captureHandler) + s.NoError(client.Update()) + snapshot := capture.Snapshot() + s.Len(snapshot["dynamic_config_update_failure"], 1) + s.InDelta(float64(0), snapshot["dynamic_config_update_failure"][0].Value, 0) + close(doneCh) +} + func (s *fileBasedClientSuite) TestUpdate_ChangedTypedValue() { dynamicconfig.NewNamespaceTypedSetting("history.fakeRetryPolicy", retrypolicy.DefaultDefaultRetrySettings, "") ctrl := gomock.NewController(s.T()) defer ctrl.Finish() - doneCh := make(chan interface{}) + doneCh := make(chan any) reader := dynamicconfig.NewMockFileReader(ctrl) mockLogger := log.NewMockLogger(ctrl) @@ -411,7 +496,7 @@ history.fakeRetryPolicy: &dynamicconfig.FileBasedClientConfig{ Filepath: "anyValue", PollInterval: updateInterval, - }, mockLogger, s.doneCh) + }, mockLogger, s.doneCh, metrics.NoopMetricsHandler) s.NoError(err) reader.EXPECT().GetModTime().Return(updatedModTime, nil) @@ -431,7 +516,7 @@ func (s *fileBasedClientSuite) TestUpdate_NewEntry() { ctrl := gomock.NewController(s.T()) defer ctrl.Finish() - doneCh := make(chan interface{}) + doneCh := make(chan any) reader := dynamicconfig.NewMockFileReader(ctrl) mockLogger := log.NewMockLogger(ctrl) @@ -466,7 +551,7 @@ testGetIntPropertyKey: &dynamicconfig.FileBasedClientConfig{ Filepath: "anyValue", PollInterval: updateInterval, - }, mockLogger, s.doneCh) + }, mockLogger, s.doneCh, metrics.NoopMetricsHandler) s.NoError(err) reader.EXPECT().GetModTime().Return(updatedModTime, nil) @@ -487,7 +572,7 @@ func (s *fileBasedClientSuite) TestUpdate_ChangeOrder_ShouldNotWriteLog() { ctrl := gomock.NewController(s.T()) defer ctrl.Finish() - doneCh := make(chan interface{}) + doneCh := make(chan any) reader := dynamicconfig.NewMockFileReader(ctrl) mockLogger := log.NewMockLogger(ctrl) @@ -528,7 +613,7 @@ testGetFloat64PropertyKey: &dynamicconfig.FileBasedClientConfig{ Filepath: "anyValue", PollInterval: updateInterval, - }, mockLogger, s.doneCh) + }, mockLogger, s.doneCh, metrics.NoopMetricsHandler) s.NoError(err) reader.EXPECT().GetModTime().Return(updatedModTime, nil) @@ -540,6 +625,273 @@ testGetFloat64PropertyKey: close(doneCh) } +func (s *fileBasedClientSuite) TestUpdate_ReadFileFailShouldRetry() { + dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "") + + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + + doneCh := make(chan any) + defer close(doneCh) + reader := dynamicconfig.NewMockFileReader(ctrl) + logger := log.NewNoopLogger() + + updateInterval := time.Minute * 5 + t1 := time.Now() + t2 := t1.Add(time.Second) + + fileData := []byte(` +testGetIntPropertyKey: +- value: 1000 + constraints: {} +`) + + // init: GetModTime called twice (validateStaticConfig + Update), ReadFile once + reader.EXPECT().GetModTime().Return(t1, nil).Times(2) + reader.EXPECT().ReadFile().Return(fileData, nil) + + client, err := dynamicconfig.NewFileBasedClientWithReader(reader, + &dynamicconfig.FileBasedClientConfig{ + Filepath: "anyValue", + PollInterval: updateInterval, + }, logger, doneCh, metrics.NoopMetricsHandler) + s.NoError(err) + + // Second update: mod time advanced to t2, but ReadFile fails transiently + reader.EXPECT().GetModTime().Return(t2, nil) + reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error")) + s.Error(client.Update()) + + // Third update: same mod time t2. + // Since retryOnErr=true for ReadFile failures, lastCheckedTime is not advanced. + // t2.After(t1) is true → ReadFile is called again. + // If bug: t2.After(t2) is false → update silently skipped → mock expectation fails. + reader.EXPECT().GetModTime().Return(t2, nil) + reader.EXPECT().ReadFile().Return(fileData, nil) + s.NoError(client.Update()) +} + +func (s *fileBasedClientSuite) TestUpdate_YamlParseErrorDoesNotRetry() { + dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "") + + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + + doneCh := make(chan any) + defer close(doneCh) + reader := dynamicconfig.NewMockFileReader(ctrl) + logger := log.NewNoopLogger() + + // update interval is set to 5 minutes to postpone the goroutine from updating the config file + // and leave room for manual calls of update() + updateInterval := time.Minute * 5 + t1 := time.Now() + t2 := t1.Add(time.Second) + + goodData := []byte(` +testGetIntPropertyKey: +- value: 1000 + constraints: {} +`) + // Invalid YAML that produces lr.Errors (decode error) + badData := []byte(`{ + "matching.numTaskqueueReadPartitions": [ + { + "value": 4 + }, + { + "constraints": { + "namespace": "cp-snapjoinerv2-staging.27ece" + }, + "value": 16 + }, + { + "constraints": { + "namespace": "ejp-automation-api-prod.2bf53", + "taskQueueName": "Interpreter_DEFAULT", + "taskType": "Activity, Workflow" + }, + "value": 8 + } + ] +}`) + + // init: GetModTime called twice (validateStaticConfig + Update), ReadFile once + reader.EXPECT().GetModTime().Return(t1, nil).Times(2) + reader.EXPECT().ReadFile().Return(goodData, nil) + + client, err := dynamicconfig.NewFileBasedClientWithReader(reader, + &dynamicconfig.FileBasedClientConfig{ + Filepath: "anyValue", + PollInterval: updateInterval, + }, logger, doneCh, metrics.NoopMetricsHandler) + s.NoError(err) + + // Second update: mod time advanced to t2, ReadFile returns bad YAML (parse error) + // retryOnErr=false → lastCheckedTime advances to t2 + reader.EXPECT().GetModTime().Return(t2, nil) + reader.EXPECT().ReadFile().Return(badData, nil) + s.Error(client.Update()) + + // Third update: same mod time t2. + // Since retryOnErr=false for parse errors, lastCheckedTime was already set to t2. + // t2.After(t2) is false → update skipped, ReadFile is NOT called. + reader.EXPECT().GetModTime().Return(t2, nil) + s.NoError(client.Update()) +} + +func (s *fileBasedClientSuite) TestUpdate_EmitsMetricOnFailure() { + dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "") + + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + + doneCh := make(chan any) + defer close(doneCh) + reader := dynamicconfig.NewMockFileReader(ctrl) + logger := log.NewNoopLogger() + captureHandler := metricstest.NewCaptureHandler() + + updateInterval := time.Minute * 5 + t1 := time.Now() + t2 := t1.Add(time.Second) + + fileData := []byte(` +testGetIntPropertyKey: +- value: 1000 + constraints: {} +`) + + // init: GetModTime x2, ReadFile x1 + reader.EXPECT().GetModTime().Return(t1, nil).Times(2) + reader.EXPECT().ReadFile().Return(fileData, nil) + + client, err := dynamicconfig.NewFileBasedClientWithReader(reader, + &dynamicconfig.FileBasedClientConfig{ + Filepath: "anyValue", + PollInterval: updateInterval, + }, logger, doneCh, captureHandler) + s.NoError(err) + + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + // Trigger a failing update + reader.EXPECT().GetModTime().Return(t2, nil) + reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error")) + s.Error(client.Update()) + + snapshot := capture.Snapshot() + s.Len(snapshot["dynamic_config_update_failure"], 1) + s.InDelta(float64(1), snapshot["dynamic_config_update_failure"][0].Value, 0) +} + +func (s *fileBasedClientSuite) TestUpdate_EmitsGaugeMetricOnFailure() { + dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "") + + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + + doneCh := make(chan any) + defer close(doneCh) + reader := dynamicconfig.NewMockFileReader(ctrl) + logger := log.NewNoopLogger() + captureHandler := metricstest.NewCaptureHandler() + + updateInterval := time.Minute * 5 + t1 := time.Now() + t2 := t1.Add(time.Second) + t3 := t2.Add(time.Second) + + fileData := []byte(` +testGetIntPropertyKey: +- value: 1000 + constraints: {} +`) + + // init: GetModTime x2, ReadFile x1 + reader.EXPECT().GetModTime().Return(t1, nil).Times(2) + reader.EXPECT().ReadFile().Return(fileData, nil) + + client, err := dynamicconfig.NewFileBasedClientWithReader(reader, + &dynamicconfig.FileBasedClientConfig{ + Filepath: "anyValue", + PollInterval: updateInterval, + }, logger, doneCh, captureHandler) + s.NoError(err) + + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + // Trigger a failing update: gauge should be 1 + reader.EXPECT().GetModTime().Return(t2, nil) + reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error")) + s.Error(client.Update()) + + snapshot := capture.Snapshot() + s.Len(snapshot["dynamic_config_update_failure"], 1) + s.InDelta(float64(1), snapshot["dynamic_config_update_failure"][0].Value, 0) + + // Fix the problem and trigger a successful update: gauge should be 0 + // lastCheckedTime is still t1 (retryOnErr=true on transient error), so t2 will trigger a read + reader.EXPECT().GetModTime().Return(t3, nil) + reader.EXPECT().ReadFile().Return(fileData, nil) + s.NoError(client.Update()) + + snapshot = capture.Snapshot() + s.Len(snapshot["dynamic_config_update_failure"], 2) + s.InDelta(float64(0), snapshot["dynamic_config_update_failure"][1].Value, 0) +} + +func (s *fileBasedClientSuite) TestUpdate_SetMetricsHandlerRecordsMetrics() { + dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "") + + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + + doneCh := make(chan any) + defer close(doneCh) + reader := dynamicconfig.NewMockFileReader(ctrl) + logger := log.NewNoopLogger() + captureHandler := metricstest.NewCaptureHandler() + + updateInterval := time.Minute * 5 + t1 := time.Now() + t2 := t1.Add(time.Second) + + fileData := []byte(` +testGetIntPropertyKey: +- value: 1000 + constraints: {} +`) + + // init: GetModTime x2, ReadFile x1; starts with NoopMetricsHandler so no captured metrics + reader.EXPECT().GetModTime().Return(t1, nil).Times(2) + reader.EXPECT().ReadFile().Return(fileData, nil) + + client, err := dynamicconfig.NewFileBasedClientWithReader(reader, + &dynamicconfig.FileBasedClientConfig{ + Filepath: "anyValue", + PollInterval: updateInterval, + }, logger, doneCh, metrics.NoopMetricsHandler) + s.NoError(err) + + // Inject the real metrics handler after construction (deferred injection pattern) + client.SetMetricsHandler(captureHandler) + + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + // Trigger a failing update — metric should now be recorded via the injected handler + reader.EXPECT().GetModTime().Return(t2, nil) + reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error")) + s.Error(client.Update()) + + snapshot := capture.Snapshot() + s.Len(snapshot["dynamic_config_update_failure"], 1) + s.InDelta(float64(1), snapshot["dynamic_config_update_failure"][0].Value, 0) +} + func (s *fileBasedClientSuite) TestWarnUnregisteredKey() { dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "") @@ -641,3 +993,22 @@ testGetBoolPropertyKey: } s.Equal(3, found) } + +func (s *fileBasedClientSuite) TestGetMetricsHandler_SetNilHandler() { + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + reader := dynamicconfig.NewMockFileReader(ctrl) + logger := log.NewNoopLogger() + doneCh := make(chan any) + defer close(doneCh) + reader.EXPECT().GetModTime().Return(time.Now(), nil).AnyTimes() + // Use a minimal valid YAML config to avoid parse errors + reader.EXPECT().ReadFile().Return([]byte("testGetIntPropertyKey:\n- value: 1000\n"), nil).AnyTimes() + client, err := dynamicconfig.NewFileBasedClientWithReader(reader, &dynamicconfig.FileBasedClientConfig{ + Filepath: "anyValue", + PollInterval: time.Minute * 5, + }, logger, doneCh, nil) + s.NoError(err) + client.SetMetricsHandler(nil) + s.NoError(client.Update()) +} diff --git a/common/dynamicconfig/gradual_change_test.go b/common/dynamicconfig/gradual_change_test.go index ead2bea812d..734055a99ab 100644 --- a/common/dynamicconfig/gradual_change_test.go +++ b/common/dynamicconfig/gradual_change_test.go @@ -306,12 +306,12 @@ func TestSubscribeGradualChange_TimerFiresAtTransitionTime(t *testing.T) { ts.Update(gc.When(key).Add(time.Second)) assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.Equal(t, []bool{true}, callbackVals.get()) + assert.Equal(c, []bool{true}, callbackVals.get()) }, time.Second, time.Millisecond) ts.Update(end.Add(time.Second)) assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.Equal(t, []bool{true}, callbackVals.get()) + assert.Equal(c, []bool{true}, callbackVals.get()) }, time.Second, time.Millisecond) } diff --git a/common/dynamicconfig/memory_client.go b/common/dynamicconfig/memory_client.go index 17e1ce9a0b6..67f4cf55678 100644 --- a/common/dynamicconfig/memory_client.go +++ b/common/dynamicconfig/memory_client.go @@ -14,9 +14,10 @@ type ( } kvpair struct { - valid bool - key Key - value any + valid bool + mergeable bool + key Key + value any } ) @@ -32,16 +33,22 @@ func (d *MemoryClient) GetValue(key Key) []ConstrainedValue { } func (d *MemoryClient) getValueLocked(key Key) []ConstrainedValue { + var result []ConstrainedValue for i := len(d.overrides) - 1; i >= 0; i-- { if d.overrides[i].valid && d.overrides[i].key == key { v := d.overrides[i].value - if value, ok := v.([]ConstrainedValue); ok { - return value + if cvs, ok := v.([]ConstrainedValue); ok { + result = append(result, cvs...) + } else { + result = append(result, ConstrainedValue{Value: v}) + } + if !d.overrides[i].mergeable { + // Non-mergeable: take this value and stop. + return result } - return []ConstrainedValue{{Value: v}} } } - return nil + return result } func (d *MemoryClient) OverrideSetting(setting GenericSetting, value any) (cleanup func()) { @@ -49,12 +56,29 @@ func (d *MemoryClient) OverrideSetting(setting GenericSetting, value any) (clean } func (d *MemoryClient) OverrideValue(key Key, value any) (cleanup func()) { + return d.overrideValue(key, value, false) +} + +// PartialOverrideSetting is like OverrideSetting but marks the override as mergeable. +// Mergeable overrides accumulate with other mergeable overrides for the same key, +// rather than shadowing them. This is used for namespace-constrained overrides on +// shared test clusters, where parallel tests need their overrides to coexist. +func (d *MemoryClient) PartialOverrideSetting(setting GenericSetting, value any) (cleanup func()) { + return d.PartialOverrideValue(setting.Key(), value) +} + +// PartialOverrideValue is like OverrideValue but marks the override as mergeable. +func (d *MemoryClient) PartialOverrideValue(key Key, value any) (cleanup func()) { + return d.overrideValue(key, value, true) +} + +func (d *MemoryClient) overrideValue(key Key, value any, mergeable bool) (cleanup func()) { d.lock.Lock() var idx atomic.Int64 idx.Store(int64(len(d.overrides))) - d.overrides = append(d.overrides, kvpair{valid: true, key: key, value: value}) + d.overrides = append(d.overrides, kvpair{valid: true, mergeable: mergeable, key: key, value: value}) newValue := d.getValueLocked(key) changed := map[Key][]ConstrainedValue{key: newValue} diff --git a/common/dynamicconfig/memory_client_test.go b/common/dynamicconfig/memory_client_test.go index 6c19dfc9e5a..04d0d59df70 100644 --- a/common/dynamicconfig/memory_client_test.go +++ b/common/dynamicconfig/memory_client_test.go @@ -42,6 +42,85 @@ func TestMemoryClient(t *testing.T) { assert.Nil(t, c.GetValue(k)) } +func TestMemoryClientPartialOverride(t *testing.T) { + c := dynamicconfig.NewMemoryClient() + k := dynamicconfig.MakeKey("key") + + nsA := dynamicconfig.Constraints{Namespace: "ns-a"} + nsB := dynamicconfig.Constraints{Namespace: "ns-b"} + + // Two partial overrides with different namespace constraints coexist. + removeA := c.PartialOverrideValue(k, []dynamicconfig.ConstrainedValue{{Constraints: nsA, Value: 1}}) + removeB := c.PartialOverrideValue(k, []dynamicconfig.ConstrainedValue{{Constraints: nsB, Value: 13}}) + + // Both are visible, most recent first. + assert.Equal(t, []dynamicconfig.ConstrainedValue{ + {Constraints: nsB, Value: 13}, + {Constraints: nsA, Value: 1}, + }, c.GetValue(k)) + + // Removing one leaves the other intact. + removeB() + assert.Equal(t, []dynamicconfig.ConstrainedValue{ + {Constraints: nsA, Value: 1}, + }, c.GetValue(k)) + + removeA() + assert.Nil(t, c.GetValue(k)) + + // A non-mergeable override stops the scan — partial overrides below it are invisible. + removePartial := c.PartialOverrideValue(k, []dynamicconfig.ConstrainedValue{{Constraints: nsA, Value: 1}}) + removeFull := c.OverrideValue(k, 99) + removePartial2 := c.PartialOverrideValue(k, []dynamicconfig.ConstrainedValue{{Constraints: nsB, Value: 13}}) + + // Scan from end: partial nsB (merge, continue) → full 99 (take, stop). Partial nsA is below the full override. + assert.Equal(t, []dynamicconfig.ConstrainedValue{ + {Constraints: nsB, Value: 13}, + {Value: 99}, + }, c.GetValue(k)) + + removePartial2() + removeFull() + removePartial() + assert.Nil(t, c.GetValue(k)) +} + +func TestMemoryClientPartialOverrideNonStackRemoval(t *testing.T) { + c := dynamicconfig.NewMemoryClient() + k := dynamicconfig.MakeKey("key") + + nsA := dynamicconfig.Constraints{Namespace: "ns-a"} + nsB := dynamicconfig.Constraints{Namespace: "ns-b"} + nsC := dynamicconfig.Constraints{Namespace: "ns-c"} + + // Three partial overrides. + removeA := c.PartialOverrideValue(k, []dynamicconfig.ConstrainedValue{{Constraints: nsA, Value: 1}}) + removeB := c.PartialOverrideValue(k, []dynamicconfig.ConstrainedValue{{Constraints: nsB, Value: 2}}) + removeC := c.PartialOverrideValue(k, []dynamicconfig.ConstrainedValue{{Constraints: nsC, Value: 3}}) + + assert.Equal(t, []dynamicconfig.ConstrainedValue{ + {Constraints: nsC, Value: 3}, + {Constraints: nsB, Value: 2}, + {Constraints: nsA, Value: 1}, + }, c.GetValue(k)) + + // Remove the middle one (non-stack order). The other two must survive. + removeB() + assert.Equal(t, []dynamicconfig.ConstrainedValue{ + {Constraints: nsC, Value: 3}, + {Constraints: nsA, Value: 1}, + }, c.GetValue(k)) + + // Remove the first one (still non-stack). Only C remains. + removeA() + assert.Equal(t, []dynamicconfig.ConstrainedValue{ + {Constraints: nsC, Value: 3}, + }, c.GetValue(k)) + + removeC() + assert.Nil(t, c.GetValue(k)) +} + func TestMemoryClientSubscriptions(t *testing.T) { c := dynamicconfig.NewMemoryClient() k := dynamicconfig.MakeKey("key") diff --git a/common/dynamicconfig/setting_gen.go b/common/dynamicconfig/setting_gen.go index 3f01ae6ecc0..297bfb4a9bc 100644 --- a/common/dynamicconfig/setting_gen.go +++ b/common/dynamicconfig/setting_gen.go @@ -19,6 +19,7 @@ const ( PrecedenceShardID PrecedenceTaskType PrecedenceDestination + PrecedenceChasmTaskType ) type GlobalBoolSetting = GlobalTypedSetting[bool] @@ -140,6 +141,23 @@ func GetBoolPropertyFnFilteredByDestination(value bool) BoolPropertyFnWithDestin return GetTypedPropertyFnFilteredByDestination(value) } +type ChasmTaskTypeBoolSetting = ChasmTaskTypeTypedSetting[bool] +type ChasmTaskTypeBoolConstrainedDefaultSetting = ChasmTaskTypeTypedConstrainedDefaultSetting[bool] + +func NewChasmTaskTypeBoolSetting(key string, def bool, description string) ChasmTaskTypeBoolSetting { + return NewChasmTaskTypeTypedSettingWithConverter[bool](key, convertBool, def, description) +} + +func NewChasmTaskTypeBoolSettingWithConstrainedDefault(key string, cdef []TypedConstrainedValue[bool], description string) ChasmTaskTypeBoolConstrainedDefaultSetting { + return NewChasmTaskTypeTypedSettingWithConstrainedDefault[bool](key, convertBool, cdef, description) +} + +type BoolPropertyFnWithChasmTaskTypeFilter = TypedPropertyFnWithChasmTaskTypeFilter[bool] + +func GetBoolPropertyFnFilteredByChasmTaskType(value bool) BoolPropertyFnWithChasmTaskTypeFilter { + return GetTypedPropertyFnFilteredByChasmTaskType(value) +} + type GlobalIntSetting = GlobalTypedSetting[int] type GlobalIntConstrainedDefaultSetting = GlobalTypedConstrainedDefaultSetting[int] @@ -259,6 +277,23 @@ func GetIntPropertyFnFilteredByDestination(value int) IntPropertyFnWithDestinati return GetTypedPropertyFnFilteredByDestination(value) } +type ChasmTaskTypeIntSetting = ChasmTaskTypeTypedSetting[int] +type ChasmTaskTypeIntConstrainedDefaultSetting = ChasmTaskTypeTypedConstrainedDefaultSetting[int] + +func NewChasmTaskTypeIntSetting(key string, def int, description string) ChasmTaskTypeIntSetting { + return NewChasmTaskTypeTypedSettingWithConverter[int](key, convertInt, def, description) +} + +func NewChasmTaskTypeIntSettingWithConstrainedDefault(key string, cdef []TypedConstrainedValue[int], description string) ChasmTaskTypeIntConstrainedDefaultSetting { + return NewChasmTaskTypeTypedSettingWithConstrainedDefault[int](key, convertInt, cdef, description) +} + +type IntPropertyFnWithChasmTaskTypeFilter = TypedPropertyFnWithChasmTaskTypeFilter[int] + +func GetIntPropertyFnFilteredByChasmTaskType(value int) IntPropertyFnWithChasmTaskTypeFilter { + return GetTypedPropertyFnFilteredByChasmTaskType(value) +} + type GlobalFloatSetting = GlobalTypedSetting[float64] type GlobalFloatConstrainedDefaultSetting = GlobalTypedConstrainedDefaultSetting[float64] @@ -378,6 +413,23 @@ func GetFloatPropertyFnFilteredByDestination(value float64) FloatPropertyFnWithD return GetTypedPropertyFnFilteredByDestination(value) } +type ChasmTaskTypeFloatSetting = ChasmTaskTypeTypedSetting[float64] +type ChasmTaskTypeFloatConstrainedDefaultSetting = ChasmTaskTypeTypedConstrainedDefaultSetting[float64] + +func NewChasmTaskTypeFloatSetting(key string, def float64, description string) ChasmTaskTypeFloatSetting { + return NewChasmTaskTypeTypedSettingWithConverter[float64](key, convertFloat, def, description) +} + +func NewChasmTaskTypeFloatSettingWithConstrainedDefault(key string, cdef []TypedConstrainedValue[float64], description string) ChasmTaskTypeFloatConstrainedDefaultSetting { + return NewChasmTaskTypeTypedSettingWithConstrainedDefault[float64](key, convertFloat, cdef, description) +} + +type FloatPropertyFnWithChasmTaskTypeFilter = TypedPropertyFnWithChasmTaskTypeFilter[float64] + +func GetFloatPropertyFnFilteredByChasmTaskType(value float64) FloatPropertyFnWithChasmTaskTypeFilter { + return GetTypedPropertyFnFilteredByChasmTaskType(value) +} + type GlobalStringSetting = GlobalTypedSetting[string] type GlobalStringConstrainedDefaultSetting = GlobalTypedConstrainedDefaultSetting[string] @@ -497,6 +549,23 @@ func GetStringPropertyFnFilteredByDestination(value string) StringPropertyFnWith return GetTypedPropertyFnFilteredByDestination(value) } +type ChasmTaskTypeStringSetting = ChasmTaskTypeTypedSetting[string] +type ChasmTaskTypeStringConstrainedDefaultSetting = ChasmTaskTypeTypedConstrainedDefaultSetting[string] + +func NewChasmTaskTypeStringSetting(key string, def string, description string) ChasmTaskTypeStringSetting { + return NewChasmTaskTypeTypedSettingWithConverter[string](key, convertString, def, description) +} + +func NewChasmTaskTypeStringSettingWithConstrainedDefault(key string, cdef []TypedConstrainedValue[string], description string) ChasmTaskTypeStringConstrainedDefaultSetting { + return NewChasmTaskTypeTypedSettingWithConstrainedDefault[string](key, convertString, cdef, description) +} + +type StringPropertyFnWithChasmTaskTypeFilter = TypedPropertyFnWithChasmTaskTypeFilter[string] + +func GetStringPropertyFnFilteredByChasmTaskType(value string) StringPropertyFnWithChasmTaskTypeFilter { + return GetTypedPropertyFnFilteredByChasmTaskType(value) +} + type GlobalDurationSetting = GlobalTypedSetting[time.Duration] type GlobalDurationConstrainedDefaultSetting = GlobalTypedConstrainedDefaultSetting[time.Duration] @@ -616,6 +685,23 @@ func GetDurationPropertyFnFilteredByDestination(value time.Duration) DurationPro return GetTypedPropertyFnFilteredByDestination(value) } +type ChasmTaskTypeDurationSetting = ChasmTaskTypeTypedSetting[time.Duration] +type ChasmTaskTypeDurationConstrainedDefaultSetting = ChasmTaskTypeTypedConstrainedDefaultSetting[time.Duration] + +func NewChasmTaskTypeDurationSetting(key string, def time.Duration, description string) ChasmTaskTypeDurationSetting { + return NewChasmTaskTypeTypedSettingWithConverter[time.Duration](key, convertDuration, def, description) +} + +func NewChasmTaskTypeDurationSettingWithConstrainedDefault(key string, cdef []TypedConstrainedValue[time.Duration], description string) ChasmTaskTypeDurationConstrainedDefaultSetting { + return NewChasmTaskTypeTypedSettingWithConstrainedDefault[time.Duration](key, convertDuration, cdef, description) +} + +type DurationPropertyFnWithChasmTaskTypeFilter = TypedPropertyFnWithChasmTaskTypeFilter[time.Duration] + +func GetDurationPropertyFnFilteredByChasmTaskType(value time.Duration) DurationPropertyFnWithChasmTaskTypeFilter { + return GetTypedPropertyFnFilteredByChasmTaskType(value) +} + type GlobalMapSetting = GlobalTypedSetting[map[string]any] type GlobalMapConstrainedDefaultSetting = GlobalTypedConstrainedDefaultSetting[map[string]any] @@ -735,6 +821,23 @@ func GetMapPropertyFnFilteredByDestination(value map[string]any) MapPropertyFnWi return GetTypedPropertyFnFilteredByDestination(value) } +type ChasmTaskTypeMapSetting = ChasmTaskTypeTypedSetting[map[string]any] +type ChasmTaskTypeMapConstrainedDefaultSetting = ChasmTaskTypeTypedConstrainedDefaultSetting[map[string]any] + +func NewChasmTaskTypeMapSetting(key string, def map[string]any, description string) ChasmTaskTypeMapSetting { + return NewChasmTaskTypeTypedSettingWithConverter[map[string]any](key, convertMap, def, description) +} + +func NewChasmTaskTypeMapSettingWithConstrainedDefault(key string, cdef []TypedConstrainedValue[map[string]any], description string) ChasmTaskTypeMapConstrainedDefaultSetting { + return NewChasmTaskTypeTypedSettingWithConstrainedDefault[map[string]any](key, convertMap, cdef, description) +} + +type MapPropertyFnWithChasmTaskTypeFilter = TypedPropertyFnWithChasmTaskTypeFilter[map[string]any] + +func GetMapPropertyFnFilteredByChasmTaskType(value map[string]any) MapPropertyFnWithChasmTaskTypeFilter { + return GetTypedPropertyFnFilteredByChasmTaskType(value) +} + type GlobalTypedSetting[T any] setting[T, func()] type GlobalTypedConstrainedDefaultSetting[T any] constrainedDefaultSetting[T, func()] @@ -1731,3 +1834,139 @@ func GetTypedPropertyFnFilteredByDestination[T any](value T) TypedPropertyFnWith } } +type ChasmTaskTypeTypedSetting[T any] setting[T, func(chasmTaskType string)] +type ChasmTaskTypeTypedConstrainedDefaultSetting[T any] constrainedDefaultSetting[T, func(chasmTaskType string)] + +// NewChasmTaskTypeTypedSetting creates a setting that uses mapstructure to handle complex structured +// values. The value from dynamic config will be _merged_ over a deep copy of 'def'. Be very careful +// when using non-empty maps or slices as defaults, the result may not be what you want. +func NewChasmTaskTypeTypedSetting[T any](key string, def T, description string) ChasmTaskTypeTypedSetting[T] { + // Warn on any shared structure used with ConvertStructure, even though we handle it by deep copying. + warnDefaultSharedStructure(key, def) + // If even deep copy won't even work, we should panic early. Do that by calling deep copy once here. + _ = deepCopyForMapstructure(def) + + s := ChasmTaskTypeTypedSetting[T]{ + key: MakeKey(key), + def: def, + convert: ConvertStructure[T](def), + description: description, + } + register(s) + return s +} + +// NewChasmTaskTypeTypedSettingWithConverter creates a setting with a custom converter function. +func NewChasmTaskTypeTypedSettingWithConverter[T any](key string, convert func(any) (T, error), def T, description string) ChasmTaskTypeTypedSetting[T] { + s := ChasmTaskTypeTypedSetting[T]{ + key: MakeKey(key), + def: def, + convert: convert, + description: description, + } + register(s) + return s +} + +// NewChasmTaskTypeTypedSettingWithConstrainedDefault creates a setting with a compound default value. +func NewChasmTaskTypeTypedSettingWithConstrainedDefault[T any](key string, convert func(any) (T, error), cdef []TypedConstrainedValue[T], description string) ChasmTaskTypeTypedConstrainedDefaultSetting[T] { + s := ChasmTaskTypeTypedConstrainedDefaultSetting[T]{ + key: MakeKey(key), + cdef: cdef, + convert: convert, + description: description, + } + register(s) + return s +} + +func (s ChasmTaskTypeTypedSetting[T]) Key() Key { return s.key } +func (s ChasmTaskTypeTypedSetting[T]) Precedence() Precedence { return PrecedenceChasmTaskType } +func (s ChasmTaskTypeTypedSetting[T]) Validate(v any) error { + _, err := s.convert(v) + return err +} + +func (s ChasmTaskTypeTypedConstrainedDefaultSetting[T]) Key() Key { return s.key } +func (s ChasmTaskTypeTypedConstrainedDefaultSetting[T]) Precedence() Precedence { return PrecedenceChasmTaskType } +func (s ChasmTaskTypeTypedConstrainedDefaultSetting[T]) Validate(v any) error { + _, err := s.convert(v) + return err +} + +func (s ChasmTaskTypeTypedSetting[T]) WithDefault(v T) ChasmTaskTypeTypedSetting[T] { + newS := s + newS.def = v + return newS +} + +type TypedPropertyFnWithChasmTaskTypeFilter[T any] func(chasmTaskType string) T + +func (s ChasmTaskTypeTypedSetting[T]) Get(c *Collection) TypedPropertyFnWithChasmTaskTypeFilter[T] { + return func(chasmTaskType string) T { + prec := []Constraints{{ChasmTaskType: chasmTaskType}, {}} + return matchAndConvert( + c, + s.key, + s.def, + s.convert, + prec, + ) + } +} + +func (s ChasmTaskTypeTypedConstrainedDefaultSetting[T]) Get(c *Collection) TypedPropertyFnWithChasmTaskTypeFilter[T] { + return func(chasmTaskType string) T { + prec := []Constraints{{ChasmTaskType: chasmTaskType}, {}} + return matchAndConvertWithConstrainedDefault( + c, + s.key, + s.cdef, + s.convert, + prec, + ) + } +} + +type TypedSubscribableWithChasmTaskTypeFilter[T any] func(chasmTaskType string, callback func(T)) (v T, cancel func()) + +func (s ChasmTaskTypeTypedSetting[T]) Subscribe(c *Collection) TypedSubscribableWithChasmTaskTypeFilter[T] { + return func(chasmTaskType string, callback func(T)) (T, func()) { + prec := []Constraints{{ChasmTaskType: chasmTaskType}, {}} + return subscribe(c, s.key, s.def, s.convert, prec, callback) + } +} + +func (s ChasmTaskTypeTypedSetting[T]) dispatchUpdate(c *Collection, sub any, cvs []ConstrainedValue) { + dispatchUpdate( + c, + s.key, + s.convert, + sub.(*subscription[T]), + cvs, + ) +} + +func (s ChasmTaskTypeTypedConstrainedDefaultSetting[T]) Subscribe(c *Collection) TypedSubscribableWithChasmTaskTypeFilter[T] { + return func(chasmTaskType string, callback func(T)) (T, func()) { + prec := []Constraints{{ChasmTaskType: chasmTaskType}, {}} + return subscribeWithConstrainedDefault(c, s.key, s.cdef, s.convert, prec, callback) + } +} + +func (s ChasmTaskTypeTypedConstrainedDefaultSetting[T]) dispatchUpdate(c *Collection, sub any, cvs []ConstrainedValue) { + dispatchUpdateWithConstrainedDefault( + c, + s.key, + s.convert, + sub.(*subscription[T]), + cvs, + ) +} + +func GetTypedPropertyFnFilteredByChasmTaskType[T any](value T) TypedPropertyFnWithChasmTaskTypeFilter[T] { + return func(chasmTaskType string) T { + return value + } +} + diff --git a/common/dynamicconfig/yaml_loader.go b/common/dynamicconfig/yaml_loader.go index cbc2373a073..15e2c80e5d3 100644 --- a/common/dynamicconfig/yaml_loader.go +++ b/common/dynamicconfig/yaml_loader.go @@ -125,19 +125,19 @@ func (lr *YamlLoader) errorf(format string, args ...any) { lr.error(fmt.Errorf(format, args...)) } -func convertKeyTypeToString(v interface{}) (interface{}, error) { +func convertKeyTypeToString(v any) (any, error) { switch v := v.(type) { - case map[interface{}]interface{}: + case map[any]any: return convertKeyTypeToStringMap(v) - case []interface{}: + case []any: return convertKeyTypeToStringSlice(v) default: return v, nil } } -func convertKeyTypeToStringMap(m map[interface{}]interface{}) (map[string]interface{}, error) { - stringKeyMap := make(map[string]interface{}) +func convertKeyTypeToStringMap(m map[any]any) (map[string]any, error) { + stringKeyMap := make(map[string]any) for key, value := range m { stringKey, ok := key.(string) if !ok { @@ -152,8 +152,8 @@ func convertKeyTypeToStringMap(m map[interface{}]interface{}) (map[string]interf return stringKeyMap, nil } -func convertKeyTypeToStringSlice(s []interface{}) ([]interface{}, error) { - stringKeySlice := make([]interface{}, len(s)) +func convertKeyTypeToStringSlice(s []any) ([]any, error) { + stringKeySlice := make([]any, len(s)) for idx, value := range s { convertedValue, err := convertKeyTypeToString(value) if err != nil { @@ -241,6 +241,13 @@ func convertYamlConstraints(key Key, m map[string]any, precedence Precedence, lr lr.errorf("destination constraint must be string") } validConstraint = precedence == PrecedenceDestination + case "chasmtasktype": + if v, ok := v.(string); ok { + cs.ChasmTaskType = v + } else { + lr.errorf("chasmtasktype constraint must be string") + } + validConstraint = precedence == PrecedenceChasmTaskType default: lr.errorf("unknown constraint type %q", k) } diff --git a/common/finalizer/finalizer.go b/common/finalizer/finalizer.go index 53f465ebfaf..561e88f69f5 100644 --- a/common/finalizer/finalizer.go +++ b/common/finalizer/finalizer.go @@ -2,10 +2,10 @@ package finalizer import ( "context" + "errors" "sync" "time" - "github.com/pkg/errors" cclock "go.temporal.io/server/common/clock" "go.temporal.io/server/common/goro" "go.temporal.io/server/common/log" diff --git a/common/future/future_test.go b/common/future/future_test.go index c53a26fa947..5a1c249cde0 100644 --- a/common/future/future_test.go +++ b/common/future/future_test.go @@ -25,9 +25,9 @@ func BenchmarkFutureAvailable(b *testing.B) { b.ReportAllocs() ctx := context.Background() - futures := make([]*FutureImpl[interface{}], b.N) + futures := make([]*FutureImpl[any], b.N) for n := 0; n < b.N; n++ { - futures[n] = NewFuture[interface{}]() + futures[n] = NewFuture[any]() } b.ResetTimer() @@ -41,7 +41,7 @@ func BenchmarkFutureAvailable(b *testing.B) { func BenchmarkFutureGet(b *testing.B) { b.ReportAllocs() - future := NewFuture[interface{}]() + future := NewFuture[any]() future.Set(nil, nil) ctx := context.Background() for n := 0; n < b.N; n++ { @@ -52,7 +52,7 @@ func BenchmarkFutureGet(b *testing.B) { func BenchmarkFutureReady(b *testing.B) { b.ReportAllocs() - future := NewFuture[interface{}]() + future := NewFuture[any]() future.Set(nil, nil) for n := 0; n < b.N; n++ { _ = future.Ready() @@ -106,7 +106,7 @@ func (s *futureSuite) TestSetGetReady_Parallel() { startWG.Wait() s.future.Set(s.value, s.err) }() - for i := 0; i < numGets; i++ { + for range numGets { go func() { defer endWG.Done() @@ -148,7 +148,7 @@ func (s *futureSuite) TestSetReadyGet_Parallel() { startWG.Wait() s.future.Set(s.value, s.err) }() - for i := 0; i < numGets; i++ { + for range numGets { go func() { defer endWG.Done() diff --git a/common/goro/adaptive_pool.go b/common/goro/adaptive_pool.go index 6ac8c702432..1fbe9e9a999 100644 --- a/common/goro/adaptive_pool.go +++ b/common/goro/adaptive_pool.go @@ -41,7 +41,7 @@ func NewAdaptivePool( ch: make(chan func()), stopCh: make(chan struct{}), } - for i := 0; i < minWorkers; i++ { + for range minWorkers { go p.work() } p.workers.Store(int64(minWorkers)) diff --git a/common/headers/caller_info.go b/common/headers/caller_info.go index 387bdc95144..89be3179be5 100644 --- a/common/headers/caller_info.go +++ b/common/headers/caller_info.go @@ -2,8 +2,6 @@ package headers import ( "context" - - "google.golang.org/grpc/metadata" ) const ( @@ -153,24 +151,6 @@ func SetOrigin( return setIncomingMD(ctx, map[string]string{CallOriginHeaderName: callOrigin}) } -func setIncomingMD( - ctx context.Context, - kv map[string]string, -) context.Context { - mdIncoming, ok := metadata.FromIncomingContext(ctx) - if !ok { - mdIncoming = metadata.MD{} - } - - for k, v := range kv { - if v != "" { - mdIncoming.Set(k, v) - } - } - - return metadata.NewIncomingContext(ctx, mdIncoming) -} - // GetCallerInfo retrieves caller information from the context if exists. Empty value is returned // if any piece of caller information is not specified in the context. func GetCallerInfo( diff --git a/common/headers/headers.go b/common/headers/headers.go index 92ad1818d16..37d0d6bd92d 100644 --- a/common/headers/headers.go +++ b/common/headers/headers.go @@ -4,6 +4,7 @@ import ( "context" "strings" + commonpb "go.temporal.io/api/common/v1" "google.golang.org/grpc/metadata" ) @@ -20,6 +21,9 @@ const ( CallerTypeHeaderName = "caller-type" CallOriginHeaderName = "call-initiation" + PrincipalTypeHeaderName = "temporal-principal-type" + PrincipalNameHeaderName = "temporal-principal-name" + ExperimentHeaderName = "temporal-experiment" ) @@ -33,6 +37,8 @@ var ( CallerNameHeaderName, CallerTypeHeaderName, CallOriginHeaderName, + PrincipalTypeHeaderName, + PrincipalNameHeaderName, } ) @@ -115,3 +121,47 @@ func IsExperimentRequested(ctx context.Context, experiment string) bool { return false } + +// StripPrincipal removes principal headers from incoming metadata to prevent +// external callers from spoofing principal identity. +func StripPrincipal(ctx context.Context) context.Context { + mdIncoming, ok := metadata.FromIncomingContext(ctx) + if !ok { + return ctx + } + mdIncoming.Delete(PrincipalTypeHeaderName) + mdIncoming.Delete(PrincipalNameHeaderName) + return metadata.NewIncomingContext(ctx, mdIncoming) +} + +// SetPrincipal sets the principal type and name headers in the incoming metadata. +func SetPrincipal(ctx context.Context, principal *commonpb.Principal) context.Context { + return setIncomingMD(ctx, map[string]string{ + PrincipalTypeHeaderName: principal.GetType(), + PrincipalNameHeaderName: principal.GetName(), + }) +} + +// GetPrincipal retrieves the principal from the context headers. Returns nil if principal is not set. +func GetPrincipal(ctx context.Context) *commonpb.Principal { + values := GetValues(ctx, PrincipalTypeHeaderName, PrincipalNameHeaderName) + if values[0] == "" && values[1] == "" { + return nil + } + return &commonpb.Principal{Type: values[0], Name: values[1]} +} + +// setIncomingMD sets the key-value pairs in the incoming metadata. +// Empty values are ignored. +func setIncomingMD(ctx context.Context, kv map[string]string) context.Context { + mdIncoming, ok := metadata.FromIncomingContext(ctx) + if !ok { + mdIncoming = metadata.MD{} + } + for k, v := range kv { + if v != "" { + mdIncoming.Set(k, v) + } + } + return metadata.NewIncomingContext(ctx, mdIncoming) +} diff --git a/common/health/check_types.go b/common/health/check_types.go new file mode 100644 index 00000000000..d1409b1c114 --- /dev/null +++ b/common/health/check_types.go @@ -0,0 +1,18 @@ +package health + +// HealthCheck type constants — short, machine-readable identifiers for each check. +// Used in HealthCheck.CheckType for programmatic matching/grouping. +// Human-readable details go in HealthCheck.Message (dynamically populated with values). +// +// We use string constants instead of a proto enum for flexibility: new check types +// can be added without proto changes, and the message field can describe exactly +// what went wrong with actual values (e.g. "RPC latency 850.00ms exceeded 500.00ms threshold"). +const ( + CheckTypeGRPCHealth = "grpc_health" + CheckTypeRPCLatency = "rpc_latency" + CheckTypeRPCErrorRatio = "rpc_error_ratio" + CheckTypePersistenceLatency = "persistence_latency" + CheckTypePersistenceErrRatio = "persistence_error_ratio" + CheckTypeHostAvailability = "host_availability" + CheckTypeTaskQueueBacklog = "task_queue_backlog" +) diff --git a/common/locks/condition_variable_test.go b/common/locks/condition_variable_test.go index 99d3a3811f5..3f4e35e4e03 100644 --- a/common/locks/condition_variable_test.go +++ b/common/locks/condition_variable_test.go @@ -139,7 +139,7 @@ func (s *conditionVariableSuite) TestBroadcast() { broadcastWaitGroup.Done() s.cv.Wait(nil) } - for i := 0; i < waitThreads; i++ { + for range waitThreads { go waitFn() } @@ -210,10 +210,10 @@ func (s *conditionVariableSuite) TestCase_ProducerConsumer() { } } - for i := 0; i < numConsumer; i++ { + for range numConsumer { go consumerFn() } - for i := 0; i < numProducer; i++ { + for range numProducer { go produceFn() } diff --git a/common/locks/id_mutex.go b/common/locks/id_mutex.go index 5318a8e8a6c..fc1e1304062 100644 --- a/common/locks/id_mutex.go +++ b/common/locks/id_mutex.go @@ -6,12 +6,12 @@ import ( type ( // HashFunc represents a hash function for string - HashFunc func(interface{}) uint32 + HashFunc func(any) uint32 // IDMutex is an interface which can lock on specific comparable identifier IDMutex interface { - LockID(identifier interface{}) - UnlockID(identifier interface{}) + LockID(identifier any) + UnlockID(identifier any) } // idMutexShardImpl is the implementation of IDMutex shard @@ -24,7 +24,7 @@ type ( // idMutexShardImpl is the implementation of IDMutex shard idMutexShardImpl struct { sync.Mutex - mutexInfos map[interface{}]*mutexInfo + mutexInfos map[any]*mutexInfo } mutexInfo struct { @@ -45,9 +45,9 @@ func NewIDMutex(numShard uint32, hashFn HashFunc) IDMutex { hashFn: hashFn, shards: make(map[uint32]*idMutexShardImpl), } - for i := uint32(0); i < numShard; i++ { + for i := range numShard { impl.shards[i] = &idMutexShardImpl{ - mutexInfos: make(map[interface{}]*mutexInfo), + mutexInfos: make(map[any]*mutexInfo), } } @@ -61,7 +61,7 @@ func newMutexInfo() *mutexInfo { } // LockID lock by specific identifier -func (idMutex *idMutexImpl) LockID(identifier interface{}) { +func (idMutex *idMutexImpl) LockID(identifier any) { shard := idMutex.shards[idMutex.getShardIndex(identifier)] shard.Lock() @@ -80,7 +80,7 @@ func (idMutex *idMutexImpl) LockID(identifier interface{}) { } // UnlockID unlock by specific identifier -func (idMutex *idMutexImpl) UnlockID(identifier interface{}) { +func (idMutex *idMutexImpl) UnlockID(identifier any) { shard := idMutex.shards[idMutex.getShardIndex(identifier)] shard.Lock() @@ -97,6 +97,6 @@ func (idMutex *idMutexImpl) UnlockID(identifier interface{}) { } } -func (idMutex *idMutexImpl) getShardIndex(key interface{}) uint32 { +func (idMutex *idMutexImpl) getShardIndex(key any) uint32 { return idMutex.hashFn(key) % idMutex.numShard } diff --git a/common/locks/id_mutex_test.go b/common/locks/id_mutex_test.go index 57ecb76cd9f..ec1c70ae721 100644 --- a/common/locks/id_mutex_test.go +++ b/common/locks/id_mutex_test.go @@ -34,7 +34,7 @@ func BenchmarkGolangMutex(b *testing.B) { func BenchmarkIDMutex_String(b *testing.B) { identifier := "random string" - idLock := NewIDMutex(32, func(key interface{}) uint32 { + idLock := NewIDMutex(32, func(key any) uint32 { id, ok := key.(string) if !ok { return 0 @@ -54,7 +54,7 @@ func BenchmarkIDMutex_Struct(b *testing.B) { B: "some random B", C: "some random C", } - idLock := NewIDMutex(32, func(key interface{}) uint32 { + idLock := NewIDMutex(32, func(key any) uint32 { id, ok := key.(testIdentifier) if !ok { return 0 @@ -70,7 +70,7 @@ func BenchmarkIDMutex_Struct(b *testing.B) { func BenchmarkIDMutex_StringConcurrent(b *testing.B) { identifier := "random string" - idLock := NewIDMutex(32, func(key interface{}) uint32 { + idLock := NewIDMutex(32, func(key any) uint32 { id, ok := key.(string) if !ok { return 0 @@ -98,7 +98,7 @@ func BenchmarkIDMutex_StringConcurrent(b *testing.B) { waitGroupEnd.Done() } - for i := 0; i < iteration; i++ { + for range iteration { go fn() } waitGroupBegin.Done() @@ -120,7 +120,7 @@ func (s *idMutexSuite) TearDownSuite() { func (s *idMutexSuite) SetupTest() { s.numShard = 32 - s.idMutex = NewIDMutex(s.numShard, func(key interface{}) uint32 { + s.idMutex = NewIDMutex(s.numShard, func(key any) uint32 { id, ok := key.(string) if !ok { return 0 @@ -193,7 +193,7 @@ func (s *idMutexSuite) TestConcurrentAccess() { waitGroupEnd.Done() } - for i := 0; i < iteration; i++ { + for range iteration { go fn() } waitGroupBegin.Done() diff --git a/common/locks/priority_mutex_test.go b/common/locks/priority_mutex_test.go index 77150efcb7f..d31d75b9c8a 100644 --- a/common/locks/priority_mutex_test.go +++ b/common/locks/priority_mutex_test.go @@ -165,7 +165,7 @@ func (s *priorityMutexSuite) TestLock_Mixed() { s.lock.UnlockHigh() endWaitGroup.Done() } - for i := 0; i < concurrency; i++ { + for range concurrency { go lowFn() go highFn() } diff --git a/common/locks/priority_semaphore_test.go b/common/locks/priority_semaphore_test.go index 0b1ccf4740a..2a572a183a6 100644 --- a/common/locks/priority_semaphore_test.go +++ b/common/locks/priority_semaphore_test.go @@ -154,7 +154,7 @@ func (s *prioritySemaphoreSuite) Test_AllThreadsAreWokenUp() { wg := sync.WaitGroup{} wg.Add(10) - for i := 0; i < 5; i++ { + for range 5 { go func() { // nolint:testifylint // Must use assert.Assertions instead of require.Assertions here because this is running in a separate goroutine. diff --git a/common/log/panic_test.go b/common/log/panic_test.go index a262814db6e..5c7f7fb5e26 100644 --- a/common/log/panic_test.go +++ b/common/log/panic_test.go @@ -17,13 +17,13 @@ func TestCapturePanic(t *testing.T) { assert.Equal(t, "error: bar", barErr.Error()) } -func testCapture(panicObj interface{}) (retErr error) { +func testCapture(panicObj any) (retErr error) { defer CapturePanic(NewNoopLogger(), &retErr) testPanic(panicObj) return nil } -func testPanic(panicObj interface{}) { +func testPanic(panicObj any) { panic(panicObj) } diff --git a/common/log/sdk_logger.go b/common/log/sdk_logger.go index 51f6dd46c20..d2a80d1d0a7 100644 --- a/common/log/sdk_logger.go +++ b/common/log/sdk_logger.go @@ -28,7 +28,7 @@ func NewSdkLogger(logger Logger) *SdkLogger { } } -func (l *SdkLogger) tags(keyvals []interface{}) []tag.Tag { +func (l *SdkLogger) tags(keyvals []any) []tag.Tag { var tags []tag.Tag for i := 0; i < len(keyvals); i++ { if t, keyvalIsTag := keyvals[i].(tag.Tag); keyvalIsTag { @@ -40,7 +40,7 @@ func (l *SdkLogger) tags(keyvals []interface{}) []tag.Tag { if !keyIsString { key = fmt.Sprintf("%v", keyvals[i]) } - var val interface{} + var val any if i+1 == len(keyvals) { val = noValue } else { @@ -54,23 +54,23 @@ func (l *SdkLogger) tags(keyvals []interface{}) []tag.Tag { return tags } -func (l *SdkLogger) Debug(msg string, keyvals ...interface{}) { +func (l *SdkLogger) Debug(msg string, keyvals ...any) { l.logger.Debug(msg, l.tags(keyvals)...) } -func (l *SdkLogger) Info(msg string, keyvals ...interface{}) { +func (l *SdkLogger) Info(msg string, keyvals ...any) { l.logger.Info(msg, l.tags(keyvals)...) } -func (l *SdkLogger) Warn(msg string, keyvals ...interface{}) { +func (l *SdkLogger) Warn(msg string, keyvals ...any) { l.logger.Warn(msg, l.tags(keyvals)...) } -func (l *SdkLogger) Error(msg string, keyvals ...interface{}) { +func (l *SdkLogger) Error(msg string, keyvals ...any) { l.logger.Error(msg, l.tags(keyvals)...) } -func (l *SdkLogger) With(keyvals ...interface{}) log.Logger { +func (l *SdkLogger) With(keyvals ...any) log.Logger { return NewSdkLogger( With(l.logger, l.tags(keyvals)...)) } diff --git a/common/log/tag/interface.go b/common/log/tag/interface.go index 000741fadc3..39f71bd2083 100644 --- a/common/log/tag/interface.go +++ b/common/log/tag/interface.go @@ -4,6 +4,6 @@ type ( // Implement Tag interface to supply custom tags to Logger interface implementation. Tag interface { Key() string - Value() interface{} + Value() any } ) diff --git a/common/log/tag/tags.go b/common/log/tag/tags.go index e716e9e9680..7b3a8a1c780 100644 --- a/common/log/tag/tags.go +++ b/common/log/tag/tags.go @@ -147,11 +147,6 @@ func WorkflowBinaryChecksum(cs string) ZapTag { return NewStringTag("wf-binary-checksum", cs) } -// WorkflowActivityID returns tag for WorkflowActivityID -func WorkflowActivityID(id string) ZapTag { - return NewStringTag("wf-activity-id", id) -} - // WorkflowTimerID returns tag for WorkflowTimerID func WorkflowTimerID(id string) ZapTag { return NewStringTag("wf-timer-id", id) @@ -463,22 +458,22 @@ func Name(k string) ZapTag { } // Value returns tag for Value -func Value(v interface{}) ZapTag { +func Value(v any) ZapTag { return NewAnyTag("value", v) } // ValueType returns tag for ValueType -func ValueType(v interface{}) ZapTag { +func ValueType(v any) ZapTag { return NewStringTag("value-type", fmt.Sprintf("%T", v)) } // DefaultValue returns tag for DefaultValue -func DefaultValue(v interface{}) ZapTag { +func DefaultValue(v any) ZapTag { return NewAnyTag("default-value", v) } // IgnoredValue returns tag for IgnoredValue -func IgnoredValue(v interface{}) ZapTag { +func IgnoredValue(v any) ZapTag { return NewAnyTag("ignored-value", v) } @@ -547,7 +542,7 @@ func CertThumbprint(thumbprint string) ZapTag { return NewStringTag("cert-thumbprint", thumbprint) } -func WorkerComponent(v interface{}) ZapTag { +func WorkerComponent(v any) ZapTag { return NewStringTag("worker-component", fmt.Sprintf("%T", v)) } @@ -562,7 +557,7 @@ func ShardID(shardID int32) ZapTag { } // ShardTime returns tag for ShardTime -func ShardTime(shardTime interface{}) ZapTag { +func ShardTime(shardTime any) ZapTag { return NewAnyTag("shard-time", shardTime) } @@ -602,7 +597,7 @@ func MaxLevel(lv int64) ZapTag { } // ShardQueueAcks returns tag for shard queue ack levels -func ShardQueueAcks(categoryName string, ackLevel interface{}) ZapTag { +func ShardQueueAcks(categoryName string, ackLevel any) ZapTag { return NewAnyTag("shard-"+categoryName+"-queue-acks", ackLevel) } @@ -614,12 +609,12 @@ func QueueReaderID(readerID int64) ZapTag { } // QueueAlert returns tag for queue alert -func QueueAlert(alert interface{}) ZapTag { +func QueueAlert(alert any) ZapTag { return NewAnyTag("queue-alert", alert) } // Task returns tag for Task -func Task(task interface{}) ZapTag { +func Task(task any) ZapTag { return NewAnyTag("queue-task", task) } @@ -629,7 +624,7 @@ func TaskID(taskID int64) ZapTag { } // TaskKey returns tag for TaskKey -func TaskKey(key interface{}) ZapTag { +func TaskKey(key any) ZapTag { return NewAnyTag("queue-task-key", key) } @@ -740,7 +735,7 @@ func ESValue(ESValue []byte) ZapTag { } // ESConfig returns tag for ESConfig -func ESConfig(c interface{}) ZapTag { +func ESConfig(c any) ZapTag { return NewAnyTag("es-config", c) } @@ -795,7 +790,7 @@ func SourceShardID(shardID int32) ZapTag { func TargetShardID(shardID int32) ZapTag { return NewInt32("xdc-target-shard-id", shardID) } -func ReplicationTask(replicationTask interface{}) ZapTag { +func ReplicationTask(replicationTask any) ZapTag { return NewAnyTag("xdc-replication-task", replicationTask) } @@ -913,15 +908,25 @@ func TransportType(transportType string) ZapTag { } // ActivityInfo returns tag for activity info -func ActivityInfo(activityInfo interface{}) ZapTag { +func ActivityInfo(activityInfo any) ZapTag { return NewAnyTag("activity-info", activityInfo) } -// ActivityID returns tag for a standalone activity ID +// ActivityID returns tag for an activity ID func ActivityID(id string) ZapTag { return NewStringTag("activity-id", id) } +// OperationID returns tag for a nexus operation ID +func OperationID(id string) ZapTag { + return NewStringTag("operation-id", id) +} + +// ChasmRunID returns tag for an entity run ID +func ChasmRunID(id string) ZapTag { + return NewStringTag("run-id", id) +} + // ActivitySize returns a tag for a standalone activity size func ActivitySize(activitySize int64) ZapTag { return NewInt64("activity-size", activitySize) @@ -933,7 +938,7 @@ func WorkflowTaskRequestId(s string) ZapTag { } // AckLevel returns tag for ack level -func AckLevel(s interface{}) ZapTag { +func AckLevel(s any) ZapTag { return NewAnyTag("ack-level", s) } diff --git a/common/log/throttle_logger.go b/common/log/throttle_logger.go index b62b6ec0d18..6bf94670163 100644 --- a/common/log/throttle_logger.go +++ b/common/log/throttle_logger.go @@ -5,6 +5,7 @@ import ( "go.temporal.io/server/common/quotas" ) +// extraSkipForThrottleLogger is the number of extra stack trace frames to skip when SkipLogger is used const extraSkipForThrottleLogger = 3 type throttledLogger struct { @@ -18,7 +19,7 @@ var _ Logger = (*throttledLogger)(nil) // log messages being emitted. The underlying implementation uses a token bucket // rate limiter and stops emitting logs once the bucket runs out of tokens // -// Fatal/Panic logs are always emitted without any throttling +// Fatal/Panic/DPanic logs are always emitted without any throttling func NewThrottledLogger(logger Logger, rps quotas.RateFn) *throttledLogger { if sl, ok := logger.(SkipLogger); ok { logger = sl.Skip(extraSkipForThrottleLogger) @@ -57,19 +58,23 @@ func (tl *throttledLogger) Error(msg string, tags ...tag.Tag) { } func (tl *throttledLogger) DPanic(msg string, tags ...tag.Tag) { - tl.rateLimit(func() { + // DPanic logs are always emitted without any throttling. Call bypassRateLimit + // to maintain the same number of stack trace frames (see extraSkipForThrottleLogger) + tl.bypassRateLimit(func() { tl.logger.DPanic(msg, tags...) }) } func (tl *throttledLogger) Panic(msg string, tags ...tag.Tag) { - tl.rateLimit(func() { + // Panic logs are always emitted without any throttling + tl.bypassRateLimit(func() { tl.logger.Panic(msg, tags...) }) } func (tl *throttledLogger) Fatal(msg string, tags ...tag.Tag) { - tl.rateLimit(func() { + // Fatal logs are always emitted without any throttling + tl.bypassRateLimit(func() { tl.logger.Fatal(msg, tags...) }) } @@ -84,7 +89,13 @@ func (tl *throttledLogger) With(tags ...tag.Tag) Logger { } func (tl *throttledLogger) rateLimit(f func()) { - if ok := tl.limiter.Allow(); ok { + if tl.limiter.Allow() { f() } } + +// bypassRateLimit bypasses the rate limit and calls the function directly. It exists to maintain the same +// number of stack trace frames as when rateLimit is called (see extraSkipForThrottleLogger) +func (tl *throttledLogger) bypassRateLimit(f func()) { + f() +} diff --git a/common/log/zap_logger.go b/common/log/zap_logger.go index 771eea0deff..66e5ec58489 100644 --- a/common/log/zap_logger.go +++ b/common/log/zap_logger.go @@ -17,8 +17,15 @@ const ( // we put a default message when it is empty so that the log can be searchable/filterable defaultMsgForEmpty = "none" // TODO: once `NewTestLogger` has been removed, move these vars into testlogger.TestLogger - TestLogFormatEnvVar = "TEMPORAL_TEST_LOG_FORMAT" // set to "json" for json logs in tests - TestLogLevelEnvVar = "TEMPORAL_TEST_LOG_LEVEL" // set to "debug" for debug level logs in tests + + // Console output (testing.T.Log): + TestLogFormatEnvVar = "TEMPORAL_TEST_LOG_FORMAT" // "console" (default) or "json" + TestLogLevelEnvVar = "TEMPORAL_TEST_LOG_LEVEL" // min level written to console (default: debug) + + // File output (written once per process to a shared file): + TestLogFileEnvVar = "TEMPORAL_TEST_LOG_FILE" // path to log file; empty disables file logging + TestLogFileFormatEnvVar = "TEMPORAL_TEST_LOG_FILE_FORMAT" // "json" (default) or "console" + TestLogFileLevelEnvVar = "TEMPORAL_TEST_LOG_FILE_LEVEL" // min level written to file (default: debug) ) var DefaultZapEncoderConfig = zapcore.EncoderConfig{ diff --git a/common/masker/masker.go b/common/masker/masker.go index ac2730bb2fd..0668d4dd657 100644 --- a/common/masker/masker.go +++ b/common/masker/masker.go @@ -21,7 +21,7 @@ func MaskYaml(yamlStr string, fieldNamesToMask []string) (string, error) { fns[fieldName] = struct{}{} } - var parsedYaml map[string]interface{} + var parsedYaml map[string]any err := yaml.Unmarshal([]byte(yamlStr), &parsedYaml) if err != nil { return yamlStr, err @@ -38,7 +38,7 @@ func MaskYaml(yamlStr string, fieldNamesToMask []string) (string, error) { // MaskStruct replace password values with mask and returns copy of the strct. // Original strct value is not modified. Doesn't go recursively through strct properties. -func MaskStruct(strct interface{}, fieldNamesToMask []string) interface{} { +func MaskStruct(strct any, fieldNamesToMask []string) any { strctV := reflect.ValueOf(strct) if strct == nil || (strctV.Kind() == reflect.Ptr && strctV.IsNil()) { @@ -64,19 +64,19 @@ func MaskStruct(strct interface{}, fieldNamesToMask []string) interface{} { return strctCopyPV.Interface() } -func pointerTo(val interface{}) reflect.Value { +func pointerTo(val any) reflect.Value { valPtr := reflect.New(reflect.TypeOf(val)) valPtr.Elem().Set(reflect.ValueOf(val)) return valPtr } -func maskMap(m map[string]interface{}, fns map[string]struct{}) { +func maskMap(m map[string]any, fns map[string]struct{}) { for key, value := range m { if _, ok := fns[key]; ok { m[key] = passwordMask } - if valueMap, ok := value.(map[string]interface{}); ok { + if valueMap, ok := value.(map[string]any); ok { maskMap(valueMap, fns) } } diff --git a/common/masker/masker_test.go b/common/masker/masker_test.go index f6455209642..d1962ce1433 100644 --- a/common/masker/masker_test.go +++ b/common/masker/masker_test.go @@ -41,7 +41,7 @@ func TestMaskStruct_Nil(t *testing.T) { maskedS1 := MaskStruct(nil, DefaultFieldNames) assert.Nil(maskedS1) - var nilInterface interface{} + var nilInterface any maskedS2 := MaskStruct(nilInterface, DefaultFieldNames) assert.Nil(maskedS2) diff --git a/common/membership/ringpop/factory.go b/common/membership/ringpop/factory.go index d53a3b95878..a0343c898d4 100644 --- a/common/membership/ringpop/factory.go +++ b/common/membership/ringpop/factory.go @@ -110,6 +110,7 @@ func (factory *factory) getMonitor() *monitor { // Empirically, ringpop updates usually propagate in under a second even in relatively large clusters. // 3 seconds is an over-estimate to be safer. maxPropagationTime := dynamicconfig.RingpopApproximateMaxPropagationTime.Get(factory.DC)() + replicaPoints := dynamicconfig.RingpopReplicaPoints.Get(factory.DC)() factory.monitor = newMonitor( factory.ServiceName, @@ -121,6 +122,7 @@ func (factory *factory) getMonitor() *monitor { factory.Config.MaxJoinDuration, maxPropagationTime, factory.getJoinTime(maxPropagationTime), + replicaPoints, ) }) diff --git a/common/membership/ringpop/monitor.go b/common/membership/ringpop/monitor.go index 61c498e350d..1dcaa35cb82 100644 --- a/common/membership/ringpop/monitor.go +++ b/common/membership/ringpop/monitor.go @@ -61,6 +61,7 @@ type monitor struct { propagationTime time.Duration joinTime time.Time rings map[primitives.ServiceName]*serviceResolver + replicaPoints int logger log.Logger metadataManager persistence.ClusterMetadataManager broadcastHostPortResolver func() (string, error) @@ -81,6 +82,7 @@ func newMonitor( maxJoinDuration time.Duration, propagationTime time.Duration, joinTime time.Time, + replicaPoints int, ) *monitor { lifecycleCtx, lifecycleCancel := context.WithCancel(context.Background()) lifecycleCtx = headers.SetCallerInfo( @@ -100,6 +102,7 @@ func newMonitor( services: services, rp: rp, rings: make(map[primitives.ServiceName]*serviceResolver), + replicaPoints: replicaPoints, logger: logger, metadataManager: metadataManager, broadcastHostPortResolver: broadcastHostPortResolver, @@ -110,7 +113,7 @@ func newMonitor( joinTime: joinTime, } for service, port := range services { - rpo.rings[service] = newServiceResolver(service, port, rp, logger) + rpo.rings[service] = newServiceResolver(service, port, rp, replicaPoints, logger) } return rpo } @@ -227,25 +230,6 @@ func (rpo *monitor) WaitUntilInitialized(ctx context.Context) error { return err } -func serviceNameToServiceTypeEnum(name primitives.ServiceName) (persistence.ServiceType, error) { - switch name { - case primitives.AllServices: - return persistence.All, nil - case primitives.FrontendService: - return persistence.Frontend, nil - case primitives.InternalFrontendService: - return persistence.InternalFrontend, nil - case primitives.HistoryService: - return persistence.History, nil - case primitives.MatchingService: - return persistence.Matching, nil - case primitives.WorkerService: - return persistence.Worker, nil - default: - return persistence.All, fmt.Errorf("unable to parse servicename '%s'", name) - } -} - func (rpo *monitor) upsertMyMembership( ctx context.Context, request *persistence.UpsertClusterMembershipRequest, @@ -348,7 +332,6 @@ func (rpo *monitor) fetchCurrentBootstrapHostports() ([]string, error) { PageSize: pageSize, NextPageToken: nextPageToken, }) - if err != nil { return nil, err } @@ -381,7 +364,6 @@ func (rpo *monitor) startHeartbeatUpsertLoop(request *persistence.UpsertClusterM default: } err := rpo.upsertMyMembership(rpo.lifecycleCtx, request) - if err != nil { rpo.logger.Error("Membership upsert failed.", tag.Error(err)) } @@ -474,3 +456,29 @@ func replaceServicePort(address string, servicePort int) (string, error) { } return net.JoinHostPort(host, convert.IntToString(servicePort)), nil } + +var ( + serviceNameToServiceTypeEnumMap = map[primitives.ServiceName]persistence.ServiceType{} +) + +// RegisterServiceNameToServiceTypeEnum must be called from a static init(). +func RegisterServiceNameToServiceTypeEnum(serviceName primitives.ServiceName, serviceType persistence.ServiceType) { + serviceNameToServiceTypeEnumMap[serviceName] = serviceType +} + +func init() { + RegisterServiceNameToServiceTypeEnum(primitives.AllServices, persistence.All) + RegisterServiceNameToServiceTypeEnum(primitives.FrontendService, persistence.Frontend) + RegisterServiceNameToServiceTypeEnum(primitives.InternalFrontendService, persistence.InternalFrontend) + RegisterServiceNameToServiceTypeEnum(primitives.HistoryService, persistence.History) + RegisterServiceNameToServiceTypeEnum(primitives.MatchingService, persistence.Matching) + RegisterServiceNameToServiceTypeEnum(primitives.WorkerService, persistence.Worker) +} + +func serviceNameToServiceTypeEnum(name primitives.ServiceName) (persistence.ServiceType, error) { + if serviceType, ok := serviceNameToServiceTypeEnumMap[name]; ok { + return serviceType, nil + } + + return persistence.All, fmt.Errorf("unable to parse servicename '%s'", name) +} diff --git a/common/membership/ringpop/monitor_test.go b/common/membership/ringpop/monitor_test.go index 7cadc63f921..292fb5a607f 100644 --- a/common/membership/ringpop/monitor_test.go +++ b/common/membership/ringpop/monitor_test.go @@ -73,7 +73,7 @@ func (s *RpoSuite) TestMonitor() { s.Fail("Timed out waiting for failure to be detected by ringpop") } - for k := 0; k < 10; k++ { + for k := range 10 { host, err = r.Lookup(fmt.Sprintf("key%d", k)) s.Nil(err, "Ringpop monitor failed to find host for key") s.NotEqual(testService.hostAddrs[1], host.GetAddress(), "Ringpop monitor assigned key to dead host") diff --git a/common/membership/ringpop/service_resolver.go b/common/membership/ringpop/service_resolver.go index 12af43b6ef5..bc4d12efb7a 100644 --- a/common/membership/ringpop/service_resolver.go +++ b/common/membership/ringpop/service_resolver.go @@ -47,7 +47,6 @@ const ( minRefreshInternal = time.Second * 4 defaultRefreshInterval = time.Second * 10 - replicaPoints = 100 ) const ( @@ -59,13 +58,14 @@ const ( type ( serviceResolver struct { - service primitives.ServiceName - port int - rp *ringpop.Ringpop - refreshChan chan struct{} - shutdownCh chan struct{} - shutdownWG sync.WaitGroup - logger log.Logger + service primitives.ServiceName + port int + rp *ringpop.Ringpop + replicaPoints int + refreshChan chan struct{} + shutdownCh chan struct{} + shutdownWG sync.WaitGroup + logger log.Logger ringAndHosts atomic.Value // holds a ringAndHosts @@ -96,12 +96,14 @@ func newServiceResolver( service primitives.ServiceName, port int, rp *ringpop.Ringpop, + replicaPoints int, logger log.Logger, ) *serviceResolver { resolver := &serviceResolver{ service: service, port: port, rp: rp, + replicaPoints: replicaPoints, refreshChan: make(chan struct{}), shutdownCh: make(chan struct{}), logger: log.With(logger, tag.ComponentServiceResolver, tag.Service(service)), @@ -109,13 +111,13 @@ func newServiceResolver( listeners: make(map[string]chan<- *membership.ChangedEvent), } resolver.ringAndHosts.Store(ringAndHosts{ - ring: newHashRing(), + ring: newHashRing(replicaPoints), hosts: make(map[string]*hostInfo), }) return resolver } -func newHashRing() *hashring.HashRing { +func newHashRing(replicaPoints int) *hashring.HashRing { return hashring.New(farm.Fingerprint32, replicaPoints) } @@ -136,7 +138,7 @@ func (r *serviceResolver) Stop() { defer r.listenerLock.Unlock() r.rp.RemoveListener(r) r.ringAndHosts.Store(ringAndHosts{ - ring: newHashRing(), + ring: newHashRing(r.replicaPoints), hosts: nil, }) r.listeners = make(map[string]chan<- *membership.ChangedEvent) @@ -293,7 +295,7 @@ func (r *serviceResolver) refreshLocked() (*membership.ChangedEvent, error) { return nil, nil } - ring := newHashRing() + ring := newHashRing(r.replicaPoints) ring.AddMembers(util.MapSlice(hosts, func(h *hostInfo) rpmembership.Member { return h })...) r.lastRefreshTime = time.Now().UTC() diff --git a/common/membership/ringpop/test_cluster.go b/common/membership/ringpop/test_cluster.go index cbc910964f2..03c0d8f097a 100644 --- a/common/membership/ringpop/test_cluster.go +++ b/common/membership/ringpop/test_cluster.go @@ -58,7 +58,7 @@ func newTestCluster( seedNode: seed, } - for i := 0; i < size; i++ { + for i := range size { var err error cluster.channels[i], err = tchannel.NewChannel(ringPopApp, nil) if err != nil { @@ -126,7 +126,7 @@ func newTestCluster( return res, nil }).AnyTimes() - for i := 0; i < size; i++ { + for i := range size { node := i resolver := func() (string, error) { return buildBroadcastHostPort(cluster.channels[node].PeerInfo(), broadcastAddress) @@ -152,6 +152,7 @@ func newTestCluster( 2*time.Second, 3*time.Second, joinTime, + 100, ) cluster.rings[i].Start() } diff --git a/common/metrics/baggage_bench_test.go b/common/metrics/baggage_bench_test.go index e7e9693dd12..bcff74d2124 100644 --- a/common/metrics/baggage_bench_test.go +++ b/common/metrics/baggage_bench_test.go @@ -97,13 +97,13 @@ func testMapBaggage(createTestObj func() testBaggage) { keys := []string{"k1", "k2", "k3", "k4", "k5"} start := time.Now() sum := int64(0) - for bag := 0; bag < baggageCount; bag++ { + for range baggageCount { testObj := createTestObj() wg := sync.WaitGroup{} wg.Add(threadCount) - for th := 0; th < threadCount; th++ { + for th := range threadCount { go func(key string) { - for upd := 0; upd < updatesPerThread; upd++ { + for range updatesPerThread { testObj.Add(key, rand.Int63()) } wg.Done() diff --git a/common/metrics/config_test.go b/common/metrics/config_test.go index abc5b80efdd..964f2d9a64b 100644 --- a/common/metrics/config_test.go +++ b/common/metrics/config_test.go @@ -138,7 +138,7 @@ func TestMetricsHandlerFromConfig(t *testing.T) { for _, c := range []struct { name string cfg *Config - expectedType interface{} + expectedType any }{ { name: "nil config", diff --git a/common/metrics/grpc.go b/common/metrics/grpc.go index 65153fda3be..902bfe6922c 100644 --- a/common/metrics/grpc.go +++ b/common/metrics/grpc.go @@ -33,10 +33,10 @@ var ( func NewServerMetricsContextInjectorInterceptor() grpc.UnaryServerInterceptor { return func( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, - ) (interface{}, error) { + ) (any, error) { ctxWithMetricsBaggage := AddMetricsContext(ctx) return handler(ctxWithMetricsBaggage, req) } @@ -46,7 +46,7 @@ func NewServerMetricsContextInjectorInterceptor() grpc.UnaryServerInterceptor { // into metrics context. func NewClientMetricsTrailerPropagatorInterceptor(logger log.Logger) grpc.UnaryClientInterceptor { return func( - ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, + ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, ) error { var trailer metadata.MD @@ -80,10 +80,10 @@ func NewClientMetricsTrailerPropagatorInterceptor(logger log.Logger) grpc.UnaryC func NewServerMetricsTrailerPropagatorInterceptor(logger log.Logger) grpc.UnaryServerInterceptor { return func( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, - ) (interface{}, error) { + ) (any, error) { // we want to return original handler response, so don't override err resp, err := handler(ctx, req) @@ -174,6 +174,6 @@ func ContextCounterGet(ctx context.Context, name string) (int64, bool) { return 0, false } - result := metricsCtx.CountersInt[name] - return result, true + result, ok := metricsCtx.CountersInt[name] + return result, ok } diff --git a/common/metrics/grpc_test.go b/common/metrics/grpc_test.go index 61e652ef19a..34724682094 100644 --- a/common/metrics/grpc_test.go +++ b/common/metrics/grpc_test.go @@ -45,16 +45,16 @@ func (s *grpcSuite) TestMetadataMetricInjection() { s.NotNil(smcii) res, err := smcii( ctx, nil, nil, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { res, err := NewServerMetricsTrailerPropagatorInterceptor(logger)( ctx, req, nil, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { cmtpi := NewClientMetricsTrailerPropagatorInterceptor(logger) s.NotNil(cmtpi) cmtpi( ctx, "any_value", nil, nil, nil, func( - ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, + ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, opts ...grpc.CallOption, ) error { trailer := opts[0].(grpc.TrailerCallOption) @@ -102,16 +102,16 @@ func (s *grpcSuite) TestMetadataMetricInjection_NoMetricPresent() { s.NotNil(smcii) res, err := smcii( ctx, nil, nil, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { res, err := NewServerMetricsTrailerPropagatorInterceptor(logger)( ctx, req, nil, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { cmtpi := NewClientMetricsTrailerPropagatorInterceptor(logger) s.NotNil(cmtpi) cmtpi( ctx, "any_value", nil, nil, nil, func( - ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, + ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, opts ...grpc.CallOption, ) error { trailer := opts[0].(grpc.TrailerCallOption) diff --git a/common/metrics/metric_defs.go b/common/metrics/metric_defs.go index 08c99e689dc..3d60ae3b126 100644 --- a/common/metrics/metric_defs.go +++ b/common/metrics/metric_defs.go @@ -2,34 +2,37 @@ package metrics // Common tags for all services const ( - OperationTagName = "operation" - ServiceRoleTagName = "service_role" - CacheTypeTagName = "cache_type" - FailureTagName = "failure" - FailureSourceTagName = "failure_source" - TaskCategoryTagName = "task_category" - TaskTypeTagName = "task_type" - TaskPriorityTagName = "task_priority" - QueueReaderIDTagName = "queue_reader_id" - QueueActionTagName = "queue_action" - QueueTypeTagName = "queue_type" - visibilityPluginNameTagName = "visibility_plugin_name" - visibilityIndexNameTagName = "visibility_index_name" - ErrorTypeTagName = "error_type" - httpStatusTagName = "http_status" - nexusMethodTagName = "method" - nexusEndpointTagName = "nexus_endpoint" - nexusServiceTagName = "nexus_service" - nexusOperationTagName = "nexus_operation" - outcomeTagName = "outcome" - versionedTagName = "versioned" - resourceExhaustedTag = "resource_exhausted_cause" - resourceExhaustedScopeTag = "resource_exhausted_scope" - PartitionTagName = "partition" - PriorityTagName = "priority" - PersistenceDBKindTagName = "db_kind" - WorkerPluginNameTagName = "worker_plugin_name" - headerCallsiteTagName = "header_callsite" + OperationTagName = "operation" + ServiceRoleTagName = "service_role" + CacheTypeTagName = "cache_type" + FailureTagName = "failure" + FailureSourceTagName = "failure_source" + TaskCategoryTagName = "task_category" + TaskTypeTagName = "task_type" + TaskPriorityTagName = "task_priority" + QueueReaderIDTagName = "queue_reader_id" + QueueActionTagName = "queue_action" + QueueTypeTagName = "queue_type" + visibilityPluginNameTagName = "visibility_plugin_name" + visibilityIndexNameTagName = "visibility_index_name" + ErrorTypeTagName = "error_type" + httpStatusTagName = "http_status" + nexusMethodTagName = "method" + nexusEndpointTagName = "nexus_endpoint" + nexusServiceTagName = "nexus_service" + nexusOperationTagName = "nexus_operation" + outcomeTagName = "outcome" + versionedTagName = "versioned" + resourceExhaustedTag = "resource_exhausted_cause" + resourceExhaustedScopeTag = "resource_exhausted_scope" + PartitionTagName = "partition" + PriorityTagName = "priority" + PersistenceDBKindTagName = "db_kind" + WorkerPluginNameTagName = "worker_plugin_name" + WorkerStorageDriverTypeTagName = "worker_storage_driver_type" + headerCallsiteTagName = "header_callsite" + ArchetypeTagName = "archetype" + ChasmTaskTypeTagName = "chasm_task_type" ) // This package should hold all the metrics and tags for temporal @@ -555,6 +558,8 @@ const ( UnknownTaskScope = "UnknownTask" // ParentClosePolicyProcessorScope is scope used by all metrics emitted by worker.ParentClosePolicyProcessor ParentClosePolicyProcessorScope = "ParentClosePolicyProcessor" + // DeleteExecutionReplicationTaskScope is the scope used by delete execution replication task processing + DeleteExecutionReplicationTaskScope = "DeleteExecutionReplicationTask" ) // History task type @@ -604,11 +609,15 @@ const ( // Schedule action types const ( - ScheduleActionTypeTag = "schedule_action" - ScheduleActionStartWorkflow = "start_workflow" - ScheduleBackendTag = "scheduler_backend" - ScheduleBackendChasm = "chasm" - ScheduleBackendLegacy = "legacy" + ScheduleActionTypeTag = "schedule_action" + ScheduleActionStartWorkflow = "start_workflow" + ScheduleBackendTag = "scheduler_backend" + ScheduleBackendChasm = "chasm" + ScheduleBackendLegacy = "legacy" + ScheduleBackendWorkflow = "workflow" + ScheduleMigrationDirectionTag = "schedule_migration_direction" + ScheduleMigrationDirectionToChasm = "to_chasm" + ScheduleMigrationDirectionToWorkflow = "to_workflow" ) var ( @@ -641,6 +650,7 @@ var ( ServiceDialLatency = NewTimerDef("service_dial_latency", WithDescription("The latency of establishing a new TCP connection.")) ServiceDialSuccessCount = NewCounterDef("service_dial_success", WithDescription("Number of TCP dial attempts that successfully established a connection.")) ServiceDialErrorCount = NewCounterDef("service_dial_error", WithDescription("Number of TCP dial attempts that failed to establish a connection.")) + DynamicConfigUpdateFailure = NewGaugeDef("dynamic_config_update_failure") ServiceLatency = NewTimerDef("service_latency") ServiceLatencyNoUserLatency = NewTimerDef("service_latency_nouserlatency") ServiceLatencyUserLatency = NewTimerDef("service_latency_userlatency") @@ -660,6 +670,7 @@ var ( TlsCertsExpired = NewGaugeDef("certificates_expired") TlsCertsExpiring = NewGaugeDef("certificates_expiring") ServiceAuthorizationLatency = NewTimerDef("service_authorization_latency") + NamespaceRateLimitWaitLatency = NewTimerDef("namespace_rate_limit_poll_wait_latency") EventBlobSize = NewBytesHistogramDef("event_blob_size") BlobSizeError = NewCounterDef( "blob_size_error", @@ -703,6 +714,7 @@ var ( "wf_too_many_pending_external_workflow_signals", WithDescription("The number of Workflow Tasks failed because they would cause the limit on the number of pending signals to external workflows to be exceeded. See https://t.mp/limits for more information."), ) + TotalNamespaces = NewGaugeDef("total_namespaces") // Frontend AddSearchAttributesWorkflowSuccessCount = NewCounterDef("add_search_attributes_workflow_success") @@ -812,6 +824,15 @@ var ( "task_latency_processing", WithDescription("Latency for processing a history task one time."), ) + // TaskPersistenceLatency is used only as a context key for accumulating persistence duration (ContextCounterAdd/Get); not emitted as a metric. + TaskPersistenceLatency = NewTimerDef( + "task_persistence_latency", + WithDescription("Context key for persistence duration; not emitted."), + ) + TaskProcessingNoPersistenceLatency = NewTimerDef( + "task_processing_no_persistence_latency", + WithDescription("Latency for processing a history task one time excluding persistence."), + ) TaskLatency = NewTimerDef( "task_latency", WithDescription("Latency for procsssing and completing a history task. This latency is across all attempts but excludes any latencies related to workflow lock or user qutoa limit."), @@ -857,7 +878,15 @@ var ( "task_errors_throttled", WithDescription("The number of history task processing errors caused by resource exhausted errors, excluding workflow busy case."), ) - TaskCorruptionCounter = NewCounterDef("task_errors_corruption") + TaskCorruptionCounter = NewCounterDef("task_errors_corruption") + ChasmPureTaskRequests = NewCounterDef( + "chasm_pure_task_requests", + WithDescription("The number of CHASM pure tasks executed."), + ) + ChasmPureTaskErrors = NewCounterDef( + "chasm_pure_task_errors", + WithDescription("The number of errors during CHASM pure task execution."), + ) TaskScheduleToStartLatency = NewTimerDef("task_schedule_to_start_latency") TaskBatchCompleteCounter = NewCounterDef("task_batch_complete_counter") TaskReschedulerPendingTasks = NewDimensionlessHistogramDef("task_rescheduler_pending_tasks") @@ -882,35 +911,34 @@ var ( "activity_schedule_to_close_latency", WithDescription("Duration of activity execution from scheduled time to terminal state. Includes retries and backoffs."), ) - ActivitySuccess = NewCounterDef("activity_success", WithDescription("Number of activities that succeeded (doesn't include retries).")) - ActivityFail = NewCounterDef("activity_fail", WithDescription("Number of activities that failed and won't be retried anymore.")) - ActivityTaskFail = NewCounterDef("activity_task_fail", WithDescription("Number of activity task failures (includes retries).")) - ActivityCancel = NewCounterDef("activity_cancel", WithDescription("Number of activities that are cancelled.")) - ActivityTerminate = NewCounterDef("activity_terminate", WithDescription("Number of activities that are terminated.")) - ActivityTaskTimeout = NewCounterDef("activity_task_timeout", WithDescription("Number of activity task timeouts (including retries).")) - ActivityTimeout = NewCounterDef("activity_timeout", WithDescription("Number of terminal activity timeouts.")) - ActivityPayloadSize = NewCounterDef("activity_payload_size", WithDescription("Size of activity payloads in bytes.")) - AckLevelUpdateCounter = NewCounterDef("ack_level_update") - AckLevelUpdateFailedCounter = NewCounterDef("ack_level_update_failed") - CommandCounter = NewCounterDef("command") - MessageTypeRequestWorkflowExecutionUpdateCounter = NewCounterDef("request_workflow_update_message") - MessageTypeAcceptWorkflowExecutionUpdateCounter = NewCounterDef("accept_workflow_update_message") - MessageTypeRespondWorkflowExecutionUpdateCounter = NewCounterDef("respond_workflow_update_message") - MessageTypeRejectWorkflowExecutionUpdateCounter = NewCounterDef("reject_workflow_update_message") - InvalidStateTransitionWorkflowExecutionUpdateCounter = NewCounterDef("invalid_state_transition_workflow_update_message") - WorkflowExecutionUpdateRegistrySize = NewBytesHistogramDef("workflow_update_registry_size") - WorkflowExecutionUpdateRegistrySizeLimited = NewCounterDef("workflow_update_registry_size_limited") - WorkflowExecutionUpdateRequestRateLimited = NewCounterDef("workflow_update_request_rate_limited") - WorkflowExecutionUpdateTooMany = NewCounterDef("workflow_update_request_too_many") - WorkflowExecutionUpdateAborted = NewCounterDef("workflow_update_aborted") - WorkflowExecutionUpdateSentToWorker = NewCounterDef("workflow_update_sent_to_worker") - WorkflowExecutionUpdateSentToWorkerAgain = NewCounterDef("workflow_update_sent_to_worker_again") - WorkflowExecutionUpdateWaitStageAccepted = NewCounterDef("workflow_update_wait_stage_accepted") - WorkflowExecutionUpdateWaitStageCompleted = NewCounterDef("workflow_update_wait_stage_completed") - WorkflowExecutionUpdateClientTimeout = NewCounterDef("workflow_update_client_timeout") - WorkflowExecutionUpdateServerTimeout = NewCounterDef("workflow_update_server_timeout") - SpeculativeWorkflowTaskCommits = NewCounterDef("speculative_workflow_task_commits") - SpeculativeWorkflowTaskRollbacks = NewCounterDef("speculative_workflow_task_rollbacks") + ActivitySuccess = NewCounterDef("activity_success", WithDescription("Number of activities that succeeded (doesn't include retries).")) + ActivityFail = NewCounterDef("activity_fail", WithDescription("Number of activities that failed and won't be retried anymore.")) + ActivityTaskFail = NewCounterDef("activity_task_fail", WithDescription("Number of activity task failures (includes retries).")) + ActivityCancel = NewCounterDef("activity_cancel", WithDescription("Number of activities that are cancelled.")) + ActivityTerminate = NewCounterDef("activity_terminate", WithDescription("Number of activities that are terminated.")) + ActivityTaskTimeout = NewCounterDef("activity_task_timeout", WithDescription("Number of activity task timeouts (including retries).")) + ActivityTimeout = NewCounterDef("activity_timeout", WithDescription("Number of terminal activity timeouts.")) + ActivityPayloadSize = NewCounterDef("activity_payload_size", WithDescription("Size of activity payloads in bytes.")) + AckLevelUpdateCounter = NewCounterDef("ack_level_update") + AckLevelUpdateFailedCounter = NewCounterDef("ack_level_update_failed") + CommandCounter = NewCounterDef("command") + MessageTypeRequestWorkflowExecutionUpdateCounter = NewCounterDef("request_workflow_update_message") + MessageTypeAcceptWorkflowExecutionUpdateCounter = NewCounterDef("accept_workflow_update_message") + MessageTypeRespondWorkflowExecutionUpdateCounter = NewCounterDef("respond_workflow_update_message") + MessageTypeRejectWorkflowExecutionUpdateCounter = NewCounterDef("reject_workflow_update_message") + WorkflowExecutionUpdateRegistrySize = NewBytesHistogramDef("workflow_update_registry_size") + WorkflowExecutionUpdateRegistrySizeLimited = NewCounterDef("workflow_update_registry_size_limited") + WorkflowExecutionUpdateRequestRateLimited = NewCounterDef("workflow_update_request_rate_limited") + WorkflowExecutionUpdateTooMany = NewCounterDef("workflow_update_request_too_many") + WorkflowExecutionUpdateAborted = NewCounterDef("workflow_update_aborted") + WorkflowExecutionUpdateSentToWorker = NewCounterDef("workflow_update_sent_to_worker") + WorkflowExecutionUpdateSentToWorkerAgain = NewCounterDef("workflow_update_sent_to_worker_again") + WorkflowExecutionUpdateWaitStageAccepted = NewCounterDef("workflow_update_wait_stage_accepted") + WorkflowExecutionUpdateWaitStageCompleted = NewCounterDef("workflow_update_wait_stage_completed") + WorkflowExecutionUpdateClientTimeout = NewCounterDef("workflow_update_client_timeout") + WorkflowExecutionUpdateServerTimeout = NewCounterDef("workflow_update_server_timeout") + SpeculativeWorkflowTaskCommits = NewCounterDef("speculative_workflow_task_commits") + SpeculativeWorkflowTaskRollbacks = NewCounterDef("speculative_workflow_task_rollbacks") ActivityEagerExecutionCounter = NewCounterDef("activity_eager_execution") // WorkflowEagerExecutionCounter is emitted any time eager workflow start is requested. @@ -1021,7 +1049,8 @@ var ( ReplicationOrphanedHistoryBranch = NewCounterDef("replication_orphaned_history_branch") // ReplicationTasksLag is a heuristic for how far behind the remote DC is for a given cluster. It measures the // difference between task IDs so its unit should be "tasks". - ReplicationTasksLag = NewDimensionlessHistogramDef("replication_tasks_lag") + ReplicationTasksLag = NewDimensionlessHistogramDef("replication_tasks_lag") + ReplicationDeleteExecutionTaskGenerationFailure = NewCounterDef("replication_delete_execution_task_generation_failure") // ReplicationTasksFetched records the number of tasks fetched by the poller. ReplicationTasksFetched = NewDimensionlessHistogramDef("replication_tasks_fetched") ReplicationLatency = NewTimerDef("replication_latency") @@ -1083,8 +1112,22 @@ var ( DynamicWorkerPoolSchedulerEnqueuedTasks = NewCounterDef("dynamic_worker_pool_scheduler_enqueued_tasks") DynamicWorkerPoolSchedulerDequeuedTasks = NewCounterDef("dynamic_worker_pool_scheduler_dequeued_tasks") DynamicWorkerPoolSchedulerRejectedTasks = NewCounterDef("dynamic_worker_pool_scheduler_rejected_tasks") - PausedActivitiesCounter = NewCounterDef("paused_activities") - ExternalPayloadUploadSize = NewBytesHistogramDef("external_payload_upload_size", WithDescription("The histogram of sizes in bytes of uploaded external payloads.")) + + // ExecutionQueueScheduler metrics + ExecutionQueueSchedulerQueueCount = NewGaugeDef("execution_queue_scheduler_queue_count") + ExecutionQueueSchedulerTasksSubmitted = NewCounterDef("execution_queue_scheduler_tasks_submitted") + ExecutionQueueSchedulerTasksCompleted = NewCounterDef("execution_queue_scheduler_tasks_completed") + ExecutionQueueSchedulerTasksFailed = NewCounterDef("execution_queue_scheduler_tasks_failed") + ExecutionQueueSchedulerSubmitRejected = NewCounterDef("execution_queue_scheduler_submit_rejected") + ExecutionQueueSchedulerTaskLatency = NewTimerDef("execution_queue_scheduler_task_latency") + ExecutionQueueSchedulerQueueWaitTime = NewTimerDef("execution_queue_scheduler_queue_wait_time") + + PausedActivitiesCounter = NewCounterDef("paused_activities") + ActivityPauseRequests = NewCounterDef("activity_pause_requests") + ActivityUnpauseRequests = NewCounterDef("activity_unpause_requests") + ActivityResetRequests = NewCounterDef("activity_reset_requests") + ActivityUpdateOptionsRequests = NewCounterDef("activity_update_options_requests") + ExternalPayloadUploadSize = NewBytesHistogramDef("external_payload_upload_size", WithDescription("The histogram of sizes in bytes of uploaded external payloads.")) // Deadlock detector metrics DDSuspectedDeadlocks = NewCounterDef("dd_suspected_deadlocks") @@ -1104,20 +1147,24 @@ var ( NamespaceRegistryRefreshLatency = NewTimerDef("namespace_registry_refresh_latency") // Matching - MatchingClientForwardedCounter = NewCounterDef("forwarded") - MatchingClientInvalidTaskQueueName = NewCounterDef("invalid_task_queue_name") - MatchingClientInvalidTaskQueuePartition = NewCounterDef("invalid_task_queue_partition") - SyncMatchLatencyPerTaskQueue = NewTimerDef("syncmatch_latency") - AsyncMatchLatencyPerTaskQueue = NewTimerDef("asyncmatch_latency") - PollSuccessPerTaskQueueCounter = NewCounterDef("poll_success") - PollTimeoutPerTaskQueueCounter = NewCounterDef("poll_timeouts") - PollSuccessWithSyncPerTaskQueueCounter = NewCounterDef("poll_success_sync") - PollLatencyPerTaskQueue = NewTimerDef("poll_latency") - LeaseRequestPerTaskQueueCounter = NewCounterDef("lease_requests") - LeaseFailurePerTaskQueueCounter = NewCounterDef("lease_failures") - ConditionFailedErrorPerTaskQueueCounter = NewCounterDef("condition_failed_errors") - RespondQueryTaskFailedPerTaskQueueCounter = NewCounterDef("respond_query_failed") - RespondNexusTaskFailedPerTaskQueueCounter = NewCounterDef("respond_nexus_failed") + MatchingClientForwardedCounter = NewCounterDef("forwarded") + MatchingClientInvalidTaskQueueName = NewCounterDef("invalid_task_queue_name") + MatchingClientInvalidTaskQueuePartition = NewCounterDef("invalid_task_queue_partition") + SyncMatchLatencyPerTaskQueue = NewTimerDef("syncmatch_latency") + AsyncMatchLatencyPerTaskQueue = NewTimerDef("asyncmatch_latency") + PollSuccessPerTaskQueueCounter = NewCounterDef("poll_success") + PollTimeoutPerTaskQueueCounter = NewCounterDef("poll_timeouts") + PollSuccessWithSyncPerTaskQueueCounter = NewCounterDef("poll_success_sync") + PollLatencyPerTaskQueue = NewTimerDef("poll_latency") + LeaseRequestPerTaskQueueCounter = NewCounterDef("lease_requests") + LeaseFailurePerTaskQueueCounter = NewCounterDef("lease_failures") + ConditionFailedErrorPerTaskQueueCounter = NewCounterDef("condition_failed_errors") + RespondQueryTaskFailedPerTaskQueueCounter = NewCounterDef("respond_query_failed") + RespondNexusTaskFailedPerTaskQueueCounter = NewCounterDef("respond_nexus_failed") + NexusTaskRequests = NewCounterDef( + "nexus_task_requests", + WithDescription("The number of Nexus task poll and respond requests received by the matching service, broken down by namespace, operation, client_name, and is_internal."), + ) SyncThrottlePerTaskQueueCounter = NewCounterDef("sync_throttle_count") BufferThrottlePerTaskQueueCounter = NewCounterDef("buffer_throttle_count") ExpiredTasksPerTaskQueueCounter = NewCounterDef("tasks_expired") @@ -1195,6 +1242,21 @@ var ( WithDescription( "Set if the worker was configured with a plugin. Dimensions: namespace, plugin_name"), ) + + // ---------------------------------------------------------------------------------------------------------------- + // Matching service: Metrics to understand storage driver adoption. + WorkerStorageDriverTypeMetric = NewGaugeDef( + "worker_storage_driver_type", + WithDescription( + "Set if the worker was configured with a storage driver. Dimensions: namespace, storage_driver_type."), + ) + // ---------------------------------------------------------------------------------------------------------------- + // Matching service: Metrics to understand poller autoscaling adoption. + PollerAutoscalingHeartbeatCount = NewCounterDef( + "poller_autoscaling_heartbeat_count", + WithDescription( + "Count of worker heartbeats with poller autoscaling enabled. Dimensions: namespace, taskqueue, task_type"), + ) // ---------------------------------------------------------------------------------------------------------------- // Versioning and Reachability @@ -1332,6 +1394,18 @@ var ( "schedule_payload_size", WithDescription("The size in bytes of a customer payload (including action results and update signals)"), ) + ScheduleMigrationStarted = NewCounterDef( + "schedule_migration_started", + WithDescription("The number of times a schedule migration is started"), + ) + ScheduleMigrationCompleted = NewCounterDef( + "schedule_migration_completed", + WithDescription("The number of times a schedule migration completes successfully"), + ) + ScheduleMigrationFailed = NewCounterDef( + "schedule_migration_failed", + WithDescription("The number of times a schedule migration fails"), + ) // Worker Versioning WorkerDeploymentCreated = NewCounterDef("worker_deployment_created") diff --git a/common/metrics/metricstest/capture_handler.go b/common/metrics/metricstest/capture_handler.go index 862fb029984..c3b6382969e 100644 --- a/common/metrics/metricstest/capture_handler.go +++ b/common/metrics/metricstest/capture_handler.go @@ -1,6 +1,8 @@ package metricstest import ( + "maps" + "slices" "sync" "sync/atomic" "time" @@ -18,19 +20,19 @@ type CapturedRecording struct { // Capture is a specific capture instance. type Capture struct { - recordings map[string][]*CapturedRecording + recordings CaptureSnapshot recordingsLock sync.RWMutex } +type CaptureSnapshot = map[string][]*CapturedRecording + // Snapshot returns a copy of all metrics recorded, keyed by name. -func (c *Capture) Snapshot() map[string][]*CapturedRecording { +func (c *Capture) Snapshot() CaptureSnapshot { c.recordingsLock.RLock() defer c.recordingsLock.RUnlock() - ret := make(map[string][]*CapturedRecording, len(c.recordings)) - for k, v := range c.recordings { - recs := make([]*CapturedRecording, len(v)) - copy(recs, v) - ret[k] = recs + ret := maps.Clone(c.recordings) + for k, v := range ret { + ret[k] = slices.Clone(v) } return ret } @@ -63,7 +65,7 @@ func NewCaptureHandler() *CaptureHandler { // StartCapture returns a started capture. StopCapture should be called on // complete. func (c *CaptureHandler) StartCapture() *Capture { - capture := &Capture{recordings: map[string][]*CapturedRecording{}} + capture := &Capture{recordings: make(CaptureSnapshot)} c.capturesLock.Lock() defer c.capturesLock.Unlock() diff --git a/common/metrics/tags.go b/common/metrics/tags.go index 5e5d7231387..a165bd57a4c 100644 --- a/common/metrics/tags.go +++ b/common/metrics/tags.go @@ -20,22 +20,26 @@ const ( buildPlatformTag = "build_platform" goVersionTag = "go_version" - instance = "instance" - namespace = "namespace" - namespaceID = "namespace_id" - namespaceState = "namespace_state" - sourceCluster = "source_cluster" - targetCluster = "target_cluster" - fromCluster = "from_cluster" - toCluster = "to_cluster" - taskQueue = "taskqueue" - workflowType = "workflowType" - activityType = "activityType" - commandType = "commandType" - serviceName = "service_name" - actionType = "action_type" - workerVersion = "worker_version" - destination = "destination" + instance = "instance" + namespace = "namespace" + namespaceID = "namespace_id" + namespaceState = "namespace_state" + sourceCluster = "source_cluster" + targetCluster = "target_cluster" + taskSourceTag = "source" + forwardedTag = "forwarded" + fromCluster = "from_cluster" + toCluster = "to_cluster" + taskQueue = "taskqueue" + workflowType = "workflowType" + activityType = "activityType" + commandType = "commandType" + serviceName = "service_name" + actionType = "action_type" + workerVersion = "worker_version" + workerDeploymentName = "worker_deployment_name" + workerDeploymentBuildID = "worker_build_id" + destination = "destination" // Generic reason tag can be used anywhere a reason is needed. reason = "reason" // See server.api.enums.v1.ReplicationTaskType @@ -56,6 +60,9 @@ const ( toUnversioned = "to_unversioned" queryTypeTag = "query_type" namespaceAllValue = "all" + clientName = "client_name" + isInternal = "is_internal" + activityTargetingMethod = "activity_targeting_method" unknownValue = "_unknown_" totalMetricSuffix = "_total" tagExcludedValue = "_tag_excluded_" @@ -187,6 +194,20 @@ func WorkerVersionTag(version string, versionBreakdown bool) Tag { return Tag{Key: workerVersion, Value: version} } +func WorkerDeploymentNameTag(deploymentName string, versionBreakdown bool) Tag { + if !versionBreakdown { + deploymentName = "" + } + return Tag{Key: workerDeploymentName, Value: deploymentName} +} + +func WorkerDeploymentBuildIDTag(buildID string, versionBreakdown bool) Tag { + if !versionBreakdown { + buildID = "" + } + return Tag{Key: workerDeploymentBuildID, Value: buildID} +} + // WorkflowTypeTag returns a new workflow type tag. func WorkflowTypeTag(value string) Tag { if len(value) == 0 { @@ -203,6 +224,11 @@ func ActivityTypeTag(value string) Tag { return Tag{Key: activityType, Value: value} } +// ActivityTargetingMethodTag returns a tag indicating how the activity was targeted: "id" or "type". +func ActivityTargetingMethodTag(value string) Tag { + return Tag{Key: activityTargetingMethod, Value: value} +} + // CommandTypeTag returns a new command type tag. func CommandTypeTag(value string) Tag { if len(value) == 0 { @@ -256,6 +282,20 @@ func TaskTypeTag(value string) Tag { return Tag{Key: TaskTypeTagName, Value: value} } +func ArchetypeTag(value string) Tag { + if len(value) == 0 { + value = unknownValue + } + return Tag{Key: ArchetypeTagName, Value: value} +} + +func ChasmTaskTypeTag(value string) Tag { + if len(value) == 0 { + value = unknownValue + } + return Tag{Key: ChasmTaskTypeTagName, Value: value} +} + func PartitionTag(partition string) Tag { return Tag{Key: PartitionTagName, Value: partition} } @@ -267,6 +307,14 @@ func TaskPriorityTag(value string) Tag { return Tag{Key: TaskPriorityTagName, Value: value} } +func TaskSourceTag(source enumsspb.TaskSource) Tag { + return Tag{Key: taskSourceTag, Value: source.String()} +} + +func ForwardedTag(forwarded bool) Tag { + return Tag{Key: forwardedTag, Value: strconv.FormatBool(forwarded)} +} + func MatchingTaskPriorityTag(value int32) Tag { priStr := "" if value != 0 { @@ -311,6 +359,10 @@ func WorkerPluginNameTag(value string) Tag { return Tag{Key: WorkerPluginNameTagName, Value: value} } +func WorkerStorageDriverTypeTag(value string) Tag { + return Tag{Key: WorkerStorageDriverTypeTagName, Value: value} +} + // VersionedTag represents whether a loaded task queue manager represents a specific version set or build ID or not. func VersionedTag(versioned string) Tag { return Tag{Key: versionedTagName, Value: versioned} @@ -500,6 +552,18 @@ var TaskExpireStageReadTag = Tag{Key: taskExpireStage, Value: "read"} var TaskExpireStageMemoryTag = Tag{Key: taskExpireStage, Value: "memory"} var TaskInvalidTag = Tag{Key: taskExpireStage, Value: "invalid"} +// ClientNameTag returns a new client_name tag for the SDK client name. +func ClientNameTag(value string) Tag { + if len(value) == 0 { + value = unknownValue + } + return Tag{Key: clientName, Value: value} +} + +func IsInternalTag(internal bool) Tag { + return Tag{Key: isInternal, Value: strconv.FormatBool(internal)} +} + func PersistenceDBKindTag(kind string) Tag { return Tag{Key: PersistenceDBKindTagName, Value: kind} } diff --git a/common/namespace/namespace.go b/common/namespace/namespace.go index 5819b9d3aef..628759d1d25 100644 --- a/common/namespace/namespace.go +++ b/common/namespace/namespace.go @@ -179,8 +179,8 @@ func (ns *Namespace) State() enumspb.NamespaceState { return ns.info.State } -func (ns *Namespace) ReplicationState() enumspb.ReplicationState { - return ns.replicationResolver.ReplicationState() +func (ns *Namespace) ReplicationState(businessID string) enumspb.ReplicationState { + return ns.replicationResolver.ReplicationState(businessID) } // ActiveClusterName observes the name of the cluster that is currently active diff --git a/common/namespace/nsregistry/registry.go b/common/namespace/nsregistry/registry.go index 08430fe9ca1..bd123b73c7f 100644 --- a/common/namespace/nsregistry/registry.go +++ b/common/namespace/nsregistry/registry.go @@ -106,6 +106,7 @@ type ( refresher *goro.Handle persistence Persistence globalNamespacesEnabled bool + currentClusterName string clock Clock metricsHandler metrics.Handler logger log.Logger @@ -150,6 +151,7 @@ var _ namespace.Registry = (*registry)(nil) func NewRegistry( aPersistence Persistence, enableGlobalNamespaces bool, + currentClusterName string, refreshInterval dynamicconfig.DurationPropertyFn, forceSearchAttributesCacheRefreshOnRead dynamicconfig.BoolPropertyFn, metricsHandler metrics.Handler, @@ -159,6 +161,7 @@ func NewRegistry( reg := ®istry{ persistence: aPersistence, globalNamespacesEnabled: enableGlobalNamespaces, + currentClusterName: currentClusterName, clock: clock.NewRealTimeSource(), metricsHandler: metricsHandler.WithTags(metrics.OperationTag(metrics.NamespaceCacheScope)), logger: logger, @@ -601,18 +604,21 @@ func (r *registry) refreshNamespaces(ctx context.Context) (err error) { } newNameToID[aNamespace.Name()] = aNamespace.ID() - if namespaceStateChanged(oldNS, aNamespace) { + if r.namespaceStateChanged(oldNS, aNamespace) { stateChanged = append(stateChanged, aNamespace) } } r.nsMapsLock.Lock() + totalNamespaceCount := len(newIDToNamespace) // record metric value within lock boundary r.idToNamespace = newIDToNamespace r.nameToID = newNameToID stateChanged = append(stateChanged, r.stateChangedDuringReadthrough...) r.stateChangedDuringReadthrough = nil r.nsMapsLock.Unlock() + metrics.TotalNamespaces.With(r.metricsHandler).Record(float64(totalNamespaceCount)) + r.stateChangeCallbacks.Range( func(_, value any) bool { //revive:disable-next-line:unchecked-type-assertion @@ -658,6 +664,9 @@ func (r *registry) processWatchEvent(event *persistence.NamespaceWatchEvent) err r.logger.Warn("Unknown namespace watch event type", tag.Int("eventType", int(event.Type))) } + idCount, _ := r.GetRegistrySize() + metrics.TotalNamespaces.With(r.metricsHandler).Record(float64(idCount)) + if executeCallbacks { isDelete := event.Type == persistence.NamespaceWatchEventTypeDelete @@ -812,7 +821,7 @@ func (r *registry) updateSingleNamespace(ns *namespace.Namespace, updatedViaWatc } r.nameToID[ns.Name()] = ns.ID() - changed := namespaceStateChanged(oldNS, ns) + changed := r.namespaceStateChanged(oldNS, ns) if changed && !updatedViaWatch { r.stateChangedDuringReadthrough = append(r.stateChangedDuringReadthrough, ns) } @@ -873,12 +882,11 @@ func (r *registry) getNamespacePersistence(request *persistence.GetNamespaceRequ // this test should include anything that might affect whether a namespace is active on // this cluster. // returns true if the state was changed or false if not -func namespaceStateChanged(old *namespace.Namespace, new *namespace.Namespace) bool { - return old == nil || - old.State() != new.State() || - old.Name() != new.Name() || - old.IsGlobalNamespace() != new.IsGlobalNamespace() || - // TODO: Refactor to use ns.ActiveInCluster() api - old.ActiveClusterName(namespace.EmptyBusinessID) != new.ActiveClusterName(namespace.EmptyBusinessID) || - old.ReplicationState() != new.ReplicationState() +func (r *registry) namespaceStateChanged(oldNS *namespace.Namespace, newNS *namespace.Namespace) bool { + return oldNS == nil || + oldNS.State() != newNS.State() || + oldNS.Name() != newNS.Name() || + oldNS.IsGlobalNamespace() != newNS.IsGlobalNamespace() || + oldNS.ActiveInCluster(r.currentClusterName) != newNS.ActiveInCluster(r.currentClusterName) || + oldNS.ReplicationState("") != newNS.ReplicationState("") } diff --git a/common/namespace/nsregistry/registry_test.go b/common/namespace/nsregistry/registry_test.go index 647aff146d6..6fd81eaa1d8 100644 --- a/common/namespace/nsregistry/registry_test.go +++ b/common/namespace/nsregistry/registry_test.go @@ -53,6 +53,7 @@ func (s *registrySuite) SetupTest() { s.registry = nsregistry.NewRegistry( s.regPersistence, true, + "active", dynamicconfig.GetDurationPropertyFn(time.Second), dynamicconfig.GetBoolPropertyFn(false), metrics.NoopMetricsHandler, @@ -326,6 +327,7 @@ func (s *registrySuite) TestUpdateCache_TriggerCallBack() { FailoverVersion: 11, FailoverNotificationVersion: 0, }, + IsGlobalNamespace: true, NotificationVersion: namespaceNotificationVersion, } namespaceNotificationVersion++ @@ -359,6 +361,7 @@ func (s *registrySuite) TestUpdateCache_TriggerCallBack() { FailoverVersion: 21, FailoverNotificationVersion: 0, }, + IsGlobalNamespace: true, NotificationVersion: namespaceNotificationVersion, } entry2Old, err := namespace.FromPersistentState( @@ -392,6 +395,7 @@ func (s *registrySuite) TestUpdateCache_TriggerCallBack() { FailoverVersion: namespaceRecord2Old.Namespace.FailoverVersion + 1, FailoverNotificationVersion: namespaceNotificationVersion, }, + IsGlobalNamespace: true, NotificationVersion: namespaceNotificationVersion, } entry2New, err := namespace.FromPersistentState( @@ -419,6 +423,7 @@ func (s *registrySuite) TestUpdateCache_TriggerCallBack() { FailoverVersion: namespaceRecord1Old.Namespace.FailoverVersion, FailoverNotificationVersion: namespaceRecord1Old.Namespace.FailoverNotificationVersion, }, + IsGlobalNamespace: true, NotificationVersion: namespaceNotificationVersion, } namespaceNotificationVersion++ @@ -539,7 +544,7 @@ func (s *registrySuite) TestGetTriggerListAndUpdateCache_ConcurrentAccess() { } } - for i := 0; i < coroutineCountGet; i++ { + for range coroutineCountGet { waitGroup.Add(1) go testGetFn() } diff --git a/common/namespace/nsregistry/registry_watch_test.go b/common/namespace/nsregistry/registry_watch_test.go index 0c880487147..b63e51c2767 100644 --- a/common/namespace/nsregistry/registry_watch_test.go +++ b/common/namespace/nsregistry/registry_watch_test.go @@ -124,6 +124,7 @@ func (s *registryWatchSuite) newRegistryWithResolverFactory( return nsregistry.NewRegistry( s.regPersistence, true, + "active", dynamicconfig.GetDurationPropertyFn(time.Second), dynamicconfig.GetBoolPropertyFn(false), s.captureHandler, @@ -161,6 +162,40 @@ func (s *registryWatchSuite) newNamespaceResponse( } } +func (s *registryWatchSuite) newGlobalNamespaceResponse( + id namespace.ID, + name string, + activeCluster string, + notificationVersion int64, +) *persistence.GetNamespaceResponse { + return &persistence.GetNamespaceResponse{ + Namespace: &persistencespb.NamespaceDetail{ + Info: &persistencespb.NamespaceInfo{ + Id: id.String(), + Name: name, + State: enumspb.NAMESPACE_STATE_REGISTERED, + Data: make(map[string]string), + }, + Config: &persistencespb.NamespaceConfig{ + Retention: timestamp.DurationFromDays(int32(1)), + BadBinaries: &namespacepb.BadBinaries{ + Binaries: map[string]*namespacepb.BadBinaryInfo{}, + }, + }, + ReplicationConfig: &persistencespb.NamespaceReplicationConfig{ + ActiveClusterName: activeCluster, + Clusters: []string{ + cluster.TestCurrentClusterName, + cluster.TestAlternativeClusterName, + }, + }, + FailoverVersion: 1, + }, + IsGlobalNamespace: true, + NotificationVersion: notificationVersion, + } +} + // defaultListRequest returns the standard ListNamespacesRequest used by the registry. func (s *registryWatchSuite) defaultListRequest() *persistence.ListNamespacesRequest { return &persistence.ListNamespacesRequest{ @@ -197,10 +232,10 @@ func (s *registryWatchSuite) waitForCallback(ch <-chan struct{}, description str // create, update, and delete events received over the watch channel. func (s *registryWatchSuite) TestWatchEvents() { ns1ID := namespace.NewID() - ns1Record := s.newNamespaceResponse(ns1ID, "initial-namespace", cluster.TestCurrentClusterName, 1) + ns1Record := s.newGlobalNamespaceResponse(ns1ID, "initial-namespace", cluster.TestCurrentClusterName, 1) ns2ID := namespace.NewID() - ns2Record := s.newNamespaceResponse(ns2ID, "created-via-watch", cluster.TestCurrentClusterName, 2) + ns2Record := s.newGlobalNamespaceResponse(ns2ID, "created-via-watch", cluster.TestCurrentClusterName, 2) watchCh := make(chan *persistence.NamespaceWatchEvent, 1) s.expectWatchAndList(watchCh, ns1Record) @@ -224,6 +259,7 @@ func (s *registryWatchSuite) TestWatchEvents() { s.Equal("initial-namespace", events[0].ns.Name().String()) s.Equal(int64(1), events[0].ns.NotificationVersion()) s.False(events[0].deleted) + s.InEpsilon(float64(1), s.capture.Snapshot()[metrics.TotalNamespaces.Name()][0].Value, 0.01) // --- Create event --- watchCh <- &persistence.NamespaceWatchEvent{ @@ -244,9 +280,10 @@ func (s *registryWatchSuite) TestWatchEvents() { s.Len(events, 2) s.Equal("created-via-watch", events[1].ns.Name().String()) s.False(events[1].deleted) + s.InEpsilon(float64(2), s.capture.Snapshot()[metrics.TotalNamespaces.Name()][1].Value, 0.01) // --- Update event (change active cluster to trigger state change callback) --- - ns1UpdatedRecord := s.newNamespaceResponse(ns1ID, "initial-namespace", cluster.TestAlternativeClusterName, 3) + ns1UpdatedRecord := s.newGlobalNamespaceResponse(ns1ID, "initial-namespace", cluster.TestAlternativeClusterName, 3) watchCh <- &persistence.NamespaceWatchEvent{ Type: persistence.NamespaceWatchEventTypeUpdate, @@ -269,6 +306,7 @@ func (s *registryWatchSuite) TestWatchEvents() { s.Equal(int64(3), events[2].ns.NotificationVersion()) s.Equal(cluster.TestAlternativeClusterName, events[2].ns.ActiveClusterName(namespace.EmptyBusinessID)) s.False(events[2].deleted) + s.InEpsilon(float64(2), s.capture.Snapshot()[metrics.TotalNamespaces.Name()][2].Value, 0.01) // update doesn't change count // --- Delete event --- watchCh <- &persistence.NamespaceWatchEvent{ @@ -301,14 +339,16 @@ func (s *registryWatchSuite) TestWatchEvents() { s.True(events[3].deleted) // Verify refresh latency metric was recorded (1 initial refresh). - s.Len(s.capture.Snapshot()[metrics.NamespaceRegistryRefreshLatency.Name()], 1) + snap := s.capture.Snapshot() + s.Len(snap[metrics.NamespaceRegistryRefreshLatency.Name()], 1) + s.InEpsilon(float64(1), snap[metrics.TotalNamespaces.Name()][3].Value, 0.01) // after delete: 1 namespace remains } // TestWatchStaleUpdateIgnored verifies that update events with a NotificationVersion // less than or equal to the cached version are ignored. func (s *registryWatchSuite) TestWatchStaleUpdateIgnored() { nsID := namespace.NewID() - nsRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 2) + nsRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 2) watchCh := make(chan *persistence.NamespaceWatchEvent, 3) s.expectWatchAndList(watchCh, nsRecord) @@ -326,7 +366,7 @@ func (s *registryWatchSuite) TestWatchStaleUpdateIgnored() { s.Equal(cluster.TestCurrentClusterName, ns.ActiveClusterName(namespace.EmptyBusinessID)) // Send valid update (NotificationVersion 3), changing cluster to trigger callback - validRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestAlternativeClusterName, 3) + validRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestAlternativeClusterName, 3) watchCh <- &persistence.NamespaceWatchEvent{ Type: persistence.NamespaceWatchEventTypeUpdate, Response: validRecord, @@ -339,7 +379,7 @@ func (s *registryWatchSuite) TestWatchStaleUpdateIgnored() { // Send stale update (NotificationVersion 1 < current version 3) // Uses different retention as a marker to verify it wasn't applied - staleRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 1) + staleRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 1) staleRecord.Namespace.Config.Retention = timestamp.DurationFromDays(int32(99)) watchCh <- &persistence.NamespaceWatchEvent{ Type: persistence.NamespaceWatchEventTypeUpdate, @@ -347,7 +387,7 @@ func (s *registryWatchSuite) TestWatchStaleUpdateIgnored() { } // Send another valid update to flush the channel and verify stale was skipped - finalRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 4) + finalRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 4) watchCh <- &persistence.NamespaceWatchEvent{ Type: persistence.NamespaceWatchEventTypeUpdate, Response: finalRecord, @@ -624,7 +664,7 @@ func (s *registryWatchSuite) TestWatchMultipleCallbacks() { // (per namespaceStateChanged) don't trigger callbacks. func (s *registryWatchSuite) TestWatchUpdateWithoutStateChange() { nsID := namespace.NewID() - nsRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 1) + nsRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 1) watchCh := make(chan *persistence.NamespaceWatchEvent, 2) s.expectWatchAndList(watchCh, nsRecord) @@ -644,7 +684,7 @@ func (s *registryWatchSuite) TestWatchUpdateWithoutStateChange() { s.Equal(24*time.Hour, ns.Retention()) // Send update that only changes retention (not a "state" field) - updatedRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 2) + updatedRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 2) updatedRecord.Namespace.Config.Retention = timestamp.DurationFromDays(int32(7)) watchCh <- &persistence.NamespaceWatchEvent{ Type: persistence.NamespaceWatchEventTypeUpdate, @@ -652,7 +692,7 @@ func (s *registryWatchSuite) TestWatchUpdateWithoutStateChange() { } // Send another update that does change state (active cluster) to flush the channel - stateChangeRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestAlternativeClusterName, 3) + stateChangeRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestAlternativeClusterName, 3) watchCh <- &persistence.NamespaceWatchEvent{ Type: persistence.NamespaceWatchEventTypeUpdate, Response: stateChangeRecord, @@ -996,6 +1036,7 @@ func (s *registryWatchSuite) TestWatchEmptyInitialRefresh() { // Verify no callbacks fired (no namespaces) s.Equal(int32(0), tracker.getCount()) + s.Zero(s.capture.Snapshot()[metrics.TotalNamespaces.Name()][0].Value) // Verify GetNamespace returns not found _, err := s.registry.GetNamespaceWithOptions( @@ -1019,6 +1060,7 @@ func (s *registryWatchSuite) TestWatchEmptyInitialRefresh() { ns, err := s.registry.GetNamespace("new-namespace") s.NoError(err) s.Equal(nsID, ns.ID()) + s.InEpsilon(float64(1), s.capture.Snapshot()[metrics.TotalNamespaces.Name()][1].Value, 0.01) } // TestWatchUpdateForUnknownNamespace verifies that an update event for a namespace @@ -1065,7 +1107,7 @@ func (s *registryWatchSuite) TestWatchUpdateForUnknownNamespace() { // namespace updates it if the version is higher. func (s *registryWatchSuite) TestWatchCreateForExistingNamespace() { nsID := namespace.NewID() - nsRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 1) + nsRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestCurrentClusterName, 1) watchCh := make(chan *persistence.NamespaceWatchEvent, 1) s.expectWatchAndList(watchCh, nsRecord) @@ -1079,7 +1121,7 @@ func (s *registryWatchSuite) TestWatchCreateForExistingNamespace() { s.waitForCallback(tracker.ch, "initial refresh") // Send Create event with higher version (changing cluster to trigger callback) - updatedRecord := s.newNamespaceResponse(nsID, "test-namespace", cluster.TestAlternativeClusterName, 2) + updatedRecord := s.newGlobalNamespaceResponse(nsID, "test-namespace", cluster.TestAlternativeClusterName, 2) watchCh <- &persistence.NamespaceWatchEvent{ Type: persistence.NamespaceWatchEventTypeCreate, Response: updatedRecord, diff --git a/common/namespace/nsreplication/data_merger.go b/common/namespace/nsreplication/data_merger.go new file mode 100644 index 00000000000..17f6305936b --- /dev/null +++ b/common/namespace/nsreplication/data_merger.go @@ -0,0 +1,21 @@ +package nsreplication + +// NamespaceDataMerger provides custom merge logic for namespace data +// during replication task execution. +type NamespaceDataMerger interface { + // MergeData performs a business specific merge of namespace data. + MergeData(currentData, taskData map[string]string) (mergedData map[string]string, merged bool) +} + +// NoopDataMerger is the default implementation that returns task data directly. +type NoopDataMerger struct{} + +// NewNoopDataMerger creates a new NoopDataMerger. +func NewNoopDataMerger() NamespaceDataMerger { + return &NoopDataMerger{} +} + +// MergeData returns taskData directly without any merging. +func (n *NoopDataMerger) MergeData(currentData, taskData map[string]string) (map[string]string, bool) { + return taskData, false +} diff --git a/common/namespace/nsreplication/replication_task_executor.go b/common/namespace/nsreplication/replication_task_executor.go index 2fef1d1ef14..5fa3110b1ae 100644 --- a/common/namespace/nsreplication/replication_task_executor.go +++ b/common/namespace/nsreplication/replication_task_executor.go @@ -50,6 +50,7 @@ type ( taskExecutorImpl struct { currentCluster string metadataManager persistence.MetadataManager + dataMerger NamespaceDataMerger logger log.Logger } ) @@ -58,12 +59,14 @@ type ( func NewTaskExecutor( currentCluster string, metadataManagerV2 persistence.MetadataManager, + dataMerger NamespaceDataMerger, logger log.Logger, ) TaskExecutor { return &taskExecutorImpl{ currentCluster: currentCluster, metadataManager: metadataManagerV2, + dataMerger: dataMerger, logger: logger, } } @@ -155,6 +158,7 @@ func (h *taskExecutorImpl) handleNamespaceCreationReplicationTask( ReplicationConfig: &persistencespb.NamespaceReplicationConfig{ ActiveClusterName: task.ReplicationConfig.GetActiveClusterName(), Clusters: ConvertClusterReplicationConfigFromProto(task.ReplicationConfig.Clusters), + State: task.ReplicationConfig.GetState(), FailoverHistory: ConvertFailoverHistoryToPersistenceProto(task.GetFailoverHistory()), }, ConfigVersion: task.GetConfigVersion(), @@ -266,15 +270,26 @@ func (h *taskExecutorImpl) handleNamespaceUpdateReplicationTask( IsGlobalNamespace: resp.IsGlobalNamespace, } + mergedData, dataMerged := h.dataMerger.MergeData(resp.Namespace.Info.Data, task.Info.Data) + if dataMerged { + recordUpdated = true + request.Namespace.Info.Data = mergedData + } + if resp.Namespace.ConfigVersion < task.GetConfigVersion() { recordUpdated = true + // Use merged data if available, otherwise use task data + data := task.Info.Data + if dataMerged { + data = mergedData + } request.Namespace.Info = &persistencespb.NamespaceInfo{ Id: task.GetId(), Name: task.Info.GetName(), State: task.Info.GetState(), Description: task.Info.GetDescription(), Owner: task.Info.GetOwnerEmail(), - Data: task.Info.Data, + Data: data, } request.Namespace.Config = &persistencespb.NamespaceConfig{ Retention: task.Config.GetWorkflowExecutionRetentionTtl(), diff --git a/common/namespace/nsreplication/replication_task_executor_test.go b/common/namespace/nsreplication/replication_task_executor_test.go index ac10bb7688a..bc304ab96fb 100644 --- a/common/namespace/nsreplication/replication_task_executor_test.go +++ b/common/namespace/nsreplication/replication_task_executor_test.go @@ -51,6 +51,7 @@ func (s *namespaceReplicationTaskExecutorSuite) SetupTest() { s.namespaceReplicator = NewTaskExecutor( "some random standby cluster name", s.mockMetadataMgr, + NewNoopDataMerger(), logger, ).(*taskExecutorImpl) } @@ -162,6 +163,7 @@ func (s *namespaceReplicationTaskExecutorSuite) TestExecute_RegisterNamespaceTas clusterStandby := "some random standby cluster name" configVersion := int64(0) failoverVersion := int64(59) + replicationState := enumspb.REPLICATION_STATE_NORMAL clusters := []*replicationpb.ClusterReplicationConfig{ { ClusterName: clusterActive, @@ -201,6 +203,7 @@ func (s *namespaceReplicationTaskExecutorSuite) TestExecute_RegisterNamespaceTas ReplicationConfig: &replicationpb.NamespaceReplicationConfig{ ActiveClusterName: clusterActive, Clusters: clusters, + State: replicationState, }, ConfigVersion: configVersion, FailoverVersion: failoverVersion, @@ -229,6 +232,7 @@ func (s *namespaceReplicationTaskExecutorSuite) TestExecute_RegisterNamespaceTas ReplicationConfig: &persistencespb.NamespaceReplicationConfig{ ActiveClusterName: task.ReplicationConfig.ActiveClusterName, Clusters: []string{clusterActive, clusterStandby}, + State: replicationState, FailoverHistory: []*persistencespb.FailoverStatus{ { FailoverTime: timestamppb.New(time.Date(2025, 9, 15, 14, 30, 0, 0, time.UTC)), diff --git a/common/namespace/nsreplication/transmission_task_handler.go b/common/namespace/nsreplication/transmission_task_handler.go index 79563f5efd4..9d23ae6bfad 100644 --- a/common/namespace/nsreplication/transmission_task_handler.go +++ b/common/namespace/nsreplication/transmission_task_handler.go @@ -29,6 +29,7 @@ type ( failoverVersion int64, isGlobalNamespace bool, failoverHistoy []*persistencespb.FailoverStatus, + forceReplicate bool, ) error } @@ -61,13 +62,16 @@ func (r *replicator) HandleTransmissionTask( failoverVersion int64, isGlobalNamespace bool, failoverHistoy []*persistencespb.FailoverStatus, + forceReplicate bool, ) error { - if !isGlobalNamespace { - return nil - } - if len(replicationConfig.Clusters) <= 1 && !replicationClusterListUpdated { - return nil + if !forceReplicate { + if !isGlobalNamespace { + return nil + } + if len(replicationConfig.Clusters) <= 1 && !replicationClusterListUpdated { + return nil + } } if info.State == enumspb.NAMESPACE_STATE_DELETED { // Don't replicate deleted namespace changes. @@ -105,6 +109,12 @@ func (r *replicator) HandleTransmissionTask( }, } + // Only replicate on Create operation, and only if state is Normal + if namespaceOperation == enumsspb.NAMESPACE_OPERATION_CREATE && + replicationConfig.State == enumspb.REPLICATION_STATE_NORMAL { + task.NamespaceTaskAttributes.ReplicationConfig.State = replicationConfig.State + } + return r.namespaceReplicationQueue.Publish( ctx, &replicationspb.ReplicationTask{ diff --git a/common/namespace/nsreplication/transmission_task_handler_test.go b/common/namespace/nsreplication/transmission_task_handler_test.go index a1fbbc1ab3e..dc1dc1dcc18 100644 --- a/common/namespace/nsreplication/transmission_task_handler_test.go +++ b/common/namespace/nsreplication/transmission_task_handler_test.go @@ -73,6 +73,7 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_RegisterNamespaceTask configVersion := int64(0) failoverVersion := int64(59) clusters := []string{clusterActive, clusterStandby} + replicationState := enumspb.REPLICATION_STATE_NORMAL namespaceOperation := enumsspb.NAMESPACE_OPERATION_CREATE info := &persistencespb.NamespaceInfo{ @@ -94,6 +95,7 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_RegisterNamespaceTask replicationConfig := &persistencespb.NamespaceReplicationConfig{ ActiveClusterName: clusterActive, Clusters: clusters, + State: replicationState, } isGlobalNamespace := true @@ -121,6 +123,7 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_RegisterNamespaceTask ReplicationConfig: &replicationpb.NamespaceReplicationConfig{ ActiveClusterName: clusterActive, Clusters: convertClusterReplicationConfigToProto(clusters), + State: replicationState, }, ConfigVersion: configVersion, FailoverVersion: failoverVersion, @@ -139,8 +142,9 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_RegisterNamespaceTask failoverVersion, isGlobalNamespace, nil, + false, // forceReplicate ) - s.Nil(err) + s.Require().NoError(err) } func (s *transmissionTaskSuite) TestHandleTransmissionTask_RegisterNamespaceTask_NotGlobalNamespace() { @@ -194,8 +198,9 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_RegisterNamespaceTask failoverVersion, isGlobalNamespace, nil, + false, // forceReplicate ) - s.Nil(err) + s.Require().NoError(err) } func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_IsGlobalNamespace() { @@ -282,8 +287,100 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_I failoverVersion, isGlobalNamespace, nil, + false, // forceReplicate ) - s.Nil(err) + s.Require().NoError(err) +} + +func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_StateNotReplicated() { + taskType := enumsspb.REPLICATION_TASK_TYPE_NAMESPACE_TASK + id := primitives.NewUUID().String() + name := "some random namespace test name" + state := enumspb.NAMESPACE_STATE_REGISTERED + description := "some random test description" + ownerEmail := "some random test owner" + data := map[string]string{"k": "v"} + retention := 10 * time.Hour * 24 + historyArchivalState := enumspb.ARCHIVAL_STATE_ENABLED + historyArchivalURI := "some random history archival uri" + visibilityArchivalState := enumspb.ARCHIVAL_STATE_ENABLED + visibilityArchivalURI := "some random visibility archival uri" + clusterActive := "some random active cluster name" + clusterStandby := "some random standby cluster name" + configVersion := int64(0) + failoverVersion := int64(59) + clusters := []string{clusterActive, clusterStandby} + + namespaceOperation := enumsspb.NAMESPACE_OPERATION_UPDATE + info := &persistencespb.NamespaceInfo{ + Id: id, + Name: name, + State: state, + Description: description, + Owner: ownerEmail, + Data: data, + } + config := &persistencespb.NamespaceConfig{ + Retention: durationpb.New(retention), + HistoryArchivalState: historyArchivalState, + HistoryArchivalUri: historyArchivalURI, + VisibilityArchivalState: visibilityArchivalState, + VisibilityArchivalUri: visibilityArchivalURI, + BadBinaries: &namespacepb.BadBinaries{Binaries: map[string]*namespacepb.BadBinaryInfo{}}, + } + replicationConfig := &persistencespb.NamespaceReplicationConfig{ + ActiveClusterName: clusterActive, + Clusters: clusters, + State: enumspb.REPLICATION_STATE_NORMAL, + } + isGlobalNamespace := true + + s.namespaceReplicationQueue.EXPECT().Publish(gomock.Any(), &replicationspb.ReplicationTask{ + TaskType: taskType, + Attributes: &replicationspb.ReplicationTask_NamespaceTaskAttributes{ + NamespaceTaskAttributes: &replicationspb.NamespaceTaskAttributes{ + NamespaceOperation: namespaceOperation, + Id: id, + Info: &namespacepb.NamespaceInfo{ + Name: name, + State: state, + Description: description, + OwnerEmail: ownerEmail, + Data: data, + }, + Config: &namespacepb.NamespaceConfig{ + WorkflowExecutionRetentionTtl: durationpb.New(retention), + HistoryArchivalState: historyArchivalState, + HistoryArchivalUri: historyArchivalURI, + VisibilityArchivalState: visibilityArchivalState, + VisibilityArchivalUri: visibilityArchivalURI, + BadBinaries: &namespacepb.BadBinaries{Binaries: map[string]*namespacepb.BadBinaryInfo{}}, + }, + ReplicationConfig: &replicationpb.NamespaceReplicationConfig{ + ActiveClusterName: clusterActive, + Clusters: convertClusterReplicationConfigToProto(clusters), + // State must not be set on UPDATE even when source state is NORMAL + }, + ConfigVersion: configVersion, + FailoverVersion: failoverVersion, + }, + }, + }).Return(nil) + + err := s.namespaceReplicator.HandleTransmissionTask( + context.Background(), + namespaceOperation, + info, + config, + replicationConfig, + true, + configVersion, + failoverVersion, + isGlobalNamespace, + nil, + false, // forceReplicate + ) + s.Require().NoError(err) } func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_NotGlobalNamespace() { @@ -336,8 +433,9 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_N failoverVersion, isGlobalNamespace, nil, + false, // forceReplicate ) - s.Nil(err) + s.Require().NoError(err) } func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_ReplicationClusterListUpdated() { @@ -424,8 +522,9 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_R failoverVersion, isGlobalNamespace, nil, + false, // forceReplicate ) - s.Nil(err) + s.Require().NoError(err) err = s.namespaceReplicator.HandleTransmissionTask( context.Background(), @@ -438,6 +537,7 @@ func (s *transmissionTaskSuite) TestHandleTransmissionTask_UpdateNamespaceTask_R failoverVersion, isGlobalNamespace, nil, + false, // forceReplicate ) - s.Nil(err) + s.Require().NoError(err) } diff --git a/common/namespace/replication_resolver.go b/common/namespace/replication_resolver.go index 66db766643a..40ca5550398 100644 --- a/common/namespace/replication_resolver.go +++ b/common/namespace/replication_resolver.go @@ -9,7 +9,7 @@ type ReplicationResolver interface { ActiveClusterName(businessID string) string ActiveInCluster(clusterName string) bool ClusterNames(businessID string) []string - ReplicationState() enumspb.ReplicationState + ReplicationState(businessID string) enumspb.ReplicationState IsGlobalNamespace() bool FailoverVersion(businessID string) int64 FailoverNotificationVersion() int64 @@ -71,7 +71,7 @@ func (r *defaultReplicationResolver) ClusterNames(businessID string) []string { return out } -func (r *defaultReplicationResolver) ReplicationState() enumspb.ReplicationState { +func (r *defaultReplicationResolver) ReplicationState(_ string) enumspb.ReplicationState { if r.replicationConfig == nil { return enumspb.REPLICATION_STATE_UNSPECIFIED } diff --git a/common/namespace/replication_resolver_test.go b/common/namespace/replication_resolver_test.go index 20c5e31793c..9f0e97ffacd 100644 --- a/common/namespace/replication_resolver_test.go +++ b/common/namespace/replication_resolver_test.go @@ -26,7 +26,7 @@ func TestNewDefaultReplicationResolverFactory(t *testing.T) { require.NotNil(t, resolver) assert.Equal(t, "active-cluster", resolver.ActiveClusterName(namespace.EmptyBusinessID)) assert.Equal(t, []string{"cluster1", "cluster2", "cluster3"}, resolver.ClusterNames(namespace.EmptyBusinessID)) - assert.Equal(t, enumspb.REPLICATION_STATE_NORMAL, resolver.ReplicationState()) + assert.Equal(t, enumspb.REPLICATION_STATE_NORMAL, resolver.ReplicationState("")) } func TestDefaultReplicationResolver_ActiveClusterName(t *testing.T) { @@ -171,7 +171,7 @@ func TestDefaultReplicationResolver_ReplicationState(t *testing.T) { } resolver := factory(detail) - got := resolver.ReplicationState() + got := resolver.ReplicationState("") assert.Equal(t, tt.want, got) }) } @@ -190,10 +190,10 @@ func TestDefaultReplicationResolver_MultipleCalls(t *testing.T) { resolver := factory(detail) // Call multiple times and verify consistency - for i := 0; i < 5; i++ { + for range 5 { assert.Equal(t, "primary", resolver.ActiveClusterName(namespace.EmptyBusinessID)) assert.Equal(t, []string{"primary", "secondary", "tertiary"}, resolver.ClusterNames(namespace.EmptyBusinessID)) - assert.Equal(t, enumspb.REPLICATION_STATE_NORMAL, resolver.ReplicationState()) + assert.Equal(t, enumspb.REPLICATION_STATE_NORMAL, resolver.ReplicationState("")) } } @@ -402,7 +402,7 @@ func TestDefaultReplicationResolver_Clone(t *testing.T) { assert.Equal(t, resolver.IsGlobalNamespace(), cloned.IsGlobalNamespace()) assert.Equal(t, resolver.FailoverVersion(namespace.EmptyBusinessID), cloned.FailoverVersion(namespace.EmptyBusinessID)) assert.Equal(t, resolver.FailoverNotificationVersion(), cloned.FailoverNotificationVersion()) - assert.Equal(t, resolver.ReplicationState(), cloned.ReplicationState()) + assert.Equal(t, resolver.ReplicationState(""), cloned.ReplicationState("")) // Verify that modifying the cloned resolver doesn't affect the original cloned.SetActiveCluster("cluster-tertiary") diff --git a/common/nexus/endpoint_registry.go b/common/nexus/endpoint_registry.go index 468c2a4bdf7..c16cc9590cc 100644 --- a/common/nexus/endpoint_registry.go +++ b/common/nexus/endpoint_registry.go @@ -26,7 +26,6 @@ import ( type ( EndpointRegistryConfig struct { - refreshEnabled dynamicconfig.TypedSubscribable[bool] refreshLongPollTimeout dynamicconfig.DurationPropertyFn refreshPageSize dynamicconfig.IntPropertyFn refreshMinWait dynamicconfig.DurationPropertyFn @@ -57,8 +56,6 @@ type ( endpointsByID map[string]*persistencespb.NexusEndpointEntry // Mapping of endpoint ID -> endpoint. endpointsByName map[string]*persistencespb.NexusEndpointEntry // Mapping of endpoint name -> endpoint. - cancelDcSub func() - matchingClient matchingservice.MatchingServiceClient persistence p.NexusEndpointManager logger log.Logger @@ -76,7 +73,6 @@ var ErrNexusDisabled = serviceerror.NewFailedPrecondition("nexus is disabled") func NewEndpointRegistryConfig(dc *dynamicconfig.Collection) *EndpointRegistryConfig { config := &EndpointRegistryConfig{ - refreshEnabled: dynamicconfig.EnableNexus.Subscribe(dc), refreshLongPollTimeout: dynamicconfig.RefreshNexusEndpointsLongPollTimeout.Get(dc), refreshPageSize: dynamicconfig.NexusEndpointListDefaultPageSize.Get(dc), refreshMinWait: dynamicconfig.RefreshNexusEndpointsMinWait.Get(dc), @@ -110,15 +106,12 @@ func NewEndpointRegistry( // StartLifecycle starts this component. It should only be invoked by an fx lifecycle hook. // Should not be called multiple times or concurrently with StopLifecycle() func (r *EndpointRegistryImpl) StartLifecycle() { - initial, cancel := r.config.refreshEnabled(r.setEnabled) - r.cancelDcSub = cancel - r.setEnabled(initial) + r.setEnabled(true) } // StopLifecycle stops this component. It should only be invoked by an fx lifecycle hook. // Should not be called multiple times or concurrently with StartLifecycle() func (r *EndpointRegistryImpl) StopLifecycle() { - r.cancelDcSub() r.setEnabled(false) } diff --git a/common/nexus/endpoint_registry_test.go b/common/nexus/endpoint_registry_test.go index c9c95e480e3..c670ac2a52f 100644 --- a/common/nexus/endpoint_registry_test.go +++ b/common/nexus/endpoint_registry_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "sync" "testing" "time" @@ -50,7 +49,7 @@ func TestGet(t *testing.T) { PageSize: int32(100), LastKnownTableVersion: int64(1), Wait: true, - }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...interface{}) (*matchingservice.ListNexusEndpointsResponse, error) { + }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...any) (*matchingservice.ListNexusEndpointsResponse, error) { time.Sleep(20 * time.Millisecond) return &matchingservice.ListNexusEndpointsResponse{TableVersion: int64(1)}, nil }).AnyTimes() @@ -90,7 +89,7 @@ func TestGetNotFound(t *testing.T) { PageSize: int32(100), LastKnownTableVersion: int64(1), Wait: true, - }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...interface{}) (*matchingservice.ListNexusEndpointsResponse, error) { + }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...any) (*matchingservice.ListNexusEndpointsResponse, error) { time.Sleep(20 * time.Millisecond) return &matchingservice.ListNexusEndpointsResponse{TableVersion: int64(1)}, nil }).AnyTimes() @@ -156,85 +155,6 @@ func TestInitializationFallback(t *testing.T) { assert.Equal(t, int64(1), reg.tableVersion) } -func TestEnableDisableEnable(t *testing.T) { - t.Parallel() - - testEntry := newEndpointEntry(t.Name()) - mocks := newTestMocks(t) - - mocks.config.refreshMinWait = dynamicconfig.GetDurationPropertyFn(time.Millisecond) - var callback func(bool) // capture callback to call later - mocks.config.refreshEnabled = func(cb func(bool)) (bool, func()) { - callback = cb - return false, func() {} - } - - // start disabled - reg := NewEndpointRegistry(mocks.config, mocks.matchingClient, mocks.persistence, log.NewNoopLogger(), metrics.NoopMetricsHandler) - reg.StartLifecycle() - defer reg.StopLifecycle() - - // check waitUntilInitialized - quickCtx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) - defer cancel() - require.ErrorIs(t, reg.waitUntilInitialized(quickCtx), ErrNexusDisabled) - - // mocks for initial load - inLongPoll := make(chan struct{}) - closeOnce := sync.OnceFunc(func() { close(inLongPoll) }) - mocks.matchingClient.EXPECT().ListNexusEndpoints(gomock.Any(), gomock.Any()).Return(&matchingservice.ListNexusEndpointsResponse{ - Entries: []*persistencespb.NexusEndpointEntry{testEntry}, - TableVersion: 1, - NextPageToken: nil, - }, nil) - mocks.matchingClient.EXPECT().ListNexusEndpoints(gomock.Any(), &matchingservice.ListNexusEndpointsRequest{ - PageSize: int32(100), - LastKnownTableVersion: int64(1), - Wait: true, - }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...interface{}) (*matchingservice.ListNexusEndpointsResponse, error) { - closeOnce() - time.Sleep(100 * time.Millisecond) - return &matchingservice.ListNexusEndpointsResponse{TableVersion: int64(1)}, nil - }) - - // enable - callback(true) - <-inLongPoll - - // check waitUntilInitialized - quickCtx, cancel = context.WithTimeout(context.Background(), time.Millisecond) - defer cancel() - require.NoError(t, reg.waitUntilInitialized(quickCtx)) - - // now disable - callback(false) - - quickCtx, cancel = context.WithTimeout(context.Background(), time.Millisecond) - defer cancel() - require.ErrorIs(t, reg.waitUntilInitialized(quickCtx), ErrNexusDisabled) - - // enable again, should not crash - - inLongPoll = make(chan struct{}) - closeOnce = sync.OnceFunc(func() { close(inLongPoll) }) - mocks.matchingClient.EXPECT().ListNexusEndpoints(gomock.Any(), gomock.Any()).Return(&matchingservice.ListNexusEndpointsResponse{ - Entries: []*persistencespb.NexusEndpointEntry{testEntry}, - TableVersion: 1, - NextPageToken: nil, - }, nil) - mocks.matchingClient.EXPECT().ListNexusEndpoints(gomock.Any(), &matchingservice.ListNexusEndpointsRequest{ - PageSize: int32(100), - LastKnownTableVersion: int64(1), - Wait: true, - }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...interface{}) (*matchingservice.ListNexusEndpointsResponse, error) { - closeOnce() - time.Sleep(100 * time.Millisecond) - return &matchingservice.ListNexusEndpointsResponse{TableVersion: int64(1)}, nil - }) - callback(true) - <-inLongPoll -} - func TestTableVersionErrorResetsMatchingPagination(t *testing.T) { t.Parallel() @@ -291,7 +211,7 @@ func TestTableVersionErrorResetsMatchingPagination(t *testing.T) { PageSize: int32(1), LastKnownTableVersion: int64(3), Wait: true, - }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...interface{}) (*matchingservice.ListNexusEndpointsResponse, error) { + }).DoAndReturn(func(context.Context, *matchingservice.ListNexusEndpointsRequest, ...any) (*matchingservice.ListNexusEndpointsResponse, error) { time.Sleep(20 * time.Millisecond) return &matchingservice.ListNexusEndpointsResponse{TableVersion: int64(1)}, nil }).MaxTimes(1) @@ -383,9 +303,6 @@ func TestTableVersionErrorResetsPersistencePagination(t *testing.T) { func newTestMocks(t *testing.T) *testMocks { ctrl := gomock.NewController(t) testConfig := NewEndpointRegistryConfig(dynamicconfig.NewNoopCollection()) - testConfig.refreshEnabled = func(func(bool)) (bool, func()) { - return true, func() {} - } return &testMocks{ config: testConfig, matchingClient: matchingservicemock.NewMockMatchingServiceClient(ctrl), diff --git a/common/nexus/failure.go b/common/nexus/failure.go index c5543f1b832..2fb3c1744dd 100644 --- a/common/nexus/failure.go +++ b/common/nexus/failure.go @@ -299,6 +299,7 @@ func nexusFailureMetadataToApplicationFailureInfo(failure nexus.Failure) (*failu } return &failurepb.Failure_ApplicationFailureInfo{ ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + Type: "NexusFailure", Details: payloads, }, }, nil diff --git a/common/nexus/failure_test.go b/common/nexus/failure_test.go index 5f7dfc7cd99..21653c09ab3 100644 --- a/common/nexus/failure_test.go +++ b/common/nexus/failure_test.go @@ -231,6 +231,59 @@ func TestFromOperationFailedError(t *testing.T) { protorequire.ProtoEqual(t, expected, converted) } +func TestNexusFailureToTemporalFailure_UnknownMetadataType(t *testing.T) { + nf := nexus.Failure{ + Message: "unknown failure", + StackTrace: "unknown stack trace", + Metadata: map[string]string{ + "type": "some.custom.Type", + }, + Details: []byte(`{"key":"value"}`), + } + + converted, err := NexusFailureToTemporalFailure(nf) + require.NoError(t, err) + + require.Equal(t, "unknown failure", converted.GetMessage()) + require.Equal(t, "unknown stack trace", converted.GetStackTrace()) + + appInfo := converted.GetApplicationFailureInfo() + require.NotNil(t, appInfo) + require.Equal(t, "NexusFailure", appInfo.GetType()) + require.NotNil(t, appInfo.GetDetails()) + require.Len(t, appInfo.GetDetails().GetPayloads(), 1) +} + +func TestNexusFailureToTemporalFailure_NoMetadataWithDetails(t *testing.T) { + nf := nexus.Failure{ + Message: "bare failure", + Details: []byte(`{"info":"data"}`), + } + + converted, err := NexusFailureToTemporalFailure(nf) + require.NoError(t, err) + + require.Equal(t, "bare failure", converted.GetMessage()) + + appInfo := converted.GetApplicationFailureInfo() + require.NotNil(t, appInfo) + require.Equal(t, "NexusFailure", appInfo.GetType()) + require.NotNil(t, appInfo.GetDetails()) + require.Len(t, appInfo.GetDetails().GetPayloads(), 1) +} + +func TestNexusFailureToTemporalFailure_NoMetadataNoDetails(t *testing.T) { + nf := nexus.Failure{ + Message: "plain failure", + } + + converted, err := NexusFailureToTemporalFailure(nf) + require.NoError(t, err) + + require.Equal(t, "plain failure", converted.GetMessage()) + require.Nil(t, converted.GetFailureInfo()) +} + func TestFromOperationCanceledError(t *testing.T) { nexusFailure, err := nexusrpc.DefaultFailureConverter().ErrorToFailure(&nexus.OperationError{ State: nexus.OperationStateCanceled, diff --git a/common/number/number.go b/common/number/number.go index 99d3d0badbf..59476011f9e 100644 --- a/common/number/number.go +++ b/common/number/number.go @@ -15,16 +15,16 @@ type ( Type int Number struct { numberType Type - value interface{} + value any } ) func NewNumber( - value interface{}, + value any, ) Number { var numberType Type - var number interface{} + var number any switch n := value.(type) { case int8: numberType = TypeInt diff --git a/common/number/number_test.go b/common/number/number_test.go index d59b718b0fd..7ea4ead12cc 100644 --- a/common/number/number_test.go +++ b/common/number/number_test.go @@ -34,7 +34,7 @@ func (s *numberSuite) TearDownTest() { func (s *numberSuite) TestInt() { number := rand.Intn(128) - for _, n := range []interface{}{ + for _, n := range []any{ int8(number), int16(number), int32(number), @@ -49,7 +49,7 @@ func (s *numberSuite) TestInt() { func (s *numberSuite) TestUint() { number := rand.Intn(256) - for _, n := range []interface{}{ + for _, n := range []any{ uint8(number), uint16(number), uint32(number), @@ -64,7 +64,7 @@ func (s *numberSuite) TestUint() { func (s *numberSuite) TestFloat() { number := rand.Float32() * float32(rand.Int()) - for _, n := range []interface{}{ + for _, n := range []any{ float32(number), float64(number), } { diff --git a/common/payload/payload.go b/common/payload/payload.go index 8030940dfec..3c502f118ac 100644 --- a/common/payload/payload.go +++ b/common/payload/payload.go @@ -28,11 +28,11 @@ func EncodeBytes(bytes []byte) *commonpb.Payload { return p } -func Encode(value interface{}) (*commonpb.Payload, error) { +func Encode(value any) (*commonpb.Payload, error) { return defaultDataConverter.ToPayload(value) } -func Decode(p *commonpb.Payload, valuePtr interface{}) error { +func Decode(p *commonpb.Payload, valuePtr any) error { return defaultDataConverter.FromPayload(p, valuePtr) } @@ -90,3 +90,35 @@ func isEqual(a, b *commonpb.Payload) bool { bEnc := a.GetMetadata()[converter.MetadataEncoding] return bytes.Equal(aEnc, bEnc) && bytes.Equal(a.GetData(), b.GetData()) } + +// FilterNilSearchAttributes returns a new SearchAttributes with nil/empty payload values filtered out. +// If the input is nil or all values are nil/empty, returns nil. +// This is used to filter out nil search attributes from workflow start and continue-as-new events. +// Reuses MergeMapOfPayload which already handles nil payload filtering. +func FilterNilSearchAttributes(sa *commonpb.SearchAttributes) *commonpb.SearchAttributes { + if sa == nil || len(sa.GetIndexedFields()) == 0 { + return nil + } + + filtered := MergeMapOfPayload(nil, sa.GetIndexedFields()) + if len(filtered) == 0 { + return nil + } + return &commonpb.SearchAttributes{IndexedFields: filtered} +} + +// FilterNilMemo returns a new Memo with nil/empty payload values filtered out. +// If the input is nil or all values are nil/empty, returns nil. +// This is used to filter out nil memo fields from workflow start, continue-as-new, and modify-properties events. +// Reuses MergeMapOfPayload which already handles nil payload filtering. +func FilterNilMemo(memo *commonpb.Memo) *commonpb.Memo { + if memo == nil || len(memo.GetFields()) == 0 { + return nil + } + + filtered := MergeMapOfPayload(nil, memo.GetFields()) + if len(filtered) == 0 { + return nil + } + return &commonpb.Memo{Fields: filtered} +} diff --git a/common/payload/payload_test.go b/common/payload/payload_test.go index e4e1517b599..f02fa528ff4 100644 --- a/common/payload/payload_test.go +++ b/common/payload/payload_test.go @@ -154,3 +154,127 @@ func TestIsEqual(t *testing.T) { b, _ = Encode("foo") s.False(isEqual(a, b)) } + +func TestFilterNilSearchAttributes(t *testing.T) { + s := assert.New(t) + + // nil input returns nil + result := FilterNilSearchAttributes(nil) + s.Nil(result) + + // empty SearchAttributes returns nil + emptySA := &commonpb.SearchAttributes{IndexedFields: map[string]*commonpb.Payload{}} + result = FilterNilSearchAttributes(emptySA) + s.Nil(result) + + // SearchAttributes with only valid values returns filtered copy + validPayload := EncodeString("value") + saNonNil := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "key1": validPayload, + }, + } + result = FilterNilSearchAttributes(saNonNil) + s.NotNil(result) + s.Len(result.IndexedFields, 1) + s.Equal(validPayload, result.IndexedFields["key1"]) + + // SearchAttributes with nil values filters them out + nilPayloadVal, _ := Encode(nil) + saMixed := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "valid": validPayload, + "nilVal": nilPayloadVal, + }, + } + result = FilterNilSearchAttributes(saMixed) + s.NotNil(result) + s.Len(result.IndexedFields, 1) + s.Equal(validPayload, result.IndexedFields["valid"]) + s.Nil(result.IndexedFields["nilVal"]) + + // SearchAttributes with empty slice values filters them out + emptySlicePayloadVal, _ := Encode([]string{}) + saEmptySlice := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "valid": validPayload, + "emptySlice": emptySlicePayloadVal, + }, + } + result = FilterNilSearchAttributes(saEmptySlice) + s.NotNil(result) + s.Len(result.IndexedFields, 1) + s.Equal(validPayload, result.IndexedFields["valid"]) + + // SearchAttributes with all nil/empty values returns nil + saAllNil := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "nil1": nilPayloadVal, + "nil2": emptySlicePayloadVal, + }, + } + result = FilterNilSearchAttributes(saAllNil) + s.Nil(result) +} + +func TestFilterNilMemo(t *testing.T) { + s := assert.New(t) + + // nil input returns nil + result := FilterNilMemo(nil) + s.Nil(result) + + // empty Memo returns nil + emptyMemo := &commonpb.Memo{Fields: map[string]*commonpb.Payload{}} + result = FilterNilMemo(emptyMemo) + s.Nil(result) + + // Memo with only valid values returns filtered copy + validPayload := EncodeString("value") + memoNonNil := &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "key1": validPayload, + }, + } + result = FilterNilMemo(memoNonNil) + s.NotNil(result) + s.Len(result.Fields, 1) + s.Equal(validPayload, result.Fields["key1"]) + + // Memo with nil values filters them out + nilPayloadVal, _ := Encode(nil) + memoMixed := &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "valid": validPayload, + "nilVal": nilPayloadVal, + }, + } + result = FilterNilMemo(memoMixed) + s.NotNil(result) + s.Len(result.Fields, 1) + s.Equal(validPayload, result.Fields["valid"]) + s.Nil(result.Fields["nilVal"]) + + // Memo with empty slice values filters them out + emptySlicePayloadVal, _ := Encode([]string{}) + memoEmptySlice := &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "valid": validPayload, + "emptySlice": emptySlicePayloadVal, + }, + } + result = FilterNilMemo(memoEmptySlice) + s.NotNil(result) + s.Len(result.Fields, 1) + s.Equal(validPayload, result.Fields["valid"]) + + // Memo with all nil/empty values returns nil + memoAllNil := &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "nil1": nilPayloadVal, + "nil2": emptySlicePayloadVal, + }, + } + result = FilterNilMemo(memoAllNil) + s.Nil(result) +} diff --git a/common/payloads/payloads.go b/common/payloads/payloads.go index ee8b0fd9df3..75d899f23c1 100644 --- a/common/payloads/payloads.go +++ b/common/payloads/payloads.go @@ -30,11 +30,11 @@ func EncodeBytes(bytes []byte) *commonpb.Payloads { return ps } -func Encode(value ...interface{}) (*commonpb.Payloads, error) { +func Encode(value ...any) (*commonpb.Payloads, error) { return defaultDataConverter.ToPayloads(value...) } -func Decode(ps *commonpb.Payloads, valuePtr ...interface{}) error { +func Decode(ps *commonpb.Payloads, valuePtr ...any) error { return defaultDataConverter.FromPayloads(ps, valuePtr...) } diff --git a/common/persistence/cassandra/cluster_metadata_store.go b/common/persistence/cassandra/cluster_metadata_store.go index 286ec0b60e4..432fb448af1 100644 --- a/common/persistence/cassandra/cluster_metadata_store.go +++ b/common/persistence/cassandra/cluster_metadata_store.go @@ -144,7 +144,7 @@ func (m *ClusterMetadataStore) SaveClusterMetadata( ).WithContext(ctx) } - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, err := query.MapScanCAS(previous) if err != nil { return false, gocql.ConvertError("SaveClusterMetadata", err) @@ -171,7 +171,7 @@ func (m *ClusterMetadataStore) GetClusterMembers( request *p.GetClusterMembersRequest, ) (*p.GetClusterMembersResponse, error) { var queryString strings.Builder - var operands []interface{} + var operands []any queryString.WriteString(templateGetClusterMembership) operands = append(operands, constMembershipPartition) diff --git a/common/persistence/cassandra/common.go b/common/persistence/cassandra/common.go index d49cef3e2be..678a57a989a 100644 --- a/common/persistence/cassandra/common.go +++ b/common/persistence/cassandra/common.go @@ -15,7 +15,7 @@ func (f FieldNotFoundError) Error() string { return f.Msg } -func newFieldNotFoundError(fieldName string, payload map[string]interface{}) error { +func newFieldNotFoundError(fieldName string, payload map[string]any) error { return &FieldNotFoundError{Msg: fmt.Sprintf("Unable to find field '%s' in payload - '%v'", fieldName, payload)} } @@ -32,9 +32,9 @@ func (f PersistedTypeMismatchError) Error() string { func newPersistedTypeMismatchError( fieldName string, - expectedType interface{}, - received interface{}, - payload map[string]interface{}, + expectedType any, + received any, + payload map[string]any, ) *PersistedTypeMismatchError { return &PersistedTypeMismatchError{ Msg: fmt.Sprintf("Field '%s' is of type '%T' but expected type '%T' in payload - '%v'", @@ -44,7 +44,7 @@ func newPersistedTypeMismatchError( // Returns a correctly typed value for fieldName retrieved from a row populated by a MapScan operation. // Returns the zero value for the provided type and an appropriate error if the field is not found in // the row or if the value cannot be cast to the provided type. -func getTypedFieldFromRow[T any](fieldName string, row map[string]interface{}) (T, error) { +func getTypedFieldFromRow[T any](fieldName string, row map[string]any) (T, error) { var zeroVal T // used as a placeholder for zero value of type T since we can't directly return nil raw, ok := row[fieldName] diff --git a/common/persistence/cassandra/errors.go b/common/persistence/cassandra/errors.go index ef86f2fb290..058cafc5919 100644 --- a/common/persistence/cassandra/errors.go +++ b/common/persistence/cassandra/errors.go @@ -38,15 +38,15 @@ type ( // Resulting value will be converted to a pointer of underlying type (i.e. *int) and stored in the map. // We do it only for "type" field which is checked for `nil` value. // All other fields are created automatically by gocql with non-pointer types (i.e. int). -func newConflictRecord() map[string]interface{} { +func newConflictRecord() map[string]any { t := new(int) - return map[string]interface{}{ + return map[string]any{ "type": &t, } } func convertErrors( - conflictRecord map[string]interface{}, + conflictRecord map[string]any, conflictIter gocql.Iter, currentRecordRunID string, requestShardID int32, @@ -55,7 +55,7 @@ func convertErrors( requestExecutionCASConditions []executionCASCondition, ) error { - conflictRecords := []map[string]interface{}{conflictRecord} + conflictRecords := []map[string]any{conflictRecord} errors := extractErrors( conflictRecord, currentRecordRunID, @@ -104,7 +104,7 @@ func convertErrors( } func extractErrors( - conflictRecord map[string]interface{}, + conflictRecord map[string]any, currentRecordRunID string, requestShardID int32, requestRangeID int64, @@ -161,7 +161,7 @@ func sortErrors( } func extractShardOwnershipLostError( - conflictRecord map[string]interface{}, + conflictRecord map[string]any, requestShardID int32, requestRangeID int64, ) error { @@ -188,7 +188,7 @@ func extractShardOwnershipLostError( } func extractCurrentWorkflowConflictError( - conflictRecord map[string]interface{}, + conflictRecord map[string]any, currentRecordRunID string, requestCurrentRunID string, ) error { @@ -235,7 +235,7 @@ func extractCurrentWorkflowConflictError( } func extractWorkflowConflictError( - conflictRecord map[string]interface{}, + conflictRecord map[string]any, requestRunID string, requestDBVersion int64, requestNextEventID int64, // TODO deprecate this variable once DB version comparison is the default @@ -284,7 +284,7 @@ func extractWorkflowConflictError( } func printRecords( - records []map[string]interface{}, + records []map[string]any, ) string { binary, _ := json.MarshalIndent(records, "", " ") return string(binary) diff --git a/common/persistence/cassandra/errors_test.go b/common/persistence/cassandra/errors_test.go index 8bdc9cfb28c..73a1f8f2d4f 100644 --- a/common/persistence/cassandra/errors_test.go +++ b/common/persistence/cassandra/errors_test.go @@ -144,18 +144,18 @@ func (s *cassandraErrorsSuite) TestSortErrors_One() { func (s *cassandraErrorsSuite) TestExtractShardOwnershipLostError_Failed() { rangeID := int64(1234) - err := extractShardOwnershipLostError(map[string]interface{}{}, rand.Int31(), rangeID) + err := extractShardOwnershipLostError(map[string]any{}, rand.Int31(), rangeID) s.NoError(err) t := rowTypeExecution - err = extractShardOwnershipLostError(map[string]interface{}{ + err = extractShardOwnershipLostError(map[string]any{ "type": &t, "range_id": rangeID, }, rand.Int31(), rangeID) s.NoError(err) t = rowTypeShard - err = extractShardOwnershipLostError(map[string]interface{}{ + err = extractShardOwnershipLostError(map[string]any{ "type": &t, "range_id": rangeID, }, rand.Int31(), rangeID) @@ -165,7 +165,7 @@ func (s *cassandraErrorsSuite) TestExtractShardOwnershipLostError_Failed() { func (s *cassandraErrorsSuite) TestExtractShardOwnershipLostError_Success() { rangeID := int64(1234) t := rowTypeShard - record := map[string]interface{}{ + record := map[string]any{ "type": &t, "range_id": rangeID, } @@ -178,11 +178,11 @@ func (s *cassandraErrorsSuite) TestExtractCurrentWorkflowConflictError_Failed() runID, _ := uuid.Parse(permanentRunID) currentRunID := uuid.New() - err := extractCurrentWorkflowConflictError(map[string]interface{}{}, permanentRunID, uuid.New().String()) + err := extractCurrentWorkflowConflictError(map[string]any{}, permanentRunID, uuid.New().String()) s.NoError(err) t := rowTypeShard - err = extractCurrentWorkflowConflictError(map[string]interface{}{ + err = extractCurrentWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "current_run_id": gocql.UUID(currentRunID), @@ -190,7 +190,7 @@ func (s *cassandraErrorsSuite) TestExtractCurrentWorkflowConflictError_Failed() s.NoError(err) t = rowTypeExecution - err = extractCurrentWorkflowConflictError(map[string]interface{}{ + err = extractCurrentWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID([16]byte{}), "current_run_id": gocql.UUID(currentRunID), @@ -198,7 +198,7 @@ func (s *cassandraErrorsSuite) TestExtractCurrentWorkflowConflictError_Failed() s.NoError(err) t = rowTypeExecution - err = extractCurrentWorkflowConflictError(map[string]interface{}{ + err = extractCurrentWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "current_run_id": gocql.UUID(currentRunID), @@ -232,7 +232,7 @@ func (s *cassandraErrorsSuite) TestExtractCurrentWorkflowConflictError_Success() lastWriteVersion := rand.Int63() s.NoError(err) t := rowTypeExecution - record := map[string]interface{}{ + record := map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "current_run_id": gocql.UUID(currentRunID), @@ -263,11 +263,11 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Failed() { runID := uuid.New() dbVersion := rand.Int63() + 1 - err := extractWorkflowConflictError(map[string]interface{}{}, runID.String(), dbVersion, rand.Int63()) + err := extractWorkflowConflictError(map[string]any{}, runID.String(), dbVersion, rand.Int63()) s.NoError(err) t := rowTypeShard - err = extractWorkflowConflictError(map[string]interface{}{ + err = extractWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "db_record_version": dbVersion, @@ -275,7 +275,7 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Failed() { s.NoError(err) t = rowTypeExecution - err = extractWorkflowConflictError(map[string]interface{}{ + err = extractWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID([16]byte{}), "db_record_version": dbVersion, @@ -283,7 +283,7 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Failed() { s.NoError(err) t = rowTypeExecution - err = extractWorkflowConflictError(map[string]interface{}{ + err = extractWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "db_record_version": dbVersion, @@ -295,7 +295,7 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Success() { runID := uuid.New() dbVersion := rand.Int63() + 1 t := rowTypeExecution - record := map[string]interface{}{ + record := map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "db_record_version": dbVersion, @@ -310,11 +310,11 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Failed_NextEvent runID := uuid.New() nextEventID := rand.Int63() - err := extractWorkflowConflictError(map[string]interface{}{}, runID.String(), 0, nextEventID) + err := extractWorkflowConflictError(map[string]any{}, runID.String(), 0, nextEventID) s.NoError(err) t := rowTypeShard - err = extractWorkflowConflictError(map[string]interface{}{ + err = extractWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "next_event_id": nextEventID + 1, @@ -322,7 +322,7 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Failed_NextEvent s.NoError(err) t = rowTypeExecution - err = extractWorkflowConflictError(map[string]interface{}{ + err = extractWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID([16]byte{}), "next_event_id": nextEventID + 1, @@ -330,7 +330,7 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Failed_NextEvent s.NoError(err) t = rowTypeExecution - err = extractWorkflowConflictError(map[string]interface{}{ + err = extractWorkflowConflictError(map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "next_event_id": nextEventID, @@ -343,7 +343,7 @@ func (s *cassandraErrorsSuite) TestExtractWorkflowConflictError_Success_NextEven runID := uuid.New() nextEventID := int64(1234) t := rowTypeExecution - record := map[string]interface{}{ + record := map[string]any{ "type": &t, "run_id": gocql.UUID(runID), "next_event_id": nextEventID, diff --git a/common/persistence/cassandra/history_store.go b/common/persistence/cassandra/history_store.go index e2a5abfd231..a2069e5a5f4 100644 --- a/common/persistence/cassandra/history_store.go +++ b/common/persistence/cassandra/history_store.go @@ -175,14 +175,14 @@ func (h *HistoryStore) ReadHistoryBranch( } nodes := make([]p.InternalHistoryNode, 0, request.PageSize) - message := make(map[string]interface{}) + message := make(map[string]any) for iter.MapScan(message) { nodes = append(nodes, convertHistoryNode(message)) - message = make(map[string]interface{}) + message = make(map[string]any) } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("ReadHistoryBranch. Close operation failed. Error: %v", err) + return nil, gocql.ConvertError("ReadHistoryBranch", err) } return &p.InternalReadHistoryBranchResponse{ @@ -334,7 +334,7 @@ func (h *HistoryStore) GetAllHistoryTreeBranches( } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("GetAllHistoryTreeBranches. Close operation failed. Error: %v", err) + return nil, gocql.ConvertError("GetAllHistoryTreeBranches", err) } response := &p.InternalGetAllHistoryTreeBranchesResponse{ @@ -401,7 +401,7 @@ func (h *HistoryStore) GetHistoryBranchUtil() p.HistoryBranchUtil { } func convertHistoryNode( - message map[string]interface{}, + message map[string]any, ) p.InternalHistoryNode { nodeID := message["node_id"].(int64) prevTxnID := message["prev_txn_id"].(int64) diff --git a/common/persistence/cassandra/matching_task_store_queue.go b/common/persistence/cassandra/matching_task_store_queue.go index fecce16fc2d..f5dc8238f00 100644 --- a/common/persistence/cassandra/matching_task_store_queue.go +++ b/common/persistence/cassandra/matching_task_store_queue.go @@ -123,7 +123,7 @@ func (d *taskQueueStore) CreateTaskQueue( request.TaskQueueInfo.EncodingType.String(), ).WithContext(ctx) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, err := query.MapScanCAS(previous) if err != nil { return gocql.ConvertError("CreateTaskQueue", err) @@ -171,7 +171,7 @@ func (d *taskQueueStore) UpdateTaskQueue( ) (*p.UpdateTaskQueueResponse, error) { var err error var applied bool - previous := make(map[string]interface{}) + previous := make(map[string]any) if d.version == matchingTaskVersion1 && request.TaskQueueKind == enumspb.TASK_QUEUE_KIND_STICKY { // V1 TTL logic - only applies to V1 @@ -261,7 +261,7 @@ func (d *taskQueueStore) DeleteTaskQueue( request.RangeID, ).WithContext(ctx) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, err := query.MapScanCAS(previous) if err != nil { return gocql.ConvertError("DeleteTaskQueue", err) diff --git a/common/persistence/cassandra/matching_task_store_user_data.go b/common/persistence/cassandra/matching_task_store_user_data.go index c80395ef545..41c0f66590a 100644 --- a/common/persistence/cassandra/matching_task_store_user_data.go +++ b/common/persistence/cassandra/matching_task_store_user_data.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "go.temporal.io/api/serviceerror" p "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/persistence/nosql/nosqlplugin/cassandra/gocql" ) @@ -67,7 +66,7 @@ func (d *userDataStore) UpdateTaskQueueUserData( ctx context.Context, request *p.InternalUpdateTaskQueueUserDataRequest, ) error { - batch := d.Session.NewBatch(gocql.UnloggedBatch).WithContext(ctx) + batch := d.Session.NewBatch(gocql.LoggedBatch).WithContext(ctx) for taskQueue, update := range request.Updates { if update.Version == 0 { @@ -139,7 +138,7 @@ func (d *userDataStore) ListTaskQueueUserDataEntries(ctx context.Context, reques iter := query.PageSize(request.PageSize).PageState(request.NextPageToken).Iter() response := &p.InternalListTaskQueueUserDataEntriesResponse{} - row := make(map[string]interface{}) + row := make(map[string]any) for iter.MapScan(row) { taskQueue, err := getTypedFieldFromRow[string]("task_queue_name", row) if err != nil { @@ -160,14 +159,14 @@ func (d *userDataStore) ListTaskQueueUserDataEntries(ctx context.Context, reques response.Entries = append(response.Entries, p.InternalTaskQueueUserDataEntry{TaskQueue: taskQueue, Data: p.NewDataBlob(data, dataEncoding), Version: version}) - row = make(map[string]interface{}) // Reinitialize map as initialized fails on unmarshalling + row = make(map[string]any) // Reinitialize map as initialized fails on unmarshalling } if len(iter.PageState()) > 0 { response.NextPageToken = iter.PageState() } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("ListTaskQueueUserDataEntries operation failed. Error: %v", err) + return nil, gocql.ConvertError("ListTaskQueueUserDataEntries", err) } return response, nil } @@ -177,7 +176,7 @@ func (d *userDataStore) GetTaskQueuesByBuildId(ctx context.Context, request *p.G iter := query.PageSize(listTaskQueueNamesByBuildIdPageSize).Iter() var taskQueues []string - row := make(map[string]interface{}) + row := make(map[string]any) for { for iter.MapScan(row) { @@ -193,7 +192,7 @@ func (d *userDataStore) GetTaskQueuesByBuildId(ctx context.Context, request *p.G taskQueues = append(taskQueues, taskQueue) - row = make(map[string]interface{}) // Reinitialize map as initialized fails on unmarshalling + row = make(map[string]any) // Reinitialize map as initialized fails on unmarshalling } if len(iter.PageState()) == 0 { break @@ -201,7 +200,7 @@ func (d *userDataStore) GetTaskQueuesByBuildId(ctx context.Context, request *p.G } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("GetTaskQueuesByBuildId operation failed. Error: %v", err) + return nil, gocql.ConvertError("GetTaskQueuesByBuildId", err) } return taskQueues, nil } diff --git a/common/persistence/cassandra/matching_task_store_v1.go b/common/persistence/cassandra/matching_task_store_v1.go index 11ba48f3d89..aa9816b6c7f 100644 --- a/common/persistence/cassandra/matching_task_store_v1.go +++ b/common/persistence/cassandra/matching_task_store_v1.go @@ -103,7 +103,7 @@ func (d *matchingTaskStoreV1) CreateTasks( request.RangeID, ) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, _, err := d.Session.MapExecuteBatchCAS(batch, previous) if err != nil { return nil, gocql.ConvertError("CreateTasks", err) @@ -140,7 +140,7 @@ func (d *matchingTaskStoreV1) GetTasks( iter := query.PageSize(request.PageSize).PageState(request.NextPageToken).Iter() response := &p.InternalGetTasksResponse{} - task := make(map[string]interface{}) + task := make(map[string]any) for iter.MapScan(task) { _, ok := task["task_id"] if !ok { // no tasks, but static column record returned @@ -168,14 +168,14 @@ func (d *matchingTaskStoreV1) GetTasks( } response.Tasks = append(response.Tasks, p.NewDataBlob(taskVal, encodingVal)) - task = make(map[string]interface{}) // Reinitialize map as initialized fails on unmarshalling + task = make(map[string]any) // Reinitialize map as initialized fails on unmarshalling } if len(iter.PageState()) > 0 { response.NextPageToken = iter.PageState() } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("GetTasks operation failed. Error: %v", err) + return nil, gocql.ConvertError("GetTasks", err) } return response, nil } diff --git a/common/persistence/cassandra/matching_task_store_v2.go b/common/persistence/cassandra/matching_task_store_v2.go index 190be5f7773..afc57514658 100644 --- a/common/persistence/cassandra/matching_task_store_v2.go +++ b/common/persistence/cassandra/matching_task_store_v2.go @@ -20,24 +20,24 @@ const ( `WHERE namespace_id = ? ` + `and task_queue_name = ? ` + `and task_queue_type = ? ` + - `and type = ? ` + - `and (pass, task_id) >= (?, ?)` + `and (type, pass, task_id) >= (?, ?, ?) ` + + `and (type, pass, task_id) < (?, ?, ?)` templateGetTasksQuery_v2_limit = `SELECT task_id, task, task_encoding ` + `FROM tasks_v2 ` + `WHERE namespace_id = ? ` + `and task_queue_name = ? ` + `and task_queue_type = ? ` + - `and type = ? ` + - `and (pass, task_id) >= (?, ?) ` + + `and (type, pass, task_id) >= (?, ?, ?) ` + + `and (type, pass, task_id) < (?, ?, ?) ` + `LIMIT ?` templateCompleteTasksLessThanQuery_v2 = `DELETE FROM tasks_v2 ` + `WHERE namespace_id = ? ` + `AND task_queue_name = ? ` + `AND task_queue_type = ? ` + - `AND type = ? ` + - `and (pass, task_id) < (?, ?)` + `AND (type, pass, task_id) >= (?, ?, ?) ` + + `AND (type, pass, task_id) < (?, ?, ?)` ) // matchingTaskStoreV2 is a fork of matchingTaskStoreV1 that uses a new task schema. @@ -97,7 +97,7 @@ func (d *matchingTaskStoreV2) CreateTasks( request.RangeID, ) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, _, err := d.Session.MapExecuteBatchCAS(batch, previous) if err != nil { return nil, gocql.ConvertError("CreateTasks", err) @@ -128,14 +128,18 @@ func (d *matchingTaskStoreV2) GetTasks( // Reading taskqueue tasks need to be quorum level consistent, otherwise we could lose tasks var query gocql.Query + rowType := rowTypeTaskInSubqueue(request.Subqueue) if request.UseLimit { query = d.Session.Query(templateGetTasksQuery_v2_limit, request.NamespaceID, request.TaskQueue, request.TaskType, - rowTypeTaskInSubqueue(request.Subqueue), + rowType, request.InclusiveMinPass, request.InclusiveMinTaskID, + rowType, + int64(math.MaxInt64), + int64(math.MaxInt64), request.PageSize, ) } else { @@ -143,15 +147,18 @@ func (d *matchingTaskStoreV2) GetTasks( request.NamespaceID, request.TaskQueue, request.TaskType, - rowTypeTaskInSubqueue(request.Subqueue), + rowType, request.InclusiveMinPass, request.InclusiveMinTaskID, + rowType, + int64(math.MaxInt64), + int64(math.MaxInt64), ) } iter := query.WithContext(ctx).PageSize(request.PageSize).PageState(request.NextPageToken).Iter() response := &p.InternalGetTasksResponse{} - task := make(map[string]interface{}) + task := make(map[string]any) for iter.MapScan(task) { _, ok := task["task_id"] if !ok { // no tasks, but static column record returned @@ -179,14 +186,14 @@ func (d *matchingTaskStoreV2) GetTasks( } response.Tasks = append(response.Tasks, p.NewDataBlob(taskVal, encodingVal)) - task = make(map[string]interface{}) // Reinitialize map as initialized fails on unmarshalling + task = make(map[string]any) // Reinitialize map as initialized fails on unmarshalling } if len(iter.PageState()) > 0 { response.NextPageToken = iter.PageState() } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("GetTasks operation failed. Error: %v", err) + return nil, gocql.ConvertError("GetTasks", err) } return response, nil } @@ -203,12 +210,16 @@ func (d *matchingTaskStoreV2) CompleteTasksLessThan( return 0, serviceerror.NewInternal("invalid CompleteTasksLessThan request on fair queue") } + rowType := rowTypeTaskInSubqueue(request.Subqueue) query := d.Session.Query( templateCompleteTasksLessThanQuery_v2, request.NamespaceID, request.TaskQueueName, request.TaskType, - rowTypeTaskInSubqueue(request.Subqueue), + rowType, + int64(0), + int64(0), + rowType, request.ExclusiveMaxPass, request.ExclusiveMaxTaskID, ).WithContext(ctx) diff --git a/common/persistence/cassandra/metadata_store.go b/common/persistence/cassandra/metadata_store.go index 74ac8139e72..cb84913caf2 100644 --- a/common/persistence/cassandra/metadata_store.go +++ b/common/persistence/cassandra/metadata_store.go @@ -102,10 +102,10 @@ func (m *MetadataStore) CreateNamespace( ) (*p.CreateNamespaceResponse, error) { query := m.session.Query(templateCreateNamespaceQuery, request.ID, request.Name).WithContext(ctx) - existingRow := make(map[string]interface{}) + existingRow := make(map[string]any) applied, err := query.MapScanCAS(existingRow) if err != nil { - return nil, serviceerror.NewUnavailablef("CreateNamespace operation failed. Inserting into namespaces table. Error: %v", err) + return nil, gocql.ConvertError("CreateNamespace", err) } if !applied { @@ -146,10 +146,10 @@ func (m *MetadataStore) CreateNamespaceInV2Table( ) m.updateMetadataBatch(batch, metadata.NotificationVersion) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, iter, err := m.session.MapExecuteBatchCAS(batch, previous) if err != nil { - return nil, serviceerror.NewUnavailablef("CreateNamespace operation failed. Inserting into namespaces table. Error: %v", err) + return nil, gocql.ConvertError("CreateNamespace", err) } defer func() { _ = iter.Close() }() deleteOrphanNamespace := func() { @@ -167,7 +167,7 @@ func (m *MetadataStore) CreateNamespaceInV2Table( return nil, err } if !matched { - m := make(map[string]interface{}) + m := make(map[string]any) if iter.MapScan(m) { previous = m } @@ -218,10 +218,10 @@ func (m *MetadataStore) UpdateNamespace( ) m.updateMetadataBatch(batch, request.NotificationVersion) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, iter, err := m.session.MapExecuteBatchCAS(batch, previous) if err != nil { - return serviceerror.NewUnavailablef("UpdateNamespace operation failed. Error: %v", err) + return gocql.ConvertError("UpdateNamespace", err) } defer func() { _ = iter.Close() }() @@ -252,7 +252,7 @@ func (m *MetadataStore) RenameNamespace( request.Name, request.Id, ).WithContext(ctx).Exec(); updateErr != nil { - return serviceerror.NewUnavailablef("RenameNamespace operation failed to update 'namespaces_by_id' table. Error: %v", updateErr) + return gocql.ConvertError("RenameNamespace", updateErr) } // Step 2. @@ -272,10 +272,10 @@ func (m *MetadataStore) RenameNamespace( ) m.updateMetadataBatch(batch, request.NotificationVersion) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, iter, err := m.session.MapExecuteBatchCAS(batch, previous) if err != nil { - return serviceerror.NewUnavailablef("RenameNamespace operation failed. Error: %v", err) + return gocql.ConvertError("RenameNamespace", err) } defer func() { _ = iter.Close() }() @@ -311,7 +311,7 @@ func (m *MetadataStore) GetNamespace( } return serviceerror.NewNamespaceNotFound(identity) } - return serviceerror.NewUnavailablef("GetNamespace operation failed. Error %v", err) + return gocql.ConvertError("GetNamespace", err) } namespace := request.Name @@ -396,7 +396,7 @@ func (m *MetadataStore) ListNamespaces( nextPageToken = nil } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("ListNamespaces operation failed. Error: %v", err) + return nil, gocql.ConvertError("ListNamespaces", err) } if len(nextPageToken) == 0 { @@ -488,12 +488,12 @@ func (m *MetadataStore) updateMetadataBatch( func (m *MetadataStore) deleteNamespace(ctx context.Context, name string, ID []byte) error { query := m.session.Query(templateDeleteNamespaceByNameQueryV2, constNamespacePartition, name).WithContext(ctx) if err := query.Exec(); err != nil { - return serviceerror.NewUnavailablef("DeleteNamespaceByName operation failed. Error %v", err) + return gocql.ConvertError("DeleteNamespaceByName", err) } query = m.session.Query(templateDeleteNamespaceQuery, ID).WithContext(ctx) if err := query.Exec(); err != nil { - return serviceerror.NewUnavailablef("DeleteNamespace operation failed. Error %v", err) + return gocql.ConvertError("DeleteNamespace", err) } return nil @@ -509,7 +509,7 @@ func (m *MetadataStore) Close() { } } -func hasNameConflict[T comparable](row map[string]interface{}, column string, value T) (bool, error) { +func hasNameConflict[T comparable](row map[string]any, column string, value T) (bool, error) { existingValue, ok := row[column] if !ok { msg := fmt.Sprintf("Unexpected error: column not found %q", column) diff --git a/common/persistence/cassandra/mutable_state_store.go b/common/persistence/cassandra/mutable_state_store.go index da6bf954ef4..9b0fc627571 100644 --- a/common/persistence/cassandra/mutable_state_store.go +++ b/common/persistence/cassandra/mutable_state_store.go @@ -506,7 +506,7 @@ func (d *MutableStateStore) GetWorkflowExecution( rowTypeExecutionTaskID, ).WithContext(ctx) - result := make(map[string]interface{}) + result := make(map[string]any) if err := query.MapScan(result); err != nil { return nil, gocql.ConvertError("GetWorkflowExecution", err) } @@ -573,7 +573,7 @@ func (d *MutableStateStore) GetWorkflowExecution( } state.ChasmNodes = chasmNodeBlobs - eList := result["buffered_events_list"].([]map[string]interface{}) + eList := result["buffered_events_list"].([]map[string]any) //nolint:revive // unchecked-type-assertion: consistent with surrounding Cassandra result parsing bufferedEventsBlobs := make([]*commonpb.DataBlob, 0, len(eList)) for _, v := range eList { blob := createHistoryEventBatchBlob(v) @@ -969,7 +969,7 @@ func (d *MutableStateStore) GetCurrentExecution( rowTypeExecutionTaskID, ).WithContext(ctx) - result := make(map[string]interface{}) + result := make(map[string]any) if err := query.MapScan(result); err != nil { return nil, gocql.ConvertError("GetCurrentExecution", err) } @@ -1060,7 +1060,7 @@ func (d *MutableStateStore) ListConcreteExecutions( iter := query.PageSize(request.PageSize).PageState(request.PageToken).Iter() response := &p.InternalListConcreteExecutionsResponse{} - result := make(map[string]interface{}) + result := make(map[string]any) for iter.MapScan(result) { if execution, ok := result["execution"]; ok { executionBytes, ok := execution.([]byte) @@ -1070,7 +1070,7 @@ func (d *MutableStateStore) ListConcreteExecutions( if len(executionBytes) == 0 { // current record has no value in execution column. - result = make(map[string]interface{}) + result = make(map[string]any) continue } @@ -1081,7 +1081,7 @@ func (d *MutableStateStore) ListConcreteExecutions( response.States = append(response.States, state) } - result = make(map[string]interface{}) + result = make(map[string]any) } if len(iter.PageState()) > 0 { response.NextPageToken = iter.PageState() @@ -1108,7 +1108,7 @@ func (d *MutableStateStore) getCurrentRecordRunID( } func mutableStateFromRow( - result map[string]interface{}, + result map[string]any, ) (*p.InternalWorkflowMutableState, error) { eiBytes, ok := result["execution"].([]byte) if !ok { @@ -1139,7 +1139,7 @@ func mutableStateFromRow( } func executionStateBlobFromRow( - result map[string]interface{}, + result map[string]any, ) (*commonpb.DataBlob, error) { state, ok := result["execution_state"].([]byte) if !ok { diff --git a/common/persistence/cassandra/mutable_state_task_store.go b/common/persistence/cassandra/mutable_state_task_store.go index 5354778b891..a07dc3ca601 100644 --- a/common/persistence/cassandra/mutable_state_task_store.go +++ b/common/persistence/cassandra/mutable_state_task_store.go @@ -200,7 +200,7 @@ func (d *MutableStateTaskStore) AddHistoryTasks( request.RangeID, ) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, iter, err := d.Session.MapExecuteBatchCAS(batch, previous) if err != nil { return gocql.ConvertError("AddTasks", err) @@ -217,7 +217,7 @@ func (d *MutableStateTaskStore) AddHistoryTasks( Msg: fmt.Sprintf("Failed to add tasks. Request RangeID: %v, Actual RangeID: %v", request.RangeID, previousRangeID), } } else { - return serviceerror.NewUnavailable("AddTasks operation failed: %v") + return serviceerror.NewUnavailable("AddTasks operation failed because of conditional failure.") } } return nil diff --git a/common/persistence/cassandra/nexus_endpoint_store.go b/common/persistence/cassandra/nexus_endpoint_store.go index 0c2e4de46a2..6698199d428 100644 --- a/common/persistence/cassandra/nexus_endpoint_store.go +++ b/common/persistence/cassandra/nexus_endpoint_store.go @@ -100,15 +100,15 @@ func (s *NexusEndpointStore) CreateOrUpdateNexusEndpoint( request.LastKnownTableVersion) } - previousPartitionStatus := make(map[string]interface{}) - applied, iter, err := s.session.MapExecuteBatchCAS(batch, previousPartitionStatus) + row1 := make(map[string]any) + applied, iter, err := s.session.MapExecuteBatchCAS(batch, row1) if err != nil { return gocql.ConvertError("CreateOrUpdateNexusEndpoint", err) } - previousEndpoint := make(map[string]interface{}) - iter.MapScan(previousEndpoint) + row2 := make(map[string]any) + iter.MapScan(row2) err = iter.Close() if err != nil { @@ -116,6 +116,23 @@ func (s *NexusEndpointStore) CreateOrUpdateNexusEndpoint( } if !applied { + // Classify the two CAS result rows by their "type" column. This is needed because + // Cassandra returns rows in clustering key order while ScyllaDB returns them in + // statement order. + var previousPartitionStatus, previousEndpoint map[string]any + rowType, err := getTypedFieldFromRow[int]("type", row1) + if err != nil { + return fmt.Errorf("CreateOrUpdateNexusEndpoint: error reading type from CAS result row: %w", err) + } + switch rowType { + case rowTypePartitionStatus: + previousPartitionStatus, previousEndpoint = row1, row2 + case rowTypeNexusEndpoint: + previousPartitionStatus, previousEndpoint = row2, row1 + default: + return fmt.Errorf("CreateOrUpdateNexusEndpoint: unexpected row type %d in CAS result", rowType) + } + currentTableVersion, err := getTypedFieldFromRow[int64]("version", previousPartitionStatus) if err != nil { return fmt.Errorf("error retrieving current table version: %w", err) @@ -195,7 +212,7 @@ func (s *NexusEndpointStore) ListNexusEndpoints( } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("ListNexusEndpoints operation failed: %v", err) + return nil, gocql.ConvertError("ListNexusEndpoints", err) } currentTableVersion, err := s.getTableVersion(ctx) @@ -225,17 +242,20 @@ func (s *NexusEndpointStore) DeleteNexusEndpoint( ) error { batch := s.session.NewBatch(gocql.LoggedBatch).WithContext(ctx) - batch.Query(templateDeleteEndpointQuery, - rowTypeNexusEndpoint, - request.ID) - + // Table version update must be the first statement so it is returned as the first CAS result + // row. CAS result row ordering differs between Cassandra (clustering key order) and ScyllaDB + // (statement order), but placing the type=0 partition status first satisfies both. batch.Query(templateUpdateTableVersion, request.LastKnownTableVersion+1, rowTypePartitionStatus, tableVersionEndpointID, request.LastKnownTableVersion) - previousPartitionStatus := make(map[string]interface{}) + batch.Query(templateDeleteEndpointQuery, + rowTypeNexusEndpoint, + request.ID) + + previousPartitionStatus := make(map[string]any) applied, iter, err := s.session.MapExecuteBatchCAS(batch, previousPartitionStatus) if err != nil { @@ -274,7 +294,7 @@ func (s *NexusEndpointStore) listFirstPageWithVersion( query := s.session.Query(templateListEndpointsFirstPageQuery).WithContext(ctx) iter := query.PageSize(request.PageSize + 1).PageState(nil).Iter() // Use PageSize+1 to account for partitionStatus row - partitionStateRow := make(map[string]interface{}) + partitionStateRow := make(map[string]any) found := iter.MapScan(partitionStateRow) if !found { cassErr := iter.Close() @@ -323,9 +343,9 @@ func (s *NexusEndpointStore) getTableVersion(ctx context.Context) (int64, error) func (s *NexusEndpointStore) getEndpointList(iter gocql.Iter) ([]p.InternalNexusEndpoint, error) { var endpoints []p.InternalNexusEndpoint - row := make(map[string]interface{}) + row := make(map[string]any) for iter.MapScan(row) { - id, err := getTypedFieldFromRow[interface{}]("id", row) + id, err := getTypedFieldFromRow[any]("id", row) if err != nil { return nil, err } @@ -348,7 +368,7 @@ func (s *NexusEndpointStore) getEndpointList(iter gocql.Iter) ([]p.InternalNexus Data: p.NewDataBlob(data, dataEncoding), }) - row = make(map[string]interface{}) + row = make(map[string]any) } return endpoints, nil diff --git a/common/persistence/cassandra/queue_store.go b/common/persistence/cassandra/queue_store.go index 0f6ba96d64d..0a85830eb12 100644 --- a/common/persistence/cassandra/queue_store.go +++ b/common/persistence/cassandra/queue_store.go @@ -6,7 +6,6 @@ import ( commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" - "go.temporal.io/api/serviceerror" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/log" "go.temporal.io/server/common/persistence" @@ -94,7 +93,7 @@ func (q *QueueStore) tryEnqueue( blob *commonpb.DataBlob, ) (int64, error) { query := q.session.Query(templateEnqueueMessageQuery, queueType, messageID, blob.Data, blob.EncodingType.String()).WithContext(ctx) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, err := query.MapScanCAS(previous) if err != nil { return persistence.EmptyQueueMessageID, gocql.ConvertError("tryEnqueue", err) @@ -113,7 +112,7 @@ func (q *QueueStore) getLastMessageID( ) (int64, error) { query := q.session.Query(templateGetLastMessageIDQuery, queueType).WithContext(ctx) - result := make(map[string]interface{}) + result := make(map[string]any) err := query.MapScan(result) if err != nil { if gocql.IsNotFoundError(err) { @@ -139,15 +138,15 @@ func (q *QueueStore) ReadMessages( iter := query.Iter() var result []*persistence.QueueMessage - message := make(map[string]interface{}) + message := make(map[string]any) for iter.MapScan(message) { queueMessage := convertQueueMessage(message) result = append(result, queueMessage) - message = make(map[string]interface{}) + message = make(map[string]any) } if err := iter.Close(); err != nil { - return nil, serviceerror.NewUnavailablef("ReadMessages operation failed. Error: %v", err) + return nil, gocql.ConvertError("ReadMessages", err) } return result, nil @@ -170,11 +169,11 @@ func (q *QueueStore) ReadMessagesFromDLQ( iter := query.PageSize(pageSize).PageState(pageToken).Iter() var result []*persistence.QueueMessage - message := make(map[string]interface{}) + message := make(map[string]any) for iter.MapScan(message) { queueMessage := convertQueueMessage(message) result = append(result, queueMessage) - message = make(map[string]interface{}) + message = make(map[string]any) } var nextPageToken []byte @@ -182,7 +181,7 @@ func (q *QueueStore) ReadMessagesFromDLQ( nextPageToken = iter.PageState() } if err := iter.Close(); err != nil { - return nil, nil, serviceerror.NewUnavailablef("ReadMessagesFromDLQ operation failed. Error: %v", err) + return nil, nil, gocql.ConvertError("ReadMessagesFromDLQ", err) } return result, nextPageToken, nil @@ -195,7 +194,7 @@ func (q *QueueStore) DeleteMessagesBefore( query := q.session.Query(templateDeleteMessagesBeforeQuery, q.queueType, messageID).WithContext(ctx) if err := query.Exec(); err != nil { - return serviceerror.NewUnavailablef("DeleteMessagesBefore operation failed. Error %v", err) + return gocql.ConvertError("DeleteMessagesBefore", err) } return nil } @@ -208,7 +207,7 @@ func (q *QueueStore) DeleteMessageFromDLQ( // Use negative queue type as the dlq type query := q.session.Query(templateDeleteMessageQuery, q.getDLQTypeFromQueueType(), messageID).WithContext(ctx) if err := query.Exec(); err != nil { - return serviceerror.NewUnavailablef("DeleteMessageFromDLQ operation failed. Error %v", err) + return gocql.ConvertError("DeleteMessageFromDLQ", err) } return nil @@ -223,7 +222,7 @@ func (q *QueueStore) RangeDeleteMessagesFromDLQ( // Use negative queue type as the dlq type query := q.session.Query(templateDeleteMessagesQuery, q.getDLQTypeFromQueueType(), firstMessageID, lastMessageID).WithContext(ctx) if err := query.Exec(); err != nil { - return serviceerror.NewUnavailablef("RangeDeleteMessagesFromDLQ operation failed. Error %v", err) + return gocql.ConvertError("RangeDeleteMessagesFromDLQ", err) } return nil @@ -282,7 +281,7 @@ func (q *QueueStore) insertInitialQueueMetadataRecord( blob.EncodingType.String(), version, ).WithContext(ctx) - _, err := query.MapScanCAS(make(map[string]interface{})) + _, err := query.MapScanCAS(make(map[string]any)) if err != nil { return fmt.Errorf("failed to insert initial queue metadata record: %v, Type: %v", err, queueType) } @@ -296,7 +295,7 @@ func (q *QueueStore) getQueueMetadata( ) (*persistence.InternalQueueMetadata, error) { query := q.session.Query(templateGetQueueMetadataQuery, queueType).WithContext(ctx) - message := make(map[string]interface{}) + message := make(map[string]any) err := query.MapScan(message) if err != nil { return nil, err @@ -325,7 +324,7 @@ func (q *QueueStore) updateAckLevel( queueType, metadata.Version, // condition update ).WithContext(ctx) - applied, err := query.MapScanCAS(make(map[string]interface{})) + applied, err := query.MapScanCAS(make(map[string]any)) if err != nil { return gocql.ConvertError("updateAckLevel", err) } @@ -369,7 +368,7 @@ func (q *QueueStore) initializeDLQMetadata( } func convertQueueMessage( - message map[string]interface{}, + message map[string]any, ) *persistence.QueueMessage { id := message["message_id"].(int64) @@ -386,7 +385,7 @@ func convertQueueMessage( } func convertQueueMetadata( - message map[string]interface{}, + message map[string]any, serializer serialization.Serializer, ) (*persistence.InternalQueueMetadata, error) { diff --git a/common/persistence/cassandra/queue_v2_store.go b/common/persistence/cassandra/queue_v2_store.go index 6ddcadd9ba8..c5f968fd44d 100644 --- a/common/persistence/cassandra/queue_v2_store.go +++ b/common/persistence/cassandra/queue_v2_store.go @@ -225,7 +225,7 @@ func (s *queueV2Store) CreateQueue( bytes, enumspb.ENCODING_TYPE_PROTO3.String(), 0, - ).WithContext(ctx).MapScanCAS(make(map[string]interface{})) + ).WithContext(ctx).MapScanCAS(make(map[string]any)) if err != nil { return nil, gocql.ConvertError("QueueV2CreateQueue", err) } @@ -320,7 +320,7 @@ func (s *queueV2Store) updateQueue( queueType, queueName, version, - ).WithContext(ctx).MapScanCAS(make(map[string]interface{})) + ).WithContext(ctx).MapScanCAS(make(map[string]any)) if err != nil { return gocql.ConvertError("QueueV2UpdateQueueMetadata", err) } @@ -350,7 +350,7 @@ func (s *queueV2Store) tryInsert( messageID, blob.Data, blob.EncodingType.String(), - ).WithContext(ctx).MapScanCAS(make(map[string]interface{})) + ).WithContext(ctx).MapScanCAS(make(map[string]any)) if err != nil { return gocql.ConvertError("QueueV2EnqueueMessage", err) } diff --git a/common/persistence/cassandra/shard_store.go b/common/persistence/cassandra/shard_store.go index 955029278e6..8e5eca59440 100644 --- a/common/persistence/cassandra/shard_store.go +++ b/common/persistence/cassandra/shard_store.go @@ -101,7 +101,7 @@ func (d *ShardStore) GetOrCreateShard( rangeID, ).WithContext(ctx) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, err := query.MapScanCAS(previous) if err != nil { return nil, gocql.ConvertError("GetOrCreateShard", err) @@ -134,7 +134,7 @@ func (d *ShardStore) UpdateShard( request.PreviousRangeID, ).WithContext(ctx) - previous := make(map[string]interface{}) + previous := make(map[string]any) applied, err := query.MapScanCAS(previous) if err != nil { return gocql.ConvertError("UpdateShard", err) diff --git a/common/persistence/cassandra/util.go b/common/persistence/cassandra/util.go index 97ea7c6c8fd..f3417c9f112 100644 --- a/common/persistence/cassandra/util.go +++ b/common/persistence/cassandra/util.go @@ -1092,11 +1092,11 @@ func updateBufferedEvents( defaultVisibilityTimestamp, rowTypeExecutionTaskID) } else if newBufferedEvents != nil { - values := make(map[string]interface{}) + values := make(map[string]any) values["encoding_type"] = newBufferedEvents.EncodingType.String() values["version"] = int64(0) values["data"] = newBufferedEvents.Data - newEventValues := []map[string]interface{}{values} + newEventValues := []map[string]any{values} batch.Query(templateAppendBufferedEventsQuery, newEventValues, shardID, @@ -1124,7 +1124,7 @@ func convertBlobMapToByteMap[T comparable]( } func createHistoryEventBatchBlob( - result map[string]interface{}, + result map[string]any, ) *commonpb.DataBlob { eventBatch := &commonpb.DataBlob{EncodingType: enumspb.ENCODING_TYPE_UNSPECIFIED} for k, v := range result { diff --git a/common/persistence/client/quotas_test.go b/common/persistence/client/quotas_test.go index f54ee3d7e49..874fdcf4413 100644 --- a/common/persistence/client/quotas_test.go +++ b/common/persistence/client/quotas_test.go @@ -105,7 +105,7 @@ func (s *quotasSuite) TestPriorityNamespaceRateLimiter_DoesLimit() { requestTime := time.Now() wasLimited := false - for i := 0; i < 2; i++ { + for range 2 { if !limiter.Allow(requestTime, request) { wasLimited = true } @@ -140,7 +140,7 @@ func (s *quotasSuite) TestPerShardNamespaceRateLimiter_DoesLimit() { requestTime := time.Now() wasLimited := false - for i := 0; i < 2; i++ { + for range 2 { if !limiter.Allow(requestTime, request) { wasLimited = true } @@ -179,7 +179,7 @@ func (s *quotasSuite) TestOperatorPrioritized() { requestTime := time.Now() wasLimited := false - for i := 0; i < 6; i++ { + for range 6 { if !limiter.Allow(requestTime, apiRequest) { wasLimited = true s.True(limiter.Allow(requestTime, operatorRequest)) diff --git a/common/persistence/history_node_util_test.go b/common/persistence/history_node_util_test.go index 73557a9021a..af82a63c149 100644 --- a/common/persistence/history_node_util_test.go +++ b/common/persistence/history_node_util_test.go @@ -50,7 +50,7 @@ func (s *historyNodeMetadataSuite) TestIndexNodeIDToNode() { transactionIDToNode := map[int64]historyNodeMetadata{} for nodeID := common.FirstEventID; nodeID < int64(numNodeIDs+1); nodeID++ { var nextTransactionID *int64 - for i := 0; i < nodePerNodeID; i++ { + for range nodePerNodeID { transactionID := rand.Int63() if nextTransactionID == nil || *nextTransactionID < transactionID { nextTransactionID = &transactionID @@ -83,7 +83,7 @@ func (s *historyNodeMetadataSuite) TestReverselyLinkNode() { transactionIDToNode := map[int64]historyNodeMetadata{} for nodeID := common.FirstEventID; nodeID < int64(numNodeIDs+1); nodeID++ { var nextTransactionID *int64 - for i := 0; i < nodePerNodeID; i++ { + for range nodePerNodeID { transactionID := rand.Int63() if nextTransactionID == nil || *nextTransactionID < transactionID { nextTransactionID = &transactionID diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/batch.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/batch.go index 1f52c90da88..efacee18a11 100644 --- a/common/persistence/nosql/nosqlplugin/cassandra/gocql/batch.go +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/batch.go @@ -32,7 +32,7 @@ func newBatch( } } -func (b *Batch) Query(stmt string, args ...interface{}) { +func (b *Batch) Query(stmt string, args ...any) { b.gocqlBatch.Query(stmt, args...) } diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/errors.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/errors.go index 90b1c38bed1..d057f4c493f 100644 --- a/common/persistence/nosql/nosqlplugin/cassandra/gocql/errors.go +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/errors.go @@ -56,6 +56,10 @@ func ConvertError( } } + if e, ok := errors.AsType[*serviceerror.ResourceExhausted](err); ok { + return e + } + return serviceerror.NewUnavailablef("operation %v encountered %v", operation, err.Error()) } diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/errors_test.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/errors_test.go new file mode 100644 index 00000000000..b82170a3d4b --- /dev/null +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/errors_test.go @@ -0,0 +1,145 @@ +package gocql + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/gocql/gocql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/common/persistence" +) + +// fakeRequestError implements gocql.RequestError for testing. +type fakeRequestError struct { + code int + message string +} + +func (e fakeRequestError) Code() int { return e.code } +func (e fakeRequestError) Message() string { return e.message } +func (e fakeRequestError) Error() string { return e.message } + +func TestConvertError(t *testing.T) { + const op = "TestOp" + + tests := map[string]struct { + input error + checkFunc func(*testing.T, error) + }{ + "nil": { + input: nil, + checkFunc: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + "context.DeadlineExceeded": { + input: context.DeadlineExceeded, + checkFunc: func(t *testing.T, err error) { + var te *persistence.TimeoutError + require.ErrorAs(t, err, &te) + assert.Contains(t, te.Msg, op) + }, + }, + "gocql.ErrTimeoutNoResponse": { + input: gocql.ErrTimeoutNoResponse, + checkFunc: func(t *testing.T, err error) { + var te *persistence.TimeoutError + require.ErrorAs(t, err, &te) + assert.Contains(t, te.Msg, op) + }, + }, + "gocql.ErrConnectionClosed": { + input: gocql.ErrConnectionClosed, + checkFunc: func(t *testing.T, err error) { + var te *persistence.TimeoutError + require.ErrorAs(t, err, &te) + assert.Contains(t, te.Msg, op) + }, + }, + "wrapped DeadlineExceeded": { + input: fmt.Errorf("outer: %w", context.DeadlineExceeded), + checkFunc: func(t *testing.T, err error) { + var te *persistence.TimeoutError + require.ErrorAs(t, err, &te) + }, + }, + "gocql.ErrNotFound": { + input: gocql.ErrNotFound, + checkFunc: func(t *testing.T, err error) { + var nf *serviceerror.NotFound + require.ErrorAs(t, err, &nf) + assert.Contains(t, nf.Message, op) + }, + }, + "RequestErrWriteTimeout": { + input: &gocql.RequestErrWriteTimeout{}, + checkFunc: func(t *testing.T, err error) { + var te *persistence.TimeoutError + require.ErrorAs(t, err, &te) + assert.Contains(t, te.Msg, op) + }, + }, + "RequestError ErrCodeOverloaded": { + input: fakeRequestError{code: gocql.ErrCodeOverloaded, message: "overloaded"}, + checkFunc: func(t *testing.T, err error) { + var re *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &re) + assert.Equal(t, enumspb.RESOURCE_EXHAUSTED_CAUSE_SYSTEM_OVERLOADED, re.Cause) + assert.Equal(t, enumspb.RESOURCE_EXHAUSTED_SCOPE_SYSTEM, re.Scope) + assert.Contains(t, re.Message, op) + }, + }, + "RequestError ErrCodeInvalid disk usage": { + input: fakeRequestError{code: gocql.ErrCodeInvalid, message: "Disk usage exceeds failure threshold"}, + checkFunc: func(t *testing.T, err error) { + var re *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &re) + assert.Equal(t, enumspb.RESOURCE_EXHAUSTED_CAUSE_PERSISTENCE_STORAGE_LIMIT, re.Cause) + assert.Equal(t, enumspb.RESOURCE_EXHAUSTED_SCOPE_SYSTEM, re.Scope) + assert.Contains(t, re.Message, op) + }, + }, + "RequestError ErrCodeInvalid other": { + input: fakeRequestError{code: gocql.ErrCodeInvalid, message: "some invalid query"}, + checkFunc: func(t *testing.T, err error) { + var unavail *serviceerror.Unavailable + require.ErrorAs(t, err, &unavail) + assert.Contains(t, unavail.Message, op) + }, + }, + "ResourceExhausted passthrough": { + input: &serviceerror.ResourceExhausted{ + Cause: enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, + Scope: enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, + Message: "rps limit", + }, + checkFunc: func(t *testing.T, err error) { + var re *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &re) + assert.Equal(t, enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, re.Cause) + assert.Equal(t, enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, re.Scope) + assert.Equal(t, "rps limit", re.Message) + }, + }, + "unknown error becomes unavailable": { + input: errors.New("some unknown error"), + checkFunc: func(t *testing.T, err error) { + var unavail *serviceerror.Unavailable + require.ErrorAs(t, err, &unavail) + assert.Contains(t, unavail.Message, op) + assert.Contains(t, unavail.Message, "some unknown error") + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tc.checkFunc(t, ConvertError(op, tc.input)) + }) + } +} diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/interfaces.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/interfaces.go index 491b2e2af52..131d82dbc53 100644 --- a/common/persistence/nosql/nosqlplugin/cassandra/gocql/interfaces.go +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/interfaces.go @@ -14,10 +14,10 @@ import ( type ( // Session is the interface for interacting with the database. Session interface { - Query(string, ...interface{}) Query + Query(string, ...any) Query NewBatch(BatchType) *Batch ExecuteBatch(*Batch) error - MapExecuteBatchCAS(*Batch, map[string]interface{}) (bool, Iter, error) + MapExecuteBatchCAS(*Batch, map[string]any) (bool, Iter, error) AwaitSchemaAgreement(ctx context.Context) error Close() } @@ -25,25 +25,25 @@ type ( // Query is the interface for query object. Query interface { Exec() error - Scan(...interface{}) error - ScanCAS(...interface{}) (bool, error) - MapScan(map[string]interface{}) error - MapScanCAS(map[string]interface{}) (bool, error) + Scan(...any) error + ScanCAS(...any) (bool, error) + MapScan(map[string]any) error + MapScanCAS(map[string]any) (bool, error) Iter() Iter PageSize(int) Query PageState([]byte) Query WithContext(context.Context) Query WithTimestamp(int64) Query Consistency(Consistency) Query - Bind(...interface{}) Query + Bind(...any) Query Idempotent(bool) Query SetSpeculativeExecutionPolicy(SpeculativeExecutionPolicy) Query } // Iter is the interface for executing and iterating over all resulting rows. Iter interface { - Scan(...interface{}) bool - MapScan(map[string]interface{}) bool + Scan(...any) bool + MapScan(map[string]any) bool PageState() []byte Close() error } diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/iter.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/iter.go index 8bfaaa9d452..c836e14c48a 100644 --- a/common/persistence/nosql/nosqlplugin/cassandra/gocql/iter.go +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/iter.go @@ -16,11 +16,11 @@ func newIter(session *session, gocqlIter *gocql.Iter) *iter { } } -func (it *iter) Scan(dest ...interface{}) bool { +func (it *iter) Scan(dest ...any) bool { return it.gocqlIter.Scan(dest...) } -func (it *iter) MapScan(m map[string]interface{}) bool { +func (it *iter) MapScan(m map[string]any) bool { return it.gocqlIter.MapScan(m) } diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/query.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/query.go index aeb64a426c3..79ab9d6ce42 100644 --- a/common/persistence/nosql/nosqlplugin/cassandra/gocql/query.go +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/query.go @@ -32,7 +32,7 @@ func (q *query) Exec() (retError error) { } func (q *query) Scan( - dest ...interface{}, + dest ...any, ) (retError error) { defer func() { q.session.handleError(retError) }() @@ -40,7 +40,7 @@ func (q *query) Scan( } func (q *query) ScanCAS( - dest ...interface{}, + dest ...any, ) (_ bool, retError error) { defer func() { q.session.handleError(retError) }() @@ -48,7 +48,7 @@ func (q *query) ScanCAS( } func (q *query) MapScan( - m map[string]interface{}, + m map[string]any, ) (retError error) { defer func() { q.session.handleError(retError) }() @@ -56,7 +56,7 @@ func (q *query) MapScan( } func (q *query) MapScanCAS( - dest map[string]interface{}, + dest map[string]any, ) (_ bool, retError error) { defer func() { q.session.handleError(retError) }() @@ -96,7 +96,7 @@ func (q *query) WithContext(ctx context.Context) Query { return newQuery(q.session, q2) } -func (q *query) Bind(v ...interface{}) Query { +func (q *query) Bind(v ...any) Query { q.gocqlQuery.Bind(v...) return newQuery(q.session, q.gocqlQuery) } diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/session.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/session.go index 707e0e5703b..cef94c653cf 100644 --- a/common/persistence/nosql/nosqlplugin/cassandra/gocql/session.go +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/session.go @@ -110,7 +110,7 @@ func initSession( func (s *session) Query( stmt string, - values ...interface{}, + values ...any, ) Query { q := s.Value.Load().(*gocql.Session).Query(stmt, values...) if q == nil { @@ -146,7 +146,7 @@ func (s *session) ExecuteBatch( func (s *session) MapExecuteBatchCAS( b *Batch, - previous map[string]interface{}, + previous map[string]any, ) (_ bool, _ Iter, retError error) { defer func() { s.handleError(retError) }() diff --git a/common/persistence/nosql/nosqlplugin/cassandra/gocql/uuid.go b/common/persistence/nosql/nosqlplugin/cassandra/gocql/uuid.go index 69f06d910c4..b55c3b6e22e 100644 --- a/common/persistence/nosql/nosqlplugin/cassandra/gocql/uuid.go +++ b/common/persistence/nosql/nosqlplugin/cassandra/gocql/uuid.go @@ -8,13 +8,13 @@ import ( ) func UUIDToString( - item interface{}, + item any, ) string { return item.(gocql.UUID).String() } func UUIDsToStringSlice( - item interface{}, + item any, ) []string { uuids := item.([]gocql.UUID) results := make([]string, len(uuids)) diff --git a/common/persistence/persistence-tests/cluster_metadata_manager.go b/common/persistence/persistence-tests/cluster_metadata_manager.go index bd8c3cb7ff1..a5aba91d026 100644 --- a/common/persistence/persistence-tests/cluster_metadata_manager.go +++ b/common/persistence/persistence-tests/cluster_metadata_manager.go @@ -87,7 +87,7 @@ func (s *ClusterMetadataManagerSuite) TestClusterMembershipUpsertCanReadAny() { // TestClusterMembershipUpsertCanPageRead verifies that we can UpsertClusterMembership and read our result func (s *ClusterMetadataManagerSuite) TestClusterMembershipUpsertCanPageRead() { expectedIds := make(map[string]int, 100) - for i := 0; i < 100; i++ { + for range 100 { hostID := uuid.New() expectedIds[hostID.String()]++ diff --git a/common/persistence/persistence-tests/history_v2_persistence.go b/common/persistence/persistence-tests/history_v2_persistence.go index c739b8a9dfb..e306a336bab 100644 --- a/common/persistence/persistence-tests/history_v2_persistence.go +++ b/common/persistence/persistence-tests/history_v2_persistence.go @@ -79,7 +79,7 @@ func (s *HistoryV2PersistenceSuite) TestGenUUIDs() { wg := sync.WaitGroup{} m := sync.Map{} concurrency := 1000 - for i := 0; i < concurrency; i++ { + for range concurrency { wg.Add(1) go func() { defer wg.Done() @@ -89,7 +89,7 @@ func (s *HistoryV2PersistenceSuite) TestGenUUIDs() { } wg.Wait() cnt := 0 - m.Range(func(k, v interface{}) bool { + m.Range(func(k, v any) bool { cnt++ return true }) @@ -108,7 +108,7 @@ func (s *HistoryV2PersistenceSuite) TestScanAllTrees() { totalTrees := 1002 pgSize := 100 - for i := 0; i < totalTrees; i++ { + for range totalTrees { treeID := uuid.NewString() bi, err := s.newHistoryBranch(treeID) s.Nil(err) @@ -345,7 +345,7 @@ func (s *HistoryV2PersistenceSuite) TestConcurrentlyCreateAndAppendBranches() { m := &sync.Map{} // test create new branch along with appending new nodes - for i := 0; i < concurrency; i++ { + for i := range concurrency { wg.Add(1) go func(idx int) { defer wg.Done() @@ -390,7 +390,7 @@ func (s *HistoryV2PersistenceSuite) TestConcurrentlyCreateAndAppendBranches() { wg = sync.WaitGroup{} // test appending nodes(override and new nodes) on each branch concurrently - for i := 0; i < concurrency; i++ { + for i := range concurrency { wg.Add(1) go func(idx int) { defer wg.Done() @@ -447,7 +447,7 @@ func (s *HistoryV2PersistenceSuite) TestConcurrentlyCreateAndAppendBranches() { wg.Wait() // Finally lets clean up all branches - m.Range(func(k, v interface{}) bool { + m.Range(func(k, v any) bool { br := v.([]byte) // delete old branches along with create new branches err := s.deleteHistoryBranch(br) @@ -506,7 +506,7 @@ func (s *HistoryV2PersistenceSuite) TestConcurrentlyForkAndAppendBranches() { level1ID := new(sync.Map) level1Br := new(sync.Map) // test forking from master branch and append nodes - for i := 0; i < concurrency; i++ { + for i := range concurrency { wg.Add(1) go func(idx int) { defer wg.Done() @@ -628,7 +628,7 @@ func (s *HistoryV2PersistenceSuite) TestConcurrentlyForkAndAppendBranches() { s.Equal(concurrency, masterCnt) // Finally lets clean up all branches - level1Br.Range(func(k, v interface{}) bool { + level1Br.Range(func(k, v any) bool { br := v.([]byte) // delete old branches along with create new branches err := s.deleteHistoryBranch(br) @@ -636,7 +636,7 @@ func (s *HistoryV2PersistenceSuite) TestConcurrentlyForkAndAppendBranches() { return true }) - level2Br.Range(func(k, v interface{}) bool { + level2Br.Range(func(k, v any) bool { br := v.([]byte) // delete old branches along with create new branches err := s.deleteHistoryBranch(br) diff --git a/common/persistence/persistence-tests/persistence_test_base.go b/common/persistence/persistence-tests/persistence_test_base.go index e53079a3ebf..96a1cb4bc79 100644 --- a/common/persistence/persistence-tests/persistence_test_base.go +++ b/common/persistence/persistence-tests/persistence_test_base.go @@ -374,7 +374,7 @@ func (s *TestBase) GetAckLevels( func (s *TestBase) PublishToNamespaceDLQ(ctx context.Context, task *replicationspb.ReplicationTask) error { retryPolicy := backoff.NewExponentialRetryPolicy(100 * time.Millisecond). WithBackoffCoefficient(1.5). - WithMaximumAttempts(5) + WithMaximumAttempts(20) return backoff.ThrottleRetryContext( ctx, diff --git a/common/persistence/persistence-tests/queue_persistence.go b/common/persistence/persistence-tests/queue_persistence.go index 6d7b655cd16..64895ccdbde 100644 --- a/common/persistence/persistence-tests/queue_persistence.go +++ b/common/persistence/persistence-tests/queue_persistence.go @@ -55,7 +55,7 @@ func (s *QueuePersistenceSuite) TestNamespaceReplicationQueue() { taskType := enumsspb.REPLICATION_TASK_TYPE_NAMESPACE_TASK go func() { - for i := 0; i < numMessages; i++ { + for i := range numMessages { messageChan <- &replicationspb.ReplicationTask{ TaskType: taskType, Attributes: &replicationspb.ReplicationTask_NamespaceTaskAttributes{ @@ -71,7 +71,7 @@ func (s *QueuePersistenceSuite) TestNamespaceReplicationQueue() { wg := sync.WaitGroup{} wg.Add(concurrentSenders) - for i := 0; i < concurrentSenders; i++ { + for i := range concurrentSenders { go func(senderNum int) { defer wg.Done() for message := range messageChan { @@ -132,7 +132,7 @@ func (s *QueuePersistenceSuite) TestNamespaceReplicationDLQ() { taskType := enumsspb.REPLICATION_TASK_TYPE_NAMESPACE_TASK go func() { - for i := 0; i < numMessages; i++ { + for i := range numMessages { messageChan <- &replicationspb.ReplicationTask{ TaskType: taskType, Attributes: &replicationspb.ReplicationTask_NamespaceTaskAttributes{ @@ -148,7 +148,7 @@ func (s *QueuePersistenceSuite) TestNamespaceReplicationDLQ() { wg := sync.WaitGroup{} wg.Add(concurrentSenders) - for i := 0; i < concurrentSenders; i++ { + for i := range concurrentSenders { go func(senderNum int) { defer wg.Done() for message := range messageChan { diff --git a/common/persistence/persistencetest/queues.go b/common/persistence/persistencetest/queues.go index 85613591695..f9001ab8cbe 100644 --- a/common/persistence/persistencetest/queues.go +++ b/common/persistence/persistencetest/queues.go @@ -78,7 +78,7 @@ func EnqueueMessage( } func EnqueueMessagesForDelete(t *testing.T, q persistence.QueueV2, queueName string, queueType persistence.QueueV2Type) { - for i := 0; i < 2; i++ { + for range 2 { // We have to actually enqueue 2 messages. Otherwise, there won't be anything to actually delete, // since we never delete the last message. _, err := EnqueueMessage(context.Background(), q, queueType, queueName) diff --git a/common/persistence/serialization/codec.go b/common/persistence/serialization/codec.go index 5e3bd2ec61b..4abc8236954 100644 --- a/common/persistence/serialization/codec.go +++ b/common/persistence/serialization/codec.go @@ -2,6 +2,9 @@ package serialization import ( "errors" + "fmt" + "os" + "strings" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -9,6 +12,32 @@ import ( "google.golang.org/protobuf/proto" ) +// SerializerDataEncodingEnvVar controls which codec is used for encoding DataBlobs. +// +// Currently supported values (case-insensitive): +// - "json" +// - "proto3" +// +// Decoding always support all encodings regardless of this setting. +// +// WARNING: This environment variable should only be used for testing; and never set it in production. +const SerializerDataEncodingEnvVar = "TEMPORAL_TEST_DATA_ENCODING" + +// EncodingTypeFromEnv returns an EncodingType based on the environment variable `TEMPORAL_TEST_DATA_ENCODING`. +// It defaults to "ENCODING_TYPE_PROTO3" codec if the environment variable is not set. +func EncodingTypeFromEnv() enumspb.EncodingType { + codecType := os.Getenv(SerializerDataEncodingEnvVar) + switch strings.ToLower(codecType) { + case "", "proto3": + return enumspb.ENCODING_TYPE_PROTO3 + case "json": + return enumspb.ENCODING_TYPE_JSON + default: + //nolint:forbidigo // should fail fast and hard if used incorrectly + panic(fmt.Sprintf("unknown codec %q for environment variable %s", codecType, SerializerDataEncodingEnvVar)) + } +} + // ProtoEncode is kept for backward compatibility. func ProtoEncode(m proto.Message) (*commonpb.DataBlob, error) { return encodeBlob(m, enumspb.ENCODING_TYPE_PROTO3) diff --git a/common/persistence/serialization/serializer.go b/common/persistence/serialization/serializer.go index 5755bc897fe..82475c81c57 100644 --- a/common/persistence/serialization/serializer.go +++ b/common/persistence/serialization/serializer.go @@ -18,14 +18,20 @@ import ( "google.golang.org/protobuf/proto" ) -// DefaultDecoder is here for convenience to skip the need to create a new Serializer when only decodig is needed. -// It does not need an encoding type; as it will use the one defined in the DataBlob. -var r Serializer = &serializerImpl{encodingType: enumspb.ENCODING_TYPE_UNSPECIFIED} -var DefaultDecoder Decoder = r +var ( + // DefaultDecoder is here for convenience to skip the need to create a new Serializer when only decoding is needed. + // It does not need an encoding type; as it will use the one defined in the DataBlob. + defaultSerializer Serializer = &serializerImpl{encodingType: enumspb.ENCODING_TYPE_UNSPECIFIED} + DefaultDecoder Decoder = defaultSerializer + + // proto3Encoder always encodes as proto3, used by EnsureProto3Encoding. + proto3Encoder Encoder = &serializerImpl{encodingType: enumspb.ENCODING_TYPE_PROTO3} +) type ( // Encoder is used to encode objects to DataBlobs. Encoder interface { + EncodingType() enumspb.EncodingType SerializeEvents(batch []*historypb.HistoryEvent) (*commonpb.DataBlob, error) SerializeEvent(event *historypb.HistoryEvent) (*commonpb.DataBlob, error) SerializeClusterMetadata(icm *persistencespb.ClusterMetadata) (*commonpb.DataBlob, error) @@ -133,7 +139,11 @@ type ( ) func NewSerializer() Serializer { - return &serializerImpl{encodingType: enumspb.ENCODING_TYPE_PROTO3} + return &serializerImpl{encodingType: EncodingTypeFromEnv()} +} + +func (t *serializerImpl) EncodingType() enumspb.EncodingType { + return t.encodingType } func (t *serializerImpl) SerializeTask( @@ -666,3 +676,25 @@ func (t *serializerImpl) QueueStateFromBlob(data *commonpb.DataBlob) (*persisten result := &persistencespb.QueueState{} return result, Decode(data, result) } + +// ReencodeEventBlobsAsProto3 re-encodes event blobs as proto3 if the serializer uses a different encoding. +// In production (proto3 encoding), this returns the input unchanged. +func ReencodeEventBlobsAsProto3(serializer Serializer, blobs []*commonpb.DataBlob) ([]*commonpb.DataBlob, error) { + if serializer.EncodingType() == enumspb.ENCODING_TYPE_PROTO3 || len(blobs) == 0 { + return blobs, nil + } + + // Re-encode all blobs as proto3. + result := make([]*commonpb.DataBlob, len(blobs)) + for i, blob := range blobs { + events, err := serializer.DeserializeEvents(blob) + if err != nil { + return nil, err + } + result[i], err = proto3Encoder.SerializeEvents(events) + if err != nil { + return nil, err + } + } + return result, nil +} diff --git a/common/persistence/serialization/serializer_test.go b/common/persistence/serialization/serializer_test.go index 44eb5b5694d..d936d285b3e 100644 --- a/common/persistence/serialization/serializer_test.go +++ b/common/persistence/serialization/serializer_test.go @@ -73,7 +73,7 @@ func (s *temporalSerializerSuite) TestSerializer() { history0 := &historypb.History{Events: []*historypb.HistoryEvent{event0, event0}} - for i := 0; i < concurrency; i++ { + for range concurrency { go func() { startWG.Wait() diff --git a/common/persistence/serialization/task_serializers.go b/common/persistence/serialization/task_serializers.go index 800a53a4ee7..ebc1aba9291 100644 --- a/common/persistence/serialization/task_serializers.go +++ b/common/persistence/serialization/task_serializers.go @@ -318,6 +318,8 @@ func (t *serializerImpl) DeserializeReplicationTask(replicationTask *persistence return replicationSyncHSMTaskFromProto(replicationTask), nil case enumsspb.TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION: return replicationSyncVersionedTransitionTaskFromProto(replicationTask, t) + case enumsspb.TASK_TYPE_REPLICATION_DELETE_EXECUTION: + return replicationDeleteExecutionTaskFromProto(replicationTask), nil default: return nil, serviceerror.NewInternalf("Unknown replication task type: %v", replicationTask.TaskType) } @@ -335,6 +337,8 @@ func (t *serializerImpl) SerializeReplicationTask(task tasks.Task) (*persistence return replicationSyncHSMTaskToProto(task), nil case *tasks.SyncVersionedTransitionTask: return replicationSyncVersionedTransitionTaskToProto(task, t) + case *tasks.DeleteExecutionReplicationTask: + return replicationDeleteExecutionTaskToProto(task), nil default: return nil, serviceerror.NewInternalf("Unknown repication task type: %v", task) } @@ -1519,3 +1523,36 @@ func deserializeOutboundTask( return nil, serviceerror.NewInternalf("unknown outbound task type while deserializing: %v", info) } } + +func replicationDeleteExecutionTaskToProto( + task *tasks.DeleteExecutionReplicationTask, +) *persistencespb.ReplicationTaskInfo { + return &persistencespb.ReplicationTaskInfo{ + NamespaceId: task.NamespaceID, + WorkflowId: task.WorkflowID, + RunId: task.RunID, + TaskType: enumsspb.TASK_TYPE_REPLICATION_DELETE_EXECUTION, + TaskId: task.TaskID, + VisibilityTime: timestamppb.New(task.VisibilityTimestamp), + ArchetypeId: task.ArchetypeID, + } +} + +func replicationDeleteExecutionTaskFromProto( + info *persistencespb.ReplicationTaskInfo, +) *tasks.DeleteExecutionReplicationTask { + visibilityTimestamp := time.Unix(0, 0) + if info.VisibilityTime != nil { + visibilityTimestamp = info.VisibilityTime.AsTime() + } + return &tasks.DeleteExecutionReplicationTask{ + WorkflowKey: definition.NewWorkflowKey( + info.NamespaceId, + info.WorkflowId, + info.RunId, + ), + VisibilityTimestamp: visibilityTimestamp, + TaskID: info.TaskId, + ArchetypeID: info.ArchetypeId, + } +} diff --git a/common/persistence/serialization/task_serializers_test.go b/common/persistence/serialization/task_serializers_test.go index febb0300be8..b472e9176b6 100644 --- a/common/persistence/serialization/task_serializers_test.go +++ b/common/persistence/serialization/task_serializers_test.go @@ -444,6 +444,17 @@ func (s *taskSerializerSuite) TestSyncWorkflowStateTask() { s.assertEqualTasks(syncWorkflowStateTask) } +func (s *taskSerializerSuite) TestDeleteExecutionReplicationTask() { + deleteExecutionReplicationTask := &tasks.DeleteExecutionReplicationTask{ + WorkflowKey: s.workflowKey, + VisibilityTimestamp: time.Unix(0, 0).UTC(), // go == compare for location as well which is striped during marshaling/unmarshaling + TaskID: rand.Int63(), + ArchetypeID: rand.Uint32(), + } + + s.assertEqualTasks(deleteExecutionReplicationTask) +} + func (s *taskSerializerSuite) TestDeleteExecutionTask() { deleteExecutionTask := &tasks.DeleteExecutionTask{ WorkflowKey: s.workflowKey, diff --git a/common/persistence/serializer_test.go b/common/persistence/serializer_test.go index eb1b6cc1e16..5bc8aa58dfb 100644 --- a/common/persistence/serializer_test.go +++ b/common/persistence/serializer_test.go @@ -68,7 +68,7 @@ func (s *temporalSerializerSuite) TestSerializer() { history0 := &historypb.History{Events: []*historypb.HistoryEvent{event0, event0}} - for i := 0; i < concurrency; i++ { + for range concurrency { go func() { diff --git a/common/persistence/sql/common.go b/common/persistence/sql/common.go index 1f90f1cd52a..1e65a456a28 100644 --- a/common/persistence/sql/common.go +++ b/common/persistence/sql/common.go @@ -80,7 +80,7 @@ func (m *SqlStore) txExecute(ctx context.Context, operation string, f func(tx sq return nil } -func gobSerialize(x interface{}) ([]byte, error) { +func gobSerialize(x any) ([]byte, error) { b := bytes.Buffer{} e := gob.NewEncoder(&b) err := e.Encode(x) @@ -90,7 +90,7 @@ func gobSerialize(x interface{}) ([]byte, error) { return b.Bytes(), nil } -func gobDeserialize(a []byte, x interface{}) error { +func gobDeserialize(a []byte, x any) error { b := bytes.NewBuffer(a) d := gob.NewDecoder(b) err := d.Decode(x) diff --git a/common/persistence/sql/sqlplugin/db_handle.go b/common/persistence/sql/sqlplugin/db_handle.go index d40a9ae813d..f67a4cab3d2 100644 --- a/common/persistence/sql/sqlplugin/db_handle.go +++ b/common/persistence/sql/sqlplugin/db_handle.go @@ -172,19 +172,19 @@ func (invalidConn) Rebind(query string) string { return query } -func (invalidConn) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { +func (invalidConn) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { return nil, DatabaseUnavailableError } -func (invalidConn) NamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error) { +func (invalidConn) NamedExecContext(ctx context.Context, query string, arg any) (sql.Result, error) { return nil, DatabaseUnavailableError } -func (invalidConn) GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { +func (invalidConn) GetContext(ctx context.Context, dest any, query string, args ...any) error { return DatabaseUnavailableError } -func (invalidConn) SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { +func (invalidConn) SelectContext(ctx context.Context, dest any, query string, args ...any) error { return DatabaseUnavailableError } diff --git a/common/persistence/sql/sqlplugin/interfaces.go b/common/persistence/sql/sqlplugin/interfaces.go index 6260e59cf9f..40af1cf904d 100644 --- a/common/persistence/sql/sqlplugin/interfaces.go +++ b/common/persistence/sql/sqlplugin/interfaces.go @@ -87,7 +87,7 @@ type ( DropAllTables(database string) error CreateDatabase(database string) error DropDatabase(database string) error - Exec(stmt string, args ...interface{}) error + Exec(stmt string, args ...any) error } // Tx defines the API for a SQL transaction @@ -122,10 +122,10 @@ type ( // Conn defines the API for a single database connection Conn interface { Rebind(query string) string - ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) - NamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error) - GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error - SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + NamedExecContext(ctx context.Context, query string, arg any) (sql.Result, error) + GetContext(ctx context.Context, dest any, query string, args ...any) error + SelectContext(ctx context.Context, dest any, query string, args ...any) error PrepareNamedContext(ctx context.Context, query string) (*sqlx.NamedStmt, error) } ) diff --git a/common/persistence/sql/sqlplugin/mysql/admin.go b/common/persistence/sql/sqlplugin/mysql/admin.go index f05687813bb..0ab9191ddb7 100644 --- a/common/persistence/sql/sqlplugin/mysql/admin.go +++ b/common/persistence/sql/sqlplugin/mysql/admin.go @@ -74,7 +74,7 @@ func (mdb *db) WriteSchemaUpdateLog(oldVersion string, newVersion string, manife } // Exec executes a sql statement -func (mdb *db) Exec(stmt string, args ...interface{}) error { +func (mdb *db) Exec(stmt string, args ...any) error { db, err := mdb.handle.DB() if err != nil { return err diff --git a/common/persistence/sql/sqlplugin/mysql/cluster_metadata.go b/common/persistence/sql/sqlplugin/mysql/cluster_metadata.go index a1d9bbe815f..e44a6ab25a4 100644 --- a/common/persistence/sql/sqlplugin/mysql/cluster_metadata.go +++ b/common/persistence/sql/sqlplugin/mysql/cluster_metadata.go @@ -170,7 +170,7 @@ func (mdb *db) GetClusterMembers( filter *sqlplugin.ClusterMembershipFilter, ) ([]sqlplugin.ClusterMembershipRow, error) { var queryString strings.Builder - var operands []interface{} + var operands []any queryString.WriteString(templateGetClusterMembership) operands = append(operands, constMembershipPartition) diff --git a/common/persistence/sql/sqlplugin/mysql/events.go b/common/persistence/sql/sqlplugin/mysql/events.go index 76a4681f6af..730ff737c81 100644 --- a/common/persistence/sql/sqlplugin/mysql/events.go +++ b/common/persistence/sql/sqlplugin/mysql/events.go @@ -95,9 +95,9 @@ func (mdb *db) RangeSelectFromHistoryNode( query = getHistoryNodesQuery } - var args []interface{} + var args []any if filter.ReverseOrder { - args = []interface{}{ + args = []any{ filter.ShardID, filter.TreeID, filter.BranchID, @@ -108,7 +108,7 @@ func (mdb *db) RangeSelectFromHistoryNode( filter.PageSize, } } else { - args = []interface{}{ + args = []any{ filter.ShardID, filter.TreeID, filter.BranchID, diff --git a/common/persistence/sql/sqlplugin/mysql/visibility.go b/common/persistence/sql/sqlplugin/mysql/visibility.go index f3444f38a86..363a3753f9d 100644 --- a/common/persistence/sql/sqlplugin/mysql/visibility.go +++ b/common/persistence/sql/sqlplugin/mysql/visibility.go @@ -329,7 +329,7 @@ func (mdb *db) processRowFromDB(row *sqlplugin.VisibilityRow) error { if row.SearchAttributes != nil { for saName, saValue := range *row.SearchAttributes { switch typedSaValue := saValue.(type) { - case []interface{}: + case []any: // the only valid type is slice of strings strSlice := make([]string, len(typedSaValue)) for i, item := range typedSaValue { diff --git a/common/persistence/sql/sqlplugin/postgresql/cluster_metadata.go b/common/persistence/sql/sqlplugin/postgresql/cluster_metadata.go index 5ec5040d444..9ed5858e73c 100644 --- a/common/persistence/sql/sqlplugin/postgresql/cluster_metadata.go +++ b/common/persistence/sql/sqlplugin/postgresql/cluster_metadata.go @@ -174,7 +174,7 @@ func (pdb *db) GetClusterMembers( filter *sqlplugin.ClusterMembershipFilter, ) ([]sqlplugin.ClusterMembershipRow, error) { var queryString strings.Builder - var operands []interface{} + var operands []any queryString.WriteString(templateGetClusterMembership) operands = append(operands, constMembershipPartition) queryString.WriteString(strconv.Itoa(len(operands))) diff --git a/common/persistence/sql/sqlplugin/postgresql/events.go b/common/persistence/sql/sqlplugin/postgresql/events.go index 8f07a4755c5..202f5b0aea1 100644 --- a/common/persistence/sql/sqlplugin/postgresql/events.go +++ b/common/persistence/sql/sqlplugin/postgresql/events.go @@ -95,9 +95,9 @@ func (pdb *db) RangeSelectFromHistoryNode( query = getHistoryNodesQuery } - var args []interface{} + var args []any if filter.ReverseOrder { - args = []interface{}{ + args = []any{ filter.ShardID, filter.TreeID, filter.BranchID, @@ -108,7 +108,7 @@ func (pdb *db) RangeSelectFromHistoryNode( filter.PageSize, } } else { - args = []interface{}{ + args = []any{ filter.ShardID, filter.TreeID, filter.BranchID, diff --git a/common/persistence/sql/sqlplugin/postgresql/visibility.go b/common/persistence/sql/sqlplugin/postgresql/visibility.go index 61f30b1d640..405213429a8 100644 --- a/common/persistence/sql/sqlplugin/postgresql/visibility.go +++ b/common/persistence/sql/sqlplugin/postgresql/visibility.go @@ -192,7 +192,7 @@ func (pdb *db) processRowFromDB(row *sqlplugin.VisibilityRow) error { if row.SearchAttributes != nil { for saName, saValue := range *row.SearchAttributes { switch typedSaValue := saValue.(type) { - case []interface{}: + case []any: // the only valid type is slice of strings strSlice := make([]string, len(typedSaValue)) for i, item := range typedSaValue { diff --git a/common/persistence/sql/sqlplugin/sqlite/admin.go b/common/persistence/sql/sqlplugin/sqlite/admin.go index 6edca154f14..e2cbc3861c6 100644 --- a/common/persistence/sql/sqlplugin/sqlite/admin.go +++ b/common/persistence/sql/sqlplugin/sqlite/admin.go @@ -62,7 +62,7 @@ func (mdb *db) WriteSchemaUpdateLog(oldVersion string, newVersion string, manife } // Exec executes a sql statement -func (mdb *db) Exec(stmt string, args ...interface{}) error { +func (mdb *db) Exec(stmt string, args ...any) error { _, err := mdb.db.Exec(stmt, args...) return err } diff --git a/common/persistence/sql/sqlplugin/sqlite/cluster_metadata.go b/common/persistence/sql/sqlplugin/sqlite/cluster_metadata.go index d35b1e5e3b1..267916e41bf 100644 --- a/common/persistence/sql/sqlplugin/sqlite/cluster_metadata.go +++ b/common/persistence/sql/sqlplugin/sqlite/cluster_metadata.go @@ -168,7 +168,7 @@ func (mdb *db) GetClusterMembers( filter *sqlplugin.ClusterMembershipFilter, ) ([]sqlplugin.ClusterMembershipRow, error) { var queryString strings.Builder - var operands []interface{} + var operands []any queryString.WriteString(templateGetClusterMembership) operands = append(operands, constMembershipPartition) diff --git a/common/persistence/sql/sqlplugin/sqlite/events.go b/common/persistence/sql/sqlplugin/sqlite/events.go index 2763917894a..e60bd4398bd 100644 --- a/common/persistence/sql/sqlplugin/sqlite/events.go +++ b/common/persistence/sql/sqlplugin/sqlite/events.go @@ -91,9 +91,9 @@ func (mdb *db) RangeSelectFromHistoryNode( query = getHistoryNodesQuery } - var args []interface{} + var args []any if filter.ReverseOrder { - args = []interface{}{ + args = []any{ filter.ShardID, filter.TreeID, filter.BranchID, @@ -104,7 +104,7 @@ func (mdb *db) RangeSelectFromHistoryNode( filter.PageSize, } } else { - args = []interface{}{ + args = []any{ filter.ShardID, filter.TreeID, filter.BranchID, diff --git a/common/persistence/sql/sqlplugin/tests/history_execution_activity.go b/common/persistence/sql/sqlplugin/tests/history_execution_activity.go index 90494c883d4..c33a3c9d5df 100644 --- a/common/persistence/sql/sqlplugin/tests/history_execution_activity.go +++ b/common/persistence/sql/sqlplugin/tests/history_execution_activity.go @@ -124,7 +124,7 @@ func (s *historyExecutionActivitySuite) TestReplaceSelect_Multiple() { runID := primitives.NewUUID() var activities []sqlplugin.ActivityInfoMapsRow - for i := 0; i < numActivities; i++ { + for range numActivities { activity := s.newRandomExecutionActivityRow(shardID, namespaceID, workflowID, runID, rand.Int63()) activities = append(activities, activity) } @@ -291,7 +291,7 @@ func (s *historyExecutionActivitySuite) TestReplaceDeleteSelect_Multiple() { var activities []sqlplugin.ActivityInfoMapsRow var activityScheduledEventIDs []int64 - for i := 0; i < numActivities; i++ { + for range numActivities { activityScheduledEventID := rand.Int63() activity := s.newRandomExecutionActivityRow(shardID, namespaceID, workflowID, runID, activityScheduledEventID) activityScheduledEventIDs = append(activityScheduledEventIDs, activityScheduledEventID) @@ -336,7 +336,7 @@ func (s *historyExecutionActivitySuite) TestReplaceDeleteSelect_All() { runID := primitives.NewUUID() var activities []sqlplugin.ActivityInfoMapsRow - for i := 0; i < numActivities; i++ { + for range numActivities { activityScheduledEventID := rand.Int63() activity := s.newRandomExecutionActivityRow(shardID, namespaceID, workflowID, runID, activityScheduledEventID) activities = append(activities, activity) diff --git a/common/persistence/sql/sqlplugin/tests/history_execution_buffer.go b/common/persistence/sql/sqlplugin/tests/history_execution_buffer.go index 9ab152970f2..e6d8fe589c9 100644 --- a/common/persistence/sql/sqlplugin/tests/history_execution_buffer.go +++ b/common/persistence/sql/sqlplugin/tests/history_execution_buffer.go @@ -92,7 +92,7 @@ func (s *historyExecutionBufferSuite) TestInsertSelect() { runID := primitives.NewUUID() var buffers []sqlplugin.BufferedEventsRow - for i := 0; i < numBufferedEvents; i++ { + for range numBufferedEvents { buffer := s.newRandomExecutionBufferRow(shardID, namespaceID, workflowID, runID) buffers = append(buffers, buffer) } @@ -145,7 +145,7 @@ func (s *historyExecutionBufferSuite) TestInsertDelete() { runID := primitives.NewUUID() var buffers []sqlplugin.BufferedEventsRow - for i := 0; i < numBufferedEvents; i++ { + for range numBufferedEvents { buffer := s.newRandomExecutionBufferRow(shardID, namespaceID, workflowID, runID) buffers = append(buffers, buffer) } diff --git a/common/persistence/sql/sqlplugin/tests/history_execution_child_workflow.go b/common/persistence/sql/sqlplugin/tests/history_execution_child_workflow.go index abd8b47dfa9..45660d40143 100644 --- a/common/persistence/sql/sqlplugin/tests/history_execution_child_workflow.go +++ b/common/persistence/sql/sqlplugin/tests/history_execution_child_workflow.go @@ -124,7 +124,7 @@ func (s *historyExecutionChildWorkflowSuite) TestReplaceSelect_Multiple() { runID := primitives.NewUUID() var childWorkflows []sqlplugin.ChildExecutionInfoMapsRow - for i := 0; i < numChildWorkflows; i++ { + for range numChildWorkflows { childWorkflow := s.newRandomExecutionChildWorkflowRow(shardID, namespaceID, workflowID, runID, rand.Int63()) childWorkflows = append(childWorkflows, childWorkflow) } @@ -291,7 +291,7 @@ func (s *historyExecutionChildWorkflowSuite) TestReplaceDeleteSelect_Multiple() var childWorkflows []sqlplugin.ChildExecutionInfoMapsRow var childWorkflowInitiatedIDs []int64 - for i := 0; i < numChildWorkflows; i++ { + for range numChildWorkflows { childWorkflowInitiatedID := rand.Int63() childWorkflow := s.newRandomExecutionChildWorkflowRow(shardID, namespaceID, workflowID, runID, childWorkflowInitiatedID) childWorkflowInitiatedIDs = append(childWorkflowInitiatedIDs, childWorkflowInitiatedID) @@ -336,7 +336,7 @@ func (s *historyExecutionChildWorkflowSuite) TestReplaceDeleteSelect_All() { runID := primitives.NewUUID() var childWorkflows []sqlplugin.ChildExecutionInfoMapsRow - for i := 0; i < numChildWorkflows; i++ { + for range numChildWorkflows { childWorkflow := s.newRandomExecutionChildWorkflowRow(shardID, namespaceID, workflowID, runID, rand.Int63()) childWorkflows = append(childWorkflows, childWorkflow) } diff --git a/common/persistence/sql/sqlplugin/tests/history_execution_request_cancel.go b/common/persistence/sql/sqlplugin/tests/history_execution_request_cancel.go index 53d27933b45..fbf94aebb4f 100644 --- a/common/persistence/sql/sqlplugin/tests/history_execution_request_cancel.go +++ b/common/persistence/sql/sqlplugin/tests/history_execution_request_cancel.go @@ -124,7 +124,7 @@ func (s *historyExecutionRequestCancelSuite) TestReplaceSelect_Multiple() { runID := primitives.NewUUID() var requestCancels []sqlplugin.RequestCancelInfoMapsRow - for i := 0; i < numRequestCancels; i++ { + for range numRequestCancels { requestCancel := s.newRandomExecutionRequestCancelRow(shardID, namespaceID, workflowID, runID, rand.Int63()) requestCancels = append(requestCancels, requestCancel) } @@ -291,7 +291,7 @@ func (s *historyExecutionRequestCancelSuite) TestReplaceDeleteSelect_Multiple() var requestCancels []sqlplugin.RequestCancelInfoMapsRow var requestCancelInitiatedIDs []int64 - for i := 0; i < numRequestCancels; i++ { + for range numRequestCancels { requestCancelInitiatedID := rand.Int63() requestCancel := s.newRandomExecutionRequestCancelRow(shardID, namespaceID, workflowID, runID, requestCancelInitiatedID) requestCancelInitiatedIDs = append(requestCancelInitiatedIDs, requestCancelInitiatedID) @@ -336,7 +336,7 @@ func (s *historyExecutionRequestCancelSuite) TestReplaceDeleteSelect_All() { runID := primitives.NewUUID() var requestCancels []sqlplugin.RequestCancelInfoMapsRow - for i := 0; i < numRequestCancels; i++ { + for range numRequestCancels { requestCancel := s.newRandomExecutionRequestCancelRow(shardID, namespaceID, workflowID, runID, rand.Int63()) requestCancels = append(requestCancels, requestCancel) } diff --git a/common/persistence/sql/sqlplugin/tests/history_execution_signal.go b/common/persistence/sql/sqlplugin/tests/history_execution_signal.go index f111b82ef21..1978666fe0d 100644 --- a/common/persistence/sql/sqlplugin/tests/history_execution_signal.go +++ b/common/persistence/sql/sqlplugin/tests/history_execution_signal.go @@ -124,7 +124,7 @@ func (s *historyExecutionSignalSuite) TestReplaceSelect_Multiple() { runID := primitives.NewUUID() var signals []sqlplugin.SignalInfoMapsRow - for i := 0; i < numSignals; i++ { + for range numSignals { signal := s.newRandomExecutionSignalRow(shardID, namespaceID, workflowID, runID, rand.Int63()) signals = append(signals, signal) } @@ -291,7 +291,7 @@ func (s *historyExecutionSignalSuite) TestReplaceDeleteSelect_Multiple() { var signals []sqlplugin.SignalInfoMapsRow var signalInitiatedIDs []int64 - for i := 0; i < numSignals; i++ { + for range numSignals { signalInitiatedID := rand.Int63() signal := s.newRandomExecutionSignalRow(shardID, namespaceID, workflowID, runID, signalInitiatedID) signalInitiatedIDs = append(signalInitiatedIDs, signalInitiatedID) @@ -336,7 +336,7 @@ func (s *historyExecutionSignalSuite) TestReplaceDeleteSelect_All() { runID := primitives.NewUUID() var signals []sqlplugin.SignalInfoMapsRow - for i := 0; i < numSignals; i++ { + for range numSignals { signal := s.newRandomExecutionSignalRow(shardID, namespaceID, workflowID, runID, rand.Int63()) signals = append(signals, signal) } diff --git a/common/persistence/sql/sqlplugin/tests/history_execution_signal_requested.go b/common/persistence/sql/sqlplugin/tests/history_execution_signal_requested.go index c4051fa6728..a2cf3268461 100644 --- a/common/persistence/sql/sqlplugin/tests/history_execution_signal_requested.go +++ b/common/persistence/sql/sqlplugin/tests/history_execution_signal_requested.go @@ -120,7 +120,7 @@ func (s *historyExecutionSignalRequestSuite) TestReplaceSelect_Multiple() { runID := primitives.NewUUID() var signalRequests []sqlplugin.SignalsRequestedSetsRow - for i := 0; i < numSignalRequests; i++ { + for range numSignalRequests { signalRequest := s.newRandomExecutionSignalRequestRow(shardID, namespaceID, workflowID, runID, shuffle.String(testHistoryExecutionSignalID)) signalRequests = append(signalRequests, signalRequest) } @@ -287,7 +287,7 @@ func (s *historyExecutionSignalRequestSuite) TestReplaceDeleteSelect_Multiple() var signalRequests []sqlplugin.SignalsRequestedSetsRow var signalRequestIDs []string - for i := 0; i < numSignalRequests; i++ { + for range numSignalRequests { signalRequestID := shuffle.String(testHistoryExecutionSignalID) signalRequest := s.newRandomExecutionSignalRequestRow(shardID, namespaceID, workflowID, runID, signalRequestID) signalRequestIDs = append(signalRequestIDs, signalRequestID) @@ -332,7 +332,7 @@ func (s *historyExecutionSignalRequestSuite) TestReplaceDeleteSelect_All() { runID := primitives.NewUUID() var signalRequests []sqlplugin.SignalsRequestedSetsRow - for i := 0; i < numSignalRequests; i++ { + for range numSignalRequests { signalRequest := s.newRandomExecutionSignalRequestRow(shardID, namespaceID, workflowID, runID, shuffle.String(testHistoryExecutionSignalID)) signalRequests = append(signalRequests, signalRequest) } diff --git a/common/persistence/sql/sqlplugin/tests/history_execution_timer.go b/common/persistence/sql/sqlplugin/tests/history_execution_timer.go index 5f9ac9997ab..cfc4c32c5e1 100644 --- a/common/persistence/sql/sqlplugin/tests/history_execution_timer.go +++ b/common/persistence/sql/sqlplugin/tests/history_execution_timer.go @@ -125,7 +125,7 @@ func (s *historyExecutionTimerSuite) TestReplaceSelect_Multiple() { runID := primitives.NewUUID() var timers []sqlplugin.TimerInfoMapsRow - for i := 0; i < numTimers; i++ { + for range numTimers { timer := s.newRandomExecutionTimerRow(shardID, namespaceID, workflowID, runID, shuffle.String(testHistoryExecutionTimerID)) timers = append(timers, timer) } @@ -292,7 +292,7 @@ func (s *historyExecutionTimerSuite) TestReplaceDeleteSelect_Multiple() { var timers []sqlplugin.TimerInfoMapsRow var timerIDs []string - for i := 0; i < numTimers; i++ { + for range numTimers { timerID := shuffle.String(testHistoryExecutionTimerID) timer := s.newRandomExecutionTimerRow(shardID, namespaceID, workflowID, runID, timerID) timerIDs = append(timerIDs, timerID) @@ -337,7 +337,7 @@ func (s *historyExecutionTimerSuite) TestReplaceDeleteSelect_All() { runID := primitives.NewUUID() var timers []sqlplugin.TimerInfoMapsRow - for i := 0; i < numTimers; i++ { + for range numTimers { timer := s.newRandomExecutionTimerRow(shardID, namespaceID, workflowID, runID, shuffle.String(testHistoryExecutionTimerID)) timers = append(timers, timer) } diff --git a/common/persistence/sql/sqlplugin/tests/history_node.go b/common/persistence/sql/sqlplugin/tests/history_node.go index 9c5cb6f7be4..a4ea0de5a66 100644 --- a/common/persistence/sql/sqlplugin/tests/history_node.go +++ b/common/persistence/sql/sqlplugin/tests/history_node.go @@ -145,8 +145,8 @@ func (s *historyNodeSuite) TestInsertSelect_Multiple() { maxNodeID := minNodeID + int64(numNodeIDs) var nodes []sqlplugin.HistoryNodeRow - for i := 0; i < numNodeIDs; i++ { - for j := 0; j < nodePerNodeID; j++ { + for range numNodeIDs { + for range nodePerNodeID { node := s.newRandomNodeRow(shardID, treeID, branchID, nodeID, rand.Int63(), rand.Int63()) result, err := s.store.InsertIntoHistoryNode(newExecutionContext(), &node) s.NoError(err) @@ -309,8 +309,8 @@ func (s *historyNodeSuite) TestInsertDeleteSelect_Multiple() { nodeID := int64(1) minNodeID := nodeID - for i := 0; i < numNodeIDs; i++ { - for j := 0; j < nodePerNodeID; j++ { + for range numNodeIDs { + for range nodePerNodeID { node := s.newRandomNodeRow(shardID, treeID, branchID, nodeID, rand.Int63(), rand.Int63()) result, err := s.store.InsertIntoHistoryNode(newExecutionContext(), &node) s.NoError(err) diff --git a/common/persistence/sql/sqlplugin/tests/history_replication_task.go b/common/persistence/sql/sqlplugin/tests/history_replication_task.go index 43127c526fe..56074c10861 100644 --- a/common/persistence/sql/sqlplugin/tests/history_replication_task.go +++ b/common/persistence/sql/sqlplugin/tests/history_replication_task.go @@ -150,7 +150,7 @@ func (s *historyHistoryReplicationTaskSuite) TestInsertSelect_Multiple() { maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.ReplicationTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomReplicationTaskRow(shardID, taskID) taskID++ tasks = append(tasks, task) @@ -273,7 +273,7 @@ func (s *historyHistoryReplicationTaskSuite) TestInsertDeleteSelect_Multiple() { maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.ReplicationTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomReplicationTaskRow(shardID, taskID) taskID++ tasks = append(tasks, task) diff --git a/common/persistence/sql/sqlplugin/tests/history_replication_task_dlq.go b/common/persistence/sql/sqlplugin/tests/history_replication_task_dlq.go index f9c27478945..b295aadb62b 100644 --- a/common/persistence/sql/sqlplugin/tests/history_replication_task_dlq.go +++ b/common/persistence/sql/sqlplugin/tests/history_replication_task_dlq.go @@ -160,7 +160,7 @@ func (s *historyHistoryReplicationDLQTaskSuite) TestInsertSelect_Multiple() { maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.ReplicationDLQTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomReplicationTasksDLQRow(sourceCluster, shardID, taskID) taskID++ tasks = append(tasks, task) @@ -297,7 +297,7 @@ func (s *historyHistoryReplicationDLQTaskSuite) TestInsertDeleteSelect_Multiple( maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.ReplicationDLQTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomReplicationTasksDLQRow(sourceCluster, shardID, taskID) taskID++ tasks = append(tasks, task) diff --git a/common/persistence/sql/sqlplugin/tests/history_timer_task.go b/common/persistence/sql/sqlplugin/tests/history_timer_task.go index 3f6fe366283..428f8691af6 100644 --- a/common/persistence/sql/sqlplugin/tests/history_timer_task.go +++ b/common/persistence/sql/sqlplugin/tests/history_timer_task.go @@ -161,7 +161,7 @@ func (s *historyHistoryTimerTaskSuite) TestInsertSelect_Multiple() { maxTimestamp := timestamp.Add(time.Duration(numTasks) * time.Millisecond) var tasks []sqlplugin.TimerTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomTimerTaskRow(shardID, timestamp, taskID) timestamp = timestamp.Add(time.Millisecond) taskID++ @@ -295,7 +295,7 @@ func (s *historyHistoryTimerTaskSuite) TestInsertDeleteSelect_Multiple() { maxTimestamp := timestamp.Add(time.Duration(numTasks) * time.Millisecond) var tasks []sqlplugin.TimerTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomTimerTaskRow(shardID, timestamp, taskID) timestamp = timestamp.Add(time.Millisecond) taskID++ diff --git a/common/persistence/sql/sqlplugin/tests/history_transfer_task.go b/common/persistence/sql/sqlplugin/tests/history_transfer_task.go index 2970a589d5c..18e638dcf56 100644 --- a/common/persistence/sql/sqlplugin/tests/history_transfer_task.go +++ b/common/persistence/sql/sqlplugin/tests/history_transfer_task.go @@ -149,7 +149,7 @@ func (s *historyHistoryTransferTaskSuite) TestInsertSelect_Multiple() { maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.TransferTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomTransferTaskRow(shardID, taskID) taskID++ tasks = append(tasks, task) @@ -275,7 +275,7 @@ func (s *historyHistoryTransferTaskSuite) TestInsertDeleteSelect_Multiple() { maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.TransferTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomTransferTaskRow(shardID, taskID) taskID++ tasks = append(tasks, task) diff --git a/common/persistence/sql/sqlplugin/tests/history_visibility_task.go b/common/persistence/sql/sqlplugin/tests/history_visibility_task.go index aca084dfd96..21ae5614e68 100644 --- a/common/persistence/sql/sqlplugin/tests/history_visibility_task.go +++ b/common/persistence/sql/sqlplugin/tests/history_visibility_task.go @@ -149,7 +149,7 @@ func (s *historyHistoryVisibilityTaskSuite) TestInsertSelect_Multiple() { maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.VisibilityTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomVisibilityTaskRow(shardID, taskID) taskID++ tasks = append(tasks, task) @@ -275,7 +275,7 @@ func (s *historyHistoryVisibilityTaskSuite) TestInsertDeleteSelect_Multiple() { maxTaskID := taskID + int64(numTasks) var tasks []sqlplugin.VisibilityTasksRow - for i := 0; i < numTasks; i++ { + for range numTasks { task := s.newRandomVisibilityTaskRow(shardID, taskID) taskID++ tasks = append(tasks, task) diff --git a/common/persistence/sql/sqlplugin/tests/namespace.go b/common/persistence/sql/sqlplugin/tests/namespace.go index 6295318ba35..8556cb615ed 100644 --- a/common/persistence/sql/sqlplugin/tests/namespace.go +++ b/common/persistence/sql/sqlplugin/tests/namespace.go @@ -280,7 +280,7 @@ func (s *namespaceSuite) TestInsertSelect_Pagination() { numNamespace := 2 numNamespacePerPage := 1 - for i := 0; i < numNamespace; i++ { + for range numNamespace { id := primitives.NewUUID() name := shuffle.String(testNamespaceName) notificationVersion := int64(1) diff --git a/common/persistence/sql/sqlplugin/tests/queue_message.go b/common/persistence/sql/sqlplugin/tests/queue_message.go index d8cbf27f078..69eb02c5c38 100644 --- a/common/persistence/sql/sqlplugin/tests/queue_message.go +++ b/common/persistence/sql/sqlplugin/tests/queue_message.go @@ -148,7 +148,7 @@ func (s *queueMessageSuite) TestInsertSelect_Multiple() { maxMessageID := messageID + int64(numMessages) var messages []sqlplugin.QueueMessageRow - for i := 0; i < numMessages; i++ { + for range numMessages { message := s.newRandomQueueMessageRow(queueType, messageID) messageID++ messages = append(messages, message) @@ -262,7 +262,7 @@ func (s *queueMessageSuite) TestInsertDeleteSelect_Multiple() { maxMessageID := messageID + int64(numMessages) var messages []sqlplugin.QueueMessageRow - for i := 0; i < numMessages; i++ { + for range numMessages { message := s.newRandomQueueMessageRow(queueType, messageID) messageID++ messages = append(messages, message) diff --git a/common/persistence/sql/sqlplugin/tests/queue_v2.go b/common/persistence/sql/sqlplugin/tests/queue_v2.go index a4aacd5b991..32b33ccdfb6 100644 --- a/common/persistence/sql/sqlplugin/tests/queue_v2.go +++ b/common/persistence/sql/sqlplugin/tests/queue_v2.go @@ -575,7 +575,7 @@ func testRangeDeleteActuallyDeletes(ctx context.Context, t *testing.T, db sqlplu QueueName: queueKey.GetQueueName(), }) require.NoError(t, err) - for i := 0; i < 3; i++ { + for range 3 { _, err = persistencetest.EnqueueMessage(context.Background(), q, queueType, queueKey.GetQueueName()) require.NoError(t, err) } diff --git a/common/persistence/sql/sqlplugin/tests/visibility.go b/common/persistence/sql/sqlplugin/tests/visibility.go index d3790425750..379e9f84ebc 100644 --- a/common/persistence/sql/sqlplugin/tests/visibility.go +++ b/common/persistence/sql/sqlplugin/tests/visibility.go @@ -445,8 +445,8 @@ func (s *visibilitySuite) TestSelect_MinStartTime_MaxStartTime_WorkflowID_Status status := int32(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING) closeTime := (*time.Time)(nil) historyLength := (*int64)(nil) - for i := 0; i < numStartTime; i++ { - for j := 0; j < visibilityPerStartTime; j++ { + for range numStartTime { + for range visibilityPerStartTime { runID := primitives.NewUUID() workflowTypeName := shuffle.String(testVisibilityWorkflowTypeName) visibility := s.newRandomVisibilityRow( @@ -570,8 +570,8 @@ func (s *visibilitySuite) TestSelect_MinStartTime_MaxStartTime_WorkflowID_Status historyLength := rand.Int63() minStartTime := closeTime maxStartTime := closeTime.Add(time.Duration(numStartTime) * time.Second) - for i := 0; i < numStartTime; i++ { - for j := 0; j < visibilityPerStartTime; j++ { + for range numStartTime { + for range visibilityPerStartTime { runID := primitives.NewUUID() workflowTypeName := shuffle.String(testVisibilityWorkflowTypeName) visibility := s.newRandomVisibilityRow( @@ -694,8 +694,8 @@ func (s *visibilitySuite) TestSelect_MinStartTime_MaxStartTime_WorkflowTypeName_ status := int32(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING) closeTime := (*time.Time)(nil) historyLength := (*int64)(nil) - for i := 0; i < numStartTime; i++ { - for j := 0; j < visibilityPerStartTime; j++ { + for range numStartTime { + for range visibilityPerStartTime { workflowID := shuffle.String(testVisibilityWorkflowID) runID := primitives.NewUUID() visibility := s.newRandomVisibilityRow( @@ -819,8 +819,8 @@ func (s *visibilitySuite) TestSelect_MinStartTime_MaxStartTime_WorkflowTypeName_ historyLength := rand.Int63() minStartTime := closeTime maxStartTime := closeTime.Add(time.Duration(numStartTime) * time.Second) - for i := 0; i < numStartTime; i++ { - for j := 0; j < visibilityPerStartTime; j++ { + for range numStartTime { + for range visibilityPerStartTime { workflowID := shuffle.String(testVisibilityWorkflowID) runID := primitives.NewUUID() visibility := s.newRandomVisibilityRow( @@ -942,8 +942,8 @@ func (s *visibilitySuite) TestSelect_MinStartTime_MaxStartTime_StatusOpen_Multip status := int32(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING) closeTime := (*time.Time)(nil) historyLength := (*int64)(nil) - for i := 0; i < numStartTime; i++ { - for j := 0; j < visibilityPerStartTime; j++ { + for range numStartTime { + for range visibilityPerStartTime { workflowID := shuffle.String(testVisibilityWorkflowID) runID := primitives.NewUUID() workflowTypeName := shuffle.String(testVisibilityWorkflowTypeName) @@ -1074,8 +1074,8 @@ func (s *visibilitySuite) TestSelect_MinStartTime_MaxStartTime_StatusClose_Multi historyLength := rand.Int63() minStartTime := closeTime maxStartTime := closeTime.Add(time.Duration(numStartTime) * time.Second) - for i := 0; i < numStartTime; i++ { - for j := 0; j < visibilityPerStartTime; j++ { + for range numStartTime { + for range visibilityPerStartTime { workflowID := shuffle.String(testVisibilityWorkflowID) runID := primitives.NewUUID() workflowTypeName := shuffle.String(testVisibilityWorkflowTypeName) @@ -1213,8 +1213,8 @@ func (s *visibilitySuite) testSelectMinStartTimeMaxStartTimeStatusCloseByTypeMul historyLength := rand.Int63() minStartTime := closeTime maxStartTime := closeTime.Add(time.Duration(numStartTime) * time.Second) - for i := 0; i < numStartTime; i++ { - for j := 0; j < visibilityPerStartTime; j++ { + for range numStartTime { + for range visibilityPerStartTime { workflowID := shuffle.String(testVisibilityWorkflowID) runID := primitives.NewUUID() workflowTypeName := shuffle.String(testVisibilityWorkflowTypeName) diff --git a/common/persistence/sql/sqlplugin/visibility.go b/common/persistence/sql/sqlplugin/visibility.go index 672114944cd..0eef18497e0 100644 --- a/common/persistence/sql/sqlplugin/visibility.go +++ b/common/persistence/sql/sqlplugin/visibility.go @@ -25,7 +25,7 @@ var ( type ( // VisibilitySearchAttributes represents the search attributes json // in executions_visibility table - VisibilitySearchAttributes map[string]interface{} + VisibilitySearchAttributes map[string]any // VisibilityRow represents a row in executions_visibility table VisibilityRow struct { @@ -69,7 +69,7 @@ type ( PageSize *int Query string - QueryArgs []interface{} + QueryArgs []any GroupBy []string } @@ -115,7 +115,7 @@ var _ driver.Valuer = (*VisibilitySearchAttributes)(nil) var DbFields = getDbFields() -func (vsa *VisibilitySearchAttributes) Scan(src interface{}) error { +func (vsa *VisibilitySearchAttributes) Scan(src any) error { if src == nil { return nil } @@ -240,7 +240,7 @@ func GenerateSelectQuery( convertToDbDateTime func(time.Time) time.Time, ) error { whereClauses := make([]string, 0, 10) - queryArgs := make([]interface{}, 0, 10) + queryArgs := make([]any, 0, 10) whereClauses = append( whereClauses, diff --git a/common/persistence/tests/cassandra_test.go b/common/persistence/tests/cassandra_test.go index 5619cbf10f7..f8b38b14c34 100644 --- a/common/persistence/tests/cassandra_test.go +++ b/common/persistence/tests/cassandra_test.go @@ -44,7 +44,7 @@ type ( } statement struct { query string - args []interface{} + args []any } // failingIter is a [gocql.Iter] which fails when iterated. failingIter struct{} @@ -78,11 +78,11 @@ type ( } ) -func (f failingIter) Scan(...interface{}) bool { +func (f failingIter) Scan(...any) bool { return false } -func (f failingIter) MapScan(map[string]interface{}) bool { +func (f failingIter) MapScan(map[string]any) bool { return false } @@ -98,7 +98,7 @@ func (q failingQuery) Iter() gocql.Iter { return failingIter{} } -func (q failingQuery) Scan(...interface{}) error { +func (q failingQuery) Scan(...any) error { return assert.AnError } @@ -110,7 +110,7 @@ func (l *testLogger) Warn(msg string, _ ...tag.Tag) { l.warningMsgs = append(l.warningMsgs, msg) } -func (s *recordingSession) Query(query string, args ...interface{}) gocql.Query { +func (s *recordingSession) Query(query string, args ...any) gocql.Query { s.statements = append(s.statements, statement{ query: query, args: args, @@ -120,6 +120,7 @@ func (s *recordingSession) Query(query string, args ...interface{}) gocql.Query } func TestCassandraShardStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -138,6 +139,7 @@ func TestCassandraShardStoreSuite(t *testing.T) { } func TestCassandraExecutionMutableStateStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -161,6 +163,7 @@ func TestCassandraExecutionMutableStateStoreSuite(t *testing.T) { } func TestCassandraExecutionMutableStateTaskStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -186,6 +189,7 @@ func TestCassandraExecutionMutableStateTaskStoreSuite(t *testing.T) { // TODO: Merge persistence-tests into the tests directory. func TestCassandraHistoryStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -199,6 +203,7 @@ func TestCassandraHistoryStoreSuite(t *testing.T) { } func TestCassandraTaskQueueSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -212,6 +217,7 @@ func TestCassandraTaskQueueSuite(t *testing.T) { } func TestCassandraFairTaskQueueSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -225,6 +231,7 @@ func TestCassandraFairTaskQueueSuite(t *testing.T) { } func TestCassandraTaskQueueTaskSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -238,6 +245,7 @@ func TestCassandraTaskQueueTaskSuite(t *testing.T) { } func TestCassandraTaskQueueFairTaskSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -251,6 +259,7 @@ func TestCassandraTaskQueueFairTaskSuite(t *testing.T) { } func TestCassandraTaskQueueUserDataSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpCassandraTest(t) defer tearDown() @@ -264,6 +273,7 @@ func TestCassandraTaskQueueUserDataSuite(t *testing.T) { } func TestCassandraHistoryV2Persistence(t *testing.T) { + t.Parallel() s := new(persistencetests.HistoryV2PersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithCassandra(&persistencetests.TestBaseOptions{}) s.TestBase.Setup(nil) @@ -271,6 +281,7 @@ func TestCassandraHistoryV2Persistence(t *testing.T) { } func TestCassandraMetadataPersistenceV2(t *testing.T) { + t.Parallel() s := new(persistencetests.MetadataPersistenceSuiteV2) s.TestBase = persistencetests.NewTestBaseWithCassandra(&persistencetests.TestBaseOptions{}) s.TestBase.Setup(nil) @@ -278,6 +289,7 @@ func TestCassandraMetadataPersistenceV2(t *testing.T) { } func TestCassandraClusterMetadataPersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.ClusterMetadataManagerSuite) s.TestBase = persistencetests.NewTestBaseWithCassandra(&persistencetests.TestBaseOptions{}) s.TestBase.Setup(nil) @@ -285,6 +297,7 @@ func TestCassandraClusterMetadataPersistence(t *testing.T) { } func TestCassandraQueuePersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.QueuePersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithCassandra(&persistencetests.TestBaseOptions{}) s.TestBase.Setup(nil) @@ -315,6 +328,7 @@ func TestCassandraQueueV2Persistence(t *testing.T) { } func TestCassandraNexusEndpointPersistence(t *testing.T) { + t.Parallel() cluster := persistencetests.NewTestClusterForCassandra(&persistencetests.TestBaseOptions{}, log.NewNoopLogger()) cluster.SetupTestDatabase() t.Cleanup(cluster.TearDownTestDatabase) @@ -528,7 +542,7 @@ func testCassandraQueueV2MultiplePartitions(t *testing.T, cluster *cassandra.Tes // Query checks if the query matches queryToBlockOn, and, if so, it notifies the test and then blocks until the test // unblocks it. -func (f *blockingSession) Query(query string, args ...interface{}) gocql.Query { +func (f *blockingSession) Query(query string, args ...any) gocql.Query { if query == f.queryToBlockOn { f.queryStarted <- struct{}{} <-f.queryCanContinue @@ -564,7 +578,7 @@ func testCassandraQueueV2EnqueueErrEnqueueMessageConflict(t *testing.T, cluster QueueName: queueName, }) require.NoError(t, err) - for i := 0; i < numConcurrentWrites; i++ { + for range numConcurrentWrites { go func() { res, err := persistencetest.EnqueueMessage(ctx, q, queueType, queueName) if err != nil { @@ -583,7 +597,7 @@ func testCassandraQueueV2EnqueueErrEnqueueMessageConflict(t *testing.T, cluster }() } - for i := 0; i < numConcurrentWrites; i++ { + for range numConcurrentWrites { select { case <-ctx.Done(): printResults(t, results) @@ -596,7 +610,7 @@ func testCassandraQueueV2EnqueueErrEnqueueMessageConflict(t *testing.T, cluster numConflicts := 0 writtenMessageIDs := make([]int, 0, 1) - for i := 0; i < numConcurrentWrites; i++ { + for range numConcurrentWrites { var res enqueueMessageResult select { case <-ctx.Done(): @@ -673,7 +687,7 @@ func testCassandraQueueV2ErrInvalidQueueMessageEncodingType(t *testing.T, cluste assert.ErrorAs(t, err, new(*serialization.UnknownEncodingTypeError)) } -func (q failingQuery) MapScanCAS(map[string]interface{}) (bool, error) { +func (q failingQuery) MapScanCAS(map[string]any) (bool, error) { return false, assert.AnError } @@ -681,7 +695,7 @@ func (q failingQuery) WithContext(context.Context) gocql.Query { return q } -func (f failingSession) Query(query string, args ...interface{}) gocql.Query { +func (f failingSession) Query(query string, args ...any) gocql.Query { for _, q := range f.failingQueries { if q == query { return failingQuery{} @@ -879,7 +893,7 @@ func testCassandraQueueV2MinMessageIDOptimization(t *testing.T, cluster *cassand QueueName: queueName, }) require.NoError(t, err) - for i := 0; i < 2; i++ { + for range 2 { _, err = persistencetest.EnqueueMessage(context.Background(), q, queueType, queueName) require.NoError(t, err) } @@ -969,7 +983,7 @@ func testCassandraQueueV2ConcurrentRangeDeleteMessages(t *testing.T, cluster *ca require.NoError(t, err) // Enqueue 3 messages - for i := 0; i < 3; i++ { + for range 3 { _, err := persistencetest.EnqueueMessage(ctx, qs[0].QueueV2, queueType, queueName) require.NoError(t, err) } @@ -984,7 +998,7 @@ func testCassandraQueueV2ConcurrentRangeDeleteMessages(t *testing.T, cluster *ca } // Wait for both queries to start - for i := 0; i < 2; i++ { + for i := range 2 { <-qs[i].session.queryStarted } @@ -1144,7 +1158,7 @@ func testCassandraQueueV2RepeatedRangeDelete(t *testing.T, cluster *cassandra.Te }) require.NoError(t, err) numMessages := 3 - for i := 0; i < numMessages; i++ { + for range numMessages { _, err := persistencetest.EnqueueMessage(ctx, q, queueType, queueName) require.NoError(t, err) } @@ -1181,7 +1195,7 @@ func getNumMessages( numMessages, // limit ).Iter() numRemainingMessages := 0 - for iter.MapScan(map[string]interface{}{}) { + for iter.MapScan(map[string]any{}) { numRemainingMessages++ } require.NoError(t, iter.Close()) @@ -1277,7 +1291,7 @@ func testCassandraNexusEndpointStoreConcurrentCreate(t *testing.T, store persist requestTableVersion := tableVersion.Load() - for i := 0; i < numConcurrentRequests; i++ { + for range numConcurrentRequests { go func() { <-starter err := store.CreateOrUpdateNexusEndpoint(ctx, &persistence.InternalCreateOrUpdateNexusEndpointRequest{ @@ -1335,7 +1349,7 @@ func testCassandraNexusEndpointStoreConcurrentUpdate(t *testing.T, store persist updateErrors := make(chan error, numConcurrentRequests) defer close(updateErrors) - for i := 0; i < numConcurrentRequests; i++ { + for range numConcurrentRequests { go func() { <-starter err := store.CreateOrUpdateNexusEndpoint(ctx, &persistence.InternalCreateOrUpdateNexusEndpointRequest{ @@ -1498,7 +1512,7 @@ func testCassandraNexusEndpointStoreDeleteWhilePaging(t *testing.T, store persis // Create some endpoints numEndpoints := 3 - for i := 0; i < numEndpoints; i++ { + for range numEndpoints { err := store.CreateOrUpdateNexusEndpoint(ctx, &persistence.InternalCreateOrUpdateNexusEndpointRequest{ LastKnownTableVersion: tableVersion.Load(), Endpoint: persistence.InternalNexusEndpoint{ diff --git a/common/persistence/tests/history_task_queue_manager_test_suite.go b/common/persistence/tests/history_task_queue_manager_test_suite.go index 63dde041550..d1270c0c496 100644 --- a/common/persistence/tests/history_task_queue_manager_test_suite.go +++ b/common/persistence/tests/history_task_queue_manager_test_suite.go @@ -157,7 +157,7 @@ func testHistoryTaskQueueManagerEnqueueTasks(t *testing.T, manager persistence.H }) require.NoError(t, err) - for i := 0; i < 2; i++ { + for i := range 2 { task := &tasks.WorkflowTask{ WorkflowKey: workflowKey, TaskID: int64(i + 1), @@ -168,7 +168,7 @@ func testHistoryTaskQueueManagerEnqueueTasks(t *testing.T, manager persistence.H } var nextPageToken []byte - for i := 0; i < 3; i++ { + for i := range 3 { readRes, err := manager.ReadTasks(ctx, &persistence.ReadTasksRequest{ QueueKey: queueKey, PageSize: 1, @@ -237,7 +237,7 @@ func testHistoryTaskQueueManagerDeleteTasks(t *testing.T, manager *persistence.H QueueKey: queueKey, }) require.NoError(t, err) - for i := 0; i < 2; i++ { + for i := range 2 { _, err := enqueueTask(ctx, manager, queueKey, &tasks.WorkflowTask{ TaskID: int64(i + 1), }) diff --git a/common/persistence/tests/mysql_test.go b/common/persistence/tests/mysql_test.go index 512bffa8422..dfff3ac3288 100644 --- a/common/persistence/tests/mysql_test.go +++ b/common/persistence/tests/mysql_test.go @@ -19,6 +19,7 @@ import ( ) func TestMySQLShardStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -37,6 +38,7 @@ func TestMySQLShardStoreSuite(t *testing.T) { } func TestMySQLExecutionMutableStateStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -60,6 +62,7 @@ func TestMySQLExecutionMutableStateStoreSuite(t *testing.T) { } func TestMySQLExecutionMutableStateTaskStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -83,6 +86,7 @@ func TestMySQLExecutionMutableStateTaskStoreSuite(t *testing.T) { } func TestMySQLHistoryStoreSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -96,6 +100,7 @@ func TestMySQLHistoryStoreSuite(t *testing.T) { } func TestMySQLTaskQueueSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -113,6 +118,7 @@ func TestMySQLTaskQueueSuite(t *testing.T) { } func TestMySQLFairTaskQueueSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -130,6 +136,7 @@ func TestMySQLFairTaskQueueSuite(t *testing.T) { } func TestMySQLTaskQueueTaskSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -143,6 +150,7 @@ func TestMySQLTaskQueueTaskSuite(t *testing.T) { } func TestMySQLTaskQueueFairTaskSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -156,6 +164,7 @@ func TestMySQLTaskQueueFairTaskSuite(t *testing.T) { } func TestMySQLTaskQueueUserDataSuite(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -169,6 +178,7 @@ func TestMySQLTaskQueueUserDataSuite(t *testing.T) { } func TestMySQLVisibilityPersistenceSuite(t *testing.T) { + t.Parallel() s := &VisibilityPersistenceSuite{ TestBase: persistencetests.NewTestBaseWithSQL(persistencetests.GetMySQLTestClusterOption()), } @@ -178,6 +188,7 @@ func TestMySQLVisibilityPersistenceSuite(t *testing.T) { // TODO: Merge persistence-tests into the tests directory. func TestMySQLHistoryV2PersistenceSuite(t *testing.T) { + t.Parallel() s := new(persistencetests.HistoryV2PersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetMySQLTestClusterOption()) s.TestBase.Setup(nil) @@ -185,6 +196,7 @@ func TestMySQLHistoryV2PersistenceSuite(t *testing.T) { } func TestMySQLMetadataPersistenceSuiteV2(t *testing.T) { + t.Parallel() s := new(persistencetests.MetadataPersistenceSuiteV2) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetMySQLTestClusterOption()) s.TestBase.Setup(nil) @@ -192,6 +204,7 @@ func TestMySQLMetadataPersistenceSuiteV2(t *testing.T) { } func TestMySQLQueuePersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.QueuePersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetMySQLTestClusterOption()) s.TestBase.Setup(nil) @@ -199,6 +212,7 @@ func TestMySQLQueuePersistence(t *testing.T) { } func TestMySQLClusterMetadataPersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.ClusterMetadataManagerSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetMySQLTestClusterOption()) s.TestBase.Setup(nil) @@ -208,6 +222,7 @@ func TestMySQLClusterMetadataPersistence(t *testing.T) { // SQL Store tests func TestMySQLNamespaceSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -225,6 +240,7 @@ func TestMySQLNamespaceSuite(t *testing.T) { } func TestMySQLQueueMessageSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -242,6 +258,7 @@ func TestMySQLQueueMessageSuite(t *testing.T) { } func TestMySQLQueueMetadataSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -259,6 +276,7 @@ func TestMySQLQueueMetadataSuite(t *testing.T) { } func TestMySQLMatchingTaskSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -276,6 +294,7 @@ func TestMySQLMatchingTaskSuite(t *testing.T) { } func TestMySQLMatchingTaskV2Suite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -293,6 +312,7 @@ func TestMySQLMatchingTaskV2Suite(t *testing.T) { } func TestMySQLMatchingTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -310,6 +330,7 @@ func TestMySQLMatchingTaskQueueSuite(t *testing.T) { } func TestMySQLMatchingFairTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -327,6 +348,7 @@ func TestMySQLMatchingFairTaskQueueSuite(t *testing.T) { } func TestMySQLHistoryShardSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -344,6 +366,7 @@ func TestMySQLHistoryShardSuite(t *testing.T) { } func TestMySQLHistoryNodeSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -361,6 +384,7 @@ func TestMySQLHistoryNodeSuite(t *testing.T) { } func TestMySQLHistoryTreeSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -378,6 +402,7 @@ func TestMySQLHistoryTreeSuite(t *testing.T) { } func TestMySQLHistoryCurrentExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -395,6 +420,7 @@ func TestMySQLHistoryCurrentExecutionSuite(t *testing.T) { } func TestMySQLHistoryCurrentChasmExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -412,6 +438,7 @@ func TestMySQLHistoryCurrentChasmExecutionSuite(t *testing.T) { } func TestMySQLHistoryExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -429,6 +456,7 @@ func TestMySQLHistoryExecutionSuite(t *testing.T) { } func TestMySQLHistoryTransferTaskSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -446,6 +474,7 @@ func TestMySQLHistoryTransferTaskSuite(t *testing.T) { } func TestMySQLHistoryTimerTaskSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -463,6 +492,7 @@ func TestMySQLHistoryTimerTaskSuite(t *testing.T) { } func TestMySQLHistoryReplicationTaskSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -480,6 +510,7 @@ func TestMySQLHistoryReplicationTaskSuite(t *testing.T) { } func TestMySQLHistoryVisibilityTaskSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -497,6 +528,7 @@ func TestMySQLHistoryVisibilityTaskSuite(t *testing.T) { } func TestMySQLHistoryReplicationDLQTaskSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -514,6 +546,7 @@ func TestMySQLHistoryReplicationDLQTaskSuite(t *testing.T) { } func TestMySQLHistoryExecutionBufferSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -531,6 +564,7 @@ func TestMySQLHistoryExecutionBufferSuite(t *testing.T) { } func TestMySQLHistoryExecutionActivitySuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -548,6 +582,7 @@ func TestMySQLHistoryExecutionActivitySuite(t *testing.T) { } func TestMySQLHistoryExecutionChildWorkflowSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -565,6 +600,7 @@ func TestMySQLHistoryExecutionChildWorkflowSuite(t *testing.T) { } func TestMySQLHistoryExecutionTimerSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -582,6 +618,7 @@ func TestMySQLHistoryExecutionTimerSuite(t *testing.T) { } func TestMySQLHistoryExecutionChasmSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -599,6 +636,7 @@ func TestMySQLHistoryExecutionChasmSuite(t *testing.T) { } func TestMySQLHistoryExecutionRequestCancelSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -616,6 +654,7 @@ func TestMySQLHistoryExecutionRequestCancelSuite(t *testing.T) { } func TestMySQLHistoryExecutionSignalSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -633,6 +672,7 @@ func TestMySQLHistoryExecutionSignalSuite(t *testing.T) { } func TestMySQLHistoryExecutionSignalRequestSuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -650,6 +690,7 @@ func TestMySQLHistoryExecutionSignalRequestSuite(t *testing.T) { } func TestMySQLVisibilitySuite(t *testing.T) { + t.Parallel() cfg := NewMySQLConfig() SetupMySQLDatabase(t, cfg) SetupMySQLSchema(t, cfg) @@ -667,6 +708,7 @@ func TestMySQLVisibilitySuite(t *testing.T) { } func TestMySQLClosedConnectionError(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() @@ -675,12 +717,14 @@ func TestMySQLClosedConnectionError(t *testing.T) { } func TestMySQLQueueV2(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) t.Cleanup(tearDown) RunQueueV2TestSuiteForSQL(t, testData.Factory) } func TestMySQLNexusEndpointPersistence(t *testing.T) { + t.Parallel() testData, tearDown := setUpMySQLTest(t) defer tearDown() diff --git a/common/persistence/tests/postgresql_test.go b/common/persistence/tests/postgresql_test.go index e9d11288d84..20c4df7246f 100644 --- a/common/persistence/tests/postgresql_test.go +++ b/common/persistence/tests/postgresql_test.go @@ -684,11 +684,13 @@ func (p *PostgreSQLSuite) TestPostgreSQLNexusEndpointPersistence() { } func TestPQ(t *testing.T) { + t.Parallel() s := &PostgreSQLSuite{pluginName: "postgres12"} suite.Run(t, s) } func TestPGX(t *testing.T) { + t.Parallel() s := &PostgreSQLSuite{pluginName: "postgres12_pgx"} suite.Run(t, s) } diff --git a/common/persistence/tests/queue_v2_test_suite.go b/common/persistence/tests/queue_v2_test_suite.go index b002ae478fb..24f68c11db3 100644 --- a/common/persistence/tests/queue_v2_test_suite.go +++ b/common/persistence/tests/queue_v2_test_suite.go @@ -241,7 +241,7 @@ func testRangeDeleteMessages(ctx context.Context, t *testing.T, queue persistenc QueueName: queueName, }) require.NoError(t, err) - for i := 0; i < 3; i++ { + for range 3 { _, err := persistencetest.EnqueueMessage(ctx, queue, queueType, queueName) require.NoError(t, err) } @@ -302,7 +302,7 @@ func testRangeDeleteMessages(ctx context.Context, t *testing.T, queue persistenc QueueName: queueName, }) require.NoError(t, err) - for i := 0; i < 3; i++ { + for i := range 3 { msg, err := persistencetest.EnqueueMessage(ctx, queue, queueType, queueName) require.NoError(t, err) assert.Equal(t, int64(persistence.FirstQueueMessageID+i), msg.Metadata.ID) @@ -338,7 +338,7 @@ func testRangeDeleteMessages(ctx context.Context, t *testing.T, queue persistenc QueueName: queueName, }) require.NoError(t, err) - for i := 0; i < 2; i++ { + for range 2 { _, err := persistencetest.EnqueueMessage(ctx, queue, queueType, queueName) require.NoError(t, err) } @@ -444,7 +444,7 @@ func testListQueues(ctx context.Context, t *testing.T, queue persistence.QueueV2 require.Equal(t, int64(0), response.Queues[1].MessageCount) // List multiple queues in pages. - for i := 0; i < 3; i++ { + for i := range 3 { queueNames = append(queueNames, "test-queue-"+t.Name()+strconv.Itoa(i)) } for _, queueName := range queueNames[2:] { diff --git a/common/persistence/tests/sqlite_test.go b/common/persistence/tests/sqlite_test.go index 9da29da95ac..a8d77eb5e0c 100644 --- a/common/persistence/tests/sqlite_test.go +++ b/common/persistence/tests/sqlite_test.go @@ -88,6 +88,7 @@ func LoadSchema(t *testing.T, db sqlplugin.AdminDB, schemaFile string) { } func TestSQLiteExecutionMutableStateStoreSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -121,6 +122,7 @@ func TestSQLiteExecutionMutableStateStoreSuite(t *testing.T) { } func TestSQLiteExecutionMutableStateTaskStoreSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -154,6 +156,7 @@ func TestSQLiteExecutionMutableStateTaskStoreSuite(t *testing.T) { } func TestSQLiteHistoryStoreSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -177,6 +180,7 @@ func TestSQLiteHistoryStoreSuite(t *testing.T) { } func TestSQLiteTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -200,6 +204,7 @@ func TestSQLiteTaskQueueSuite(t *testing.T) { } func TestSQLiteFairTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -223,6 +228,7 @@ func TestSQLiteFairTaskQueueSuite(t *testing.T) { } func TestSQLiteTaskQueueTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -246,6 +252,7 @@ func TestSQLiteTaskQueueTaskSuite(t *testing.T) { } func TestSQLiteTaskQueueFairTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -269,6 +276,7 @@ func TestSQLiteTaskQueueFairTaskSuite(t *testing.T) { } func TestSQLiteTaskQueueUserDataSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() logger := log.NewNoopLogger() factory := sql.NewFactory( @@ -292,6 +300,7 @@ func TestSQLiteTaskQueueUserDataSuite(t *testing.T) { } func TestSQLiteFileExecutionMutableStateStoreSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -329,6 +338,7 @@ func TestSQLiteFileExecutionMutableStateStoreSuite(t *testing.T) { } func TestSQLiteFileExecutionMutableStateTaskStoreSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -366,6 +376,7 @@ func TestSQLiteFileExecutionMutableStateTaskStoreSuite(t *testing.T) { } func TestSQLiteFileHistoryStoreSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -393,6 +404,7 @@ func TestSQLiteFileHistoryStoreSuite(t *testing.T) { } func TestSQLiteFileTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -420,6 +432,7 @@ func TestSQLiteFileTaskQueueSuite(t *testing.T) { } func TestSQLiteFileFairTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -447,6 +460,7 @@ func TestSQLiteFileFairTaskQueueSuite(t *testing.T) { } func TestSQLiteFileTaskQueueTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -474,6 +488,7 @@ func TestSQLiteFileTaskQueueTaskSuite(t *testing.T) { } func TestSQLiteFileTaskQueueFairTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -501,6 +516,7 @@ func TestSQLiteFileTaskQueueFairTaskSuite(t *testing.T) { } func TestSQLiteFileTaskQueueUserDataSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) defer func() { @@ -530,12 +546,14 @@ func TestSQLiteFileTaskQueueUserDataSuite(t *testing.T) { // TODO: Merge persistence-tests into the tests directory. func TestSQLiteVisibilityPersistenceSuite(t *testing.T) { + t.Parallel() s := new(VisibilityPersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteMemoryTestClusterOption()) suite.Run(t, s) } func TestSQLiteHistoryV2PersistenceSuite(t *testing.T) { + t.Parallel() s := new(persistencetests.HistoryV2PersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteMemoryTestClusterOption()) s.TestBase.Setup(nil) @@ -543,6 +561,7 @@ func TestSQLiteHistoryV2PersistenceSuite(t *testing.T) { } func TestSQLiteMetadataPersistenceSuiteV2(t *testing.T) { + t.Parallel() s := new(persistencetests.MetadataPersistenceSuiteV2) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteMemoryTestClusterOption()) s.TestBase.Setup(nil) @@ -550,6 +569,7 @@ func TestSQLiteMetadataPersistenceSuiteV2(t *testing.T) { } func TestSQLiteClusterMetadataPersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.ClusterMetadataManagerSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteMemoryTestClusterOption()) s.TestBase.Setup(nil) @@ -557,6 +577,7 @@ func TestSQLiteClusterMetadataPersistence(t *testing.T) { } func TestSQLiteQueuePersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.QueuePersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteMemoryTestClusterOption()) s.TestBase.Setup(nil) @@ -564,6 +585,7 @@ func TestSQLiteQueuePersistence(t *testing.T) { } func TestSQLiteFileHistoryV2PersistenceSuite(t *testing.T) { + t.Parallel() s := new(persistencetests.HistoryV2PersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteFileTestClusterOption()) s.TestBase.Setup(nil) @@ -571,6 +593,7 @@ func TestSQLiteFileHistoryV2PersistenceSuite(t *testing.T) { } func TestSQLiteFileMetadataPersistenceSuiteV2(t *testing.T) { + t.Parallel() s := new(persistencetests.MetadataPersistenceSuiteV2) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteFileTestClusterOption()) s.TestBase.Setup(nil) @@ -578,6 +601,7 @@ func TestSQLiteFileMetadataPersistenceSuiteV2(t *testing.T) { } func TestSQLiteFileClusterMetadataPersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.ClusterMetadataManagerSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteFileTestClusterOption()) s.TestBase.Setup(nil) @@ -585,6 +609,7 @@ func TestSQLiteFileClusterMetadataPersistence(t *testing.T) { } func TestSQLiteFileQueuePersistence(t *testing.T) { + t.Parallel() s := new(persistencetests.QueuePersistenceSuite) s.TestBase = persistencetests.NewTestBaseWithSQL(persistencetests.GetSQLiteFileTestClusterOption()) s.TestBase.Setup(nil) @@ -594,6 +619,7 @@ func TestSQLiteFileQueuePersistence(t *testing.T) { // SQL store tests func TestSQLiteNamespaceSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -608,6 +634,7 @@ func TestSQLiteNamespaceSuite(t *testing.T) { } func TestSQLiteQueueMessageSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -622,6 +649,7 @@ func TestSQLiteQueueMessageSuite(t *testing.T) { } func TestSQLiteQueueMetadataSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -636,6 +664,7 @@ func TestSQLiteQueueMetadataSuite(t *testing.T) { } func TestSQLiteMatchingTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -650,6 +679,7 @@ func TestSQLiteMatchingTaskSuite(t *testing.T) { } func TestSQLiteMatchingTaskV2Suite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -664,6 +694,7 @@ func TestSQLiteMatchingTaskV2Suite(t *testing.T) { } func TestSQLiteMatchingTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -678,6 +709,7 @@ func TestSQLiteMatchingTaskQueueSuite(t *testing.T) { } func TestSQLiteMatchingTaskQueueV2Suite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -692,6 +724,7 @@ func TestSQLiteMatchingTaskQueueV2Suite(t *testing.T) { } func TestSQLiteHistoryShardSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -706,6 +739,7 @@ func TestSQLiteHistoryShardSuite(t *testing.T) { } func TestSQLiteHistoryNodeSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -720,6 +754,7 @@ func TestSQLiteHistoryNodeSuite(t *testing.T) { } func TestSQLiteHistoryTreeSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -734,6 +769,7 @@ func TestSQLiteHistoryTreeSuite(t *testing.T) { } func TestSQLiteHistoryCurrentExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -748,6 +784,7 @@ func TestSQLiteHistoryCurrentExecutionSuite(t *testing.T) { } func TestSQLiteHistoryCurrentChasmExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -762,6 +799,7 @@ func TestSQLiteHistoryCurrentChasmExecutionSuite(t *testing.T) { } func TestSQLiteHistoryExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -776,6 +814,7 @@ func TestSQLiteHistoryExecutionSuite(t *testing.T) { } func TestSQLiteHistoryTransferTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -790,6 +829,7 @@ func TestSQLiteHistoryTransferTaskSuite(t *testing.T) { } func TestSQLiteHistoryTimerTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -804,6 +844,7 @@ func TestSQLiteHistoryTimerTaskSuite(t *testing.T) { } func TestSQLiteHistoryReplicationTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -818,6 +859,7 @@ func TestSQLiteHistoryReplicationTaskSuite(t *testing.T) { } func TestSQLiteHistoryVisibilityTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -832,6 +874,7 @@ func TestSQLiteHistoryVisibilityTaskSuite(t *testing.T) { } func TestSQLiteHistoryReplicationDLQTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -846,6 +889,7 @@ func TestSQLiteHistoryReplicationDLQTaskSuite(t *testing.T) { } func TestSQLiteHistoryExecutionBufferSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -860,6 +904,7 @@ func TestSQLiteHistoryExecutionBufferSuite(t *testing.T) { } func TestSQLiteHistoryExecutionActivitySuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -874,6 +919,7 @@ func TestSQLiteHistoryExecutionActivitySuite(t *testing.T) { } func TestSQLiteHistoryExecutionChildWorkflowSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -888,6 +934,7 @@ func TestSQLiteHistoryExecutionChildWorkflowSuite(t *testing.T) { } func TestSQLiteHistoryExecutionTimerSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -902,6 +949,7 @@ func TestSQLiteHistoryExecutionTimerSuite(t *testing.T) { } func TestSQLiteHistoryExecutionChasmSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -916,6 +964,7 @@ func TestSQLiteHistoryExecutionChasmSuite(t *testing.T) { } func TestSQLiteHistoryExecutionRequestCancelSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -930,6 +979,7 @@ func TestSQLiteHistoryExecutionRequestCancelSuite(t *testing.T) { } func TestSQLiteHistoryExecutionSignalSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -944,6 +994,7 @@ func TestSQLiteHistoryExecutionSignalSuite(t *testing.T) { } func TestSQLiteHistoryExecutionSignalRequestSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -958,6 +1009,7 @@ func TestSQLiteHistoryExecutionSignalRequestSuite(t *testing.T) { } func TestSQLiteVisibilitySuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() store, err := sql.NewSQLDB(sqlplugin.DbKindVisibility, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { @@ -972,6 +1024,7 @@ func TestSQLiteVisibilitySuite(t *testing.T) { } func TestSQLiteFileNamespaceSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -987,6 +1040,7 @@ func TestSQLiteFileNamespaceSuite(t *testing.T) { } func TestSQLiteFileQueueMessageSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1002,6 +1056,7 @@ func TestSQLiteFileQueueMessageSuite(t *testing.T) { } func TestSQLiteFileQueueMetadataSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1017,6 +1072,7 @@ func TestSQLiteFileQueueMetadataSuite(t *testing.T) { } func TestSQLiteFileMatchingTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1032,6 +1088,7 @@ func TestSQLiteFileMatchingTaskSuite(t *testing.T) { } func TestSQLiteFileMatchingTaskQueueSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1047,6 +1104,7 @@ func TestSQLiteFileMatchingTaskQueueSuite(t *testing.T) { } func TestSQLiteFileMatchingTaskQueueV2Suite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1062,6 +1120,7 @@ func TestSQLiteFileMatchingTaskQueueV2Suite(t *testing.T) { } func TestSQLiteFileHistoryShardSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1077,6 +1136,7 @@ func TestSQLiteFileHistoryShardSuite(t *testing.T) { } func TestSQLiteFileHistoryNodeSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1092,6 +1152,7 @@ func TestSQLiteFileHistoryNodeSuite(t *testing.T) { } func TestSQLiteFileHistoryTreeSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1107,6 +1168,7 @@ func TestSQLiteFileHistoryTreeSuite(t *testing.T) { } func TestSQLiteFileHistoryCurrentExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1122,6 +1184,7 @@ func TestSQLiteFileHistoryCurrentExecutionSuite(t *testing.T) { } func TestSQLiteFileHistoryCurrentChasmExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1137,6 +1200,7 @@ func TestSQLiteFileHistoryCurrentChasmExecutionSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1152,6 +1216,7 @@ func TestSQLiteFileHistoryExecutionSuite(t *testing.T) { } func TestSQLiteFileHistoryTransferTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1167,6 +1232,7 @@ func TestSQLiteFileHistoryTransferTaskSuite(t *testing.T) { } func TestSQLiteFileHistoryTimerTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1182,6 +1248,7 @@ func TestSQLiteFileHistoryTimerTaskSuite(t *testing.T) { } func TestSQLiteFileHistoryReplicationTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1197,6 +1264,7 @@ func TestSQLiteFileHistoryReplicationTaskSuite(t *testing.T) { } func TestSQLiteFileHistoryVisibilityTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1212,6 +1280,7 @@ func TestSQLiteFileHistoryVisibilityTaskSuite(t *testing.T) { } func TestSQLiteFileHistoryReplicationDLQTaskSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1227,6 +1296,7 @@ func TestSQLiteFileHistoryReplicationDLQTaskSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionBufferSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1242,6 +1312,7 @@ func TestSQLiteFileHistoryExecutionBufferSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionActivitySuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1257,6 +1328,7 @@ func TestSQLiteFileHistoryExecutionActivitySuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionChildWorkflowSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1272,6 +1344,7 @@ func TestSQLiteFileHistoryExecutionChildWorkflowSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionTimerSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1287,6 +1360,7 @@ func TestSQLiteFileHistoryExecutionTimerSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionChasmSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1302,6 +1376,7 @@ func TestSQLiteFileHistoryExecutionChasmSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionRequestCancelSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1317,6 +1392,7 @@ func TestSQLiteFileHistoryExecutionRequestCancelSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionSignalSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1332,6 +1408,7 @@ func TestSQLiteFileHistoryExecutionSignalSuite(t *testing.T) { } func TestSQLiteFileHistoryExecutionSignalRequestSuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindMain, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1347,6 +1424,7 @@ func TestSQLiteFileHistoryExecutionSignalRequestSuite(t *testing.T) { } func TestSQLiteFileVisibilitySuite(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) store, err := sql.NewSQLDB(sqlplugin.DbKindVisibility, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) @@ -1362,6 +1440,7 @@ func TestSQLiteFileVisibilitySuite(t *testing.T) { } func TestSQLiteQueueV2(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) logger := log.NewNoopLogger() @@ -1381,6 +1460,7 @@ func TestSQLiteQueueV2(t *testing.T) { } func TestSQLiteNexusEndpointPersistence(t *testing.T) { + t.Parallel() cfg := NewSQLiteFileConfig() SetupSQLiteDatabase(t, cfg) logger := log.NewNoopLogger() @@ -1403,6 +1483,7 @@ func TestSQLiteNexusEndpointPersistence(t *testing.T) { // connection to the sqlite database, we will lose the db in this case. We fixed this by extending the driver in // modernc.org/sqlite. This test verifies that fix. func TestSQLiteTransactionContextCancellation(t *testing.T) { + t.Parallel() cfg := NewSQLiteMemoryConfig() db, err := sql.NewSQLDB(sqlplugin.DbKindVisibility, cfg, resolver.NewNoopResolver(), log.NewTestLogger(), metrics.NoopMetricsHandler) if err != nil { diff --git a/common/persistence/tests/task_queue_task.go b/common/persistence/tests/task_queue_task.go index 71753b40b8d..937801898d6 100644 --- a/common/persistence/tests/task_queue_task.go +++ b/common/persistence/tests/task_queue_task.go @@ -146,9 +146,9 @@ func (s *TaskQueueTaskSuite) TestCreateGet_Multiple() { taskQueue := s.createTaskQueue(rangeID) var expectedTasks []*persistencespb.AllocatedTaskInfo - for i := 0; i < numCreateBatch; i++ { + for i := range numCreateBatch { var tasks []*persistencespb.AllocatedTaskInfo - for j := 0; j < createBatchSize; j++ { + for j := range createBatchSize { taskID := minTaskID + int64(i*numCreateBatch+j) task := s.randomTask(taskID) tasks = append(tasks, task) @@ -193,9 +193,9 @@ func (s *TaskQueueTaskSuite) TestCreateDelete_Multiple() { rangeID := rand.Int63() taskQueue := s.createTaskQueue(rangeID) - for i := 0; i < numCreateBatch; i++ { + for i := range numCreateBatch { var tasks []*persistencespb.AllocatedTaskInfo - for j := 0; j < createBatchSize; j++ { + for j := range createBatchSize { taskID := minTaskID + int64(i*numCreateBatch+j) task := s.randomTask(taskID) tasks = append(tasks, task) diff --git a/common/persistence/tests/visibility_persistence_suite.go b/common/persistence/tests/visibility_persistence_suite.go index e06af1b7a9d..40bb9fbb6e3 100644 --- a/common/persistence/tests/visibility_persistence_suite.go +++ b/common/persistence/tests/visibility_persistence_suite.go @@ -705,7 +705,7 @@ func (s *VisibilityPersistenceSuite) TestDeleteWorkflow() { startTime := closeTime.Add(-5 * time.Second) executionTime := closeTime.Add(-4 * time.Second) var startRequests []*manager.RecordWorkflowExecutionStartedRequest - for i := 0; i < openRows; i++ { + for range openRows { startReq := s.createOpenWorkflowRecord( testNamespaceUUID, uuid.NewString(), @@ -717,7 +717,7 @@ func (s *VisibilityPersistenceSuite) TestDeleteWorkflow() { startRequests = append(startRequests, startReq) } - for i := 0; i < closedRows; i++ { + for i := range closedRows { s.createClosedWorkflowRecord( startRequests[i], closeTime, @@ -867,7 +867,7 @@ func (s *VisibilityPersistenceSuite) TestGetWorkflowExecution() { startTime := closeTime.Add(-5 * time.Second) var startRequests []*manager.RecordWorkflowExecutionStartedRequest - for i := 0; i < 5; i++ { + for range 5 { startRequests = append( startRequests, s.createOpenWorkflowRecord( @@ -919,7 +919,7 @@ func (s *VisibilityPersistenceSuite) TestAdvancedVisibilityPagination() { // Generate 5 workflow records, keep 2 open and 3 closed. var startReqs []*manager.RecordWorkflowExecutionStartedRequest var closeReqs []*manager.RecordWorkflowExecutionClosedRequest - for i := 0; i < 5; i++ { + for i := range 5 { startTime := time.Now() startReq := s.createOpenWorkflowRecord( testNamespaceUUID, @@ -971,7 +971,7 @@ func (s *VisibilityPersistenceSuite) TestCountWorkflowExecutions() { closeTime := time.Now().UTC() startTime := closeTime.Add(-5 * time.Second) - for i := 0; i < 5; i++ { + for range 5 { s.createOpenWorkflowRecord( testNamespaceUUID, "visibility-workflow-test", @@ -1000,7 +1000,7 @@ func (s *VisibilityPersistenceSuite) TestCountGroupByWorkflowExecutions() { startTime := closeTime.Add(-5 * time.Second) var startRequests []*manager.RecordWorkflowExecutionStartedRequest - for i := 0; i < 5; i++ { + for range 5 { startRequests = append( startRequests, s.createOpenWorkflowRecord( @@ -1014,7 +1014,7 @@ func (s *VisibilityPersistenceSuite) TestCountGroupByWorkflowExecutions() { ) } - runningStatusPayload, _ := searchattribute.EncodeValue( + runningStatusPayload, _ := sadefs.EncodeValue( enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String(), enumspb.INDEXED_VALUE_TYPE_KEYWORD, ) @@ -1037,7 +1037,7 @@ func (s *VisibilityPersistenceSuite) TestCountGroupByWorkflowExecutions() { resp.Groups, ) - for i := 0; i < 2; i++ { + for i := range 2 { s.createClosedWorkflowRecord( startRequests[i], closeTime, diff --git a/common/persistence/versionhistory/version_history_item.go b/common/persistence/versionhistory/version_history_item.go index 27a92af4e35..2e0c07ce55b 100644 --- a/common/persistence/versionhistory/version_history_item.go +++ b/common/persistence/versionhistory/version_history_item.go @@ -30,7 +30,7 @@ func IsEqualVersionHistoryItems(items1 []*historyspb.VersionHistoryItem, items2 if len(items1) != len(items2) { return false } - for i := 0; i < len(items1); i++ { + for i := range items1 { if !IsEqualVersionHistoryItem(items1[i], items2[i]) { return false } diff --git a/common/persistence/visibility/chasm_visibility_manager.go b/common/persistence/visibility/chasm_visibility_manager.go index 05cbd9d2b16..2171e071059 100644 --- a/common/persistence/visibility/chasm_visibility_manager.go +++ b/common/persistence/visibility/chasm_visibility_manager.go @@ -4,8 +4,8 @@ import ( "context" "reflect" - commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence/visibility/manager" @@ -13,6 +13,7 @@ import ( type ChasmVisibilityManager struct { registry *chasm.Registry + nsRegistry namespace.Registry visibilityMgr manager.VisibilityManager } @@ -20,19 +21,22 @@ var _ chasm.VisibilityManager = (*ChasmVisibilityManager)(nil) func NewChasmVisibilityManager( registry *chasm.Registry, + nsRegistry namespace.Registry, visibilityMgr manager.VisibilityManager, ) *ChasmVisibilityManager { return &ChasmVisibilityManager{ registry: registry, + nsRegistry: nsRegistry, visibilityMgr: visibilityMgr, } } func ChasmVisibilityManagerProvider( registry *chasm.Registry, + nsRegistry namespace.Registry, visibilityMgr manager.VisibilityManager, ) chasm.VisibilityManager { - return NewChasmVisibilityManager(registry, visibilityMgr) + return NewChasmVisibilityManager(registry, nsRegistry, visibilityMgr) } // ListExecutions implements the Engine interface for visibility queries. @@ -40,17 +44,22 @@ func (e *ChasmVisibilityManager) ListExecutions( ctx context.Context, archetypeType reflect.Type, request *chasm.ListExecutionsRequest, -) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { +) (*visibilityservice.ListChasmExecutionsResponse, error) { archetypeID, ok := e.registry.ArchetypeIDOf(archetypeType) if !ok { return nil, serviceerror.NewInternal("unknown chasm component type: " + archetypeType.String()) } - visReq := &manager.ListChasmExecutionsRequest{ - ArchetypeID: archetypeID, - NamespaceID: namespace.ID(request.NamespaceID), - Namespace: namespace.Name(request.NamespaceName), - PageSize: request.PageSize, + namespaceID, err := e.nsRegistry.GetNamespaceID(namespace.Name(request.NamespaceName)) + if err != nil { + return nil, err + } + + visReq := &visibilityservice.ListChasmExecutionsRequest{ + ArchetypeId: archetypeID, + NamespaceId: namespaceID.String(), + Namespace: request.NamespaceName, + PageSize: int32(request.PageSize), NextPageToken: request.NextPageToken, Query: request.Query, } @@ -63,16 +72,21 @@ func (e *ChasmVisibilityManager) CountExecutions( ctx context.Context, archetypeType reflect.Type, request *chasm.CountExecutionsRequest, -) (*chasm.CountExecutionsResponse, error) { +) (*visibilityservice.CountChasmExecutionsResponse, error) { archetypeID, ok := e.registry.ArchetypeIDOf(archetypeType) if !ok { return nil, serviceerror.NewInternal("unknown chasm component type: " + archetypeType.String()) } - visReq := &manager.CountChasmExecutionsRequest{ - ArchetypeID: archetypeID, - NamespaceID: namespace.ID(request.NamespaceID), - Namespace: namespace.Name(request.NamespaceName), + namespaceID, err := e.nsRegistry.GetNamespaceID(namespace.Name(request.NamespaceName)) + if err != nil { + return nil, err + } + + visReq := &visibilityservice.CountChasmExecutionsRequest{ + ArchetypeId: archetypeID, + NamespaceId: namespaceID.String(), + Namespace: request.NamespaceName, Query: request.Query, } diff --git a/common/persistence/visibility/chasm_visibility_manager_test.go b/common/persistence/visibility/chasm_visibility_manager_test.go index a9e264c7af1..aded3aeb011 100644 --- a/common/persistence/visibility/chasm_visibility_manager_test.go +++ b/common/persistence/visibility/chasm_visibility_manager_test.go @@ -9,14 +9,19 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + chasmspb "go.temporal.io/server/api/chasm/v1" persistencespb "go.temporal.io/server/api/persistence/v1" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/payload" "go.temporal.io/server/common/persistence/visibility/manager" + "go.temporal.io/server/common/searchattribute/sadefs" "go.uber.org/mock/gomock" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) type ( @@ -26,6 +31,7 @@ type ( controller *gomock.Controller registry *chasm.Registry + nsRegistry *namespace.MockRegistry visibilityManager *manager.MockVisibilityManager visibilityMgr *ChasmVisibilityManager } @@ -94,8 +100,12 @@ func (s *ChasmVisibilityManagerSuite) SetupTest() { s.visibilityManager = manager.NewMockVisibilityManager(s.controller) + s.nsRegistry = namespace.NewMockRegistry(s.controller) + s.nsRegistry.EXPECT().GetNamespaceID(testChasmNamespace).Return(testChasmNamespaceID, nil).AnyTimes() + s.visibilityMgr = NewChasmVisibilityManager( s.registry, + s.nsRegistry, s.visibilityManager, ) } @@ -138,33 +148,37 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_Success() { s.True(ok) // Create chasm search attributes map - chasmSearchAttributes := chasm.NewSearchAttributesMap(map[string]chasm.VisibilityValue{ - testChasmSA1Name: chasm.VisibilityValueInt64(123), - testChasmSA2Name: chasm.VisibilityValueString("test-value"), - }) + chasmSearchAttributes := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + testChasmSA1Name: sadefs.MustEncodeValue(int64(123), enumspb.INDEXED_VALUE_TYPE_INT), + testChasmSA2Name: sadefs.MustEncodeValue("test-value", enumspb.INDEXED_VALUE_TYPE_KEYWORD), + }, + } // Create custom search attributes map - customSearchAttributes := map[string]*commonpb.Payload{ - testCustomSAName: customSAPayload, + customSearchAttributes := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + testCustomSAName: customSAPayload, + }, } // Setup visibility manager mock - expectedRequest := &manager.ListChasmExecutionsRequest{ - ArchetypeID: archetypeID, - NamespaceID: testChasmNamespaceID, - Namespace: testChasmNamespace, - PageSize: pageSize, + expectedRequest := &visibilityservice.ListChasmExecutionsRequest{ + ArchetypeId: archetypeID, + NamespaceId: testChasmNamespaceID.String(), + Namespace: testChasmNamespace.String(), + PageSize: int32(pageSize), NextPageToken: pageToken, Query: query, } - expectedResponse := &chasm.ListExecutionsResponse[*commonpb.Payload]{ - Executions: []*chasm.ExecutionInfo[*commonpb.Payload]{ + expectedResponse := &visibilityservice.ListChasmExecutionsResponse{ + Executions: []*chasmspb.VisibilityExecutionInfo{ { - BusinessID: testBusinessID, - RunID: testRunID, - StartTime: testComponentStartTime, - CloseTime: testComponentCloseTime, + BusinessId: testBusinessID, + RunId: testRunID, + StartTime: timestamppb.New(testComponentStartTime), + CloseTime: timestamppb.New(testComponentCloseTime), HistoryLength: 100, HistorySizeBytes: 5000, StateTransitionCount: 42, @@ -179,9 +193,12 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_Success() { s.visibilityManager.EXPECT(). ListChasmExecutions(ctx, gomock.Any()). - DoAndReturn(func(_ context.Context, req *manager.ListChasmExecutionsRequest) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { - s.Equal(expectedRequest.ArchetypeID, req.ArchetypeID) - s.Equal(expectedRequest.NamespaceID, req.NamespaceID) + DoAndReturn(func( + _ context.Context, + req *visibilityservice.ListChasmExecutionsRequest, + ) (*visibilityservice.ListChasmExecutionsResponse, error) { + s.Equal(expectedRequest.ArchetypeId, req.ArchetypeId) + s.Equal(expectedRequest.NamespaceId, req.NamespaceId) s.Equal(expectedRequest.Namespace, req.Namespace) s.Equal(expectedRequest.PageSize, req.PageSize) s.Equal(expectedRequest.NextPageToken, req.NextPageToken) @@ -191,7 +208,6 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_Success() { // Call ListExecutions request := &chasm.ListExecutionsRequest{ - NamespaceID: string(testChasmNamespaceID), NamespaceName: string(testChasmNamespace), Query: query, PageSize: pageSize, @@ -211,10 +227,10 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_Success() { s.Equal([]byte("next-token"), response.NextPageToken) execution := response.Executions[0] - s.Equal(testBusinessID, execution.BusinessID) - s.Equal(testRunID, execution.RunID) - s.Equal(testComponentStartTime, execution.StartTime) - s.Equal(testComponentCloseTime, execution.CloseTime) + s.Equal(testBusinessID, execution.BusinessId) + s.Equal(testRunID, execution.RunId) + s.Equal(testComponentStartTime, execution.StartTime.AsTime()) + s.Equal(testComponentCloseTime, execution.CloseTime.AsTime()) s.Equal(int64(100), execution.HistoryLength) s.Equal(int64(5000), execution.HistorySizeBytes) s.Equal(int64(42), execution.StateTransitionCount) @@ -246,22 +262,25 @@ func (s *ChasmVisibilityManagerSuite) TestCountExecutions_Success() { expectedCount := int64(42) // Setup visibility manager mock - expectedRequest := &manager.CountChasmExecutionsRequest{ - ArchetypeID: archetypeID, - NamespaceID: testChasmNamespaceID, - Namespace: testChasmNamespace, + expectedRequest := &visibilityservice.CountChasmExecutionsRequest{ + ArchetypeId: archetypeID, + NamespaceId: testChasmNamespaceID.String(), + Namespace: testChasmNamespace.String(), Query: query, } - expectedResponse := &chasm.CountExecutionsResponse{ + expectedResponse := &visibilityservice.CountChasmExecutionsResponse{ Count: expectedCount, } s.visibilityManager.EXPECT(). CountChasmExecutions(ctx, gomock.Any()). - DoAndReturn(func(_ context.Context, req *manager.CountChasmExecutionsRequest) (*chasm.CountExecutionsResponse, error) { - s.Equal(expectedRequest.ArchetypeID, req.ArchetypeID) - s.Equal(expectedRequest.NamespaceID, req.NamespaceID) + DoAndReturn(func( + _ context.Context, + req *visibilityservice.CountChasmExecutionsRequest, + ) (*visibilityservice.CountChasmExecutionsResponse, error) { + s.Equal(expectedRequest.ArchetypeId, req.ArchetypeId) + s.Equal(expectedRequest.NamespaceId, req.NamespaceId) s.Equal(expectedRequest.Namespace, req.Namespace) s.Equal(expectedRequest.Query, req.Query) return expectedResponse, nil @@ -269,7 +288,6 @@ func (s *ChasmVisibilityManagerSuite) TestCountExecutions_Success() { // Call CountExecutions request := &chasm.CountExecutionsRequest{ - NamespaceID: string(testChasmNamespaceID), NamespaceName: string(testChasmNamespace), Query: query, } @@ -293,7 +311,6 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_InvalidArchetypeType() invalidType := reflect.TypeFor[struct{ Field string }]() request := &chasm.ListExecutionsRequest{ - NamespaceID: string(testChasmNamespaceID), NamespaceName: string(testChasmNamespace), Query: "StartTime > '2024-01-01T00:00:00Z'", } @@ -315,7 +332,6 @@ func (s *ChasmVisibilityManagerSuite) TestCountExecutions_InvalidArchetypeType() invalidType := reflect.TypeFor[struct{ Field string }]() request := &chasm.CountExecutionsRequest{ - NamespaceID: string(testChasmNamespaceID), NamespaceName: string(testChasmNamespace), Query: "StartTime > '2024-01-01T00:00:00Z'", } @@ -342,13 +358,15 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_VisibilityManagerError( // Setup visibility manager mock to return an error s.visibilityManager.EXPECT(). ListChasmExecutions(ctx, gomock.Any()). - DoAndReturn(func(_ context.Context, req *manager.ListChasmExecutionsRequest) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { - s.Equal(archetypeID, req.ArchetypeID) + DoAndReturn(func( + _ context.Context, + req *visibilityservice.ListChasmExecutionsRequest, + ) (*visibilityservice.ListChasmExecutionsResponse, error) { + s.Equal(archetypeID, req.ArchetypeId) return nil, errTestVisibilityError }) request := &chasm.ListExecutionsRequest{ - NamespaceID: string(testChasmNamespaceID), NamespaceName: string(testChasmNamespace), Query: query, } @@ -376,13 +394,15 @@ func (s *ChasmVisibilityManagerSuite) TestCountExecutions_VisibilityManagerError // Setup visibility manager mock to return an error s.visibilityManager.EXPECT(). CountChasmExecutions(ctx, gomock.Any()). - DoAndReturn(func(_ context.Context, req *manager.CountChasmExecutionsRequest) (*chasm.CountExecutionsResponse, error) { - s.Equal(archetypeID, req.ArchetypeID) + DoAndReturn(func( + _ context.Context, + req *visibilityservice.CountChasmExecutionsRequest, + ) (*visibilityservice.CountChasmExecutionsResponse, error) { + s.Equal(archetypeID, req.ArchetypeId) return nil, errTestVisibilityError }) request := &chasm.CountExecutionsRequest{ - NamespaceID: string(testChasmNamespaceID), NamespaceName: string(testChasmNamespace), Query: query, } @@ -431,7 +451,9 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_WithTaskQueueSearchAttr s.NoError(err) visibilityMgr := manager.NewMockVisibilityManager(ctrl) - chasmVisMgr := NewChasmVisibilityManager(registry, visibilityMgr) + nsRegistry := namespace.NewMockRegistry(ctrl) + nsRegistry.EXPECT().GetNamespaceID(testChasmNamespace).Return(testChasmNamespaceID, nil).AnyTimes() + chasmVisMgr := NewChasmVisibilityManager(registry, nsRegistry, visibilityMgr) ctx := context.Background() query := "TaskQueue = 'my-task-queue'" @@ -441,17 +463,19 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_WithTaskQueueSearchAttr s.True(ok) expectedTaskQueue := "my-task-queue" - chasmSearchAttributes := chasm.NewSearchAttributesMap(map[string]chasm.VisibilityValue{ - "TaskQueue": chasm.VisibilityValueString(expectedTaskQueue), - }) + chasmSearchAttributes := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + sadefs.TaskQueue: sadefs.MustEncodeValue(expectedTaskQueue, enumspb.INDEXED_VALUE_TYPE_KEYWORD), + }, + } - expectedResponse := &chasm.ListExecutionsResponse[*commonpb.Payload]{ - Executions: []*chasm.ExecutionInfo[*commonpb.Payload]{ + expectedResponse := &visibilityservice.ListChasmExecutionsResponse{ + Executions: []*chasmspb.VisibilityExecutionInfo{ { - BusinessID: testBusinessID, - RunID: testRunID, - StartTime: testComponentStartTime, - CloseTime: testComponentCloseTime, + BusinessId: testBusinessID, + RunId: testRunID, + StartTime: timestamppb.New(testComponentStartTime), + CloseTime: timestamppb.New(testComponentCloseTime), ChasmSearchAttributes: chasmSearchAttributes, }, }, @@ -460,14 +484,16 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_WithTaskQueueSearchAttr visibilityMgr.EXPECT(). ListChasmExecutions(ctx, gomock.Any()). - DoAndReturn(func(_ context.Context, req *manager.ListChasmExecutionsRequest) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { - s.Equal(archetypeID, req.ArchetypeID) + DoAndReturn(func( + _ context.Context, + req *visibilityservice.ListChasmExecutionsRequest, + ) (*visibilityservice.ListChasmExecutionsResponse, error) { + s.Equal(archetypeID, req.ArchetypeId) return expectedResponse, nil }) // Call ListExecutions request := &chasm.ListExecutionsRequest{ - NamespaceID: string(testChasmNamespaceID), NamespaceName: string(testChasmNamespace), Query: query, PageSize: pageSize, @@ -485,8 +511,13 @@ func (s *ChasmVisibilityManagerSuite) TestListExecutions_WithTaskQueueSearchAttr execution := response.Executions[0] s.NotNil(execution.ChasmSearchAttributes) - // Verify TaskQueue is in the CHASM search attributes using GetValue with the preallocated search attribute - taskQueueVal, ok := chasm.SearchAttributeValue(execution.ChasmSearchAttributes, chasm.SearchAttributeTaskQueue) - s.True(ok) + // Verify TaskQueue is in the CHASM search attributes + s.Contains(execution.ChasmSearchAttributes.GetIndexedFields(), sadefs.TaskQueue) + taskQueueVal, err := sadefs.DecodeValue( + execution.ChasmSearchAttributes.IndexedFields[sadefs.TaskQueue], + enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, + false, + ) + s.NoError(err) s.Equal(expectedTaskQueue, taskQueueVal) } diff --git a/common/persistence/visibility/manager/visibility_manager.go b/common/persistence/visibility/manager/visibility_manager.go index 3c93880d61e..130f17bad15 100644 --- a/common/persistence/visibility/manager/visibility_manager.go +++ b/common/persistence/visibility/manager/visibility_manager.go @@ -11,7 +11,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" - "go.temporal.io/server/chasm" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" ) @@ -34,11 +34,18 @@ type ( // Read APIs. ListWorkflowExecutions(ctx context.Context, request *ListWorkflowExecutionsRequestV2) (*ListWorkflowExecutionsResponse, error) - ListChasmExecutions(ctx context.Context, request *ListChasmExecutionsRequest) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) CountWorkflowExecutions(ctx context.Context, request *CountWorkflowExecutionsRequest) (*CountWorkflowExecutionsResponse, error) - CountChasmExecutions(ctx context.Context, request *CountChasmExecutionsRequest) (*chasm.CountExecutionsResponse, error) GetWorkflowExecution(ctx context.Context, request *GetWorkflowExecutionRequest) (*GetWorkflowExecutionResponse, error) + ListChasmExecutions( + ctx context.Context, + request *visibilityservice.ListChasmExecutionsRequest, + ) (*visibilityservice.ListChasmExecutionsResponse, error) + CountChasmExecutions( + ctx context.Context, + request *visibilityservice.CountChasmExecutionsRequest, + ) (*visibilityservice.CountChasmExecutionsResponse, error) + // Admin APIs AddSearchAttributes(ctx context.Context, request *AddSearchAttributesRequest) error } @@ -113,24 +120,6 @@ type ( NextPageToken []byte } - ListChasmExecutionsRequest struct { - ArchetypeID chasm.ArchetypeID - NamespaceID namespace.ID - Namespace namespace.Name - PageSize int // Maximum number of workflow executions per page - Query string - // Token to continue reading next page of workflow executions. - // Pass in empty slice for first page. - NextPageToken []byte - } - - CountChasmExecutionsRequest struct { - ArchetypeID chasm.ArchetypeID - NamespaceID namespace.ID - Namespace namespace.Name - Query string - } - // CountWorkflowExecutionsRequest is request from CountWorkflowExecutions CountWorkflowExecutionsRequest struct { NamespaceID namespace.ID diff --git a/common/persistence/visibility/manager/visibility_manager_mock.go b/common/persistence/visibility/manager/visibility_manager_mock.go index e2291de3dcf..84e19593ee3 100644 --- a/common/persistence/visibility/manager/visibility_manager_mock.go +++ b/common/persistence/visibility/manager/visibility_manager_mock.go @@ -13,8 +13,7 @@ import ( context "context" reflect "reflect" - common "go.temporal.io/api/common/v1" - chasm "go.temporal.io/server/chasm" + visibilityservice "go.temporal.io/server/api/visibilityservice/v1" namespace "go.temporal.io/server/common/namespace" gomock "go.uber.org/mock/gomock" ) @@ -70,10 +69,10 @@ func (mr *MockVisibilityManagerMockRecorder) Close() *gomock.Call { } // CountChasmExecutions mocks base method. -func (m *MockVisibilityManager) CountChasmExecutions(ctx context.Context, request *CountChasmExecutionsRequest) (*chasm.CountExecutionsResponse, error) { +func (m *MockVisibilityManager) CountChasmExecutions(ctx context.Context, request *visibilityservice.CountChasmExecutionsRequest) (*visibilityservice.CountChasmExecutionsResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountChasmExecutions", ctx, request) - ret0, _ := ret[0].(*chasm.CountExecutionsResponse) + ret0, _ := ret[0].(*visibilityservice.CountChasmExecutionsResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -185,10 +184,10 @@ func (mr *MockVisibilityManagerMockRecorder) HasStoreName(stName any) *gomock.Ca } // ListChasmExecutions mocks base method. -func (m *MockVisibilityManager) ListChasmExecutions(ctx context.Context, request *ListChasmExecutionsRequest) (*chasm.ListExecutionsResponse[*common.Payload], error) { +func (m *MockVisibilityManager) ListChasmExecutions(ctx context.Context, request *visibilityservice.ListChasmExecutionsRequest) (*visibilityservice.ListChasmExecutionsResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListChasmExecutions", ctx, request) - ret0, _ := ret[0].(*chasm.ListExecutionsResponse[*common.Payload]) + ret0, _ := ret[0].(*visibilityservice.ListChasmExecutionsResponse) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/common/persistence/visibility/store/elasticsearch/client/bulk_processor.go b/common/persistence/visibility/store/elasticsearch/client/bulk_processor.go index ec61666c3c9..9f20dbeddbf 100644 --- a/common/persistence/visibility/store/elasticsearch/client/bulk_processor.go +++ b/common/persistence/visibility/store/elasticsearch/client/bulk_processor.go @@ -37,6 +37,6 @@ type ( Index string ID string Version int64 - Doc map[string]interface{} + Doc map[string]any } ) diff --git a/common/persistence/visibility/store/elasticsearch/client/client.go b/common/persistence/visibility/store/elasticsearch/client/client.go index dbc5f644f47..d1720675474 100644 --- a/common/persistence/visibility/store/elasticsearch/client/client.go +++ b/common/persistence/visibility/store/elasticsearch/client/client.go @@ -57,6 +57,6 @@ type ( Query elastic.Query PageSize int Sorter []elastic.Sorter - SearchAfter []interface{} + SearchAfter []any } ) diff --git a/common/persistence/visibility/store/elasticsearch/client/client_v7.go b/common/persistence/visibility/store/elasticsearch/client/client_v7.go index 4004184cc1f..e4632c23cf3 100644 --- a/common/persistence/visibility/store/elasticsearch/client/client_v7.go +++ b/common/persistence/visibility/store/elasticsearch/client/client_v7.go @@ -212,7 +212,7 @@ func (c *clientImpl) GetMapping(ctx context.Context, index string) (map[string]s } // Decode body - var body map[string]interface{} + var body map[string]any if err := json.Unmarshal(res.Body, &body); err != nil { return nil, err } @@ -226,7 +226,7 @@ func (c *clientImpl) GetDateFieldType() string { func (c *clientImpl) CreateIndex(ctx context.Context, index string, body map[string]any) (bool, error) { if body == nil { - body = make(map[string]interface{}) + body = make(map[string]any) } resp, err := c.esClient.CreateIndex(index).BodyJson(body).Do(ctx) if err != nil { @@ -365,45 +365,45 @@ func getLoggerOptions(logLevel string, logger log.Logger) []elastic.ClientOption } } -func buildMappingBody(mapping map[string]enumspb.IndexedValueType) map[string]interface{} { - properties := make(map[string]interface{}, len(mapping)) +func buildMappingBody(mapping map[string]enumspb.IndexedValueType) map[string]any { + properties := make(map[string]any, len(mapping)) for fieldName, fieldType := range mapping { - var typeMap map[string]interface{} + var typeMap map[string]any switch fieldType { case enumspb.INDEXED_VALUE_TYPE_TEXT: - typeMap = map[string]interface{}{"type": "text"} + typeMap = map[string]any{"type": "text"} case enumspb.INDEXED_VALUE_TYPE_KEYWORD, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST: - typeMap = map[string]interface{}{"type": "keyword"} + typeMap = map[string]any{"type": "keyword"} case enumspb.INDEXED_VALUE_TYPE_INT: - typeMap = map[string]interface{}{"type": "long"} + typeMap = map[string]any{"type": "long"} case enumspb.INDEXED_VALUE_TYPE_DOUBLE: - typeMap = map[string]interface{}{ + typeMap = map[string]any{ "type": "scaled_float", "scaling_factor": 10000, } case enumspb.INDEXED_VALUE_TYPE_BOOL: - typeMap = map[string]interface{}{"type": "boolean"} + typeMap = map[string]any{"type": "boolean"} case enumspb.INDEXED_VALUE_TYPE_DATETIME: - typeMap = map[string]interface{}{"type": "date_nanos"} + typeMap = map[string]any{"type": "date_nanos"} } if typeMap != nil { properties[fieldName] = typeMap } } - body := map[string]interface{}{ + body := map[string]any{ "properties": properties, } return body } -func convertMappingBody(esMapping map[string]interface{}, indexName string) map[string]string { +func convertMappingBody(esMapping map[string]any, indexName string) map[string]string { result := make(map[string]string) index, ok := esMapping[indexName] if !ok { return result } - indexMap, ok := index.(map[string]interface{}) + indexMap, ok := index.(map[string]any) if !ok { return result } @@ -411,7 +411,7 @@ func convertMappingBody(esMapping map[string]interface{}, indexName string) map[ if !ok { return result } - mappingsMap, ok := mappings.(map[string]interface{}) + mappingsMap, ok := mappings.(map[string]any) if !ok { return result } @@ -420,13 +420,13 @@ func convertMappingBody(esMapping map[string]interface{}, indexName string) map[ if !ok { return result } - propMap, ok := properties.(map[string]interface{}) + propMap, ok := properties.(map[string]any) if !ok { return result } for fieldName, fieldProp := range propMap { - fieldPropMap, ok := fieldProp.(map[string]interface{}) + fieldPropMap, ok := fieldProp.(map[string]any) if !ok { continue } diff --git a/common/persistence/visibility/store/elasticsearch/client/logger.go b/common/persistence/visibility/store/elasticsearch/client/logger.go index 5049be8e67c..0cb4ffe2f5d 100644 --- a/common/persistence/visibility/store/elasticsearch/client/logger.go +++ b/common/persistence/visibility/store/elasticsearch/client/logger.go @@ -20,7 +20,7 @@ func newErrorLogger(logger log.Logger) *errorLogger { return &errorLogger{logger} } -func (l *errorLogger) Printf(format string, v ...interface{}) { +func (l *errorLogger) Printf(format string, v ...any) { l.Error(fmt.Sprintf(format, v...)) } @@ -28,6 +28,6 @@ func newInfoLogger(logger log.Logger) *infoLogger { return &infoLogger{logger} } -func (l *infoLogger) Printf(format string, v ...interface{}) { +func (l *infoLogger) Printf(format string, v ...any) { l.Info(fmt.Sprintf(format, v...)) } diff --git a/common/persistence/visibility/store/elasticsearch/converter_test.go b/common/persistence/visibility/store/elasticsearch/converter_test.go index 0401bbeefd4..0549fba13e3 100644 --- a/common/persistence/visibility/store/elasticsearch/converter_test.go +++ b/common/persistence/visibility/store/elasticsearch/converter_test.go @@ -152,8 +152,8 @@ func TestEmptySelectWhere(t *testing.T) { assert.Nil(t, queryParams.Query) assert.Len(t, queryParams.Sorter, 1) actualSorterMap, _ := queryParams.Sorter[0].Source() - actualSorterJson, _ := json.Marshal([]interface{}{actualSorterMap}) - assert.JSONEq(t, `[{"Id":{"missing":"_last","order":"desc"}}]`, string(actualSorterJson)) + actualSorterJSON, _ := json.Marshal([]any{actualSorterMap}) + assert.JSONEq(t, `[{"Id":{"missing":"_last","order":"desc"}}]`, string(actualSorterJSON)) } func TestSupportedSelectWhereOrder(t *testing.T) { @@ -167,7 +167,7 @@ func TestSupportedSelectWhereOrder(t *testing.T) { actualQueryJson, _ := json.Marshal(actualQueryMap) assert.Equal(t, expectedJson.query, string(actualQueryJson), fmt.Sprintf("sql: %s", sql)) - var actualSorterMaps []interface{} + var actualSorterMaps []any for _, sorter := range queryParams.Sorter { actualSorterMap, _ := sorter.Source() actualSorterMaps = append(actualSorterMaps, actualSorterMap) diff --git a/common/persistence/visibility/store/elasticsearch/processor.go b/common/persistence/visibility/store/elasticsearch/processor.go index 34d8645ef44..88736955d88 100644 --- a/common/persistence/visibility/store/elasticsearch/processor.go +++ b/common/persistence/visibility/store/elasticsearch/processor.go @@ -141,7 +141,7 @@ func (p *processorImpl) Stop() { } } -func (p *processorImpl) hashFn(key interface{}) uint32 { +func (p *processorImpl) hashFn(key any) uint32 { id, ok := key.(string) if !ok { return 0 @@ -164,7 +164,7 @@ func (p *processorImpl) Add(request *client.BulkableRequest, visibilityTaskKey s return newFuture.future } - _, isDup, _ := p.mapToAckFuture.PutOrDo(visibilityTaskKey, newFuture, func(key interface{}, value interface{}) error { + _, isDup, _ := p.mapToAckFuture.PutOrDo(visibilityTaskKey, newFuture, func(key any, value any) error { existingFuture, ok := value.(*ackFuture) if !ok { p.logger.Fatal(fmt.Sprintf("mapToAckFuture has item of a wrong type %T (%T expected).", value, &ackFuture{}), tag.Value(key)) @@ -193,7 +193,7 @@ func (p *processorImpl) bulkBeforeAction(_ int64, requests []elastic.BulkableReq if visibilityTaskKey == "" { continue } - _, _, _ = p.mapToAckFuture.GetAndDo(visibilityTaskKey, func(key interface{}, value interface{}) error { + _, _, _ = p.mapToAckFuture.GetAndDo(visibilityTaskKey, func(key any, value any) error { ackF, ok := value.(*ackFuture) if !ok { p.logger.Fatal(fmt.Sprintf("mapToAckFuture has item of a wrong type %T (%T expected).", value, &ackFuture{}), tag.Value(key)) @@ -293,7 +293,7 @@ func (p *processorImpl) buildResponseIndex(response *elastic.BulkResponse) map[s func (p *processorImpl) notifyResult(visibilityTaskKey string, ack bool) { // Use RemoveIf here to prevent race condition with de-dup logic in Add method. - _ = p.mapToAckFuture.RemoveIf(visibilityTaskKey, func(key interface{}, value interface{}) bool { + _ = p.mapToAckFuture.RemoveIf(visibilityTaskKey, func(key any, value any) bool { ackF, ok := value.(*ackFuture) if !ok { p.logger.Fatal(fmt.Sprintf("mapToAckFuture has item of a wrong type %T (%T expected).", value, &ackFuture{}), tag.ESKey(visibilityTaskKey)) @@ -313,7 +313,7 @@ func (p *processorImpl) extractVisibilityTaskKey(request elastic.BulkableRequest } if len(req) == 2 { // index or update requests - var body map[string]interface{} + var body map[string]any if err = json.Unmarshal([]byte(req[1]), &body); err != nil { p.logger.Error("Unable to unmarshal ES request body.", tag.Error(err)) metrics.ElasticsearchBulkProcessorCorruptedData.With(p.metricsHandler).Record(1) @@ -341,7 +341,7 @@ func (p *processorImpl) extractDocID(request elastic.BulkableRequest) string { return "" } - var body map[string]map[string]interface{} + var body map[string]map[string]any if err = json.Unmarshal([]byte(req[0]), &body); err != nil { p.logger.Error("Unable to unmarshal ES request body.", tag.Error(err), tag.ESRequest(request.String())) metrics.ElasticsearchBulkProcessorCorruptedData.With(p.metricsHandler).Record(1) diff --git a/common/persistence/visibility/store/elasticsearch/processor_test.go b/common/persistence/visibility/store/elasticsearch/processor_test.go index 92c0bd52bd2..119745ef4d2 100644 --- a/common/persistence/visibility/store/elasticsearch/processor_test.go +++ b/common/persistence/visibility/store/elasticsearch/processor_test.go @@ -143,7 +143,7 @@ func (s *processorSuite) TestAdd_ConcurrentAdd() { wg.Add(parallelFactor) s.mockBulkProcessor.EXPECT().Add(request).Times(docsCount) s.mockMetricHandler.EXPECT().Timer(metrics.ElasticsearchBulkProcessorWaitAddLatency.Name()).Return(metrics.NoopTimerMetricFunc).Times(docsCount) - for i := 0; i < parallelFactor; i++ { + for i := range parallelFactor { go func(i int) { for j := 0; j < docsCount/parallelFactor; j++ { futures[i*docsCount/parallelFactor+j] = s.esProcessor.Add(request, fmt.Sprintf("test-key-%d-%d", i, j)) @@ -154,7 +154,7 @@ func (s *processorSuite) TestAdd_ConcurrentAdd() { wg.Wait() s.Equal(docsCount, s.esProcessor.mapToAckFuture.Len()) - for i := 0; i < docsCount; i++ { + for i := range docsCount { if futures[i].Ready() { s.Fail("all request must be in the bulk") } @@ -176,7 +176,7 @@ func (s *processorSuite) TestAdd_ConcurrentAdd_Duplicates() { wg := sync.WaitGroup{} wg.Add(duplicates) s.mockBulkProcessor.EXPECT().Add(request) - for i := 0; i < duplicates; i++ { + for i := range duplicates { go func(i int) { futures[i] = s.esProcessor.Add(request, key) wg.Done() @@ -184,7 +184,7 @@ func (s *processorSuite) TestAdd_ConcurrentAdd_Duplicates() { } wg.Wait() pendingRequestsCount := 0 - for i := 0; i < duplicates; i++ { + for i := range duplicates { if futures[i].Ready() { s.Fail("all request must be in the bulk") } else { @@ -210,7 +210,7 @@ func (s *processorSuite) TestAdd_ConcurrentAdd_Shutdown() { wg := sync.WaitGroup{} wg.Add(parallelFactor + 1) // +1 for separate shutdown goroutine - for i := 0; i < parallelFactor; i++ { + for i := range parallelFactor { go func(i int) { for j := 0; j < docsCount/parallelFactor; j++ { futures[i*docsCount/parallelFactor+j] = s.esProcessor.Add(request, fmt.Sprintf("test-key-%d-%d", i, j)) @@ -240,7 +240,7 @@ func (s *processorSuite) TestBulkAfterAction_Ack() { Index(testIndex). Id(testID). Version(version). - Doc(map[string]interface{}{sadefs.VisibilityTaskKey: testKey}) + Doc(map[string]any{sadefs.VisibilityTaskKey: testKey}) requests := []elastic.BulkableRequest{request} mSuccess := map[string]*elastic.BulkResponseItem{ @@ -285,7 +285,7 @@ func (s *processorSuite) TestBulkAfterAction_Nack() { Index(testIndex). Id(testID). Version(version). - Doc(map[string]interface{}{ + Doc(map[string]any{ sadefs.VisibilityTaskKey: testKey, sadefs.NamespaceID: namespaceID, sadefs.WorkflowID: wid, @@ -329,7 +329,7 @@ func (s *processorSuite) TestBulkAfterAction_Nack() { func (s *processorSuite) TestBulkAfterAction_Error() { version := int64(3) - doc := map[string]interface{}{ + doc := map[string]any{ sadefs.VisibilityTaskKey: "str", } @@ -367,7 +367,7 @@ func (s *processorSuite) TestBulkBeforeAction() { Index(testIndex). Id(testID). Version(version). - Doc(map[string]interface{}{sadefs.VisibilityTaskKey: testKey}) + Doc(map[string]any{sadefs.VisibilityTaskKey: testKey}) requests := []elastic.BulkableRequest{request} counterMetric := metrics.NewMockCounterIface(s.controller) @@ -438,7 +438,7 @@ func (s *processorSuite) TestExtractVisibilityTaskKey() { visibilityTaskKey := s.esProcessor.extractVisibilityTaskKey(request) s.Equal("", visibilityTaskKey) - m := map[string]interface{}{ + m := map[string]any{ sadefs.VisibilityTaskKey: 1, } request.Doc(m) @@ -457,7 +457,7 @@ func (s *processorSuite) TestExtractVisibilityTaskKey_Delete() { source, err := request.Source() s.NoError(err) s.Equal(1, len(source)) - var body map[string]map[string]interface{} + var body map[string]map[string]any err = json.Unmarshal([]byte(source[0]), &body) s.NoError(err) _, ok := body["delete"] @@ -523,7 +523,7 @@ func (s *processorSuite) Test_End2End() { s.mockMetricHandler.EXPECT(). Timer(metrics.ElasticsearchBulkProcessorWaitAddLatency.Name()). Return(metrics.NoopTimerMetricFunc).Times(docsCount) - for i := 0; i < parallelFactor; i++ { + for i := range parallelFactor { go func(i int) { for j := 0; j < docsCount/parallelFactor; j++ { docIndex := i*docsCount/parallelFactor + j @@ -534,7 +534,7 @@ func (s *processorSuite) Test_End2End() { Index(testIndex). Id(docId). Version(version). - Doc(map[string]interface{}{sadefs.VisibilityTaskKey: testKey}) + Doc(map[string]any{sadefs.VisibilityTaskKey: testKey}) mSuccess := map[string]*elastic.BulkResponseItem{ "index": { @@ -577,7 +577,7 @@ func (s *processorSuite) Test_End2End() { s.mockMetricHandler.EXPECT().Timer(metrics.ElasticsearchBulkProcessorCommitLatency.Name()).Return(metrics.NoopTimerMetricFunc).Times(docsCount) s.esProcessor.bulkAfterAction(0, bulkIndexRequests, bulkIndexResponse, nil) - for i := 0; i < docsCount; i++ { + for i := range docsCount { result, err := futures[i].Get(context.Background()) s.NoError(err) s.True(result) diff --git a/common/persistence/visibility/store/elasticsearch/query_interceptors.go b/common/persistence/visibility/store/elasticsearch/query_interceptors.go index 6a452af26ba..c7276b23333 100644 --- a/common/persistence/visibility/store/elasticsearch/query_interceptors.go +++ b/common/persistence/visibility/store/elasticsearch/query_interceptors.go @@ -115,7 +115,7 @@ func (ni *nameInterceptor) Name(name string, usage query.FieldNameUsage) (string return fieldName, nil } -func (vi *valuesInterceptor) Values(name string, fieldName string, values ...interface{}) ([]interface{}, error) { +func (vi *valuesInterceptor) Values(name string, fieldName string, values ...any) ([]any, error) { var fieldType enumspb.IndexedValueType var err error @@ -124,7 +124,7 @@ func (vi *valuesInterceptor) Values(name string, fieldName string, values ...int return nil, query.NewConverterError("invalid search attribute: %s", name) } - var result []interface{} + var result []any for _, value := range values { value, err = parseSystemSearchAttributeValues(name, value) if err != nil { diff --git a/common/persistence/visibility/store/elasticsearch/query_interceptors_test.go b/common/persistence/visibility/store/elasticsearch/query_interceptors_test.go index 912d6aae8a4..7d9be9c355f 100644 --- a/common/persistence/visibility/store/elasticsearch/query_interceptors_test.go +++ b/common/persistence/visibility/store/elasticsearch/query_interceptors_test.go @@ -47,7 +47,7 @@ func (s *QueryInterceptorSuite) TestTimeProcessFunc() { cases := []struct { key string - value interface{} + value any }{ {key: sadefs.StartTime, value: int64(1528358645123456789)}, {key: sadefs.CloseTime, value: "2018-06-07T15:04:05+07:00"}, @@ -87,7 +87,7 @@ func (s *QueryInterceptorSuite) TestStatusProcessFunc() { cases := []struct { key string - value interface{} + value any }{ {key: sadefs.ExecutionStatus, value: "Completed"}, {key: sadefs.ExecutionStatus, value: int64(1)}, @@ -133,7 +133,7 @@ func (s *QueryInterceptorSuite) TestDurationProcessFunc() { cases := []struct { key string - value interface{} + value any }{ {key: sadefs.ExecutionDuration, value: "1"}, {key: sadefs.ExecutionDuration, value: int64(1)}, @@ -144,7 +144,7 @@ func (s *QueryInterceptorSuite) TestDurationProcessFunc() { {key: sadefs.WorkflowID, value: "should not be modified"}, } expected := []struct { - value interface{} + value any returnErr bool }{ {value: int64(1), returnErr: false}, diff --git a/common/persistence/visibility/store/elasticsearch/visibility_store.go b/common/persistence/visibility/store/elasticsearch/visibility_store.go index 037204b082e..c92b3259b46 100644 --- a/common/persistence/visibility/store/elasticsearch/visibility_store.go +++ b/common/persistence/visibility/store/elasticsearch/visibility_store.go @@ -17,9 +17,11 @@ import ( commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" @@ -55,7 +57,7 @@ type ( } visibilityPageToken struct { - SearchAfter []interface{} + SearchAfter []any } esQueryParams struct { @@ -308,7 +310,7 @@ func GetVisibilityTaskKey(shardID int32, taskID int64) string { func (s *VisibilityStore) addBulkIndexRequestAndWait( ctx context.Context, request *store.InternalVisibilityRequestBase, - esDoc map[string]interface{}, + esDoc map[string]any, visibilityTaskKey string, ) error { bulkIndexRequest := &client.BulkableRequest{ @@ -389,11 +391,11 @@ func (s *VisibilityStore) ListWorkflowExecutions( func (s *VisibilityStore) ListChasmExecutions( ctx context.Context, - request *manager.ListChasmExecutionsRequest, + request *visibilityservice.ListChasmExecutionsRequest, ) (*store.InternalListExecutionsResponse, error) { - rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeID) + rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeId) if !ok { - return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("unknown archetype ID: %d", request.ArchetypeID)) + return nil, serviceerror.NewInvalidArgumentf("unknown archetype ID: %d", request.ArchetypeId) } chasmMapper := rc.SearchAttributesMapper() @@ -407,28 +409,45 @@ func (s *VisibilityStore) ListChasmExecutions( return nil, ConvertElasticsearchClientError("ListChasmExecutions failed", err) } - return s.GetListWorkflowExecutionsResponse(searchResult, request.Namespace, request.PageSize, chasmMapper) + return s.GetListWorkflowExecutionsResponse( + searchResult, + namespace.Name(request.Namespace), + int(request.PageSize), + chasmMapper, + ) } func (s *VisibilityStore) CountChasmExecutions( ctx context.Context, - request *manager.CountChasmExecutionsRequest, + request *visibilityservice.CountChasmExecutionsRequest, ) (*store.InternalCountExecutionsResponse, error) { - rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeID) + rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeId) if !ok { - return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("unknown archetype ID: %d", request.ArchetypeID)) + return nil, serviceerror.NewInvalidArgumentf("unknown archetype ID: %d", request.ArchetypeId) } mapper := rc.SearchAttributesMapper() var queryParams *esQueryParams var err error if s.enableUnifiedQueryConverter() { - queryParams, err = s.convertQuery(request.Namespace, request.NamespaceID, request.Query, mapper, request.ArchetypeID) + queryParams, err = s.convertQuery( + namespace.Name(request.Namespace), + namespace.ID(request.NamespaceId), + request.Query, + mapper, + request.ArchetypeId, + ) if err != nil { return nil, err } } else { - queryParamsLegacy, err := s.convertQueryLegacy(request.Namespace, request.NamespaceID, request.Query, mapper, request.ArchetypeID) + queryParamsLegacy, err := s.convertQueryLegacy( + namespace.Name(request.Namespace), + namespace.ID(request.NamespaceId), + request.Query, + mapper, + request.ArchetypeId, + ) if err != nil { return nil, err } @@ -571,18 +590,18 @@ func (s *VisibilityStore) BuildSearchParametersV2( } func (s *VisibilityStore) BuildChasmSearchParameters( - request *manager.ListChasmExecutionsRequest, + request *visibilityservice.ListChasmExecutionsRequest, getFieldSorter func([]elastic.Sorter) ([]elastic.Sorter, error), chasmMapper *chasm.VisibilitySearchAttributesMapper, ) (*client.SearchParameters, error) { return s.buildSearchParametersInternal(&searchParametersInternal{ - NamespaceName: request.Namespace, - NamespaceID: request.NamespaceID, + NamespaceName: namespace.Name(request.Namespace), + NamespaceID: namespace.ID(request.NamespaceId), Query: request.Query, - PageSize: request.PageSize, + PageSize: int(request.PageSize), NextPageToken: request.NextPageToken, ChasmMapper: chasmMapper, - ArchetypeID: request.ArchetypeID, + ArchetypeID: request.ArchetypeId, }) } @@ -851,7 +870,7 @@ func (s *VisibilityStore) GetListWorkflowExecutionsResponse( response := &store.InternalListExecutionsResponse{ Executions: make([]*store.InternalExecutionInfo, 0, len(searchResult.Hits.Hits)), } - var lastHitSort []interface{} + var lastHitSort []any for _, hit := range searchResult.Hits.Hits { workflowExecutionInfo, err := s.ParseESDoc(hit.Id, hit.Source, typeMap, namespace, chasmMapper) if err != nil { @@ -904,8 +923,8 @@ func (s *VisibilityStore) serializePageToken(token *visibilityPageToken) ([]byte func (s *VisibilityStore) GenerateESDoc( request *store.InternalVisibilityRequestBase, visibilityTaskKey string, -) (map[string]interface{}, error) { - doc := map[string]interface{}{ +) (map[string]any, error) { + doc := map[string]any{ sadefs.VisibilityTaskKey: visibilityTaskKey, sadefs.NamespaceID: request.NamespaceID, sadefs.WorkflowID: request.WorkflowID, @@ -942,6 +961,11 @@ func (s *VisibilityStore) GenerateESDoc( metrics.ElasticsearchDocumentGenerateFailuresCount.With(s.metricsHandler).Record(1) return nil, serviceerror.NewInternalf("unable to decode search attributes: %v", err) } + for name := range request.SearchAttributes.GetIndexedFields() { + if _, ok := searchAttributes[name]; !ok { + s.logger.Warn("Skipping unknown search attribute while generating visibility record", tag.String("search-attribute", name)) + } + } // This is to prevent existing tasks to fail indefinitely. // If it's only invalid values error, then silently continue without them. searchAttributes, err = s.ValidateCustomSearchAttributes(searchAttributes) @@ -965,7 +989,7 @@ func (s *VisibilityStore) GenerateESDoc( func (s *VisibilityStore) GenerateClosedESDoc( request *store.InternalRecordWorkflowExecutionClosedRequest, visibilityTaskKey string, -) (map[string]interface{}, error) { +) (map[string]any, error) { doc, err := s.GenerateESDoc(request.InternalVisibilityRequestBase, visibilityTaskKey) if err != nil { return nil, err @@ -988,12 +1012,12 @@ func (s *VisibilityStore) ParseESDoc( namespaceName namespace.Name, chasmMapper *chasm.VisibilitySearchAttributesMapper, ) (*store.InternalExecutionInfo, error) { - logParseError := func(fieldName string, fieldValue interface{}, err error, docID string) error { + logParseError := func(fieldName string, fieldValue any, err error, docID string) error { metrics.ElasticsearchDocumentParseFailuresCount.With(s.metricsHandler).Record(1) return serviceerror.NewInternalf("unable to parse Elasticsearch document(%s) %q field value %q: %v", docID, fieldName, fieldValue, err) } - var sourceMap map[string]interface{} + var sourceMap map[string]any d := json.NewDecoder(bytes.NewReader(docSource)) // Very important line. See finishParseJSONValue bellow. d.UseNumber() @@ -1008,7 +1032,7 @@ func (s *VisibilityStore) ParseESDoc( isValidType bool memo []byte memoEncoding string - allSearchAttributes map[string]interface{} + allSearchAttributes map[string]any ) record := &store.InternalExecutionInfo{} for fieldName, fieldValue := range sourceMap { @@ -1037,7 +1061,7 @@ func (s *VisibilityStore) ParseESDoc( fieldType, err := combinedTypeMap.GetType(fieldName) if err != nil { // Silently ignore ErrInvalidName because it indicates an unknown field in an Elasticsearch document. - if errors.Is(err, searchattribute.ErrInvalidName) { + if errors.Is(err, sadefs.ErrInvalidName) { continue } metrics.ElasticsearchDocumentParseFailuresCount.With(s.metricsHandler).Record(1) @@ -1088,7 +1112,7 @@ func (s *VisibilityStore) ParseESDoc( record.RootRunID = fieldValueParsed.(string) default: if allSearchAttributes == nil { - allSearchAttributes = map[string]interface{}{} + allSearchAttributes = map[string]any{} } allSearchAttributes[fieldName] = fieldValueParsed } @@ -1184,7 +1208,7 @@ func (s *VisibilityStore) parseCountGroupByResponse( if err != nil { return fmt.Errorf("unable to parse value %v: %w", bucket["key"], err) } - payload, err := searchattribute.EncodeValue(value, groupByTypes[index]) + payload, err := sadefs.EncodeValue(value, groupByTypes[index]) if err != nil { return fmt.Errorf("unable to encode value %v: %w", value, err) } @@ -1217,12 +1241,12 @@ func (s *VisibilityStore) parseCountGroupByResponse( // []interface{}, for JSON arrays // map[string]interface{}, for JSON objects (should never be a case) // nil for JSON null -func finishParseJSONValue(val interface{}, t enumspb.IndexedValueType) (interface{}, error) { +func finishParseJSONValue(val any, t enumspb.IndexedValueType) (any, error) { // Custom search attributes support array of a particular type. - if arrayValue, isArray := val.([]interface{}); isArray { - retArray := make([]interface{}, len(arrayValue)) + if arrayValue, isArray := val.([]any); isArray { + retArray := make([]any, len(arrayValue)) var lastErr error - for i := 0; i < len(retArray); i++ { + for i := range retArray { retArray[i], lastErr = finishParseJSONValue(arrayValue[i], t) } return retArray, lastErr @@ -1304,7 +1328,7 @@ func isDefaultSorter(sorter []elastic.Sorter) bool { if len(sorter) != len(defaultSorter) { return false } - for i := 0; i < len(defaultSorter); i++ { + for i := range defaultSorter { if &sorter[i] != &defaultSorter[i] { return false } @@ -1336,7 +1360,7 @@ func buildPaginationQuery( } parsedSearchAfter := make([]any, n) - for i := 0; i < n; i++ { + for i := range n { tp, err := saTypeMap.GetType(sorterFields[i].name) if err != nil { return nil, err @@ -1356,7 +1380,7 @@ func buildPaginationQuery( } shouldQueries := make([]elastic.Query, 0, len(sorterFields)) - for k := 0; k < len(sorterFields); k++ { + for k := range sorterFields { bq := elastic.NewBoolQuery() for i := 0; i <= k; i++ { field := sorterFields[i] diff --git a/common/persistence/visibility/store/elasticsearch/visibility_store_read_test.go b/common/persistence/visibility/store/elasticsearch/visibility_store_read_test.go index a5c535ad74a..7ddd5645f77 100644 --- a/common/persistence/visibility/store/elasticsearch/visibility_store_read_test.go +++ b/common/persistence/visibility/store/elasticsearch/visibility_store_read_test.go @@ -17,6 +17,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/temporalproto" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/debug" "go.temporal.io/server/common/dynamicconfig" @@ -85,8 +86,8 @@ var ( ) ) -func mustEncodeValue(val interface{}, valueType enumspb.IndexedValueType) *commonpb.Payload { - p, err := searchattribute.EncodeValue(val, valueType) +func mustEncodeValue(val any, valueType enumspb.IndexedValueType) *commonpb.Payload { + p, err := sadefs.EncodeValue(val, valueType) if err != nil { panic(fmt.Sprintf("failed to encode value %v: %v", val, err)) } @@ -330,7 +331,7 @@ func (s *ESVisibilitySuite) queryToJSON(q elastic.Query) string { } func (s *ESVisibilitySuite) sorterToJSON(sorters []elastic.Sorter) string { - var ms []interface{} + var ms []any for _, sorter := range sorters { m, err := sorter.Source() s.NoError(err) @@ -669,13 +670,13 @@ func (s *ESVisibilitySuite) TestGetListWorkflowExecutionsResponse() { source := json.RawMessage(data) searchHit := &elastic.SearchHit{ Source: source, - Sort: []interface{}{1547596872371234567, "e481009e-14b3-45ae-91af-dce6e2a88365"}, + Sort: []any{1547596872371234567, "e481009e-14b3-45ae-91af-dce6e2a88365"}, } searchResult.Hits.Hits = []*elastic.SearchHit{searchHit} searchResult.Hits.TotalHits.Value = 1 resp, err = s.visibilityStore.GetListWorkflowExecutionsResponse(searchResult, testNamespace, 1, nil) s.NoError(err) - serializedToken, _ := s.visibilityStore.serializePageToken(&visibilityPageToken{SearchAfter: []interface{}{1547596872371234567, "e481009e-14b3-45ae-91af-dce6e2a88365"}}) + serializedToken, _ := s.visibilityStore.serializePageToken(&visibilityPageToken{SearchAfter: []any{1547596872371234567, "e481009e-14b3-45ae-91af-dce6e2a88365"}}) s.Equal(serializedToken, resp.NextPageToken) s.Equal(1, len(resp.Executions)) @@ -720,7 +721,7 @@ func (s *ESVisibilitySuite) TestDeserializePageToken() { s.NoError(err) s.Nil(result) - token := &visibilityPageToken{SearchAfter: []interface{}{int64(1629936710090695939), "unique"}} + token := &visibilityPageToken{SearchAfter: []any{int64(1629936710090695939), "unique"}} data, err := s.visibilityStore.serializePageToken(token) s.NoError(err) result, err = s.visibilityStore.deserializePageToken(data) @@ -740,7 +741,7 @@ func (s *ESVisibilitySuite) TestSerializePageToken() { sortTime := int64(123) tieBreaker := "unique" - newToken := &visibilityPageToken{SearchAfter: []interface{}{sortTime, tieBreaker}} + newToken := &visibilityPageToken{SearchAfter: []any{sortTime, tieBreaker}} data, err = s.visibilityStore.serializePageToken(newToken) s.NoError(err) s.True(len(data) > 0) @@ -1405,7 +1406,7 @@ func (s *ESVisibilitySuite) TestGetWorkflowExecution() { func(ctx context.Context, index string, docID string) (*elastic.GetResult, error) { s.Equal(testIndex, index) s.Equal(testWorkflowID+delimiter+testRunID, docID) - data := map[string]interface{}{ + data := map[string]any{ "ExecutionStatus": "Running", "NamespaceId": testNamespaceID.String(), "StateTransitionCount": 22, @@ -2119,10 +2120,10 @@ func (s *ESVisibilitySuite) TestBuildSearchParametersV2_ChasmMapper() { newBoolQuery().Filter(filterQuery).MustNot(namespaceDivisionExists), ) p, err := s.visibilityStore.BuildChasmSearchParameters( - &manager.ListChasmExecutionsRequest{ - NamespaceID: testNamespaceID, - Namespace: testNamespace, - PageSize: testPageSize, + &visibilityservice.ListChasmExecutionsRequest{ + NamespaceId: testNamespaceID.String(), + Namespace: testNamespace.String(), + PageSize: int32(testPageSize), Query: request.Query, }, s.visibilityStore.GetListFieldSorter, @@ -2146,10 +2147,10 @@ func (s *ESVisibilitySuite) TestBuildSearchParametersV2_ChasmMapper() { s.mockMetricsHandler.EXPECT().WithTags(metrics.NamespaceTag(request.Namespace.String())).Return(s.mockMetricsHandler).AnyTimes() s.mockMetricsHandler.EXPECT().Counter(metrics.ElasticsearchCustomOrderByClauseCount.Name()).Return(metrics.NoopCounterMetricFunc) p, err = s.visibilityStore.BuildChasmSearchParameters( - &manager.ListChasmExecutionsRequest{ - NamespaceID: testNamespaceID, - Namespace: testNamespace, - PageSize: testPageSize, + &visibilityservice.ListChasmExecutionsRequest{ + NamespaceId: testNamespaceID.String(), + Namespace: testNamespace.String(), + PageSize: int32(testPageSize), Query: request.Query, }, s.visibilityStore.GetListFieldSorter, diff --git a/common/persistence/visibility/store/query/converter_legacy.go b/common/persistence/visibility/store/query/converter_legacy.go index 13ff5923554..ce2b99f5b6c 100644 --- a/common/persistence/visibility/store/query/converter_legacy.go +++ b/common/persistence/visibility/store/query/converter_legacy.go @@ -444,10 +444,10 @@ func (c *comparisonExprConverter) Convert(expr sqlparser.Expr) (elastic.Query, e ) } - colValues, isArray := colValue.([]interface{}) + colValues, isArray := colValue.([]any) // colValue should be an array only for "in (1,2,3)" queries. if !isArray { - colValues = []interface{}{colValue} + colValues = []any{colValue} } colValues, err = c.fvInterceptor.Values(alias, colName, colValues...) @@ -512,7 +512,7 @@ func (c *comparisonExprConverter) Convert(expr sqlparser.Expr) (elastic.Query, e // convertComparisonExprValue returns a string, int64, float64, bool or // a slice with each value of one of those types. -func convertComparisonExprValue(expr sqlparser.Expr) (interface{}, error) { +func convertComparisonExprValue(expr sqlparser.Expr) (any, error) { switch e := expr.(type) { case *sqlparser.SQLVal: v, err := sqlquery.ParseValue(sqlparser.String(e)) @@ -525,7 +525,7 @@ func convertComparisonExprValue(expr sqlparser.Expr) (interface{}, error) { case sqlparser.ValTuple: // This is "in (1,2,3)" case. exprs := []sqlparser.Expr(e) - var result []interface{} + var result []any for _, expr := range exprs { v, err := convertComparisonExprValue(expr) if err != nil { diff --git a/common/persistence/visibility/store/query/converter_test.go b/common/persistence/visibility/store/query/converter_test.go index 9347bedb28d..de4e6e699c5 100644 --- a/common/persistence/visibility/store/query/converter_test.go +++ b/common/persistence/visibility/store/query/converter_test.go @@ -23,6 +23,16 @@ const ( testNamespaceID = namespace.ID("test-namespace-id") ) +type identityMapper struct{} + +func (identityMapper) GetAlias(fieldName string, _ string) (string, error) { + return fieldName, nil +} + +func (identityMapper) GetFieldName(alias string, _ string) (string, error) { + return alias, nil +} + func TestWithSearchAttributeInterceptor(t *testing.T) { t.Parallel() r := require.New(t) @@ -1917,7 +1927,7 @@ func TestQueryConverter_ResolveSearchAttributeAlias(t *testing.T) { ) if tc.useNoopMapper { - queryConverter.saMapper = searchattribute.NewNoopMapper() + queryConverter.saMapper = identityMapper{} } fn, ft, err := queryConverter.resolveSearchAttributeAlias(tc.in) diff --git a/common/persistence/visibility/store/query/errors.go b/common/persistence/visibility/store/query/errors.go index 390a3ec8f9d..f87ad34c33e 100644 --- a/common/persistence/visibility/store/query/errors.go +++ b/common/persistence/visibility/store/query/errors.go @@ -21,7 +21,7 @@ var ( InvalidExpressionErrMessage = "invalid expression" ) -func NewConverterError(format string, a ...interface{}) error { +func NewConverterError(format string, a ...any) error { message := fmt.Sprintf(format, a...) return &ConverterError{message: message} } diff --git a/common/persistence/visibility/store/query/interceptors_legacy.go b/common/persistence/visibility/store/query/interceptors_legacy.go index ee300ed6b99..222be30e5fd 100644 --- a/common/persistence/visibility/store/query/interceptors_legacy.go +++ b/common/persistence/visibility/store/query/interceptors_legacy.go @@ -5,7 +5,7 @@ type ( Name(name string, usage FieldNameUsage) (string, error) } FieldValuesInterceptor interface { - Values(name string, fieldName string, values ...interface{}) ([]interface{}, error) + Values(name string, fieldName string, values ...any) ([]any, error) } NopFieldNameInterceptor struct{} @@ -25,6 +25,6 @@ func (n *NopFieldNameInterceptor) Name(name string, _ FieldNameUsage) (string, e return name, nil } -func (n *NopFieldValuesInterceptor) Values(_ string, _ string, values ...interface{}) ([]interface{}, error) { +func (n *NopFieldValuesInterceptor) Values(_ string, _ string, values ...any) ([]any, error) { return values, nil } diff --git a/common/persistence/visibility/store/query/interceptors_legacy_test.go b/common/persistence/visibility/store/query/interceptors_legacy_test.go index 5541577fe1b..08915e7415b 100644 --- a/common/persistence/visibility/store/query/interceptors_legacy_test.go +++ b/common/persistence/visibility/store/query/interceptors_legacy_test.go @@ -25,12 +25,12 @@ func (t *testNameInterceptor) Name(name string, usage FieldNameUsage) (string, e return name + "1", nil } -func (t *testValuesInterceptor) Values(name string, fieldName string, values ...interface{}) ([]interface{}, error) { +func (t *testValuesInterceptor) Values(name string, fieldName string, values ...any) ([]any, error) { if name == "error" { return nil, errors.New("interceptor error") } - var result []interface{} + var result []any for _, value := range values { if name == "ExecutionStatus" { intVal, isIntVal := value.(int64) @@ -53,7 +53,7 @@ func TestNameInterceptor(t *testing.T) { //nolint:staticcheck actualQueryJson, _ := json.Marshal(actualQueryMap) require.JSONEq(t, `{"bool":{"filter":{"term":{"ExecutionStatus1":"Running"}}}}`, string(actualQueryJson)) - var actualSorterMaps []interface{} + var actualSorterMaps []any for _, sorter := range queryParams.Sorter { actualSorterMap, _ := sorter.Source() actualSorterMaps = append(actualSorterMaps, actualSorterMap) diff --git a/common/persistence/visibility/store/query/resolve.go b/common/persistence/visibility/store/query/resolve.go index b829cd8f861..d94cab8dcef 100644 --- a/common/persistence/visibility/store/query/resolve.go +++ b/common/persistence/visibility/store/query/resolve.go @@ -37,11 +37,11 @@ func ResolveSearchAttributeAlias( saType, _ := saTypeMap.GetType(sadefs.WorkflowID) return sadefs.WorkflowID, saType, nil } + } - fieldName, fieldType = tryChasmMapper(name, chasmMapper) - if fieldName != "" { - return fieldName, fieldType, nil - } + fieldName, fieldType := tryChasmMapper(name, chasmMapper) + if fieldName != "" { + return fieldName, fieldType, nil } fieldName, fieldType, found := tryDirectAndPrefixedLookup(name, saTypeMap) diff --git a/common/persistence/visibility/store/query/resolve_test.go b/common/persistence/visibility/store/query/resolve_test.go index 6c7a09e6de6..6b3590c6be4 100644 --- a/common/persistence/visibility/store/query/resolve_test.go +++ b/common/persistence/visibility/store/query/resolve_test.go @@ -180,6 +180,31 @@ func TestResolveSearchAttributeAlias_WithChasmMapper(t *testing.T) { } } +func TestResolveSearchAttributeAlias_ChasmOverridesSystemAttribute(t *testing.T) { + ns := namespace.Name("test-namespace") + saTypeMap := searchattribute.NewNameTypeMapStub(map[string]enumspb.IndexedValueType{ + "ExecutionStatus": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }) + mapper := customMapper{ + fieldToAlias: map[string]string{}, + aliasToField: map[string]string{}, + } + + chasmMapper := chasm.NewTestVisibilitySearchAttributesMapper( + map[string]string{ + "TemporalLowCardinalityKeyword01": "ExecutionStatus", + }, + map[string]enumspb.IndexedValueType{ + "TemporalLowCardinalityKeyword01": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + ) + + fieldName, fieldType, err := ResolveSearchAttributeAlias("ExecutionStatus", ns, mapper, saTypeMap, chasmMapper) + require.NoError(t, err) + require.Equal(t, "TemporalLowCardinalityKeyword01", fieldName) + require.Equal(t, enumspb.INDEXED_VALUE_TYPE_KEYWORD, fieldType) +} + func TestResolveSearchAttributeAlias_ChasmPriority(t *testing.T) { ns := namespace.Name("test-namespace") saTypeMap := searchattribute.NewNameTypeMapStub(map[string]enumspb.IndexedValueType{ diff --git a/common/persistence/visibility/store/sql/visibility_store.go b/common/persistence/visibility/store/sql/visibility_store.go index 6eca6056080..26af6189c95 100644 --- a/common/persistence/visibility/store/sql/visibility_store.go +++ b/common/persistence/visibility/store/sql/visibility_store.go @@ -10,10 +10,12 @@ import ( commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/config" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" @@ -34,6 +36,8 @@ type ( searchAttributesProvider searchattribute.Provider searchAttributesMapperProvider searchattribute.MapperProvider chasmRegistry *chasm.Registry + metricsHandler metrics.Handler + logger log.Logger enableUnifiedQueryConverter dynamicconfig.BoolPropertyFn } @@ -75,6 +79,8 @@ func NewSQLVisibilityStore( searchAttributesProvider: searchAttributesProvider, searchAttributesMapperProvider: searchAttributesMapperProvider, chasmRegistry: chasmRegistry, + metricsHandler: metricsHandler, + logger: logger, enableUnifiedQueryConverter: enableUnifiedQueryConverter, }, nil @@ -199,22 +205,22 @@ func (s *VisibilityStore) ListWorkflowExecutions( func (s *VisibilityStore) ListChasmExecutions( ctx context.Context, - request *manager.ListChasmExecutionsRequest, + request *visibilityservice.ListChasmExecutionsRequest, ) (*store.InternalListExecutionsResponse, error) { - rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeID) + rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeId) if !ok { - return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("unknown archetype ID: %d", request.ArchetypeID)) + return nil, serviceerror.NewInvalidArgumentf("unknown archetype ID: %d", request.ArchetypeId) } mapper := rc.SearchAttributesMapper() requestInternal := &listExecutionsRequestInternal{ - NamespaceID: request.NamespaceID, - Namespace: request.Namespace, + NamespaceID: namespace.ID(request.NamespaceId), + Namespace: namespace.Name(request.Namespace), Query: request.Query, - PageSize: request.PageSize, + PageSize: int(request.PageSize), NextPageToken: request.NextPageToken, ChasmMapper: mapper, - ArchetypeID: request.ArchetypeID, + ArchetypeID: request.ArchetypeId, } if s.enableUnifiedQueryConverter() { @@ -226,11 +232,11 @@ func (s *VisibilityStore) ListChasmExecutions( func (s *VisibilityStore) CountChasmExecutions( ctx context.Context, - request *manager.CountChasmExecutionsRequest, + request *visibilityservice.CountChasmExecutionsRequest, ) (*store.InternalCountExecutionsResponse, error) { - rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeID) + rc, ok := s.chasmRegistry.ComponentByID(request.ArchetypeId) if !ok { - return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("unknown archetype ID: %d", request.ArchetypeID)) + return nil, serviceerror.NewInvalidArgumentf("unknown archetype ID: %d", request.ArchetypeId) } mapper := rc.SearchAttributesMapper() @@ -242,7 +248,7 @@ func (s *VisibilityStore) CountChasmExecutions( func (s *VisibilityStore) countChasmExecutions( ctx context.Context, - request *manager.CountChasmExecutionsRequest, + request *visibilityservice.CountChasmExecutionsRequest, mapper *chasm.VisibilitySearchAttributesMapper, ) (*store.InternalCountExecutionsResponse, error) { sqlQC, err := NewSQLQueryConverter(s.GetName()) @@ -255,20 +261,20 @@ func (s *VisibilityStore) countChasmExecutions( return nil, err } - saMapper, err := s.searchAttributesMapperProvider.GetMapper(request.Namespace) + saMapper, err := s.searchAttributesMapperProvider.GetMapper(namespace.Name(request.Namespace)) if err != nil { return nil, err } queryParams, err := buildQueryParams( - request.NamespaceID, - request.Namespace, + namespace.ID(request.NamespaceId), + namespace.Name(request.Namespace), request.Query, sqlQC, saTypeMap, saMapper, mapper, - request.ArchetypeID, + request.ArchetypeId, ) if err != nil { var converterErr *query.ConverterError @@ -295,7 +301,7 @@ func (s *VisibilityStore) countChasmExecutions( func (s *VisibilityStore) countChasmExecutionsLegacy( ctx context.Context, - request *manager.CountChasmExecutionsRequest, + request *visibilityservice.CountChasmExecutionsRequest, mapper *chasm.VisibilitySearchAttributesMapper, ) (*store.InternalCountExecutionsResponse, error) { saTypeMap, err := s.searchAttributesProvider.GetSearchAttributes(s.GetIndexName(), false) @@ -303,20 +309,20 @@ func (s *VisibilityStore) countChasmExecutionsLegacy( return nil, err } - saMapper, err := s.searchAttributesMapperProvider.GetMapper(request.Namespace) + saMapper, err := s.searchAttributesMapperProvider.GetMapper(namespace.Name(request.Namespace)) if err != nil { return nil, err } converter := NewQueryConverterLegacy( s.GetName(), - request.Namespace, - request.NamespaceID, + namespace.Name(request.Namespace), + namespace.ID(request.NamespaceId), saTypeMap, saMapper, request.Query, mapper, - request.ArchetypeID, + request.ArchetypeId, ) selectFilter, err := converter.BuildCountStmt() if err != nil { @@ -707,7 +713,7 @@ func (s *VisibilityStore) countGroupByExecutions( for _, row := range rows { groupValues := make([]*commonpb.Payload, len(row.GroupValues)) for i, val := range row.GroupValues { - groupValues[i], err = searchattribute.EncodeValue(val, groupByTypes[i]) + groupValues[i], err = sadefs.EncodeValue(val, groupByTypes[i]) if err != nil { return nil, err } @@ -790,6 +796,13 @@ func (s *VisibilityStore) prepareSearchAttributesForDb( if err != nil { return nil, err } + if len(request.SearchAttributes.GetIndexedFields()) != len(searchAttributes) { + for name := range request.SearchAttributes.GetIndexedFields() { + if _, ok := searchAttributes[name]; !ok { + s.logger.Warn("Skipping unknown search attribute while generating visibility record", tag.String("search-attribute", name)) + } + } + } // This is to prevent existing tasks to fail indefinitely. // If it's only invalid values error, then silently continue without them. searchAttributes, err = s.ValidateCustomSearchAttributes(searchAttributes) @@ -876,8 +889,7 @@ func (s *VisibilityStore) encodeRowSearchAttributes( for name, value := range rowSearchAttributes { tp, err := combinedTypeMap.GetType(name) if err != nil { - // Silently ignore ErrInvalidName for unregistered chasm search attributes - if sadefs.IsChasmSearchAttribute(name) && errors.Is(err, searchattribute.ErrInvalidName) { + if errors.Is(err, sadefs.ErrInvalidName) { continue } return nil, err diff --git a/common/persistence/visibility/store/util.go b/common/persistence/visibility/store/util.go index 7e2714b6fa5..e5ea403e2fc 100644 --- a/common/persistence/visibility/store/util.go +++ b/common/persistence/visibility/store/util.go @@ -1,16 +1,16 @@ package store import ( - "maps" - - enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/searchattribute" ) -func CombineTypeMaps(customTypeMap searchattribute.NameTypeMap, chasmTypeMap *chasm.VisibilitySearchAttributesMapper) searchattribute.NameTypeMap { - combinedTypeMap := make(map[string]enumspb.IndexedValueType) - maps.Copy(combinedTypeMap, customTypeMap.Custom()) - maps.Copy(combinedTypeMap, chasmTypeMap.SATypeMap()) - return searchattribute.NewNameTypeMap(combinedTypeMap) +func CombineTypeMaps( + customTypeMap searchattribute.NameTypeMap, + chasmTypeMap *chasm.VisibilitySearchAttributesMapper, +) searchattribute.NameTypeMap { + return searchattribute.MergeNameTypeMaps( + customTypeMap, + searchattribute.NewNameTypeMap(chasmTypeMap.SATypeMap()), + ) } diff --git a/common/persistence/visibility/store/visibility_store.go b/common/persistence/visibility/store/visibility_store.go index fbed6d2bf62..51608892789 100644 --- a/common/persistence/visibility/store/visibility_store.go +++ b/common/persistence/visibility/store/visibility_store.go @@ -9,6 +9,7 @@ import ( commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/persistence/visibility/manager" ) @@ -33,11 +34,12 @@ type ( // Read APIs. ListWorkflowExecutions(ctx context.Context, request *manager.ListWorkflowExecutionsRequestV2) (*InternalListExecutionsResponse, error) - ListChasmExecutions(ctx context.Context, request *manager.ListChasmExecutionsRequest) (*InternalListExecutionsResponse, error) CountWorkflowExecutions(ctx context.Context, request *manager.CountWorkflowExecutionsRequest) (*InternalCountExecutionsResponse, error) - CountChasmExecutions(ctx context.Context, request *manager.CountChasmExecutionsRequest) (*InternalCountExecutionsResponse, error) GetWorkflowExecution(ctx context.Context, request *manager.GetWorkflowExecutionRequest) (*InternalGetWorkflowExecutionResponse, error) + ListChasmExecutions(ctx context.Context, request *visibilityservice.ListChasmExecutionsRequest) (*InternalListExecutionsResponse, error) + CountChasmExecutions(ctx context.Context, request *visibilityservice.CountChasmExecutionsRequest) (*InternalCountExecutionsResponse, error) + // Admin APIs // AddSearchAttributes makes schema changes to add the search attributes. This function must be diff --git a/common/persistence/visibility/store/visibility_store_mock.go b/common/persistence/visibility/store/visibility_store_mock.go index ea255c940f5..a264739c8e9 100644 --- a/common/persistence/visibility/store/visibility_store_mock.go +++ b/common/persistence/visibility/store/visibility_store_mock.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + visibilityservice "go.temporal.io/server/api/visibilityservice/v1" manager "go.temporal.io/server/common/persistence/visibility/manager" gomock "go.uber.org/mock/gomock" ) @@ -68,7 +69,7 @@ func (mr *MockVisibilityStoreMockRecorder) Close() *gomock.Call { } // CountChasmExecutions mocks base method. -func (m *MockVisibilityStore) CountChasmExecutions(ctx context.Context, request *manager.CountChasmExecutionsRequest) (*InternalCountExecutionsResponse, error) { +func (m *MockVisibilityStore) CountChasmExecutions(ctx context.Context, request *visibilityservice.CountChasmExecutionsRequest) (*InternalCountExecutionsResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountChasmExecutions", ctx, request) ret0, _ := ret[0].(*InternalCountExecutionsResponse) @@ -155,7 +156,7 @@ func (mr *MockVisibilityStoreMockRecorder) GetWorkflowExecution(ctx, request any } // ListChasmExecutions mocks base method. -func (m *MockVisibilityStore) ListChasmExecutions(ctx context.Context, request *manager.ListChasmExecutionsRequest) (*InternalListExecutionsResponse, error) { +func (m *MockVisibilityStore) ListChasmExecutions(ctx context.Context, request *visibilityservice.ListChasmExecutionsRequest) (*InternalListExecutionsResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListChasmExecutions", ctx, request) ret0, _ := ret[0].(*InternalListExecutionsResponse) diff --git a/common/persistence/visibility/visibility_manager_dual.go b/common/persistence/visibility/visibility_manager_dual.go index 9bd009f4d81..68afb94f9c0 100644 --- a/common/persistence/visibility/visibility_manager_dual.go +++ b/common/persistence/visibility/visibility_manager_dual.go @@ -5,8 +5,7 @@ import ( "errors" "sync" - commonpb "go.temporal.io/api/common/v1" - "go.temporal.io/server/chasm" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence/visibility/manager" @@ -163,13 +162,13 @@ func (v *VisibilityManagerDual) ListWorkflowExecutions( func (v *VisibilityManagerDual) ListChasmExecutions( ctx context.Context, - request *manager.ListChasmExecutionsRequest, -) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { + request *visibilityservice.ListChasmExecutionsRequest, +) (*visibilityservice.ListChasmExecutionsResponse, error) { return dualReadWrapper( ctx, v, request, - request.Namespace, + namespace.Name(request.Namespace), manager.VisibilityManager.ListChasmExecutions, ) } @@ -189,13 +188,13 @@ func (v *VisibilityManagerDual) CountWorkflowExecutions( func (v *VisibilityManagerDual) CountChasmExecutions( ctx context.Context, - request *manager.CountChasmExecutionsRequest, -) (*chasm.CountExecutionsResponse, error) { + request *visibilityservice.CountChasmExecutionsRequest, +) (*visibilityservice.CountChasmExecutionsResponse, error) { return dualReadWrapper( ctx, v, request, - request.Namespace, + namespace.Name(request.Namespace), manager.VisibilityManager.CountChasmExecutions, ) } diff --git a/common/persistence/visibility/visibility_manager_impl.go b/common/persistence/visibility/visibility_manager_impl.go index 82921bc5dce..272139f7ce7 100644 --- a/common/persistence/visibility/visibility_manager_impl.go +++ b/common/persistence/visibility/visibility_manager_impl.go @@ -12,6 +12,8 @@ import ( "go.temporal.io/api/serviceerror" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" + chasmspb "go.temporal.io/server/api/chasm/v1" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" @@ -154,57 +156,61 @@ func (p *visibilityManagerImpl) ListWorkflowExecutions( func (p *visibilityManagerImpl) ListChasmExecutions( ctx context.Context, - request *manager.ListChasmExecutionsRequest, -) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { + request *visibilityservice.ListChasmExecutionsRequest, +) (*visibilityservice.ListChasmExecutionsResponse, error) { response, err := p.store.ListChasmExecutions(ctx, request) if err != nil { return nil, err } - rc, ok := p.chasmRegistry.ComponentByID(request.ArchetypeID) + rc, ok := p.chasmRegistry.ComponentByID(request.ArchetypeId) if !ok { - return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("unknown archetype ID: %d", request.ArchetypeID)) + return nil, serviceerror.NewInvalidArgument(fmt.Sprintf("unknown archetype ID: %d", request.ArchetypeId)) } mapper := rc.SearchAttributesMapper() - executions := make([]*chasm.ExecutionInfo[*commonpb.Payload], 0, len(response.Executions)) + executions := make([]*chasmspb.VisibilityExecutionInfo, 0, len(response.Executions)) for _, exec := range response.Executions { - executionInfo, err := p.convertToChasmExecutionInfo(exec, mapper, request.Namespace) + executionInfo, err := p.convertToChasmExecutionInfo( + exec, + mapper, + namespace.Name(request.Namespace), + ) if err != nil { return nil, err } executions = append(executions, executionInfo) } - return &chasm.ListExecutionsResponse[*commonpb.Payload]{ + return &visibilityservice.ListChasmExecutionsResponse{ Executions: executions, NextPageToken: response.NextPageToken, }, nil } -// convertInternalExecutionToChasmInfo converts a store.InternalExecutionInfo to chasm.ExecutionInfo. +// convertInternalExecutionToChasmInfo converts a store.InternalExecutionInfo to ChasmExecutionInfo. func (p *visibilityManagerImpl) convertToChasmExecutionInfo( exec *store.InternalExecutionInfo, mapper *chasm.VisibilitySearchAttributesMapper, namespaceName namespace.Name, -) (*chasm.ExecutionInfo[*commonpb.Payload], error) { +) (*chasmspb.VisibilityExecutionInfo, error) { customSAs, chasmSAs := splitSearchAttributes(exec.SearchAttributes) - chasmTypeMap := searchattribute.NewNameTypeMap(mapper.SATypeMap()) - decodedChasmSAs, err := searchattribute.Decode(chasmSAs, &chasmTypeMap, false) - if err != nil { - return nil, err - } - chasmAliasedSAs, err := aliasChasmSearchAttributes(decodedChasmSAs, mapper) + chasmAliasedSAs, err := aliasChasmSearchAttributes(chasmSAs, mapper) if err != nil { return nil, err } - if chasmAliasedSAs == nil { - chasmAliasedSAs = make(map[string]chasm.VisibilityValue) + if len(chasmAliasedSAs.GetIndexedFields()) == 0 { + chasmAliasedSAs = &commonpb.SearchAttributes{ + IndexedFields: make(map[string]*commonpb.Payload), + } } if exec.TaskQueue != "" { - chasmAliasedSAs[sadefs.TaskQueue] = chasm.VisibilityValueString(exec.TaskQueue) + chasmAliasedSAs.IndexedFields[sadefs.TaskQueue] = sadefs.MustEncodeValue( + exec.TaskQueue, + enumspb.INDEXED_VALUE_TYPE_KEYWORD, + ) } customAliasedSAs, err := searchattribute.AliasFields( @@ -221,16 +227,16 @@ func (p *visibilityManagerImpl) convertToChasmExecutionInfo( return nil, err } - return &chasm.ExecutionInfo[*commonpb.Payload]{ - BusinessID: exec.WorkflowID, - RunID: exec.RunID, - StartTime: exec.StartTime, - CloseTime: exec.CloseTime, + return &chasmspb.VisibilityExecutionInfo{ + BusinessId: exec.WorkflowID, + RunId: exec.RunID, + StartTime: timestamppb.New(exec.StartTime), + CloseTime: timestamppb.New(exec.CloseTime), HistoryLength: exec.HistoryLength, HistorySizeBytes: exec.HistorySizeBytes, StateTransitionCount: exec.StateTransitionCount, - ChasmSearchAttributes: chasm.NewSearchAttributesMap(chasmAliasedSAs), - CustomSearchAttributes: customAliasedSAs.GetIndexedFields(), + ChasmSearchAttributes: chasmAliasedSAs, + CustomSearchAttributes: customAliasedSAs, Memo: userMemo, ChasmMemo: chasmMemoPayload, }, nil @@ -274,8 +280,8 @@ func splitUserAndChasmMemo(exec *store.InternalExecutionInfo) (userMemo *commonp func (p *visibilityManagerImpl) CountChasmExecutions( ctx context.Context, - request *manager.CountChasmExecutionsRequest, -) (*chasm.CountExecutionsResponse, error) { + request *visibilityservice.CountChasmExecutionsRequest, +) (*visibilityservice.CountChasmExecutionsResponse, error) { internalResp, err := p.store.CountChasmExecutions(ctx, request) if err != nil { return nil, err @@ -340,17 +346,20 @@ func (p *visibilityManagerImpl) convertToCountWorkflowExecutionsResponse( func (p *visibilityManagerImpl) convertToCountChasmExecutionsResponse( internal *store.InternalCountExecutionsResponse, -) (*chasm.CountExecutionsResponse, error) { - response := &chasm.CountExecutionsResponse{ - Count: internal.Count, - Groups: make([]chasm.Group, 0, len(internal.Groups)), +) (*visibilityservice.CountChasmExecutionsResponse, error) { + response := &visibilityservice.CountChasmExecutionsResponse{ + Count: internal.Count, + Groups: make( + []*visibilityservice.CountChasmExecutionsResponse_AggregationGroup, + len(internal.Groups), + ), } - for _, group := range internal.Groups { - response.Groups = append(response.Groups, chasm.Group{ - Values: group.GroupValues, - Count: group.Count, - }) + for k, group := range internal.Groups { + response.Groups[k] = &visibilityservice.CountChasmExecutionsResponse_AggregationGroup{ + GroupValues: group.GroupValues, + Count: group.Count, + } } return response, nil @@ -546,18 +555,20 @@ func isChasmExecution(searchAttributes *commonpb.SearchAttributes) bool { return false } -// aliasChasmSearchAttributes aliases CHASM search attribute field names and converts them to VisibilityValue. +// aliasChasmSearchAttributes aliases CHASM search attribute field names. // This function mirrors the pattern of searchattribute.AliasFields for custom search attributes. func aliasChasmSearchAttributes( - decodedSearchAttributes map[string]interface{}, + searchAttributes *commonpb.SearchAttributes, mapper *chasm.VisibilitySearchAttributesMapper, -) (map[string]chasm.VisibilityValue, error) { - if mapper == nil || len(decodedSearchAttributes) == 0 { +) (*commonpb.SearchAttributes, error) { + if mapper == nil || len(searchAttributes.GetIndexedFields()) == 0 { return nil, nil } - result := make(map[string]chasm.VisibilityValue) - for fieldName, value := range decodedSearchAttributes { + result := &commonpb.SearchAttributes{ + IndexedFields: make(map[string]*commonpb.Payload), + } + for fieldName, saPayload := range searchAttributes.GetIndexedFields() { if !sadefs.IsChasmSearchAttribute(fieldName) { continue } @@ -573,44 +584,8 @@ func aliasChasmSearchAttributes( return nil, err } - // Convert value to VisibilityValue (types should be correct after Decode) - visValue := convertToVisibilityValue(value) - result[aliasName] = visValue + result.IndexedFields[aliasName] = saPayload } return result, nil } - -// convertToVisibilityValue converts a value to VisibilityValue based on its runtime type. -// After Decode, the types should be correct, so simple type detection is sufficient. -func convertToVisibilityValue(value interface{}) chasm.VisibilityValue { - switch val := value.(type) { - case int: - return chasm.VisibilityValueInt64(int64(val)) - case int32: - return chasm.VisibilityValueInt64(int64(val)) - case int64: - return chasm.VisibilityValueInt64(val) - case float32: - return chasm.VisibilityValueFloat64(float64(val)) - case float64: - return chasm.VisibilityValueFloat64(val) - case bool: - return chasm.VisibilityValueBool(val) - case time.Time: - return chasm.VisibilityValueTime(val) - case string: - // Try to parse as datetime first - if parsedTime, err := time.Parse(time.RFC3339, val); err == nil { - return chasm.VisibilityValueTime(parsedTime) - } - return chasm.VisibilityValueString(val) - case []byte: - return chasm.VisibilityValueByteSlice(val) - case []string: - return chasm.VisibilityValueStringSlice(val) - default: - // Return as string if type is unknown - return chasm.VisibilityValueString(fmt.Sprintf("%v", val)) - } -} diff --git a/common/persistence/visibility/visibility_manager_rate_limited.go b/common/persistence/visibility/visibility_manager_rate_limited.go index 811245f3d40..07e0376c518 100644 --- a/common/persistence/visibility/visibility_manager_rate_limited.go +++ b/common/persistence/visibility/visibility_manager_rate_limited.go @@ -4,8 +4,7 @@ import ( "context" "time" - commonpb "go.temporal.io/api/common/v1" - "go.temporal.io/server/chasm" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/headers" "go.temporal.io/server/common/namespace" @@ -120,8 +119,8 @@ func (m *visibilityManagerRateLimited) ListWorkflowExecutions( func (m *visibilityManagerRateLimited) ListChasmExecutions( ctx context.Context, - request *manager.ListChasmExecutionsRequest, -) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { + request *visibilityservice.ListChasmExecutionsRequest, +) (*visibilityservice.ListChasmExecutionsResponse, error) { if ok := allow(ctx, "ListChasmExecutions", m.readRateLimiter); !ok { return nil, persistence.ErrPersistenceSystemLimitExceeded } @@ -140,8 +139,8 @@ func (m *visibilityManagerRateLimited) CountWorkflowExecutions( func (m *visibilityManagerRateLimited) CountChasmExecutions( ctx context.Context, - request *manager.CountChasmExecutionsRequest, -) (*chasm.CountExecutionsResponse, error) { + request *visibilityservice.CountChasmExecutionsRequest, +) (*visibilityservice.CountChasmExecutionsResponse, error) { if ok := allow(ctx, "CountChasmExecutions", m.readRateLimiter); !ok { return nil, persistence.ErrPersistenceSystemLimitExceeded } diff --git a/common/persistence/visibility/visibility_manager_test.go b/common/persistence/visibility/visibility_manager_test.go index 7b8bee1910f..3ec38f7aa81 100644 --- a/common/persistence/visibility/visibility_manager_test.go +++ b/common/persistence/visibility/visibility_manager_test.go @@ -119,6 +119,55 @@ func (s *VisibilityManagerSuite) TestRecordWorkflowExecutionStarted() { s.ErrorIs(err, persistence.ErrPersistenceSystemLimitExceeded) } +func (s *VisibilityManagerSuite) TestRecordWorkflowExecutionStarted_AddsPersistenceDurationToContext() { + ctx := metrics.AddMetricsContext(context.Background()) + startTime := time.Now().UTC() + executionTime := startTime.Add(1 * time.Minute) + request := &manager.RecordWorkflowExecutionStartedRequest{ + VisibilityRequestBase: &manager.VisibilityRequestBase{ + NamespaceID: testNamespaceUUID, + Namespace: testNamespace, + Execution: &testWorkflowExecution, + WorkflowTypeName: testWorkflowTypeName, + StartTime: startTime, + ExecutionTime: executionTime, + Status: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, + }, + } + + memoBlob, err := serializeMemo(request.Memo) + s.NoError(err) + + s.visibilityStore.EXPECT().RecordWorkflowExecutionStarted( + gomock.Any(), + &store.InternalRecordWorkflowExecutionStartedRequest{ + InternalVisibilityRequestBase: &store.InternalVisibilityRequestBase{ + NamespaceID: request.NamespaceID.String(), + WorkflowID: request.Execution.GetWorkflowId(), + RunID: request.Execution.GetRunId(), + WorkflowTypeName: request.WorkflowTypeName, + StartTime: request.StartTime, + ExecutionTime: request.ExecutionTime, + Status: request.Status, + Memo: memoBlob, + }, + }, + ).Return(nil) + s.metricsHandler.EXPECT(). + WithTags( + metrics.OperationTag(metrics.VisibilityPersistenceRecordWorkflowExecutionStartedScope), + metrics.VisibilityPluginNameTag(s.visibilityStore.GetName()), + metrics.VisibilityIndexNameTag(s.visibilityStore.GetIndexName()), + ). + Return(metrics.NoopMetricsHandler).AnyTimes() + + s.NoError(s.visibilityManager.RecordWorkflowExecutionStarted(ctx, request)) + + val, ok := metrics.ContextCounterGet(ctx, metrics.TaskPersistenceLatency.Name()) + s.True(ok, "context should have TaskPersistenceLatency accumulated") + s.Positive(val, "persistence duration should be non-zero (nanoseconds)") +} + func (s *VisibilityManagerSuite) TestRecordWorkflowExecutionClosed() { startTime := time.Now().UTC() executionTime := startTime.Add(1 * time.Minute) diff --git a/common/persistence/visibility/visiblity_manager_metrics.go b/common/persistence/visibility/visiblity_manager_metrics.go index facd2b4797d..d8038331619 100644 --- a/common/persistence/visibility/visiblity_manager_metrics.go +++ b/common/persistence/visibility/visiblity_manager_metrics.go @@ -4,9 +4,8 @@ import ( "context" "time" - commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" - "go.temporal.io/server/chasm" + "go.temporal.io/server/api/visibilityservice/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" @@ -79,7 +78,9 @@ func (m *visibilityManagerMetrics) RecordWorkflowExecutionStarted( ) error { handler, startTime := m.tagScope(metrics.VisibilityPersistenceRecordWorkflowExecutionStartedScope) err := m.delegate.RecordWorkflowExecutionStarted(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return m.updateErrorMetric(handler, err) } @@ -89,7 +90,9 @@ func (m *visibilityManagerMetrics) RecordWorkflowExecutionClosed( ) error { handler, startTime := m.tagScope(metrics.VisibilityPersistenceRecordWorkflowExecutionClosedScope) err := m.delegate.RecordWorkflowExecutionClosed(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return m.updateErrorMetric(handler, err) } @@ -99,7 +102,9 @@ func (m *visibilityManagerMetrics) UpsertWorkflowExecution( ) error { handler, startTime := m.tagScope(metrics.VisibilityPersistenceUpsertWorkflowExecutionScope) err := m.delegate.UpsertWorkflowExecution(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return m.updateErrorMetric(handler, err) } @@ -109,7 +114,9 @@ func (m *visibilityManagerMetrics) DeleteWorkflowExecution( ) error { handler, startTime := m.tagScope(metrics.VisibilityPersistenceDeleteWorkflowExecutionScope) err := m.delegate.DeleteWorkflowExecution(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return m.updateErrorMetric(handler, err) } @@ -128,13 +135,14 @@ func (m *visibilityManagerMetrics) ListWorkflowExecutions( ) } metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return response, m.updateErrorMetric(handler, err) } func (m *visibilityManagerMetrics) ListChasmExecutions( ctx context.Context, - request *manager.ListChasmExecutionsRequest, -) (*chasm.ListExecutionsResponse[*commonpb.Payload], error) { + request *visibilityservice.ListChasmExecutionsRequest, +) (*visibilityservice.ListChasmExecutionsResponse, error) { handler, startTime := m.tagScope(metrics.VisibilityPersistenceListChasmExecutionsScope) response, err := m.delegate.ListChasmExecutions(ctx, request) elapsed := time.Since(startTime) @@ -142,10 +150,11 @@ func (m *visibilityManagerMetrics) ListChasmExecutions( m.logger.Warn("List query exceeded threshold", tag.Duration("duration", elapsed), tag.String("visibility-query", request.Query), - tag.Stringer("namespace", request.Namespace), + tag.String("namespace", request.Namespace), ) } metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return response, m.updateErrorMetric(handler, err) } @@ -155,17 +164,21 @@ func (m *visibilityManagerMetrics) CountWorkflowExecutions( ) (*manager.CountWorkflowExecutionsResponse, error) { handler, startTime := m.tagScope(metrics.VisibilityPersistenceCountWorkflowExecutionsScope) response, err := m.delegate.CountWorkflowExecutions(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return response, m.updateErrorMetric(handler, err) } func (m *visibilityManagerMetrics) CountChasmExecutions( ctx context.Context, - request *manager.CountChasmExecutionsRequest, -) (*chasm.CountExecutionsResponse, error) { + request *visibilityservice.CountChasmExecutionsRequest, +) (*visibilityservice.CountChasmExecutionsResponse, error) { handler, startTime := m.tagScope(metrics.VisibilityPersistenceCountChasmExecutionsScope) response, err := m.delegate.CountChasmExecutions(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return response, m.updateErrorMetric(handler, err) } @@ -175,7 +188,9 @@ func (m *visibilityManagerMetrics) GetWorkflowExecution( ) (*manager.GetWorkflowExecutionResponse, error) { handler, startTime := m.tagScope(metrics.VisibilityPersistenceGetWorkflowExecutionScope) response, err := m.delegate.GetWorkflowExecution(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return response, m.updateErrorMetric(handler, err) } @@ -185,7 +200,9 @@ func (m *visibilityManagerMetrics) AddSearchAttributes( ) error { handler, startTime := m.tagScope(metrics.VisibilityPersistenceAddSearchAttributesScope) err := m.delegate.AddSearchAttributes(ctx, request) - metrics.VisibilityPersistenceLatency.With(handler).Record(time.Since(startTime)) + elapsed := time.Since(startTime) + metrics.VisibilityPersistenceLatency.With(handler).Record(elapsed) + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), elapsed.Nanoseconds()) return m.updateErrorMetric(handler, err) } diff --git a/common/primitives/task_queues.go b/common/primitives/task_queues.go index f6a0d4cb53d..2a8433ff706 100644 --- a/common/primitives/task_queues.go +++ b/common/primitives/task_queues.go @@ -1,6 +1,8 @@ package primitives import ( + "fmt" + "go.temporal.io/api/serviceerror" ) @@ -33,11 +35,11 @@ func CheckInternalPerNsTaskQueueAllowed(targetTaskQueue, parentTaskQueue string) return nil } if !IsInternalPerNsTaskQueue(parentTaskQueue) { - return serviceerror.NewInvalidArgumentf( - "cannot use internal per namespace task queue:%s (in parent component task queue: %s)", - targetTaskQueue, - parentTaskQueue, - ) + errMessage := fmt.Sprintf("cannot use internal per-namespace task queue:%s", targetTaskQueue) + if parentTaskQueue != "" { + errMessage += fmt.Sprintf(" (in parent component task queue: %s)", parentTaskQueue) + } + return serviceerror.NewInvalidArgument(errMessage) } return nil } diff --git a/common/primitives/uuid.go b/common/primitives/uuid.go index df2f5dbf0ae..73d486aab7b 100644 --- a/common/primitives/uuid.go +++ b/common/primitives/uuid.go @@ -118,7 +118,7 @@ func stringPtr(v string) *string { // Scan implements sql.Scanner interface to allow this type to be // parsed transparently by database drivers -func (u *UUID) Scan(src interface{}) error { +func (u *UUID) Scan(src any) error { if src == nil { return nil } diff --git a/common/resource/fx.go b/common/resource/fx.go index 8413e5fe02e..04eee5abda5 100644 --- a/common/resource/fx.go +++ b/common/resource/fx.go @@ -156,11 +156,12 @@ func SearchAttributeMapperProviderProvider( searchAttributeProvider searchattribute.Provider, persistenceConfig *config.Persistence, ) searchattribute.MapperProvider { + primaryVisibilityStoreConfig := persistenceConfig.GetVisibilityStoreConfig() return searchattribute.NewMapperProvider( saMapper, namespaceRegistry, searchAttributeProvider, - persistenceConfig.IsSQLVisibilityStore() || persistenceConfig.IsCustomVisibilityStore(), + primaryVisibilityStoreConfig.GetIndexName(), ) } @@ -225,6 +226,7 @@ func NamespaceRegistryProvider( return nsregistry.NewRegistry( metadataManager, clusterMetadata.IsGlobalNamespaceEnabled(), + clusterMetadata.GetCurrentClusterName(), dynamicconfig.NamespaceCacheRefreshInterval.Get(dynamicCollection), dynamicconfig.ForceSearchAttributesCacheRefreshOnRead.Get(dynamicCollection), metricsHandler, @@ -344,6 +346,8 @@ func ArchivalMetadataProvider(dc *dynamicconfig.Collection, cfg *config.Config) func ArchiverProviderProvider( cfg *config.Config, + customHistoryArchiverFactory provider.CustomHistoryArchiverFactory, + customVisibilityArchiverFactory provider.CustomVisibilityArchiverFactory, persistenceExecutionManager persistence.ExecutionManager, logger log.SnTaggedLogger, metricsHandler metrics.Handler, @@ -351,6 +355,8 @@ func ArchiverProviderProvider( return provider.NewArchiverProvider( cfg.Archival.History.Provider, cfg.Archival.Visibility.Provider, + customHistoryArchiverFactory, + customVisibilityArchiverFactory, persistenceExecutionManager, logger, metricsHandler, diff --git a/common/rpc/grpc.go b/common/rpc/grpc.go index ab6a66c3d1c..ffe9e0dc321 100644 --- a/common/rpc/grpc.go +++ b/common/rpc/grpc.go @@ -114,7 +114,7 @@ func Dial( func errorInterceptor( ctx context.Context, method string, - req, reply interface{}, + req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, @@ -127,7 +127,7 @@ func errorInterceptor( func headersInterceptor( ctx context.Context, method string, - req, reply interface{}, + req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, diff --git a/common/rpc/interceptor/business_id_extractor.go b/common/rpc/interceptor/business_id_extractor.go index f6142e5af1a..4f145e33809 100644 --- a/common/rpc/interceptor/business_id_extractor.go +++ b/common/rpc/interceptor/business_id_extractor.go @@ -5,6 +5,9 @@ import ( "strings" commonpb "go.temporal.io/api/common/v1" + deploymentpb "go.temporal.io/api/deployment/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + updatepb "go.temporal.io/api/update/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/common/api" "go.temporal.io/server/common/namespace" @@ -57,6 +60,34 @@ type ( taskTokenGetter interface { GetTaskToken() []byte } + + taskQueueNameGetter interface { + GetTaskQueue() string + } + + taskQueueNameFromMessageGetter interface { + GetTaskQueue() *taskqueuepb.TaskQueue + } + + deploymentNameGetter interface { + GetDeploymentName() string + } + + deploymentVersionGetter interface { + GetDeploymentVersion() *deploymentpb.WorkerDeploymentVersion + } + + pollerGroupIDGetter interface { + GetPollerGroupId() string + } + + namespaceGetter interface { + GetNamespace() string + } + + updateRefGetter interface { + GetUpdateRef() *updatepb.UpdateRef + } ) // Extract extracts business ID from the request using the specified pattern. @@ -99,6 +130,45 @@ func (e BusinessIDExtractor) Extract(req any, pattern BusinessIDPattern) string case PatternMultiOperation: return e.extractMultiOperation(req) + case PatternTaskQueueName: + if getter, ok := req.(taskQueueNameGetter); ok { + return getter.GetTaskQueue() + } + + case PatternTaskQueueNameFromMessage: + if getter, ok := req.(taskQueueNameFromMessageGetter); ok { + if tq := getter.GetTaskQueue(); tq != nil { + return tq.GetName() + } + } + + case PatternDeploymentName: + if getter, ok := req.(deploymentNameGetter); ok { + return getter.GetDeploymentName() + } + + case PatternDeploymentVersion: + if getter, ok := req.(deploymentVersionGetter); ok { + if dv := getter.GetDeploymentVersion(); dv != nil { + return dv.GetDeploymentName() + } + } + + case PatternPollerGroupID: + if getter, ok := req.(pollerGroupIDGetter); ok { + return getter.GetPollerGroupId() + } + + case PatternNamespace: + if getter, ok := req.(namespaceGetter); ok { + return getter.GetNamespace() + } + + case PatternUpdateRef: + if getter, ok := req.(updateRefGetter); ok { + return getter.GetUpdateRef().GetWorkflowExecution().GetWorkflowId() + } + case PatternNone: // No extraction needed diff --git a/common/rpc/interceptor/business_id_interceptor.go b/common/rpc/interceptor/business_id_interceptor.go index e71374770f7..f201bb33774 100644 --- a/common/rpc/interceptor/business_id_interceptor.go +++ b/common/rpc/interceptor/business_id_interceptor.go @@ -42,6 +42,20 @@ const ( PatternTaskToken // PatternMultiOperation indicates extraction from ExecuteMultiOperationRequest PatternMultiOperation + // PatternTaskQueueName indicates extraction via GetTaskQueue() string method + PatternTaskQueueName + // PatternTaskQueueNameFromMessage indicates extraction via GetTaskQueue().GetName() (TaskQueue message) + PatternTaskQueueNameFromMessage + // PatternDeploymentName indicates extraction via GetDeploymentName() method + PatternDeploymentName + // PatternDeploymentVersion indicates extraction via GetDeploymentVersion().GetDeploymentName() + PatternDeploymentVersion + // PatternPollerGroupID indicates extraction via GetPollerGroupId() directly + PatternPollerGroupID + // PatternNamespace indicates extraction via GetNamespace() - used when we want to send all calls to a particular api and namespace to a single cell at a time. + PatternNamespace + // PatternUpdateRef indicates extraction via GetUpdateRef().GetWorkflowExecution().GetWorkflowId() + PatternUpdateRef ) // methodToPattern maps API method names to their expected business ID extraction pattern. @@ -88,6 +102,40 @@ var methodToPattern = map[string]BusinessIDPattern{ // Pattern: ExecuteMultiOperation special handling "ExecuteMultiOperation": PatternMultiOperation, + + // task queue name + "UpdateTaskQueueConfig": PatternTaskQueueName, + + // task queue name (from TaskQueue message) + "ListTaskQueuePartitions": PatternTaskQueueNameFromMessage, + + // deployment name + "DescribeWorkerDeployment": PatternDeploymentName, + "DeleteWorkerDeployment": PatternDeploymentName, + "SetWorkerDeploymentCurrentVersion": PatternDeploymentName, + "SetWorkerDeploymentManager": PatternDeploymentName, + "SetWorkerDeploymentRampingVersion": PatternDeploymentName, + + // deployment name (from WorkerDeploymentVersion message) + "DescribeWorkerDeploymentVersion": PatternDeploymentVersion, + "DeleteWorkerDeploymentVersion": PatternDeploymentVersion, + "UpdateWorkerDeploymentVersionMetadata": PatternDeploymentVersion, + + // namespace (deterministic routing to a single cell for the namespace) + // TODO: Switch to worker_grouping_key when available for load balancing + "FetchWorkerConfig": PatternNamespace, + "UpdateWorkerConfig": PatternNamespace, + "DescribeWorker": PatternNamespace, + "RecordWorkerHeartbeat": PatternNamespace, + + // workflow ID (from UpdateRef) + "PollWorkflowExecutionUpdate": PatternUpdateRef, + + // TODO: Uncomment when poller_group_id field is added to requests + // "PollWorkflowTaskQueue": PatternPollerGroupID, + // "PollActivityTaskQueue": PatternPollerGroupID, + // "PollNexusTaskQueue": PatternPollerGroupID, + } // NewBusinessIDInterceptor creates a new BusinessIDInterceptor with the given extractor functions. diff --git a/common/rpc/interceptor/business_id_interceptor_test.go b/common/rpc/interceptor/business_id_interceptor_test.go index 8313016d6bc..ebed923fc2d 100644 --- a/common/rpc/interceptor/business_id_interceptor_test.go +++ b/common/rpc/interceptor/business_id_interceptor_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" + deploymentpb "go.temporal.io/api/deployment/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" updatepb "go.temporal.io/api/update/v1" "go.temporal.io/api/workflowservice/v1" tokenspb "go.temporal.io/server/api/token/v1" @@ -215,6 +217,96 @@ func TestBusinessIDInterceptor_AllMethods(t *testing.T) { }, expectedBusinessID: "wf-id", }, + + // task queue name + { + methodName: "UpdateTaskQueueConfig", + request: &workflowservice.UpdateTaskQueueConfigRequest{TaskQueue: "test-task-queue"}, + expectedBusinessID: "test-task-queue", + }, + + // task queue name (from TaskQueue message) + { + methodName: "ListTaskQueuePartitions", + request: &workflowservice.ListTaskQueuePartitionsRequest{TaskQueue: &taskqueuepb.TaskQueue{Name: "test-task-queue"}}, + expectedBusinessID: "test-task-queue", + }, + + // deployment name + { + methodName: "DescribeWorkerDeployment", + request: &workflowservice.DescribeWorkerDeploymentRequest{DeploymentName: "test-deployment"}, + expectedBusinessID: "test-deployment", + }, + { + methodName: "DeleteWorkerDeployment", + request: &workflowservice.DeleteWorkerDeploymentRequest{DeploymentName: "test-deployment"}, + expectedBusinessID: "test-deployment", + }, + { + methodName: "SetWorkerDeploymentCurrentVersion", + request: &workflowservice.SetWorkerDeploymentCurrentVersionRequest{DeploymentName: "test-deployment"}, + expectedBusinessID: "test-deployment", + }, + { + methodName: "SetWorkerDeploymentManager", + request: &workflowservice.SetWorkerDeploymentManagerRequest{DeploymentName: "test-deployment"}, + expectedBusinessID: "test-deployment", + }, + { + methodName: "SetWorkerDeploymentRampingVersion", + request: &workflowservice.SetWorkerDeploymentRampingVersionRequest{DeploymentName: "test-deployment"}, + expectedBusinessID: "test-deployment", + }, + + // deployment name (from WorkerDeploymentVersion message) + { + methodName: "DescribeWorkerDeploymentVersion", + request: &workflowservice.DescribeWorkerDeploymentVersionRequest{DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "test-deployment"}}, + expectedBusinessID: "test-deployment", + }, + { + methodName: "DeleteWorkerDeploymentVersion", + request: &workflowservice.DeleteWorkerDeploymentVersionRequest{DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "test-deployment"}}, + expectedBusinessID: "test-deployment", + }, + { + methodName: "UpdateWorkerDeploymentVersionMetadata", + request: &workflowservice.UpdateWorkerDeploymentVersionMetadataRequest{DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "test-deployment"}}, + expectedBusinessID: "test-deployment", + }, + + // namespace + { + methodName: "FetchWorkerConfig", + request: &workflowservice.FetchWorkerConfigRequest{Namespace: "test-namespace"}, + expectedBusinessID: "test-namespace", + }, + { + methodName: "UpdateWorkerConfig", + request: &workflowservice.UpdateWorkerConfigRequest{Namespace: "test-namespace"}, + expectedBusinessID: "test-namespace", + }, + { + methodName: "DescribeWorker", + request: &workflowservice.DescribeWorkerRequest{Namespace: "test-namespace"}, + expectedBusinessID: "test-namespace", + }, + { + methodName: "RecordWorkerHeartbeat", + request: &workflowservice.RecordWorkerHeartbeatRequest{Namespace: "test-namespace"}, + expectedBusinessID: "test-namespace", + }, + // workflow ID (from UpdateRef) + { + methodName: "PollWorkflowExecutionUpdate", + request: &workflowservice.PollWorkflowExecutionUpdateRequest{ + UpdateRef: &updatepb.UpdateRef{ + WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: "test-workflow-id"}, + }, + }, + expectedBusinessID: "test-workflow-id", + }, } for _, tc := range testCases { @@ -567,6 +659,32 @@ func TestMethodToPatternMapping(t *testing.T) { // PatternMultiOperation "ExecuteMultiOperation": PatternMultiOperation, + + // PatternTaskQueueName + "UpdateTaskQueueConfig": PatternTaskQueueName, + + // PatternTaskQueueNameFromMessage + "ListTaskQueuePartitions": PatternTaskQueueNameFromMessage, + + // PatternDeploymentName + "DescribeWorkerDeployment": PatternDeploymentName, + "DeleteWorkerDeployment": PatternDeploymentName, + "SetWorkerDeploymentCurrentVersion": PatternDeploymentName, + "SetWorkerDeploymentManager": PatternDeploymentName, + "SetWorkerDeploymentRampingVersion": PatternDeploymentName, + + // PatternDeploymentVersion + "DescribeWorkerDeploymentVersion": PatternDeploymentVersion, + "DeleteWorkerDeploymentVersion": PatternDeploymentVersion, + "UpdateWorkerDeploymentVersionMetadata": PatternDeploymentVersion, + + // PatternNamespace + "FetchWorkerConfig": PatternNamespace, + "UpdateWorkerConfig": PatternNamespace, + "DescribeWorker": PatternNamespace, + "RecordWorkerHeartbeat": PatternNamespace, + + "PollWorkflowExecutionUpdate": PatternUpdateRef, } require.Equal(t, expectedMappings, methodToPattern) diff --git a/common/rpc/interceptor/caller_info.go b/common/rpc/interceptor/caller_info.go index 99080eb7411..a8937f7b4b4 100644 --- a/common/rpc/interceptor/caller_info.go +++ b/common/rpc/interceptor/caller_info.go @@ -27,10 +27,10 @@ func NewCallerInfoInterceptor( func (i *CallerInfoInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { ctx = PopulateCallerInfo( ctx, func() string { return string(MustGetNamespaceName(i.namespaceRegistry, req)) }, diff --git a/common/rpc/interceptor/caller_info_test.go b/common/rpc/interceptor/caller_info_test.go index d99d4ba2f6e..e028cb4266b 100644 --- a/common/rpc/interceptor/caller_info_test.go +++ b/common/rpc/interceptor/caller_info_test.go @@ -49,7 +49,7 @@ func (s *callerInfoSuite) TestIntercept_CallerName() { testCases := []struct { setupIncomingCtx func() context.Context - request interface{} + request any expectedCallerName string }{ { @@ -112,7 +112,7 @@ func (s *callerInfoSuite) TestIntercept_CallerName() { ctx, testCase.request, &grpc.UnaryServerInfo{}, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { resultingCtx = ctx return nil, nil }, @@ -129,7 +129,7 @@ func (s *callerInfoSuite) TestIntercept_CallerType() { testCases := []struct { setupIncomingCtx func() context.Context - request interface{} + request any expectedCallerType string }{ { @@ -182,7 +182,7 @@ func (s *callerInfoSuite) TestIntercept_CallerType() { ctx, testCase.request, &grpc.UnaryServerInfo{}, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { resultingCtx = ctx return nil, nil }, @@ -203,7 +203,7 @@ func (s *callerInfoSuite) TestIntercept_CallOrigin() { testCases := []struct { setupIncomingCtx func() context.Context - request interface{} + request any expectedCallOrigin string }{ { @@ -264,7 +264,7 @@ func (s *callerInfoSuite) TestIntercept_CallOrigin() { ctx, testCase.request, serverInfo, - func(ctx context.Context, req interface{}) (interface{}, error) { + func(ctx context.Context, req any) (any, error) { resultingCtx = ctx return nil, nil }, diff --git a/common/rpc/interceptor/concurrent_request_limit.go b/common/rpc/interceptor/concurrent_request_limit.go index 0e83a47501c..0014c794ea6 100644 --- a/common/rpc/interceptor/concurrent_request_limit.go +++ b/common/rpc/interceptor/concurrent_request_limit.go @@ -68,10 +68,10 @@ func NewConcurrentRequestLimitInterceptor( func (ni *ConcurrentRequestLimitInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { nsName := MustGetNamespaceName(ni.namespaceRegistry, req) mh := GetMetricsHandlerFromContext(ctx, ni.logger) cleanup, err := ni.Allow(nsName, info.FullMethod, mh, req) diff --git a/common/rpc/interceptor/concurrent_request_limit_test.go b/common/rpc/interceptor/concurrent_request_limit_test.go index 286eb38507f..e14c7159204 100644 --- a/common/rpc/interceptor/concurrent_request_limit_test.go +++ b/common/rpc/interceptor/concurrent_request_limit_test.go @@ -211,7 +211,7 @@ func (tc *nsCountLimitTestCase) createInterceptor(ctrl *gomock.Controller) *Conc } // noopHandler is a grpc.UnaryHandler which does nothing. -func noopHandler(context.Context, interface{}) (interface{}, error) { +func noopHandler(context.Context, any) (any, error) { return nil, nil } @@ -227,7 +227,7 @@ func (h testRequestHandler) Unblock() { } // Handle signals that the request has started and then blocks until signaled to respond. -func (h testRequestHandler) Handle(context.Context, interface{}) (interface{}, error) { +func (h testRequestHandler) Handle(context.Context, any) (any, error) { h.started <- struct{}{} <-h.respond diff --git a/common/rpc/interceptor/frontend_service_error.go b/common/rpc/interceptor/frontend_service_error.go index e4801a59f85..faf1422b59b 100644 --- a/common/rpc/interceptor/frontend_service_error.go +++ b/common/rpc/interceptor/frontend_service_error.go @@ -28,10 +28,10 @@ func NewFrontendServiceErrorInterceptor( ) grpc.UnaryServerInterceptor { return func( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, - ) (interface{}, error) { + ) (any, error) { resp, err := handler(ctx, req) if err == nil { return resp, nil diff --git a/common/rpc/interceptor/health.go b/common/rpc/interceptor/health.go index d6a73e3bfa6..3023dd6ff5a 100644 --- a/common/rpc/interceptor/health.go +++ b/common/rpc/interceptor/health.go @@ -28,10 +28,10 @@ func NewHealthInterceptor() *HealthInterceptor { func (i *HealthInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { // only enforce health check on WorkflowService and OperatorService if strings.HasPrefix(info.FullMethod, api.WorkflowServicePrefix) || strings.HasPrefix(info.FullMethod, api.OperatorServicePrefix) { diff --git a/common/rpc/interceptor/health_check.go b/common/rpc/interceptor/health_check.go index be7e318ab0f..171b13b7de1 100644 --- a/common/rpc/interceptor/health_check.go +++ b/common/rpc/interceptor/health_check.go @@ -2,16 +2,29 @@ package interceptor import ( "context" + "fmt" + "strings" + "sync" "time" "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + commonspb "go.temporal.io/server/api/common/v1" "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/aggregate" - "go.temporal.io/server/common/api" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/descriptorpb" +) + +const ( + chasmProtoPrefix = "temporal/server/chasm/lib/" + serviceProtoSuffix = "/service.proto" ) type ( @@ -41,14 +54,60 @@ type ( } ) -var excludedAPIsForHealthSignal = map[string]struct{}{ - "DeepHealthCheck": {}, - "PollMutableState": {}, - "PollWorkflowExecutionUpdate": {}, - "PollWorkflowExecutionHistory": {}, - "UpdateWorkflowExecution": {}, +// excludedAPIs maps full method names to true if they should be excluded from health signals. +// This includes both long-polling APIs and system APIs. +// Built lazily on first use from proto method options. +var ( + excludedAPIs map[string]bool + excludedAPIsOnce sync.Once +) + +func initExcludedAPIs() { + excludedAPIs = make(map[string]bool) + excludedCategories := map[commonspb.ApiCategory]bool{ + commonspb.API_CATEGORY_LONG_POLL: true, + commonspb.API_CATEGORY_SYSTEM: true, + } + + // Process HistoryService explicitly. + processServiceFile(historyservice.File_temporal_server_api_historyservice_v1_service_proto, excludedCategories) + + // Auto-detect all registered chasm/lib service files. + // New services under chasm/lib are picked up automatically without code changes here. + protoregistry.GlobalFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + path := string(fd.Path()) + if strings.HasPrefix(path, chasmProtoPrefix) && strings.HasSuffix(path, serviceProtoSuffix) { + processServiceFile(fd, excludedCategories) + } + return true + }) +} + +// processServiceFile enumerates all methods in a service file and adds excluded categories to excludedAPIs. +func processServiceFile(file protoreflect.FileDescriptor, excludedCategories map[commonspb.ApiCategory]bool) { + services := file.Services() + for i := 0; i < services.Len(); i++ { + service := services.Get(i) + methods := service.Methods() + for j := 0; j < methods.Len(); j++ { + method := methods.Get(j) + opts, ok := method.Options().(*descriptorpb.MethodOptions) + if ok && proto.HasExtension(opts, commonspb.E_ApiCategory) { + categoryOpts, ok := proto.GetExtension(opts, commonspb.E_ApiCategory).(*commonspb.ApiCategoryOptions) + if ok && categoryOpts != nil && excludedCategories[categoryOpts.GetCategory()] { + fullMethod := fmt.Sprintf("/%s/%s", service.FullName(), method.Name()) + excludedAPIs[fullMethod] = true + } + } + } + } +} + +// isExcludedAPI checks if an API is marked as a non-standard API via proto options. +func isExcludedAPI(fullMethod string) bool { + excludedAPIsOnce.Do(initExcludedAPIs) + return excludedAPIs[fullMethod] } -var getWorkflowExecutionHistoryAPI = "GetWorkflowExecutionHistory" // NewHealthCheckInterceptor creates a new health check interceptor func NewHealthCheckInterceptor(healthSignalAggregator HealthSignalAggregator) *HealthCheckInterceptor { @@ -60,32 +119,48 @@ func NewHealthCheckInterceptor(healthSignalAggregator HealthSignalAggregator) *H // UnaryIntercept implements the gRPC unary interceptor interface func (h *HealthCheckInterceptor) UnaryIntercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { startTime := time.Now() resp, err := handler(ctx, req) elapsed := time.Since(startTime) - // Skip health check recording for specific methods - methodName := api.MethodName(info.FullMethod) - - // Skip GetWorkflowExecutionHistory polling request - if methodName == getWorkflowExecutionHistoryAPI { - if request, ok := req.(*historyservice.GetWorkflowExecutionHistoryRequest); ok { - if r := request.GetRequest(); r != nil && r.GetWaitNewEvent() { - return resp, err - } - } + // Skip health signal recording for non-standard APIs + if isExcludedAPI(info.FullMethod) { + return resp, err } - if _, ok := excludedAPIsForHealthSignal[methodName]; !ok { - h.healthSignalAggregator.Record(elapsed, err) + if specialCaseAPIIsPolling(req) { + return resp, err } + + // Record health signal for standard APIs + h.healthSignalAggregator.Record(elapsed, err) return resp, err } +// specialCaseAPIIsPolling checks if an API is a long-polling API and should be excluded from health signals. +// Note that this interceptor may run in multiple Temporal services, so it needs to handle every version of +// each special request type. (for example, historyservice GetWorkflowExecutionHistory vs. workflowservice) +func specialCaseAPIIsPolling(req any) bool { + switch request := req.(type) { + // history + case *historyservice.GetWorkflowExecutionHistoryRequest: + inner := request.GetRequest() + return inner != nil && inner.GetWaitNewEvent() + + // frontend + case *workflowservice.GetWorkflowExecutionHistoryRequest: + return request.GetWaitNewEvent() + case *workflowservice.DescribeActivityExecutionRequest: + return len(request.GetLongPollToken()) > 0 + default: + return false + } +} + // NewHealthSignalAggregator creates a new instance of HealthSignalAggregatorImpl func NewHealthSignalAggregator( logger log.Logger, diff --git a/common/rpc/interceptor/health_check_test.go b/common/rpc/interceptor/health_check_test.go new file mode 100644 index 00000000000..f58894e4edb --- /dev/null +++ b/common/rpc/interceptor/health_check_test.go @@ -0,0 +1,50 @@ +package interceptor + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" + // Blank imports register chasm/lib service file descriptors into protoregistry.GlobalFiles. + _ "go.temporal.io/server/chasm/lib/activity/gen/activitypb/v1" + _ "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" +) + +// resetExcludedAPIs resets the package-level sync.Once and map so each test starts fresh. +func resetExcludedAPIs(t *testing.T) { + t.Helper() + excludedAPIsOnce = sync.Once{} + excludedAPIs = nil +} + +func TestIsExcludedAPI_HistoryService(t *testing.T) { + resetExcludedAPIs(t) + + // PollMutableState is API_CATEGORY_LONG_POLL in historyservice + assert.True(t, isExcludedAPI("/temporal.server.api.historyservice.v1.HistoryService/PollMutableState")) + // RecordActivityTaskHeartbeat is API_CATEGORY_STANDARD + assert.False(t, isExcludedAPI("/temporal.server.api.historyservice.v1.HistoryService/RecordActivityTaskHeartbeat")) +} + +func TestIsExcludedAPI_ChasmActivityService(t *testing.T) { + resetExcludedAPIs(t) + + // PollActivityExecution is API_CATEGORY_LONG_POLL — must be excluded + assert.True(t, isExcludedAPI("/temporal.server.chasm.lib.activity.proto.v1.ActivityService/PollActivityExecution")) + // StartActivityExecution is API_CATEGORY_STANDARD — must not be excluded + assert.False(t, isExcludedAPI("/temporal.server.chasm.lib.activity.proto.v1.ActivityService/StartActivityExecution")) +} + +func TestIsExcludedAPI_ChasmSchedulerService(t *testing.T) { + resetExcludedAPIs(t) + + // All SchedulerService methods are API_CATEGORY_STANDARD — none excluded + assert.False(t, isExcludedAPI("/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/CreateSchedule")) + assert.False(t, isExcludedAPI("/temporal.server.chasm.lib.scheduler.proto.v1.SchedulerService/DeleteSchedule")) +} + +func TestIsExcludedAPI_UnknownMethod(t *testing.T) { + resetExcludedAPIs(t) + + assert.False(t, isExcludedAPI("/some.unknown.Service/DoSomething")) +} diff --git a/common/rpc/interceptor/logtags/history_service_server_gen.go b/common/rpc/interceptor/logtags/history_service_server_gen.go index 711f3ea5fe6..06b436d6d0e 100644 --- a/common/rpc/interceptor/logtags/history_service_server_gen.go +++ b/common/rpc/interceptor/logtags/history_service_server_gen.go @@ -14,7 +14,9 @@ func (wt *WorkflowTags) extractFromHistoryServiceServerMessage(message any) []ta case *historyservice.AddTasksResponse: return nil case *historyservice.CancelNexusOperationRequest: - return nil + return []tag.Tag{ + tag.OperationID(r.GetRequest().GetOperationId()), + } case *historyservice.CancelNexusOperationResponse: return nil case *historyservice.CloseShardRequest: diff --git a/common/rpc/interceptor/logtags/workflow_service_server_gen.go b/common/rpc/interceptor/logtags/workflow_service_server_gen.go index 9857e7341be..efbccad9a67 100644 --- a/common/rpc/interceptor/logtags/workflow_service_server_gen.go +++ b/common/rpc/interceptor/logtags/workflow_service_server_gen.go @@ -25,13 +25,22 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.CreateScheduleResponse: return nil + case *workflowservice.CreateWorkerDeploymentRequest: + return nil + case *workflowservice.CreateWorkerDeploymentResponse: + return nil + case *workflowservice.CreateWorkerDeploymentVersionRequest: + return nil + case *workflowservice.CreateWorkerDeploymentVersionResponse: + return nil case *workflowservice.CreateWorkflowRuleRequest: return nil case *workflowservice.CreateWorkflowRuleResponse: return nil case *workflowservice.DeleteActivityExecutionRequest: return []tag.Tag{ - tag.WorkflowRunID(r.GetRunId()), + tag.ActivityID(r.GetActivityId()), + tag.ChasmRunID(r.GetRunId()), } case *workflowservice.DeleteActivityExecutionResponse: return nil @@ -64,7 +73,8 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.DescribeActivityExecutionRequest: return []tag.Tag{ - tag.WorkflowRunID(r.GetRunId()), + tag.ActivityID(r.GetActivityId()), + tag.ChasmRunID(r.GetRunId()), } case *workflowservice.DescribeActivityExecutionResponse: return []tag.Tag{ @@ -243,7 +253,8 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.PollActivityExecutionRequest: return []tag.Tag{ - tag.WorkflowRunID(r.GetRunId()), + tag.ActivityID(r.GetActivityId()), + tag.ChasmRunID(r.GetRunId()), } case *workflowservice.PollActivityExecutionResponse: return []tag.Tag{ @@ -291,6 +302,7 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t case *workflowservice.RecordActivityTaskHeartbeatByIdRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowId()), + tag.ActivityID(r.GetActivityId()), tag.WorkflowRunID(r.GetRunId()), } case *workflowservice.RecordActivityTaskHeartbeatByIdResponse: @@ -305,7 +317,8 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.RequestCancelActivityExecutionRequest: return []tag.Tag{ - tag.WorkflowRunID(r.GetRunId()), + tag.ActivityID(r.GetActivityId()), + tag.ChasmRunID(r.GetRunId()), } case *workflowservice.RequestCancelActivityExecutionResponse: return nil @@ -346,6 +359,7 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t case *workflowservice.RespondActivityTaskCanceledByIdRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowId()), + tag.ActivityID(r.GetActivityId()), tag.WorkflowRunID(r.GetRunId()), } case *workflowservice.RespondActivityTaskCanceledByIdResponse: @@ -357,6 +371,7 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t case *workflowservice.RespondActivityTaskCompletedByIdRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowId()), + tag.ActivityID(r.GetActivityId()), tag.WorkflowRunID(r.GetRunId()), } case *workflowservice.RespondActivityTaskCompletedByIdResponse: @@ -368,6 +383,7 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t case *workflowservice.RespondActivityTaskFailedByIdRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowId()), + tag.ActivityID(r.GetActivityId()), tag.WorkflowRunID(r.GetRunId()), } case *workflowservice.RespondActivityTaskFailedByIdResponse: @@ -432,7 +448,9 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t case *workflowservice.SignalWorkflowExecutionResponse: return nil case *workflowservice.StartActivityExecutionRequest: - return nil + return []tag.Tag{ + tag.ActivityID(r.GetActivityId()), + } case *workflowservice.StartActivityExecutionResponse: return []tag.Tag{ tag.WorkflowRunID(r.GetRunId()), @@ -455,7 +473,8 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.TerminateActivityExecutionRequest: return []tag.Tag{ - tag.WorkflowRunID(r.GetRunId()), + tag.ActivityID(r.GetActivityId()), + tag.ChasmRunID(r.GetRunId()), } case *workflowservice.TerminateActivityExecutionResponse: return nil @@ -514,6 +533,10 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.UpdateWorkerConfigResponse: return nil + case *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest: + return nil + case *workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse: + return nil case *workflowservice.UpdateWorkerDeploymentVersionMetadataRequest: return nil case *workflowservice.UpdateWorkerDeploymentVersionMetadataResponse: @@ -539,6 +562,10 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t } case *workflowservice.UpdateWorkflowExecutionOptionsResponse: return nil + case *workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest: + return nil + case *workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse: + return nil default: return nil } diff --git a/common/rpc/interceptor/logtags/workflow_tags_test.go b/common/rpc/interceptor/logtags/workflow_tags_test.go index 85fccf8bb22..9627b87e06a 100644 --- a/common/rpc/interceptor/logtags/workflow_tags_test.go +++ b/common/rpc/interceptor/logtags/workflow_tags_test.go @@ -35,7 +35,7 @@ func TestExtract(t *testing.T) { testCases := []struct { name string - req interface{} + req any fullMethod string workflowID string runID string diff --git a/common/rpc/interceptor/mask_internal_error.go b/common/rpc/interceptor/mask_internal_error.go index 27094caa750..0676dcdbb96 100644 --- a/common/rpc/interceptor/mask_internal_error.go +++ b/common/rpc/interceptor/mask_internal_error.go @@ -43,10 +43,10 @@ func NewMaskInternalErrorDetailsInterceptor( func (mi *MaskInternalErrorDetailsInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { resp, err := handler(ctx, req) @@ -65,7 +65,7 @@ func (mi *MaskInternalErrorDetailsInterceptor) shouldMaskErrors(req any) bool { } func (mi *MaskInternalErrorDetailsInterceptor) maskUnknownOrInternalErrors( - req interface{}, fullMethodName string, err error, + req any, fullMethodName string, err error, ) error { statusCode := serviceerror.ToStatus(err).Code() diff --git a/common/rpc/interceptor/mask_internal_error_test.go b/common/rpc/interceptor/mask_internal_error_test.go index 87d15fd3412..6f0bfd47677 100644 --- a/common/rpc/interceptor/mask_internal_error_test.go +++ b/common/rpc/interceptor/mask_internal_error_test.go @@ -80,6 +80,6 @@ func TestMaskInternalErrorDetailsInterceptor(t *testing.T) { mockRegistry.EXPECT().GetNamespace(namespace.Name(empty_namespace)).Return(nil, serviceerror.NewNamespaceNotFound("missing-namespace")) assert.False(t, errorMask.shouldMaskErrors(req)) - var ei interface{} + var ei any assert.False(t, errorMask.shouldMaskErrors(ei)) } diff --git a/common/rpc/interceptor/metadata_context.go b/common/rpc/interceptor/metadata_context.go index 8c48431482f..b594aeebcce 100644 --- a/common/rpc/interceptor/metadata_context.go +++ b/common/rpc/interceptor/metadata_context.go @@ -17,10 +17,10 @@ func NewMetadataContextInterceptor() *MetadataContextInterceptor { // Intercept adds metadata context to all incoming gRPC requests func (m *MetadataContextInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { ctx = contextutil.WithMetadataContext(ctx) return handler(ctx, req) } diff --git a/common/rpc/interceptor/namespace.go b/common/rpc/interceptor/namespace.go index 8394b9057cc..fdeded48add 100644 --- a/common/rpc/interceptor/namespace.go +++ b/common/rpc/interceptor/namespace.go @@ -23,7 +23,7 @@ type ( // e.g. unable to find namespace func MustGetNamespaceName( namespaceRegistry namespace.Registry, - req interface{}, + req any, ) namespace.Name { namespaceName, err := GetNamespaceName(namespaceRegistry, req) if err != nil { @@ -34,7 +34,7 @@ func MustGetNamespaceName( func GetNamespaceName( namespaceRegistry namespace.Registry, - req interface{}, + req any, ) (namespace.Name, error) { switch request := req.(type) { case *workflowservice.RegisterNamespaceRequest: diff --git a/common/rpc/interceptor/namespace_handover.go b/common/rpc/interceptor/namespace_handover.go index 426879bebd1..17c47503d78 100644 --- a/common/rpc/interceptor/namespace_handover.go +++ b/common/rpc/interceptor/namespace_handover.go @@ -120,7 +120,8 @@ func (i *NamespaceHandoverInterceptor) waitNamespaceHandoverUpdate( if err != nil { return nil, err } - if namespaceData.ReplicationState() == enumspb.REPLICATION_STATE_HANDOVER { + businessID := GetBusinessIDFromContext(ctx) + if namespaceData.ReplicationState(businessID) == enumspb.REPLICATION_STATE_HANDOVER { cbID := uuid.New() waitReplicationStateUpdate := make(chan struct{}) i.namespaceRegistry.RegisterStateChangeCallback(cbID, func(ns *namespace.Namespace, deletedFromDb bool) { @@ -129,7 +130,7 @@ func (i *NamespaceHandoverInterceptor) waitNamespaceHandoverUpdate( } if ns.State() != enumspb.NAMESPACE_STATE_REGISTERED || deletedFromDb || - ns.ReplicationState() != enumspb.REPLICATION_STATE_HANDOVER || + ns.ReplicationState(businessID) != enumspb.REPLICATION_STATE_HANDOVER || !ns.IsGlobalNamespace() { // Stop wait on state change if: // 1. namespace is deleting/deleted diff --git a/common/rpc/interceptor/namespace_logger.go b/common/rpc/interceptor/namespace_logger.go index cddffde931c..8f5a99f1ec8 100644 --- a/common/rpc/interceptor/namespace_logger.go +++ b/common/rpc/interceptor/namespace_logger.go @@ -32,10 +32,10 @@ func NewNamespaceLogInterceptor(namespaceRegistry namespace.Registry, logger log func (nli *NamespaceLogInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { if nli.logger != nil { methodName := api.MethodName(info.FullMethod) diff --git a/common/rpc/interceptor/namespace_rate_limit.go b/common/rpc/interceptor/namespace_rate_limit.go index a67ee7782da..1bcf3b26a20 100644 --- a/common/rpc/interceptor/namespace_rate_limit.go +++ b/common/rpc/interceptor/namespace_rate_limit.go @@ -7,8 +7,10 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/quotas" "go.temporal.io/server/service/frontend/configs" @@ -31,6 +33,7 @@ type ( NamespaceRateLimitInterceptor interface { Intercept(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) Allow(namespaceName namespace.Name, methodName string, headerGetter headers.HeaderGetter) error + Wait(ctx context.Context, namespaceName namespace.Name, methodName string, headerGetter headers.HeaderGetter) error } NamespaceRateLimitInterceptorImpl struct { @@ -38,6 +41,9 @@ type ( rateLimiter quotas.RequestRateLimiter tokens map[string]int reducePollWorkflowHistoryPriority dynamicconfig.BoolPropertyFn + pollMethods map[string]struct{} + pollWaitForToken dynamicconfig.BoolPropertyFnWithNamespaceFilter + metricsHandler metrics.Handler } ) @@ -48,20 +54,26 @@ func NewNamespaceRateLimitInterceptor( namespaceRegistry namespace.Registry, rateLimiter quotas.RequestRateLimiter, tokens map[string]int, + pollMethods map[string]struct{}, + pollWaitForToken dynamicconfig.BoolPropertyFnWithNamespaceFilter, + metricsHandler metrics.Handler, ) NamespaceRateLimitInterceptor { return &NamespaceRateLimitInterceptorImpl{ namespaceRegistry: namespaceRegistry, rateLimiter: rateLimiter, tokens: tokens, + pollMethods: pollMethods, + pollWaitForToken: pollWaitForToken, + metricsHandler: metricsHandler, } } func (ni *NamespaceRateLimitInterceptorImpl) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { if ns := MustGetNamespaceName(ni.namespaceRegistry, req); ns != namespace.EmptyName { method := info.FullMethod if IsLongPollGetWorkflowExecutionHistoryRequest(req) { @@ -69,6 +81,14 @@ func (ni *NamespaceRateLimitInterceptorImpl) Intercept( } else if IsLongPollDescribeActivityExecutionRequest(req) { method = configs.PollActivityExecutionAPIName } + if ni.pollWaitForToken(ns.String()) { + if _, ok := ni.pollMethods[info.FullMethod]; ok { + if err := ni.Wait(ctx, ns, method, headers.NewGRPCHeaderGetter(ctx)); err != nil { + return nil, err + } + return handler(ctx, req) + } + } if err := ni.Allow(ns, method, headers.NewGRPCHeaderGetter(ctx)); err != nil { return nil, err } @@ -77,6 +97,48 @@ func (ni *NamespaceRateLimitInterceptorImpl) Intercept( return handler(ctx, req) } +func (ni *NamespaceRateLimitInterceptorImpl) Wait(ctx context.Context, namespaceName namespace.Name, methodName string, headerGetter headers.HeaderGetter) error { + token, ok := ni.tokens[methodName] + if !ok { + token = NamespaceRateLimitDefaultToken + } + request := quotas.NewRequest( + methodName, + token, + namespaceName.String(), + headerGetter.Get(headers.CallerTypeHeaderName), + 0, // this interceptor layer does not throttle based on caller segment + "", // this interceptor layer does not throttle based on call initiation + ) + + if ni.rateLimiter.Allow(time.Now().UTC(), request) { + return nil + } + + waitCtx := ctx + var cancel context.CancelFunc = func() {} + if deadline, ok := ctx.Deadline(); ok { + if time.Until(deadline) <= common.CriticalLongPollTimeout { + return ErrNamespaceRateLimitServerBusy + } + waitCtx, cancel = context.WithDeadline(ctx, deadline.Add(-common.CriticalLongPollTimeout)) + } + defer cancel() + + start := time.Now() + err := ni.rateLimiter.Wait(waitCtx, request) + metrics.NamespaceRateLimitWaitLatency.With(ni.metricsHandler).Record( + time.Since(start), + metrics.NamespaceTag(namespaceName.String()), + metrics.OperationTag(methodName), + ) + + if err != nil && ctx.Err() == nil { + return ErrNamespaceRateLimitServerBusy + } + return ctx.Err() +} + func (ni *NamespaceRateLimitInterceptorImpl) Allow(namespaceName namespace.Name, methodName string, headerGetter headers.HeaderGetter) error { token, ok := ni.tokens[methodName] if !ok { @@ -97,7 +159,7 @@ func (ni *NamespaceRateLimitInterceptorImpl) Allow(namespaceName namespace.Name, } func IsLongPollGetWorkflowExecutionHistoryRequest( - req interface{}, + req any, ) bool { switch request := req.(type) { case *workflowservice.GetWorkflowExecutionHistoryRequest: @@ -107,7 +169,7 @@ func IsLongPollGetWorkflowExecutionHistoryRequest( } func IsLongPollDescribeActivityExecutionRequest( - req interface{}, + req any, ) bool { switch request := req.(type) { case *workflowservice.DescribeActivityExecutionRequest: diff --git a/common/rpc/interceptor/namespace_rate_limit_test.go b/common/rpc/interceptor/namespace_rate_limit_test.go new file mode 100644 index 00000000000..dc8d4f90be3 --- /dev/null +++ b/common/rpc/interceptor/namespace_rate_limit_test.go @@ -0,0 +1,239 @@ +package interceptor + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/quotas" + "go.uber.org/mock/gomock" + "google.golang.org/grpc" +) + +const ( + pollWorkflowTaskQueueMethod = "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowTaskQueue" + otherMethod = "/temporal.api.workflowservice.v1.WorkflowService/StartWorkflowExecution" + testNamespace = "test-namespace" +) + +type namespaceRateLimitInterceptorSuite struct { + suite.Suite + *require.Assertions + + controller *gomock.Controller + mockRateLimiter *quotas.MockRequestRateLimiter + mockRegistry *namespace.MockRegistry +} + +func TestNamespaceRateLimitInterceptorSuite(t *testing.T) { + suite.Run(t, &namespaceRateLimitInterceptorSuite{}) +} + +func (s *namespaceRateLimitInterceptorSuite) SetupTest() { + s.Assertions = require.New(s.T()) + s.controller = gomock.NewController(s.T()) + s.mockRateLimiter = quotas.NewMockRequestRateLimiter(s.controller) + s.mockRegistry = namespace.NewMockRegistry(s.controller) +} + +func (s *namespaceRateLimitInterceptorSuite) newImpl(pollWaitForToken bool) *NamespaceRateLimitInterceptorImpl { + return &NamespaceRateLimitInterceptorImpl{ + namespaceRegistry: s.mockRegistry, + rateLimiter: s.mockRateLimiter, + tokens: map[string]int{}, + pollMethods: map[string]struct{}{ + pollWorkflowTaskQueueMethod: {}, + }, + pollWaitForToken: func(_ string) bool { return pollWaitForToken }, + metricsHandler: metrics.NoopMetricsHandler, + } +} + +// Wait() tests + +func (s *namespaceRateLimitInterceptorSuite) TestWait_TokenImmediatelyAvailable() { + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(true) + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()).Times(0) + + ni := s.newImpl(true) + err := ni.Wait(context.Background(), testNamespace, pollWorkflowTaskQueueMethod, noopHeaderGetter{}) + s.NoError(err) +} + +func (s *namespaceRateLimitInterceptorSuite) TestWait_WaitSucceeds() { + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(false) + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()).Return(nil) + + ni := s.newImpl(true) + err := ni.Wait(context.Background(), testNamespace, pollWorkflowTaskQueueMethod, noopHeaderGetter{}) + s.NoError(err) +} + +func (s *namespaceRateLimitInterceptorSuite) TestWait_NoDeadlineOnCtx() { + // No deadline → waitCtx == ctx; should not panic and should succeed. + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(false) + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()).Return(nil) + + ni := s.newImpl(true) + err := ni.Wait(context.Background(), testNamespace, pollWorkflowTaskQueueMethod, noopHeaderGetter{}) + s.NoError(err) +} + +func (s *namespaceRateLimitInterceptorSuite) TestWait_ShortenedDeadlineExpires_OriginalCtxValid() { + // Outer ctx has deadline = now + CriticalLongPollTimeout + 2s. + // Shortened waitCtx deadline = now + 2s → expires quickly. + // Original ctx is still alive → expect ErrNamespaceRateLimitServerBusy. + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(false) + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, _ quotas.Request) error { + <-ctx.Done() + return ctx.Err() + }) + + outerDeadline := time.Now().Add(common.CriticalLongPollTimeout + 2*time.Second) + ctx, cancel := context.WithDeadline(context.Background(), outerDeadline) + defer cancel() + + ni := s.newImpl(true) + err := ni.Wait(ctx, testNamespace, pollWorkflowTaskQueueMethod, noopHeaderGetter{}) + s.ErrorIs(err, ErrNamespaceRateLimitServerBusy) +} + +func (s *namespaceRateLimitInterceptorSuite) TestWait_DeadlineTooShortToWait() { + // Outer ctx has deadline <= now + CriticalLongPollTimeout → no time to wait. + // Expect immediate ErrNamespaceRateLimitServerBusy without calling rateLimiter.Wait(). + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(false) + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()).Times(0) + + outerDeadline := time.Now().Add(common.CriticalLongPollTimeout - time.Millisecond) + ctx, cancel := context.WithDeadline(context.Background(), outerDeadline) + defer cancel() + + ni := s.newImpl(true) + err := ni.Wait(ctx, testNamespace, pollWorkflowTaskQueueMethod, noopHeaderGetter{}) + s.ErrorIs(err, ErrNamespaceRateLimitServerBusy) +} + +func (s *namespaceRateLimitInterceptorSuite) TestWait_OriginalCtxCancelled() { + // When the original context is cancelled, Wait() should propagate ctx.Err(). + ctx, cancel := context.WithCancel(context.Background()) + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(false) + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, _ quotas.Request) error { + cancel() + <-ctx.Done() + return ctx.Err() + }) + + ni := s.newImpl(true) + err := ni.Wait(ctx, testNamespace, pollWorkflowTaskQueueMethod, noopHeaderGetter{}) + s.ErrorIs(err, context.Canceled) +} + +// Intercept() routing tests + +func (s *namespaceRateLimitInterceptorSuite) TestIntercept_PollMethod_WaitForTokenEnabled() { + // Poll method + pollWaitForToken=true → calls Wait(), handler invoked. + s.mockRegistry.EXPECT().GetNamespace(namespace.Name(testNamespace)).Return(nil, nil) + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(true) // Wait() fast path + + handlerCalled := false + handler := func(ctx context.Context, req any) (any, error) { + handlerCalled = true + return nil, nil + } + + ni := s.newImpl(true) + req := &workflowservice.PollWorkflowTaskQueueRequest{Namespace: testNamespace} + _, err := ni.Intercept(context.Background(), req, &grpc.UnaryServerInfo{FullMethod: pollWorkflowTaskQueueMethod}, handler) + s.NoError(err) + s.True(handlerCalled) +} + +func (s *namespaceRateLimitInterceptorSuite) TestIntercept_PollMethod_WaitDisabled() { + // Poll method + pollWaitForToken=false → falls through to Allow(). + s.mockRegistry.EXPECT().GetNamespace(namespace.Name(testNamespace)).Return(nil, nil) + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(true) + + handlerCalled := false + handler := func(ctx context.Context, req any) (any, error) { + handlerCalled = true + return nil, nil + } + + ni := s.newImpl(false) + req := &workflowservice.PollWorkflowTaskQueueRequest{Namespace: testNamespace} + _, err := ni.Intercept(context.Background(), req, &grpc.UnaryServerInfo{FullMethod: pollWorkflowTaskQueueMethod}, handler) + s.NoError(err) + s.True(handlerCalled) +} + +func (s *namespaceRateLimitInterceptorSuite) TestIntercept_NonPollMethod_WaitEnabled() { + // Non-poll method + pollWaitForToken=true → uses Allow(), not Wait(). + s.mockRegistry.EXPECT().GetNamespace(namespace.Name(testNamespace)).Return(nil, nil) + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(true) + + handlerCalled := false + handler := func(ctx context.Context, req any) (any, error) { + handlerCalled = true + return nil, nil + } + + ni := s.newImpl(true) + req := &workflowservice.StartWorkflowExecutionRequest{Namespace: testNamespace} + _, err := ni.Intercept(context.Background(), req, &grpc.UnaryServerInfo{FullMethod: otherMethod}, handler) + s.NoError(err) + s.True(handlerCalled) +} + +func (s *namespaceRateLimitInterceptorSuite) TestIntercept_PollMethod_WaitEnabled_RateLimited() { + // Poll method + pollWaitForToken=true, rate limited → error returned, handler not called. + s.mockRegistry.EXPECT().GetNamespace(namespace.Name(testNamespace)).Return(nil, nil) + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(false) // Wait() slow path + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()).Return(nil) // token granted + + handlerCalled := false + handler := func(ctx context.Context, req any) (any, error) { + handlerCalled = true + return nil, nil + } + + ni := s.newImpl(true) + req := &workflowservice.PollWorkflowTaskQueueRequest{Namespace: testNamespace} + _, err := ni.Intercept(context.Background(), req, &grpc.UnaryServerInfo{FullMethod: pollWorkflowTaskQueueMethod}, handler) + s.NoError(err) + s.True(handlerCalled) +} + +func (s *namespaceRateLimitInterceptorSuite) TestIntercept_PollMethod_WaitEnabled_ContextExpired() { + // Poll method + pollWaitForToken=true, shortened deadline fires → ErrNamespaceRateLimitServerBusy. + s.mockRegistry.EXPECT().GetNamespace(namespace.Name(testNamespace)).Return(nil, nil) + s.mockRateLimiter.EXPECT().Allow(gomock.Any(), gomock.Any()).Return(false) + s.mockRateLimiter.EXPECT().Wait(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, _ quotas.Request) error { + <-ctx.Done() + return ctx.Err() + }) + + outerDeadline := time.Now().Add(common.CriticalLongPollTimeout + 2*time.Second) + ctx, cancel := context.WithDeadline(context.Background(), outerDeadline) + defer cancel() + + ni := s.newImpl(true) + req := &workflowservice.PollWorkflowTaskQueueRequest{Namespace: testNamespace} + _, err := ni.Intercept(ctx, req, &grpc.UnaryServerInfo{FullMethod: pollWorkflowTaskQueueMethod}, func(_ context.Context, _ any) (any, error) { + return nil, nil + }) + s.ErrorIs(err, ErrNamespaceRateLimitServerBusy) +} + +// noopHeaderGetter implements headers.HeaderGetter with empty values. +type noopHeaderGetter struct{} + +func (noopHeaderGetter) Get(_ string) string { return "" } diff --git a/common/rpc/interceptor/namespace_test.go b/common/rpc/interceptor/namespace_test.go index d1091feb6b2..91e695f0f95 100644 --- a/common/rpc/interceptor/namespace_test.go +++ b/common/rpc/interceptor/namespace_test.go @@ -156,7 +156,7 @@ func (s *namespaceSuite) TestGetNamespace() { register.EXPECT().GetNamespaceName(namespace.ID("exist")).Return(namespace.Name("exist"), nil) register.EXPECT().GetNamespaceName(namespace.ID("nonexist")).Return(namespace.EmptyName, errors.New("not found")) testCases := []struct { - method interface{} + method any namespaceName namespace.Name }{ { diff --git a/common/rpc/interceptor/namespace_validator.go b/common/rpc/interceptor/namespace_validator.go index 4f3cd0c0deb..b0e27cd9cd5 100644 --- a/common/rpc/interceptor/namespace_validator.go +++ b/common/rpc/interceptor/namespace_validator.go @@ -103,10 +103,10 @@ func NewNamespaceValidatorInterceptor( func (ni *NamespaceValidatorInterceptor) NamespaceValidateIntercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { err := ni.setNamespaceIfNotPresent(req) if err != nil { return nil, err @@ -130,7 +130,7 @@ func (ni *NamespaceValidatorInterceptor) ValidateName(ns string) error { } func (ni *NamespaceValidatorInterceptor) setNamespaceIfNotPresent( - req interface{}, + req any, ) error { switch request := req.(type) { case NamespaceNameGetter: @@ -149,7 +149,7 @@ func (ni *NamespaceValidatorInterceptor) setNamespaceIfNotPresent( func (ni *NamespaceValidatorInterceptor) setNamespace( namespaceEntry *namespace.Namespace, - req interface{}, + req any, ) { switch request := req.(type) { case *workflowservice.RespondQueryTaskCompletedRequest: @@ -194,16 +194,16 @@ func (ni *NamespaceValidatorInterceptor) setNamespace( // StateValidationIntercept runs ValidateState - see docstring for that method. func (ni *NamespaceValidatorInterceptor) StateValidationIntercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { namespaceEntry, err := ni.extractNamespace(req) if err != nil { return nil, err } - if err := ni.ValidateState(namespaceEntry, info.FullMethod); err != nil { + if err := ni.ValidateState(namespaceEntry, info.FullMethod, GetBusinessIDFromContext(ctx)); err != nil { return nil, err } @@ -216,14 +216,14 @@ func (ni *NamespaceValidatorInterceptor) StateValidationIntercept( // 3. Namespace exists. // 4. Namespace from request match namespace from task token, if check is enabled with dynamic config. // 5. Namespace is in correct state. -func (ni *NamespaceValidatorInterceptor) ValidateState(namespaceEntry *namespace.Namespace, fullMethod string) error { +func (ni *NamespaceValidatorInterceptor) ValidateState(namespaceEntry *namespace.Namespace, fullMethod string, businessID string) error { if err := ni.checkNamespaceState(namespaceEntry, fullMethod); err != nil { return err } - return ni.checkReplicationState(namespaceEntry, fullMethod) + return ni.checkReplicationState(namespaceEntry, fullMethod, businessID) } -func (ni *NamespaceValidatorInterceptor) extractNamespace(req interface{}) (*namespace.Namespace, error) { +func (ni *NamespaceValidatorInterceptor) extractNamespace(req any) (*namespace.Namespace, error) { // Token namespace has priority over request namespace. Check it first. tokenNamespaceEntry, tokenErr := ni.extractNamespaceFromTaskToken(req) if tokenErr != nil { @@ -249,7 +249,7 @@ func (ni *NamespaceValidatorInterceptor) extractNamespace(req interface{}) (*nam return requestNamespaceEntry, nil } -func (ni *NamespaceValidatorInterceptor) extractNamespaceFromRequest(req interface{}) (*namespace.Namespace, error) { +func (ni *NamespaceValidatorInterceptor) extractNamespaceFromRequest(req any) (*namespace.Namespace, error) { reqWithNamespace, hasNamespace := req.(NamespaceNameGetter) if !hasNamespace { return nil, nil @@ -314,7 +314,7 @@ func (ni *NamespaceValidatorInterceptor) extractNamespaceFromRequest(req interfa } } -func (ni *NamespaceValidatorInterceptor) extractNamespaceFromTaskToken(req interface{}) (*namespace.Namespace, error) { +func (ni *NamespaceValidatorInterceptor) extractNamespaceFromTaskToken(req any) (*namespace.Namespace, error) { reqWithTaskToken, hasTaskToken := req.(TaskTokenGetter) if !hasTaskToken { return nil, nil @@ -378,11 +378,11 @@ func (ni *NamespaceValidatorInterceptor) checkNamespaceState(namespaceEntry *nam return serviceerror.NewNamespaceInvalidState(namespaceEntry.Name().String(), namespaceEntry.State(), allowedStates) } -func (ni *NamespaceValidatorInterceptor) checkReplicationState(namespaceEntry *namespace.Namespace, fullMethod string) error { +func (ni *NamespaceValidatorInterceptor) checkReplicationState(namespaceEntry *namespace.Namespace, fullMethod string, businessID string) error { if namespaceEntry == nil { return nil } - if namespaceEntry.ReplicationState() != enumspb.REPLICATION_STATE_HANDOVER { + if namespaceEntry.ReplicationState(businessID) != enumspb.REPLICATION_STATE_HANDOVER { return nil } diff --git a/common/rpc/interceptor/namespace_validator_test.go b/common/rpc/interceptor/namespace_validator_test.go index 0a1dd22fbc2..ed6623c9363 100644 --- a/common/rpc/interceptor/namespace_validator_test.go +++ b/common/rpc/interceptor/namespace_validator_test.go @@ -71,7 +71,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_NamespaceNotSet( testCases := []struct { expectedErr error - req interface{} + req any }{ { req: &workflowservice.StartWorkflowExecutionRequest{}, @@ -95,7 +95,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_NamespaceNotSet( for _, testCase := range testCases { handlerCalled := false - _, err := nvi.StateValidationIntercept(context.Background(), testCase.req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := nvi.StateValidationIntercept(context.Background(), testCase.req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.StartWorkflowExecutionResponse{}, nil }) @@ -123,7 +123,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_NamespaceNotFoun s.mockRegistry.EXPECT().GetNamespace(namespace.Name("not-found-namespace")).Return(nil, serviceerror.NewNamespaceNotFound("missing-namespace")) req := &workflowservice.StartWorkflowExecutionRequest{Namespace: "not-found-namespace"} handlerCalled := false - _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.StartWorkflowExecutionResponse{}, nil }) @@ -140,7 +140,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_NamespaceNotFoun TaskToken: taskToken, } handlerCalled = false - _, err = nvi.StateValidationIntercept(context.Background(), tokenReq, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = nvi.StateValidationIntercept(context.Background(), tokenReq, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.RespondWorkflowTaskCompletedResponse{}, nil }) @@ -396,7 +396,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_StatusFromNamesp } handlerCalled := false - _, err := nvi.StateValidationIntercept(context.Background(), testCase.req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := nvi.StateValidationIntercept(context.Background(), testCase.req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.StartWorkflowExecutionResponse{}, nil }) @@ -421,7 +421,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_StatusFromToken( state enumspb.NamespaceState expectedErr error method string - req interface{} + req any }{ // RespondWorkflowTaskCompleted { @@ -472,7 +472,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_StatusFromToken( } handlerCalled := false - _, err := nvi.StateValidationIntercept(context.Background(), testCase.req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := nvi.StateValidationIntercept(context.Background(), testCase.req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.RespondWorkflowTaskCompletedResponse{}, nil }) @@ -498,7 +498,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_DescribeNamespac req := &workflowservice.DescribeNamespaceRequest{Id: "test-namespace-id"} handlerCalled := false - _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.DescribeNamespaceResponse{}, nil }) @@ -508,7 +508,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_DescribeNamespac req = &workflowservice.DescribeNamespaceRequest{} handlerCalled = false - _, err = nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.DescribeNamespaceResponse{}, nil }) @@ -529,7 +529,7 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_GetClusterInfo() // Example of API which doesn't have namespace field. req := &workflowservice.GetClusterInfoRequest{} handlerCalled := false - _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.GetClusterInfoResponse{}, nil }) @@ -549,7 +549,7 @@ func (s *namespaceValidatorSuite) Test_Intercept_RegisterNamespace() { req := &workflowservice.RegisterNamespaceRequest{Namespace: "new-namespace"} handlerCalled := false - _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.RegisterNamespaceResponse{}, nil }) @@ -559,7 +559,7 @@ func (s *namespaceValidatorSuite) Test_Intercept_RegisterNamespace() { req = &workflowservice.RegisterNamespaceRequest{} handlerCalled = false - _, err = nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.RegisterNamespaceResponse{}, nil }) @@ -661,11 +661,11 @@ func (s *namespaceValidatorSuite) Test_StateValidationIntercept_TokenNamespaceEn } handlerCalled := false - _, err = nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = nvi.StateValidationIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.RespondWorkflowTaskCompletedResponse{}, nil }) - _, queryErr := nvi.StateValidationIntercept(context.Background(), queryReq, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, queryErr := nvi.StateValidationIntercept(context.Background(), queryReq, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.RespondQueryTaskCompletedResponse{}, nil }) @@ -841,7 +841,7 @@ func (s *namespaceValidatorSuite) Test_NamespaceValidateIntercept() { req := &workflowservice.StartWorkflowExecutionRequest{Namespace: "namespace"} handlerCalled := false - _, err = nvi.NamespaceValidateIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = nvi.NamespaceValidateIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.StartWorkflowExecutionResponse{}, nil }) @@ -850,7 +850,7 @@ func (s *namespaceValidatorSuite) Test_NamespaceValidateIntercept() { req = &workflowservice.StartWorkflowExecutionRequest{Namespace: "namespaceTooLong"} handlerCalled = false - _, err = nvi.NamespaceValidateIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = nvi.NamespaceValidateIntercept(context.Background(), req, serverInfo, func(ctx context.Context, req any) (any, error) { handlerCalled = true return &workflowservice.StartWorkflowExecutionResponse{}, nil }) diff --git a/common/rpc/interceptor/rate_limit.go b/common/rpc/interceptor/rate_limit.go index 50fd6c02608..7a5f67c6d1b 100644 --- a/common/rpc/interceptor/rate_limit.go +++ b/common/rpc/interceptor/rate_limit.go @@ -45,10 +45,10 @@ func NewRateLimitInterceptor( func (i *RateLimitInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { methodName := info.FullMethod // for DescribeTaskQueueRequest, we want to use visibility rate limit only if reachability is queried diff --git a/common/rpc/interceptor/redirection.go b/common/rpc/interceptor/redirection.go index b88f1eff438..334a65a82e0 100644 --- a/common/rpc/interceptor/redirection.go +++ b/common/rpc/interceptor/redirection.go @@ -112,20 +112,24 @@ var ( "ResetActivity": func() any { return &workflowservice.ResetActivityResponse{} }, "UpdateWorkflowExecutionOptions": func() any { return &workflowservice.UpdateWorkflowExecutionOptionsResponse{} }, - "DescribeDeployment": func() any { return &workflowservice.DescribeDeploymentResponse{} }, // [cleanup-wv-pre-release] - "ListDeployments": func() any { return &workflowservice.ListDeploymentsResponse{} }, // [cleanup-wv-pre-release] - "GetDeploymentReachability": func() any { return &workflowservice.GetDeploymentReachabilityResponse{} }, // [cleanup-wv-pre-release] - "GetCurrentDeployment": func() any { return &workflowservice.GetCurrentDeploymentResponse{} }, // [cleanup-wv-pre-release] - "SetCurrentDeployment": func() any { return &workflowservice.SetCurrentDeploymentResponse{} }, // [cleanup-wv-pre-release] - "DescribeWorkerDeployment": func() any { return &workflowservice.DescribeWorkerDeploymentResponse{} }, - "DescribeWorkerDeploymentVersion": func() any { return &workflowservice.DescribeWorkerDeploymentVersionResponse{} }, - "SetWorkerDeploymentCurrentVersion": func() any { return &workflowservice.SetWorkerDeploymentCurrentVersionResponse{} }, - "SetWorkerDeploymentRampingVersion": func() any { return &workflowservice.SetWorkerDeploymentRampingVersionResponse{} }, - "SetWorkerDeploymentManager": func() any { return &workflowservice.SetWorkerDeploymentManagerResponse{} }, - "ListWorkerDeployments": func() any { return &workflowservice.ListWorkerDeploymentsResponse{} }, - "DeleteWorkerDeployment": func() any { return &workflowservice.DeleteWorkerDeploymentResponse{} }, - "DeleteWorkerDeploymentVersion": func() any { return &workflowservice.DeleteWorkerDeploymentVersionResponse{} }, - "UpdateWorkerDeploymentVersionMetadata": func() any { return &workflowservice.UpdateWorkerDeploymentVersionMetadataResponse{} }, + "DescribeDeployment": func() any { return &workflowservice.DescribeDeploymentResponse{} }, // [cleanup-wv-pre-release] + "ListDeployments": func() any { return &workflowservice.ListDeploymentsResponse{} }, // [cleanup-wv-pre-release] + "GetDeploymentReachability": func() any { return &workflowservice.GetDeploymentReachabilityResponse{} }, // [cleanup-wv-pre-release] + "GetCurrentDeployment": func() any { return &workflowservice.GetCurrentDeploymentResponse{} }, // [cleanup-wv-pre-release] + "SetCurrentDeployment": func() any { return &workflowservice.SetCurrentDeploymentResponse{} }, // [cleanup-wv-pre-release] + "DescribeWorkerDeployment": func() any { return &workflowservice.DescribeWorkerDeploymentResponse{} }, + "DescribeWorkerDeploymentVersion": func() any { return &workflowservice.DescribeWorkerDeploymentVersionResponse{} }, + "SetWorkerDeploymentCurrentVersion": func() any { return &workflowservice.SetWorkerDeploymentCurrentVersionResponse{} }, + "SetWorkerDeploymentRampingVersion": func() any { return &workflowservice.SetWorkerDeploymentRampingVersionResponse{} }, + "SetWorkerDeploymentManager": func() any { return &workflowservice.SetWorkerDeploymentManagerResponse{} }, + "ListWorkerDeployments": func() any { return &workflowservice.ListWorkerDeploymentsResponse{} }, + "CreateWorkerDeployment": func() any { return &workflowservice.CreateWorkerDeploymentResponse{} }, + "DeleteWorkerDeployment": func() any { return &workflowservice.DeleteWorkerDeploymentResponse{} }, + "CreateWorkerDeploymentVersion": func() any { return &workflowservice.CreateWorkerDeploymentVersionResponse{} }, + "UpdateWorkerDeploymentVersionComputeConfig": func() any { return &workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse{} }, + "ValidateWorkerDeploymentVersionComputeConfig": func() any { return &workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse{} }, + "DeleteWorkerDeploymentVersion": func() any { return &workflowservice.DeleteWorkerDeploymentVersionResponse{} }, + "UpdateWorkerDeploymentVersionMetadata": func() any { return &workflowservice.UpdateWorkerDeploymentVersionMetadataResponse{} }, "CreateWorkflowRule": func() any { return &workflowservice.CreateWorkflowRuleResponse{} }, "DescribeWorkflowRule": func() any { return &workflowservice.DescribeWorkflowRuleResponse{} }, diff --git a/common/rpc/interceptor/redirection_test.go b/common/rpc/interceptor/redirection_test.go index 51ccfbfe483..52dcb6c3b61 100644 --- a/common/rpc/interceptor/redirection_test.go +++ b/common/rpc/interceptor/redirection_test.go @@ -161,40 +161,44 @@ func (s *redirectionInterceptorSuite) TestGlobalAPI() { "GetWorkerVersioningRules": {}, "GetWorkerTaskReachability": {}, - "StartBatchOperation": {}, - "StopBatchOperation": {}, - "DescribeBatchOperation": {}, - "ListBatchOperations": {}, - "UpdateActivityOptions": {}, - "PauseActivity": {}, - "UnpauseActivity": {}, - "ResetActivity": {}, - "UpdateWorkflowExecutionOptions": {}, - "DescribeDeployment": {}, - "ListDeployments": {}, - "GetDeploymentReachability": {}, - "GetCurrentDeployment": {}, - "SetCurrentDeployment": {}, - "DescribeWorkerDeploymentVersion": {}, - "SetWorkerDeploymentCurrentVersion": {}, - "SetWorkerDeploymentRampingVersion": {}, - "SetWorkerDeploymentManager": {}, - "DescribeWorkerDeployment": {}, - "ListWorkerDeployments": {}, - "DeleteWorkerDeployment": {}, - "DeleteWorkerDeploymentVersion": {}, - "UpdateWorkerDeploymentVersionMetadata": {}, - "CreateWorkflowRule": {}, - "DescribeWorkflowRule": {}, - "DeleteWorkflowRule": {}, - "ListWorkflowRules": {}, - "TriggerWorkflowRule": {}, - "RecordWorkerHeartbeat": {}, - "ListWorkers": {}, - "DescribeWorker": {}, - "UpdateTaskQueueConfig": {}, - "FetchWorkerConfig": {}, - "UpdateWorkerConfig": {}, + "StartBatchOperation": {}, + "StopBatchOperation": {}, + "DescribeBatchOperation": {}, + "ListBatchOperations": {}, + "UpdateActivityOptions": {}, + "PauseActivity": {}, + "UnpauseActivity": {}, + "ResetActivity": {}, + "UpdateWorkflowExecutionOptions": {}, + "DescribeDeployment": {}, + "ListDeployments": {}, + "GetDeploymentReachability": {}, + "GetCurrentDeployment": {}, + "SetCurrentDeployment": {}, + "DescribeWorkerDeploymentVersion": {}, + "SetWorkerDeploymentCurrentVersion": {}, + "SetWorkerDeploymentRampingVersion": {}, + "SetWorkerDeploymentManager": {}, + "DescribeWorkerDeployment": {}, + "ListWorkerDeployments": {}, + "CreateWorkerDeployment": {}, + "DeleteWorkerDeployment": {}, + "CreateWorkerDeploymentVersion": {}, + "UpdateWorkerDeploymentVersionComputeConfig": {}, + "ValidateWorkerDeploymentVersionComputeConfig": {}, + "DeleteWorkerDeploymentVersion": {}, + "UpdateWorkerDeploymentVersionMetadata": {}, + "CreateWorkflowRule": {}, + "DescribeWorkflowRule": {}, + "DeleteWorkflowRule": {}, + "ListWorkflowRules": {}, + "TriggerWorkflowRule": {}, + "RecordWorkerHeartbeat": {}, + "ListWorkers": {}, + "DescribeWorker": {}, + "UpdateTaskQueueConfig": {}, + "FetchWorkerConfig": {}, + "UpdateWorkerConfig": {}, "StartActivityExecution": {}, "CountActivityExecutions": {}, @@ -210,12 +214,12 @@ func (s *redirectionInterceptorSuite) TestGlobalAPI() { func (s *redirectionInterceptorSuite) TestAPIResultMapping() { var service workflowservice.WorkflowServiceServer t := reflect.TypeOf(&service).Elem() - expectedAPIs := make(map[string]interface{}, t.NumMethod()) + expectedAPIs := make(map[string]any, t.NumMethod()) temporalapi.WalkExportedMethods(&service, func(m reflect.Method) { expectedAPIs[m.Name] = m.Type.Out(0) }) - actualAPIs := make(map[string]interface{}) + actualAPIs := make(map[string]any) for api, respAllocFn := range localAPIResponses { actualAPIs[api] = reflect.TypeOf(respAllocFn()) } @@ -230,7 +234,7 @@ func (s *redirectionInterceptorSuite) TestHandleLocalAPIInvocation() { ctx := context.Background() req := &workflowservice.RegisterNamespaceRequest{} functionInvoked := false - handler := func(ctx context.Context, req interface{}) (interface{}, error) { + handler := func(ctx context.Context, req any) (any, error) { functionInvoked = true return &workflowservice.RegisterNamespaceResponse{}, nil } @@ -252,7 +256,7 @@ func (s *redirectionInterceptorSuite) TestHandleGlobalAPIInvocation_Local() { req := &workflowservice.SignalWithStartWorkflowExecutionRequest{} info := &grpc.UnaryServerInfo{} functionInvoked := false - handler := func(ctx context.Context, req interface{}) (interface{}, error) { + handler := func(ctx context.Context, req any) (any, error) { functionInvoked = true return &workflowservice.SignalWithStartWorkflowExecutionResponse{}, nil } @@ -386,7 +390,7 @@ type ( mockClientConnInterface struct { *suite.Suite targetMethod string - targetResponse interface{} + targetResponse any } ) @@ -395,8 +399,8 @@ var _ grpc.ClientConnInterface = (*mockClientConnInterface)(nil) func (s *mockClientConnInterface) Invoke( _ context.Context, method string, - _ interface{}, - reply interface{}, + _ any, + reply any, _ ...grpc.CallOption, ) error { s.Equal(s.targetMethod, method) @@ -433,7 +437,7 @@ func (s *redirectionInterceptorSuite) TestHandleLocalAPIInvocation_NoRedirection ctx := context.Background() req := &workflowservice.RegisterNamespaceRequest{} - handler := func(ctx context.Context, req interface{}) (interface{}, error) { + handler := func(ctx context.Context, req any) (any, error) { return &workflowservice.RegisterNamespaceResponse{}, nil } methodName := "RegisterNamespace" @@ -469,7 +473,7 @@ func (s *redirectionInterceptorSuite) TestHandleGlobalAPIInvocation_LocalRouting ctx := context.Background() req := &workflowservice.SignalWithStartWorkflowExecutionRequest{} info := &grpc.UnaryServerInfo{} - handler := func(ctx context.Context, req interface{}) (interface{}, error) { + handler := func(ctx context.Context, req any) (any, error) { return &workflowservice.SignalWithStartWorkflowExecutionResponse{}, nil } namespaceName := namespace.Name("test-namespace") @@ -586,7 +590,7 @@ func (s *redirectionInterceptorSuite) TestHandleGlobalAPIInvocation_LocalRouting req := &workflowservice.SignalWithStartWorkflowExecutionRequest{} info := &grpc.UnaryServerInfo{} expectedError := serviceerror.NewInternal("local processing error") - handler := func(ctx context.Context, req interface{}) (interface{}, error) { + handler := func(ctx context.Context, req any) (any, error) { return nil, expectedError } namespaceName := namespace.Name("test-namespace") diff --git a/common/rpc/interceptor/retry.go b/common/rpc/interceptor/retry.go index 14b1a794541..fe2f818b893 100644 --- a/common/rpc/interceptor/retry.go +++ b/common/rpc/interceptor/retry.go @@ -28,11 +28,11 @@ func NewRetryableInterceptor( func (i *RetryableInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { - var response interface{} +) (any, error) { + var response any op := func(ctx context.Context) error { var err error response, err = handler(ctx, req) diff --git a/common/rpc/interceptor/sdk_version.go b/common/rpc/interceptor/sdk_version.go index 9151dfba20a..6fdb34ffa8f 100644 --- a/common/rpc/interceptor/sdk_version.go +++ b/common/rpc/interceptor/sdk_version.go @@ -30,10 +30,10 @@ func NewSDKVersionInterceptor() *SDKVersionInterceptor { // Intercept a grpc request func (vi *SDKVersionInterceptor) Intercept( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { sdkName, sdkVersion := headers.GetClientNameAndVersion(ctx) if sdkName != "" && sdkVersion != "" { vi.RecordSDKInfo(sdkName, sdkVersion) diff --git a/common/rpc/interceptor/sdk_version_test.go b/common/rpc/interceptor/sdk_version_test.go index 7cfeb1e681f..25ed973100e 100644 --- a/common/rpc/interceptor/sdk_version_test.go +++ b/common/rpc/interceptor/sdk_version_test.go @@ -21,35 +21,35 @@ func TestSDKVersionRecorder(t *testing.T) { // Record first tuple ctx := headers.SetVersionsForTests(context.Background(), sdkVersion, headers.ClientNameGoSDK, headers.SupportedServerVersions, headers.AllFeatures) - _, err := interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req any) (any, error) { return nil, nil }) assert.NoError(t, err) // Record second tuple ctx = headers.SetVersionsForTests(context.Background(), sdkVersion, headers.ClientNameTypeScriptSDK, headers.SupportedServerVersions, headers.AllFeatures) - _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req any) (any, error) { return nil, nil }) assert.NoError(t, err) // Do not record when over capacity ctx = headers.SetVersionsForTests(context.Background(), sdkVersion, headers.ClientNameJavaSDK, headers.SupportedServerVersions, headers.AllFeatures) - _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req any) (any, error) { return nil, nil }) assert.NoError(t, err) // Empty SDK version should not be recorded ctx = headers.SetVersionsForTests(context.Background(), "", headers.ClientNameGoSDK, headers.SupportedServerVersions, headers.AllFeatures) - _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req any) (any, error) { return nil, nil }) assert.NoError(t, err) // Empty SDK name should not be recorded ctx = headers.SetVersionsForTests(context.Background(), sdkVersion, "", headers.SupportedServerVersions, headers.AllFeatures) - _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err = interceptor.Intercept(ctx, nil, nil, func(ctx context.Context, req any) (any, error) { return nil, nil }) assert.NoError(t, err) diff --git a/common/rpc/interceptor/service_error_interceptor.go b/common/rpc/interceptor/service_error_interceptor.go index 2840281b34e..3008b543725 100644 --- a/common/rpc/interceptor/service_error_interceptor.go +++ b/common/rpc/interceptor/service_error_interceptor.go @@ -11,10 +11,10 @@ import ( func ServiceErrorInterceptor( ctx context.Context, - req interface{}, + req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { resp, err := handler(ctx, req) diff --git a/common/rpc/interceptor/slow_request_logger.go b/common/rpc/interceptor/slow_request_logger.go index b3db4fc071e..dd8dddc7871 100644 --- a/common/rpc/interceptor/slow_request_logger.go +++ b/common/rpc/interceptor/slow_request_logger.go @@ -32,10 +32,10 @@ func NewSlowRequestLoggerInterceptor( func (i *SlowRequestLoggerInterceptor) Intercept( ctx context.Context, - request interface{}, + request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (interface{}, error) { +) (any, error) { // Long-polled methods aren't useful logged. if api.GetMethodMetadata(info.FullMethod).Polling == api.PollingNone { startTime := time.Now() @@ -52,7 +52,7 @@ func (i *SlowRequestLoggerInterceptor) Intercept( } func (i *SlowRequestLoggerInterceptor) logSlowRequest( - request interface{}, + request any, info *grpc.UnaryServerInfo, elapsed time.Duration, ) { diff --git a/common/rpc/interceptor/stream_error.go b/common/rpc/interceptor/stream_error.go index 943d3ae0e01..dd3a3a0c1c8 100644 --- a/common/rpc/interceptor/stream_error.go +++ b/common/rpc/interceptor/stream_error.go @@ -31,11 +31,11 @@ func (c *ClientStreamErrorInterceptor) CloseSend() error { return errorConvert(c.ClientStream.CloseSend()) } -func (c *ClientStreamErrorInterceptor) SendMsg(m interface{}) error { +func (c *ClientStreamErrorInterceptor) SendMsg(m any) error { return errorConvert(c.ClientStream.SendMsg(m)) } -func (c *ClientStreamErrorInterceptor) RecvMsg(m interface{}) error { +func (c *ClientStreamErrorInterceptor) RecvMsg(m any) error { return errorConvert(c.ClientStream.RecvMsg(m)) } @@ -55,7 +55,7 @@ func StreamErrorInterceptor( } func CustomErrorStreamInterceptor( - srv interface{}, + srv any, serverStream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, diff --git a/common/rpc/interceptor/telemetry_test.go b/common/rpc/interceptor/telemetry_test.go index e1fcaf84a7a..c57f9efb1dd 100644 --- a/common/rpc/interceptor/telemetry_test.go +++ b/common/rpc/interceptor/telemetry_test.go @@ -482,7 +482,7 @@ func TestOperationOverride(t *testing.T) { testCases := []struct { methodName string fullName string - req interface{} + req any expectedOperation string }{ { diff --git a/common/rpc/request_issues.go b/common/rpc/request_issues.go index 740264b718f..f56a14b54f6 100644 --- a/common/rpc/request_issues.go +++ b/common/rpc/request_issues.go @@ -19,7 +19,7 @@ func (ri *RequestIssues) Append(issue string) { } // Appendf appends a formatted issue to the set. -func (ri *RequestIssues) Appendf(format string, args ...interface{}) { +func (ri *RequestIssues) Appendf(format string, args ...any) { ri.Append(fmt.Sprintf(format, args...)) } diff --git a/common/rpc/test/rpc_common_test.go b/common/rpc/test/rpc_common_test.go index 470b5a95ab1..6ae5f479629 100644 --- a/common/rpc/test/rpc_common_test.go +++ b/common/rpc/test/rpc_common_test.go @@ -122,7 +122,7 @@ func runTestServerMultipleDials( server, port := startTestServiceServer(s, serverFactory) defer server.Stop() - for i := 0; i < nDials; i++ { + for range nDials { tlsInfo, err := dialTestServiceAndGetTLSInfo(s, host+":"+port, clientFactory, serverFactory.serverUsage) validator(tlsInfo, err) } diff --git a/common/sdk/factory.go b/common/sdk/factory.go index 1379c1f047a..21f0d4e0e6d 100644 --- a/common/sdk/factory.go +++ b/common/sdk/factory.go @@ -131,7 +131,7 @@ func sdkClientNameHeadersInjectorInterceptor() grpc.UnaryClientInterceptor { return func( ctx context.Context, method string, - req, reply interface{}, + req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, diff --git a/common/searchattribute/encode.go b/common/searchattribute/encode.go index db8f4881fd7..670651c3ab3 100644 --- a/common/searchattribute/encode.go +++ b/common/searchattribute/encode.go @@ -1,6 +1,8 @@ package searchattribute import ( + "errors" + commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/server/common/payload" @@ -10,7 +12,7 @@ import ( // Encode encodes map of search attribute values to search attributes. // typeMap can be nil (then MetadataType field won't be set). // In case of error, it will continue to next search attribute and return last error. -func Encode(searchAttributes map[string]interface{}, typeMap *NameTypeMap) (*commonpb.SearchAttributes, error) { +func Encode(searchAttributes map[string]any, typeMap *NameTypeMap) (*commonpb.SearchAttributes, error) { if len(searchAttributes) == 0 { return nil, nil } @@ -30,6 +32,13 @@ func Encode(searchAttributes map[string]interface{}, typeMap *NameTypeMap) (*com if typeMap != nil { saType, err = typeMap.getType(saName, customCategory|predefinedCategory) if err != nil { + if errors.Is(err, sadefs.ErrInvalidName) { + // Silently skip unknown search attributes. This can happen due to + // version mismatches where a newer server wrote a predefined SA + // that this server doesn't recognize. + delete(indexedFields, saName) + continue + } lastErr = err continue } @@ -47,25 +56,34 @@ func Decode( searchAttributes *commonpb.SearchAttributes, typeMap *NameTypeMap, allowList bool, -) (map[string]interface{}, error) { +) (map[string]any, error) { if len(searchAttributes.GetIndexedFields()) == 0 { return nil, nil } - result := make(map[string]interface{}, len(searchAttributes.GetIndexedFields())) + result := make(map[string]any, len(searchAttributes.GetIndexedFields())) var lastErr error for saName, saPayload := range searchAttributes.GetIndexedFields() { saType := enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED if typeMap != nil { var err error saType, err = typeMap.getType(saName, customCategory|predefinedCategory) - // TODO: Evaluate if we should get the chasm search attribute mapper when upserting search attributes. - if err != nil && !sadefs.IsChasmSearchAttribute(saName) { - lastErr = err + if err != nil { + if sadefs.IsChasmSearchAttribute(saName) { + // Chasm search attributes are not in the standard type map; + // allow them through with UNSPECIFIED type. + } else if errors.Is(err, sadefs.ErrInvalidName) { + // Silently skip unknown search attributes. This can happen due to + // version mismatches where a newer server wrote a predefined SA + // that this server doesn't recognize. + continue + } else { + lastErr = err + } } } - searchAttributeValue, err := DecodeValue(saPayload, saType, allowList) + searchAttributeValue, err := sadefs.DecodeValue(saPayload, saType, allowList) if err != nil { lastErr = err result[saName] = nil diff --git a/common/searchattribute/encode_test.go b/common/searchattribute/encode_test.go index fa22b23624c..4aad5890c15 100644 --- a/common/searchattribute/encode_test.go +++ b/common/searchattribute/encode_test.go @@ -5,12 +5,13 @@ import ( "github.com/stretchr/testify/require" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/common/searchattribute/sadefs" ) func Test_Encode_Success(t *testing.T) { r := require.New(t) - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": "val1", "key2": 2, "key3": true, @@ -47,7 +48,7 @@ func Test_Encode_Success(t *testing.T) { func Test_Encode_NilMap(t *testing.T) { r := require.New(t) - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": "val1", "key2": 2, "key3": true, @@ -69,24 +70,23 @@ func Test_Encode_NilMap(t *testing.T) { r.Equal("json/plain", string(sa.IndexedFields["key6"].GetMetadata()["encoding"])) } -func Test_Encode_Error(t *testing.T) { +func Test_Encode_SkipsUnknownSearchAttributes(t *testing.T) { r := require.New(t) - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": "val1", "key2": 2, "key3": true, }, &NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, - "key4": enumspb.INDEXED_VALUE_TYPE_INT, "key3": enumspb.INDEXED_VALUE_TYPE_BOOL, }}) - r.Error(err) - r.ErrorIs(err, ErrInvalidName) - r.Len(sa.IndexedFields, 3) + // key2 is unknown and should be silently skipped. + r.NoError(err) + r.Len(sa.IndexedFields, 2) r.Equal(`"val1"`, string(sa.IndexedFields["key1"].GetData())) r.Equal("Text", string(sa.IndexedFields["key1"].GetMetadata()["type"])) - r.Equal("2", string(sa.IndexedFields["key2"].GetData())) + r.NotContains(sa.IndexedFields, "key2") r.Equal("true", string(sa.IndexedFields["key3"].GetData())) r.Equal("Bool", string(sa.IndexedFields["key3"].GetMetadata()["type"])) } @@ -102,7 +102,7 @@ func Test_Decode_Success(t *testing.T) { "key5": enumspb.INDEXED_VALUE_TYPE_KEYWORD, "key6": enumspb.INDEXED_VALUE_TYPE_KEYWORD, }} - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": "val1", "key2": 2, "key3": true, @@ -150,7 +150,7 @@ func Test_Decode_NilMap(t *testing.T) { "key5": enumspb.INDEXED_VALUE_TYPE_KEYWORD, "key6": enumspb.INDEXED_VALUE_TYPE_KEYWORD, }} - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": "val1", "key2": 2, "key3": true, @@ -171,7 +171,7 @@ func Test_Decode_NilMap(t *testing.T) { r.Nil(vals["key6"]) } -func Test_Decode_Error(t *testing.T) { +func Test_Decode_SkipsUnknownSearchAttributes(t *testing.T) { r := require.New(t) typeMap := &NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ @@ -179,36 +179,51 @@ func Test_Decode_Error(t *testing.T) { "key2": enumspb.INDEXED_VALUE_TYPE_INT, "key3": enumspb.INDEXED_VALUE_TYPE_BOOL, }} - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": "val1", "key2": 2, "key3": true, }, typeMap) r.NoError(err) + // Decode with a typeMap that doesn't include key2: key2 should be silently skipped. vals, err := Decode( sa, &NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, - "key4": enumspb.INDEXED_VALUE_TYPE_INT, "key3": enumspb.INDEXED_VALUE_TYPE_BOOL, }}, true, ) - r.Error(err) - r.ErrorIs(err, ErrInvalidName) - r.Len(sa.IndexedFields, 3) + r.NoError(err) + r.Len(vals, 2) r.Equal("val1", vals["key1"]) - r.Equal(int64(2), vals["key2"]) + r.NotContains(vals, "key2") r.Equal(true, vals["key3"]) +} + +func Test_Decode_Error(t *testing.T) { + r := require.New(t) + + typeMap := &NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ + "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, + "key2": enumspb.INDEXED_VALUE_TYPE_INT, + "key3": enumspb.INDEXED_VALUE_TYPE_BOOL, + }} + sa, err := Encode(map[string]any{ + "key1": "val1", + "key2": 2, + "key3": true, + }, typeMap) + r.NoError(err) delete(sa.IndexedFields["key1"].Metadata, "type") delete(sa.IndexedFields["key2"].Metadata, "type") delete(sa.IndexedFields["key3"].Metadata, "type") - vals, err = Decode(sa, nil, true) + vals, err := Decode(sa, nil, true) r.Error(err) - r.ErrorIs(err, ErrInvalidType) + r.ErrorIs(err, sadefs.ErrInvalidType) r.Len(vals, 3) r.Nil(vals["key1"]) r.Nil(vals["key2"]) diff --git a/common/searchattribute/manager.go b/common/searchattribute/manager.go index 82bd54a63b0..e3277b0c2f0 100644 --- a/common/searchattribute/manager.go +++ b/common/searchattribute/manager.go @@ -76,7 +76,7 @@ func (m *managerImpl) GetSearchAttributes( forceRefreshCache bool, ) (NameTypeMap, error) { now := m.timeSource.Now() - result := NameTypeMap{} + result := NewNameTypeMap(nil) saCache, err := m.refreshCache(forceRefreshCache, now) if err != nil { m.logger.Error("failed to refresh search attributes cache", tag.Error(err)) diff --git a/common/searchattribute/manager_test.go b/common/searchattribute/manager_test.go index f5879b3164f..3d2df087526 100644 --- a/common/searchattribute/manager_test.go +++ b/common/searchattribute/manager_test.go @@ -104,7 +104,7 @@ func (s *searchAttributesManagerSuite) TestGetSearchAttributesCache() { wg := sync.WaitGroup{} wg.Add(10) - for goroutine := 0; goroutine < 10; goroutine++ { + for goroutine := range 10 { go func(goroutine int) { defer wg.Done() for i := 1; i < 1500; i++ { diff --git a/common/searchattribute/mapper.go b/common/searchattribute/mapper.go index 27ce0129b68..3cea012e970 100644 --- a/common/searchattribute/mapper.go +++ b/common/searchattribute/mapper.go @@ -4,8 +4,10 @@ package searchattribute import ( "errors" + "fmt" commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/searchattribute/sadefs" @@ -20,15 +22,12 @@ type ( GetFieldName(alias string, namespace string) (string, error) } - noopMapper struct{} - - // This mapper is to be backwards compatible with versions before v1.20. - // Users using standard visibility might have registered custom search attributes. - // Those search attributes won't be searchable, as they weren't before version v1.20. - // Thus, this mapper will allow those search attributes to be used without being alised. - backCompMapper_v1_20 struct { - mapper Mapper - emptyStringNameTypeMap NameTypeMap + // This mapper preserves legacy custom search attribute behavior by falling back + // to identity mapping when the wrapped mapper misses but cluster metadata still + // recognizes the name as a legacy custom search attribute. + backCompMapper struct { + mapper Mapper + fallbackNameTypeMap NameTypeMap } MapperProvider interface { @@ -36,63 +35,57 @@ type ( } mapperProviderImpl struct { - customMapper Mapper - namespaceRegistry namespace.Registry - searchAttributesProvider Provider - enableMapperFromNamespace bool + customMapper Mapper + namespaceRegistry namespace.Registry + searchAttributesProvider Provider + fallbackIndexName string } ) -var _ Mapper = (*noopMapper)(nil) -var _ Mapper = (*backCompMapper_v1_20)(nil) +var _ Mapper = (*backCompMapper)(nil) var _ Mapper = (*namespace.CustomSearchAttributesMapper)(nil) var _ MapperProvider = (*mapperProviderImpl)(nil) -func (m *noopMapper) GetAlias(fieldName string, _ string) (string, error) { - return fieldName, nil -} - -func (m *noopMapper) GetFieldName(alias string, _ string) (string, error) { - return alias, nil -} - -func (m *backCompMapper_v1_20) GetAlias(fieldName string, namespaceName string) (string, error) { +func (m *backCompMapper) GetAlias(fieldName string, namespaceName string) (string, error) { alias, firstErr := m.mapper.GetAlias(fieldName, namespaceName) if firstErr != nil { - _, err := m.emptyStringNameTypeMap.getType(fieldName, customCategory) - if err != nil { + if !m.isLegacyCustomSearchAttribute(fieldName) { return "", firstErr } - // this is custom search attribute registered in pre-v1.20 + // this is a custom search attribute registered through cluster metadata. return fieldName, nil } return alias, nil } -func (m *backCompMapper_v1_20) GetFieldName(alias string, namespaceName string) (string, error) { +func (m *backCompMapper) GetFieldName(alias string, namespaceName string) (string, error) { fieldName, firstErr := m.mapper.GetFieldName(alias, namespaceName) if firstErr != nil { - _, err := m.emptyStringNameTypeMap.getType(alias, customCategory) - if err != nil { + if !m.isLegacyCustomSearchAttribute(alias) { return "", firstErr } - // this is custom search attribute registered in pre-v1.20 + // this is a custom search attribute registered through cluster metadata. return alias, nil } return fieldName, nil } +func (m *backCompMapper) isLegacyCustomSearchAttribute(name string) bool { + _, err := m.fallbackNameTypeMap.getType(name, customCategory) + return err == nil +} + func NewMapperProvider( customMapper Mapper, namespaceRegistry namespace.Registry, searchAttributesProvider Provider, - enableMapperFromNamespace bool, + fallbackIndexName string, ) MapperProvider { return &mapperProviderImpl{ - customMapper: customMapper, - namespaceRegistry: namespaceRegistry, - searchAttributesProvider: searchAttributesProvider, - enableMapperFromNamespace: enableMapperFromNamespace, + customMapper: customMapper, + namespaceRegistry: namespaceRegistry, + searchAttributesProvider: searchAttributesProvider, + fallbackIndexName: fallbackIndexName, } } @@ -100,21 +93,35 @@ func (m *mapperProviderImpl) GetMapper(nsName namespace.Name) (Mapper, error) { if m.customMapper != nil { return m.customMapper, nil } - if !m.enableMapperFromNamespace { - return &noopMapper{}, nil - } saMapper, err := m.namespaceRegistry.GetCustomSearchAttributesMapper(nsName) if err != nil { return nil, err } - // if there's an error, it returns an empty object, which is expected here - emptyStringNameTypeMap, _ := m.searchAttributesProvider.GetSearchAttributes("", false) - return &backCompMapper_v1_20{ - mapper: &saMapper, - emptyStringNameTypeMap: emptyStringNameTypeMap, + fallbackNameTypeMap := NameTypeMap{} + if m.fallbackIndexName != "" { + nameTypeMap, err := m.searchAttributesProvider.GetSearchAttributes(m.fallbackIndexName, false) + if err != nil { + return nil, fmt.Errorf("failed to load search attributes for fallback index %q: %w", m.fallbackIndexName, err) + } + fallbackNameTypeMap = legacyCustomSearchAttributes(nameTypeMap) + } + return &backCompMapper{ + mapper: &saMapper, + fallbackNameTypeMap: fallbackNameTypeMap, }, nil } +func legacyCustomSearchAttributes(nameTypeMap NameTypeMap) NameTypeMap { + legacyCustomSearchAttributes := make(map[string]enumspb.IndexedValueType) + for name, valueType := range nameTypeMap.Custom() { + if sadefs.IsPreallocatedCSAFieldName(name, valueType) { + continue + } + legacyCustomSearchAttributes[name] = valueType + } + return NewNameTypeMap(legacyCustomSearchAttributes) +} + // AliasFields returns SearchAttributes struct where each custom search attribute name is replaced with alias. // If no replacement where made, it returns nil which means that original SearchAttributes struct should be used. func AliasFields( diff --git a/common/searchattribute/mapper_test.go b/common/searchattribute/mapper_test.go index 5367113be7f..39011231cef 100644 --- a/common/searchattribute/mapper_test.go +++ b/common/searchattribute/mapper_test.go @@ -1,11 +1,15 @@ package searchattribute import ( + "errors" "testing" "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" + "go.temporal.io/server/common/namespace" + "go.uber.org/mock/gomock" ) func Test_AliasFields(t *testing.T) { @@ -137,3 +141,115 @@ func Test_UnaliasFields(t *testing.T) { require.NoError(t, err) require.Equal(t, sa, sb, "when there is nothin to unalias should return received attributes") } + +type staticSearchAttributesProvider struct { + nameTypeMaps map[string]NameTypeMap + err error +} + +func (s staticSearchAttributesProvider) GetSearchAttributes(indexName string, _ bool) (NameTypeMap, error) { + if s.err != nil { + return NameTypeMap{}, s.err + } + if nameTypeMap, ok := s.nameTypeMaps[indexName]; ok { + return nameTypeMap, nil + } + return NameTypeMap{}, nil +} + +func Test_BackCompMapperFallsBackToClusterMetadataFields(t *testing.T) { + mapper := &backCompMapper{ + mapper: &TestMapper{}, + fallbackNameTypeMap: TestNameTypeMap(), + } + + alias, err := mapper.GetAlias("Keyword02", "error-namespace") + require.NoError(t, err) + require.Equal(t, "Keyword02", alias) + + fieldName, err := mapper.GetFieldName("Keyword02", "error-namespace") + require.NoError(t, err) + require.Equal(t, "Keyword02", fieldName) +} + +func TestMapperProviderUsesConfiguredVisibilityIndexForBackCompatFallback(t *testing.T) { + controller := gomock.NewController(t) + nsRegistry := namespace.NewMockRegistry(controller) + nsRegistry.EXPECT(). + GetCustomSearchAttributesMapper(namespace.Name("test-namespace")). + Return(namespace.CustomSearchAttributesMapper{}, nil) + + mapperProvider := NewMapperProvider( + nil, + nsRegistry, + staticSearchAttributesProvider{ + nameTypeMaps: map[string]NameTypeMap{ + "test-visibility-index": NewNameTypeMap(map[string]enumspb.IndexedValueType{ + "LegacyKeyword": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }), + }, + }, + "test-visibility-index", + ) + + mapper, err := mapperProvider.GetMapper(namespace.Name("test-namespace")) + require.NoError(t, err) + + alias, err := mapper.GetAlias("LegacyKeyword", "error-namespace") + require.NoError(t, err) + require.Equal(t, "LegacyKeyword", alias) + + fieldName, err := mapper.GetFieldName("LegacyKeyword", "error-namespace") + require.NoError(t, err) + require.Equal(t, "LegacyKeyword", fieldName) +} + +func TestMapperProviderDoesNotTreatPreallocatedFieldsAsLegacyCustomAttributes(t *testing.T) { + controller := gomock.NewController(t) + nsRegistry := namespace.NewMockRegistry(controller) + nsRegistry.EXPECT(). + GetCustomSearchAttributesMapper(namespace.Name("test-namespace")). + Return(namespace.CustomSearchAttributesMapper{}, nil) + + mapperProvider := NewMapperProvider( + nil, + nsRegistry, + staticSearchAttributesProvider{ + nameTypeMaps: map[string]NameTypeMap{ + "test-visibility-index": TestNameTypeMap(), + }, + }, + "test-visibility-index", + ) + + mapper, err := mapperProvider.GetMapper(namespace.Name("test-namespace")) + require.NoError(t, err) + + _, err = mapper.GetFieldName("Text01", "error-namespace") + require.Error(t, err) + var invalidArgumentErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgumentErr) +} + +func TestMapperProviderReturnsFallbackLookupError(t *testing.T) { + controller := gomock.NewController(t) + nsRegistry := namespace.NewMockRegistry(controller) + nsRegistry.EXPECT(). + GetCustomSearchAttributesMapper(namespace.Name("test-namespace")). + Return(namespace.CustomSearchAttributesMapper{}, nil) + + expectedErr := errors.New("boom") + mapperProvider := NewMapperProvider( + nil, + nsRegistry, + staticSearchAttributesProvider{ + err: expectedErr, + }, + "test-visibility-index", + ) + + _, err := mapperProvider.GetMapper(namespace.Name("test-namespace")) + require.Error(t, err) + require.ErrorContains(t, err, `failed to load search attributes for fallback index "test-visibility-index"`) + require.ErrorContains(t, err, expectedErr.Error()) +} diff --git a/common/searchattribute/name_type_map.go b/common/searchattribute/name_type_map.go index 2dee541540b..e8a965719b5 100644 --- a/common/searchattribute/name_type_map.go +++ b/common/searchattribute/name_type_map.go @@ -9,15 +9,6 @@ import ( "go.temporal.io/server/common/searchattribute/sadefs" ) -type ( - NameTypeMap struct { - // customSearchAttributes are defined by cluster admin per cluster level and passed and stored in SearchAttributes object. - customSearchAttributes map[string]enumspb.IndexedValueType - } - - category int32 -) - const ( systemCategory category = 1 << iota predefinedCategory @@ -29,12 +20,30 @@ var ( predefined = sadefs.Predefined() ) -func buildIndexNameTypeMap(indexSearchAttributes map[string]*persistencespb.IndexSearchAttributes) map[string]NameTypeMap { +type ( + NameTypeMap struct { + // systemSearchAttributes are by default defined internally (sadefs.System()). + // You can overwrite it by calling WithSystemSearchAttributes. + systemSearchAttributes map[string]enumspb.IndexedValueType + + // predefinedSearchAttributes are by default defined internally (sadefs.Predefined()). + // You can overwrite it by calling WithPredefinedSearchAttributes. + predefinedSearchAttributes map[string]enumspb.IndexedValueType + + // customSearchAttributes are defined by cluster admin per cluster level and + // passed and stored in SearchAttributes object. + customSearchAttributes map[string]enumspb.IndexedValueType + } + + category int32 +) + +func buildIndexNameTypeMap( + indexSearchAttributes map[string]*persistencespb.IndexSearchAttributes, +) map[string]NameTypeMap { indexNameTypeMap := make(map[string]NameTypeMap, len(indexSearchAttributes)) for indexName, customSearchAttributes := range indexSearchAttributes { - indexNameTypeMap[indexName] = NameTypeMap{ - customSearchAttributes: customSearchAttributes.GetCustomSearchAttributes(), - } + indexNameTypeMap[indexName] = NewNameTypeMap(customSearchAttributes.GetCustomSearchAttributes()) } return indexNameTypeMap } @@ -42,14 +51,71 @@ func buildIndexNameTypeMap(indexSearchAttributes map[string]*persistencespb.Inde // NewNameTypeMap creates a new NameTypeMap with the given custom search attributes. func NewNameTypeMap(customSearchAttributes map[string]enumspb.IndexedValueType) NameTypeMap { return NameTypeMap{ - customSearchAttributes: customSearchAttributes, + systemSearchAttributes: system, + predefinedSearchAttributes: predefined, + customSearchAttributes: customSearchAttributes, + } +} + +// WithSystemSearchAttributes returns a new NameTypeMap overriding the system search +// attributes with the given input. +// The default value is the sadefs.System() map which contains the internal system search +// attributes. +// If you need to overwrite it while preserving the internal system search attributes, you can +// call as follows: +// +// base := NewNameTypeMap(nil) +// systemSearchAttributes := sadefs.System() +// systemSearchAttributes["your_system_key"] = +// result = base.WithSystemSearchAttributes(systemSearchAttributes) +func (m NameTypeMap) WithSystemSearchAttributes( + systemSearchAttributes map[string]enumspb.IndexedValueType, +) NameTypeMap { + m.systemSearchAttributes = systemSearchAttributes + return m +} + +// WithPredefinedSearchAttributes returns a new NameTypeMap overriding the predefined search +// attributes with the given input. +// The default value is the sadefs.Predefined() map which contains the internal predefined search +// attributes. +// If you need to overwrite it while preserving the internal predefined search attributes, you can +// call as follows: +// +// base := NewNameTypeMap(nil) +// predefinedSearchAttributes := sadefs.Predefined() +// predefinedSearchAttributes["your_predefined_key"] = +// result = base.WithPredefinedSearchAttributes(predefinedSearchAttributes) +func (m NameTypeMap) WithPredefinedSearchAttributes( + predefinedSearchAttributes map[string]enumspb.IndexedValueType, +) NameTypeMap { + m.predefinedSearchAttributes = predefinedSearchAttributes + return m +} + +func (m NameTypeMap) system() map[string]enumspb.IndexedValueType { + if len(m.systemSearchAttributes) == 0 { + return system + } + return m.systemSearchAttributes +} + +func (m NameTypeMap) predefined() map[string]enumspb.IndexedValueType { + if len(m.predefinedSearchAttributes) == 0 { + return predefined } + return m.predefinedSearchAttributes } func (m NameTypeMap) System() map[string]enumspb.IndexedValueType { - allSystem := make(map[string]enumspb.IndexedValueType, len(system)+len(predefined)) - maps.Copy(allSystem, system) - maps.Copy(allSystem, predefined) + systemSearchAttributes := m.system() + predefinedSearchAttributes := m.predefined() + allSystem := make( + map[string]enumspb.IndexedValueType, + len(systemSearchAttributes)+len(predefinedSearchAttributes), + ) + maps.Copy(allSystem, systemSearchAttributes) + maps.Copy(allSystem, predefinedSearchAttributes) return allSystem } @@ -58,9 +124,14 @@ func (m NameTypeMap) Custom() map[string]enumspb.IndexedValueType { } func (m NameTypeMap) All() map[string]enumspb.IndexedValueType { - allSearchAttributes := make(map[string]enumspb.IndexedValueType, len(system)+len(m.customSearchAttributes)+len(predefined)) - maps.Copy(allSearchAttributes, system) - maps.Copy(allSearchAttributes, predefined) + systemSearchAttributes := m.system() + predefinedSearchAttributes := m.predefined() + allSearchAttributes := make( + map[string]enumspb.IndexedValueType, + len(systemSearchAttributes)+len(predefinedSearchAttributes)+len(m.customSearchAttributes), + ) + maps.Copy(allSearchAttributes, systemSearchAttributes) + maps.Copy(allSearchAttributes, predefinedSearchAttributes) maps.Copy(allSearchAttributes, m.customSearchAttributes) return allSearchAttributes } @@ -78,17 +149,18 @@ func (m NameTypeMap) getType(name string, cat category) (enumspb.IndexedValueTyp } } if cat|predefinedCategory == cat { - predefined := sadefs.Predefined() - if t, isPredefined := predefined[name]; isPredefined { + predefinedSearchAttributes := m.predefined() + if t, isPredefined := predefinedSearchAttributes[name]; isPredefined { return t, nil } } if cat|systemCategory == cat { - if t, isSystem := system[name]; isSystem { + systemSearchAttributes := m.system() + if t, isSystem := systemSearchAttributes[name]; isSystem { return t, nil } } - return enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, fmt.Errorf("%w: %s", ErrInvalidName, name) + return enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, fmt.Errorf("%w: %s", sadefs.ErrInvalidName, name) } func (m NameTypeMap) IsDefined(name string) bool { @@ -97,3 +169,21 @@ func (m NameTypeMap) IsDefined(name string) bool { } return false } + +// MergeNameTypeMaps merges two NameTypeMap. The first NameTypeMap is used as base, and the second +// NameTypeMap is added to the first map, ie., in case of conflicts, elements from the second map +// overwrites elements from the first map. +func MergeNameTypeMaps(a NameTypeMap, b NameTypeMap) NameTypeMap { + res := NameTypeMap{ + systemSearchAttributes: make(map[string]enumspb.IndexedValueType), + predefinedSearchAttributes: make(map[string]enumspb.IndexedValueType), + customSearchAttributes: make(map[string]enumspb.IndexedValueType), + } + maps.Copy(res.systemSearchAttributes, a.systemSearchAttributes) + maps.Copy(res.systemSearchAttributes, b.systemSearchAttributes) + maps.Copy(res.predefinedSearchAttributes, a.predefinedSearchAttributes) + maps.Copy(res.predefinedSearchAttributes, b.predefinedSearchAttributes) + maps.Copy(res.customSearchAttributes, a.customSearchAttributes) + maps.Copy(res.customSearchAttributes, b.customSearchAttributes) + return res +} diff --git a/common/searchattribute/name_type_map_test.go b/common/searchattribute/name_type_map_test.go index 92864cfb778..1d036a951c0 100644 --- a/common/searchattribute/name_type_map_test.go +++ b/common/searchattribute/name_type_map_test.go @@ -1,15 +1,15 @@ package searchattribute import ( - "errors" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/common/searchattribute/sadefs" ) func Test_IsValid(t *testing.T) { - assert := assert.New(t) + r := require.New(t) typeMap := NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, "key2": enumspb.INDEXED_VALUE_TYPE_INT, @@ -17,53 +17,644 @@ func Test_IsValid(t *testing.T) { }} isDefined := typeMap.IsDefined("RunId") - assert.True(isDefined) + r.True(isDefined) isDefined = typeMap.IsDefined("TemporalChangeVersion") - assert.True(isDefined) + r.True(isDefined) isDefined = typeMap.IsDefined("key1") - assert.True(isDefined) + r.True(isDefined) isDefined = NameTypeMap{}.IsDefined("key1") - assert.False(isDefined) + r.False(isDefined) isDefined = typeMap.IsDefined("key4") - assert.False(isDefined) + r.False(isDefined) isDefined = typeMap.IsDefined("NamespaceId") - assert.False(isDefined) + r.False(isDefined) } func Test_GetType(t *testing.T) { - assert := assert.New(t) - typeMap := NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ - "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, - "key2": enumspb.INDEXED_VALUE_TYPE_INT, - "key3": enumspb.INDEXED_VALUE_TYPE_BOOL, - }} + t.Run("CustomAndDefaultSystemPredefined", func(t *testing.T) { + r := require.New(t) + typeMap := NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ + "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, + "key2": enumspb.INDEXED_VALUE_TYPE_INT, + "key3": enumspb.INDEXED_VALUE_TYPE_BOOL, + }} + + // Custom attributes resolve. + ivt, err := typeMap.GetType("key1") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + ivt, err = typeMap.GetType("key2") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, ivt) + ivt, err = typeMap.GetType("key3") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_BOOL, ivt) + + // Default system SA resolves. + ivt, err = typeMap.GetType("RunId") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) + + // Default predefined SA resolves. + ivt, err = typeMap.GetType("TemporalChangeVersion") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, ivt) + + // NamespaceId is not a public SA. + ivt, err = typeMap.GetType("NamespaceId") + r.Error(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, ivt) + }) + + t.Run("ErrorCases", func(t *testing.T) { + r := require.New(t) + typeMap := NameTypeMap{customSearchAttributes: map[string]enumspb.IndexedValueType{ + "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, + }} + + // Unknown key on empty map. + ivt, err := NameTypeMap{}.GetType("key1") + r.Error(err) + r.ErrorIs(err, sadefs.ErrInvalidName) + r.Equal(enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, ivt) + + // Unknown key on populated map. + ivt, err = typeMap.GetType("nonexistent") + r.Error(err) + r.ErrorIs(err, sadefs.ErrInvalidName) + r.Equal(enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, ivt) + }) + + t.Run("OverriddenSystemAndPredefined", func(t *testing.T) { + r := require.New(t) + typeMap := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "MySys": enumspb.INDEXED_VALUE_TYPE_INT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "MyPred": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "MyCustom": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + } + + // All three categories resolve. + ivt, err := typeMap.GetType("MySys") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, ivt) + + ivt, err = typeMap.GetType("MyPred") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + + ivt, err = typeMap.GetType("MyCustom") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + + // Default system/predefined SAs are no longer found after override. + _, err = typeMap.GetType("RunId") + r.ErrorIs(err, sadefs.ErrInvalidName) + _, err = typeMap.GetType("TemporalChangeVersion") + r.ErrorIs(err, sadefs.ErrInvalidName) + }) + + t.Run("CustomTakesPriorityOverPredefinedAndSystem", func(t *testing.T) { + r := require.New(t) + // Same key in all three categories — custom should win. + typeMap := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_INT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + } + ivt, err := typeMap.GetType("Shared") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + }) + + t.Run("PredefinedTakesPriorityOverSystem", func(t *testing.T) { + r := require.New(t) + // Same key in system and predefined — predefined should win. + typeMap := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_INT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + } + ivt, err := typeMap.GetType("Shared") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + }) +} + +func Test_System(t *testing.T) { + t.Run("DefaultSystemAndPredefined", func(t *testing.T) { + r := require.New(t) + typeMap := NewNameTypeMap(map[string]enumspb.IndexedValueType{ + "MyCustom": enumspb.INDEXED_VALUE_TYPE_TEXT, + }) + + sys := typeMap.System() + // Contains default system SAs. + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, sys["RunId"]) + // Contains default predefined SAs. + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, sys["TemporalChangeVersion"]) + // Does not contain custom SAs. + _, hasCustom := sys["MyCustom"] + r.False(hasCustom) + }) + + t.Run("OverriddenSystemAndPredefined", func(t *testing.T) { + r := require.New(t) + typeMap := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "MySys": enumspb.INDEXED_VALUE_TYPE_INT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "MyPred": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "MyCustom": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + } + + sys := typeMap.System() + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, sys["MySys"]) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, sys["MyPred"]) + r.Len(sys, 2) + // Custom SAs excluded. + _, hasCustom := sys["MyCustom"] + r.False(hasCustom) + }) + + t.Run("PredefinedOverridesSystemOnConflict", func(t *testing.T) { + r := require.New(t) + // System() copies system first, then predefined — predefined wins on conflict. + typeMap := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_INT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + } + sys := typeMap.System() + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, sys["Shared"]) + }) + + t.Run("EmptyMap", func(t *testing.T) { + r := require.New(t) + // Zero-value NameTypeMap falls back to global defaults. + sys := NameTypeMap{}.System() + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, sys["RunId"]) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, sys["TemporalChangeVersion"]) + }) +} + +func Test_Custom(t *testing.T) { + t.Run("ReturnsCustomOnly", func(t *testing.T) { + r := require.New(t) + custom := map[string]enumspb.IndexedValueType{ + "key1": enumspb.INDEXED_VALUE_TYPE_TEXT, + "key2": enumspb.INDEXED_VALUE_TYPE_INT, + } + typeMap := NewNameTypeMap(custom) + r.Equal(custom, typeMap.Custom()) + }) + + t.Run("NilWhenNoCustom", func(t *testing.T) { + r := require.New(t) + typeMap := NewNameTypeMap(nil) + r.Nil(typeMap.Custom()) + }) + + t.Run("EmptyMap", func(t *testing.T) { + r := require.New(t) + r.Nil(NameTypeMap{}.Custom()) + }) +} + +func Test_All(t *testing.T) { + t.Run("DefaultSystemAndPredefined", func(t *testing.T) { + r := require.New(t) + custom := map[string]enumspb.IndexedValueType{ + "MyCustom": enumspb.INDEXED_VALUE_TYPE_TEXT, + } + typeMap := NewNameTypeMap(custom) + + all := typeMap.All() + // Contains default system SAs. + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, all["RunId"]) + // Contains default predefined SAs. + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, all["TemporalChangeVersion"]) + // Contains custom SAs. + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, all["MyCustom"]) + }) + + t.Run("OverriddenSystemAndPredefined", func(t *testing.T) { + r := require.New(t) + typeMap := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "MySys": enumspb.INDEXED_VALUE_TYPE_INT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "MyPred": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "MyCustom": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + } + + all := typeMap.All() + r.Len(all, 3) + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, all["MySys"]) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, all["MyPred"]) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, all["MyCustom"]) + + // Default SAs are not present. + _, hasRunID := all["RunId"] + r.False(hasRunID) + }) + + t.Run("CustomOverridesPredefinedOverridesSystem", func(t *testing.T) { + r := require.New(t) + // All() copies system, then predefined, then custom — last write wins. + typeMap := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SysOnly": enumspb.INDEXED_VALUE_TYPE_INT, + "SysPred": enumspb.INDEXED_VALUE_TYPE_INT, + "SysCustom": enumspb.INDEXED_VALUE_TYPE_INT, + "AllThree": enumspb.INDEXED_VALUE_TYPE_INT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "PredOnly": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + "SysPred": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + "PredCustom": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + "AllThree": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "CustomOnly": enumspb.INDEXED_VALUE_TYPE_TEXT, + "SysCustom": enumspb.INDEXED_VALUE_TYPE_TEXT, + "PredCustom": enumspb.INDEXED_VALUE_TYPE_TEXT, + "AllThree": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + } + + all := typeMap.All() + // Unique keys resolve to their own type. + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, all["SysOnly"]) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, all["PredOnly"]) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, all["CustomOnly"]) + // Predefined overwrites system. + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, all["SysPred"]) + // Custom overwrites system. + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, all["SysCustom"]) + // Custom overwrites predefined. + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, all["PredCustom"]) + // Custom overwrites all. + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, all["AllThree"]) + r.Len(all, 7) + }) + + t.Run("EmptyMap", func(t *testing.T) { + r := require.New(t) + // Zero-value NameTypeMap falls back to global defaults (no custom). + all := NameTypeMap{}.All() + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, all["RunId"]) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, all["TemporalChangeVersion"]) + }) +} + +func Test_WithPredefinedSearchAttributes(t *testing.T) { + r := require.New(t) + + customSA := map[string]enumspb.IndexedValueType{ + "CustomKey": enumspb.INDEXED_VALUE_TYPE_TEXT, + } + base := NewNameTypeMap(customSA) + + // Baseline: default predefined includes TemporalChangeVersion from sadefs.Predefined(). + r.True(base.IsDefined("TemporalChangeVersion")) + ivt, err := base.GetType("TemporalChangeVersion") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, ivt) + + // Custom attributes are preserved. + ivt, err = base.GetType("CustomKey") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + + // Override predefined with a custom set that does NOT include TemporalChangeVersion. + overriddenPredefined := map[string]enumspb.IndexedValueType{ + "MyPredefined": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + } + overridden := base.WithPredefinedSearchAttributes(overriddenPredefined) + + // New predefined attribute is resolved. + ivt, err = overridden.GetType("MyPredefined") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + + // TemporalChangeVersion is no longer found via predefined (only system SAs remain). + r.False(overridden.IsDefined("TemporalChangeVersion")) + + // Custom attributes are still preserved after override. + ivt, err = overridden.GetType("CustomKey") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + + // System search attributes (e.g. RunId) are still accessible. + ivt, err = overridden.GetType("RunId") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) + + // Original base map is not mutated. + r.True(base.IsDefined("TemporalChangeVersion")) + r.False(base.IsDefined("MyPredefined")) + + // Chaining: override system first, then override predefined. + // WithPredefinedSearchAttributes should preserve the system override. + overriddenSystem := map[string]enumspb.IndexedValueType{ + "MySystem": enumspb.INDEXED_VALUE_TYPE_INT, + } + chained := base.WithSystemSearchAttributes(overriddenSystem).WithPredefinedSearchAttributes(overriddenPredefined) + + // Overridden system attribute is preserved through the chain. + ivt, err = chained.GetType("MySystem") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, ivt) + + // Default system SA (RunId) is no longer found since system was overridden. + r.False(chained.IsDefined("RunId")) + + // Overridden predefined attribute is present. + ivt, err = chained.GetType("MyPredefined") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + + // Default predefined SA (TemporalChangeVersion) is no longer found since predefined was overridden. + r.False(chained.IsDefined("TemporalChangeVersion")) + + // Custom attributes are still preserved. + ivt, err = chained.GetType("CustomKey") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) +} + +func Test_WithSystemSearchAttributes(t *testing.T) { + r := require.New(t) + + customSA := map[string]enumspb.IndexedValueType{ + "CustomKey": enumspb.INDEXED_VALUE_TYPE_TEXT, + } + base := NewNameTypeMap(customSA) + + // Baseline: default system includes RunId from sadefs.System(). + r.True(base.IsDefined("RunId")) + ivt, err := base.GetType("RunId") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) + + // Custom attributes are preserved. + ivt, err = base.GetType("CustomKey") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + + // Override system with a custom set that does NOT include RunId. + overriddenSystem := map[string]enumspb.IndexedValueType{ + "MySystem": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + } + overridden := base.WithSystemSearchAttributes(overriddenSystem) + + // New system attribute is resolved. + ivt, err = overridden.GetType("MySystem") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + + // RunId is no longer found via system (only predefined and custom remain). + r.False(overridden.IsDefined("RunId")) + + // Custom attributes are still preserved after override. + ivt, err = overridden.GetType("CustomKey") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + + // Predefined search attributes (e.g. TemporalChangeVersion) are still accessible. + ivt, err = overridden.GetType("TemporalChangeVersion") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, ivt) + + // Original base map is not mutated. + r.True(base.IsDefined("RunId")) + r.False(base.IsDefined("MySystem")) +} + +func Test_MergeNameTypeMaps(t *testing.T) { + t.Run("DisjointMaps", func(t *testing.T) { + r := require.New(t) + a := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SysA": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "PredA": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "CustomA": enumspb.INDEXED_VALUE_TYPE_INT, + }, + } + b := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SysB": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "PredB": enumspb.INDEXED_VALUE_TYPE_BOOL, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "CustomB": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + } + + merged := MergeNameTypeMaps(a, b) + + ivt, err := merged.GetType("SysA") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) + + ivt, err = merged.GetType("SysB") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + + ivt, err = merged.GetType("PredA") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) + + ivt, err = merged.GetType("PredB") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_BOOL, ivt) + + ivt, err = merged.GetType("CustomA") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, ivt) + + ivt, err = merged.GetType("CustomB") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + }) + + t.Run("SecondOverwritesFirst", func(t *testing.T) { + r := require.New(t) + a := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SharedSys": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "SharedCustom": enumspb.INDEXED_VALUE_TYPE_INT, + }, + } + b := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SharedSys": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "Shared": enumspb.INDEXED_VALUE_TYPE_BOOL, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "SharedCustom": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + } + + merged := MergeNameTypeMaps(a, b) + + // b's values win on conflict. + ivt, err := merged.GetType("SharedSys") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + + ivt, err = merged.GetType("Shared") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_BOOL, ivt) + + ivt, err = merged.GetType("SharedCustom") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + }) + + t.Run("DoesNotMutateInputs", func(t *testing.T) { + r := require.New(t) + a := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SysA": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "PredA": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "CustomA": enumspb.INDEXED_VALUE_TYPE_INT, + }, + } + b := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SysB": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "PredB": enumspb.INDEXED_VALUE_TYPE_BOOL, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "CustomB": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + } + + _ = MergeNameTypeMaps(a, b) + + // a should not contain b's entries. + r.Len(a.systemSearchAttributes, 1) + r.Len(a.predefinedSearchAttributes, 1) + r.Len(a.customSearchAttributes, 1) + _, hasSysB := a.systemSearchAttributes["SysB"] + r.False(hasSysB) + _, hasPredB := a.predefinedSearchAttributes["PredB"] + r.False(hasPredB) + _, hasCustomB := a.customSearchAttributes["CustomB"] + r.False(hasCustomB) + }) + + t.Run("EmptyFirstMap", func(t *testing.T) { + r := require.New(t) + + b := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SysB": enumspb.INDEXED_VALUE_TYPE_TEXT, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "PredB": enumspb.INDEXED_VALUE_TYPE_BOOL, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "CustomB": enumspb.INDEXED_VALUE_TYPE_DOUBLE, + }, + } + empty := NameTypeMap{} + + // Merge with empty first map preserves second. + merged := MergeNameTypeMaps(empty, b) + ivt, err := merged.GetType("SysB") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) + ivt, err = merged.GetType("PredB") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_BOOL, ivt) + ivt, err = merged.GetType("CustomB") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_DOUBLE, ivt) + }) + + t.Run("EmptySecondMap", func(t *testing.T) { + r := require.New(t) + + a := NameTypeMap{ + systemSearchAttributes: map[string]enumspb.IndexedValueType{ + "SysA": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + predefinedSearchAttributes: map[string]enumspb.IndexedValueType{ + "PredA": enumspb.INDEXED_VALUE_TYPE_KEYWORD, + }, + customSearchAttributes: map[string]enumspb.IndexedValueType{ + "CustomA": enumspb.INDEXED_VALUE_TYPE_INT, + }, + } + empty := NameTypeMap{} + + // Merge with empty second map preserves first. + merged := MergeNameTypeMaps(a, empty) + ivt, err := merged.GetType("SysA") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) + ivt, err = merged.GetType("PredA") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) + ivt, err = merged.GetType("CustomA") + r.NoError(err) + r.Equal(enumspb.INDEXED_VALUE_TYPE_INT, ivt) + }) - ivt, err := typeMap.GetType("key1") - assert.NoError(err) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_TEXT, ivt) - ivt, err = typeMap.GetType("key2") - assert.NoError(err) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_INT, ivt) - ivt, err = typeMap.GetType("key3") - assert.NoError(err) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_BOOL, ivt) - ivt, err = typeMap.GetType("RunId") - assert.NoError(err) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD, ivt) - ivt, err = typeMap.GetType("TemporalChangeVersion") - assert.NoError(err) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, ivt) - ivt, err = typeMap.GetType("NamespaceId") - assert.Error(err) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, ivt) - - ivt, err = NameTypeMap{}.GetType("key1") - assert.Error(err) - assert.True(errors.Is(err, ErrInvalidName)) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, ivt) - ivt, err = typeMap.GetType("key4") - assert.Error(err) - assert.True(errors.Is(err, ErrInvalidName)) - assert.Equal(enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, ivt) + t.Run("BothEmpty", func(t *testing.T) { + r := require.New(t) + empty := NameTypeMap{} + merged := MergeNameTypeMaps(empty, empty) + r.Empty(merged.systemSearchAttributes) + r.Empty(merged.predefinedSearchAttributes) + r.Empty(merged.customSearchAttributes) + }) } diff --git a/common/searchattribute/encode_value.go b/common/searchattribute/sadefs/encode_value.go similarity index 86% rename from common/searchattribute/encode_value.go rename to common/searchattribute/sadefs/encode_value.go index 262f28d7c82..0e52609244c 100644 --- a/common/searchattribute/encode_value.go +++ b/common/searchattribute/sadefs/encode_value.go @@ -1,7 +1,6 @@ -package searchattribute +package sadefs import ( - "errors" "fmt" "time" "unicode/utf8" @@ -9,22 +8,30 @@ import ( commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/server/common/payload" - "go.temporal.io/server/common/searchattribute/sadefs" ) -var ErrInvalidString = errors.New("SearchAttribute value is not a valid UTF-8 string") - // EncodeValue encodes search attribute value and IndexedValueType to Payload. -func EncodeValue(val interface{}, t enumspb.IndexedValueType) (*commonpb.Payload, error) { +func EncodeValue(val any, t enumspb.IndexedValueType) (*commonpb.Payload, error) { valPayload, err := payload.Encode(val) if err != nil { return nil, err } - sadefs.SetMetadataType(valPayload, t) + SetMetadataType(valPayload, t) return valPayload, nil } +// MustEncodeValue encodes search attribute value and IndexedValueType to Payload. +// Panics if it fails to encode. +func MustEncodeValue(val any, t enumspb.IndexedValueType) *commonpb.Payload { + valPayload, err := EncodeValue(val, t) + if err != nil { + // nolint:forbidigo + panic(err) + } + return valPayload +} + // DecodeValue decodes search attribute value from Payload using (in order): // 1. passed type t. // 2. type from MetadataType field, if t is not specified. @@ -35,11 +42,10 @@ func DecodeValue( allowList bool, ) (any, error) { if t == enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED { - var err error - t, err = enumspb.IndexedValueTypeFromString(string(value.Metadata[MetadataType])) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrInvalidType, t) - } + t = GetMetadataType(value) + } + if t == enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED { + return nil, fmt.Errorf("%w: %v", ErrInvalidType, t) } switch t { diff --git a/common/searchattribute/encode_value_test.go b/common/searchattribute/sadefs/encode_value_test.go similarity index 99% rename from common/searchattribute/encode_value_test.go rename to common/searchattribute/sadefs/encode_value_test.go index 78d1e82d00e..f6a0838f4dd 100644 --- a/common/searchattribute/encode_value_test.go +++ b/common/searchattribute/sadefs/encode_value_test.go @@ -1,4 +1,4 @@ -package searchattribute +package sadefs import ( "errors" diff --git a/common/searchattribute/sadefs/errors.go b/common/searchattribute/sadefs/errors.go new file mode 100644 index 00000000000..a9839e459b8 --- /dev/null +++ b/common/searchattribute/sadefs/errors.go @@ -0,0 +1,9 @@ +package sadefs + +import "errors" + +var ( + ErrInvalidName = errors.New("invalid search attribute name") + ErrInvalidType = errors.New("invalid search attribute type") + ErrInvalidString = errors.New("SearchAttribute value is not a valid UTF-8 string") +) diff --git a/common/searchattribute/sadefs/util.go b/common/searchattribute/sadefs/util.go index 2f624d83382..354c7bf611a 100644 --- a/common/searchattribute/sadefs/util.go +++ b/common/searchattribute/sadefs/util.go @@ -11,6 +11,14 @@ const ( MetadataType = "type" ) +func GetMetadataType(p *commonpb.Payload) enumspb.IndexedValueType { + t, err := enumspb.IndexedValueTypeFromString(string(p.Metadata[MetadataType])) + if err != nil { + return enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED + } + return t +} + func SetMetadataType(p *commonpb.Payload, t enumspb.IndexedValueType) { if t == enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED { return @@ -21,5 +29,5 @@ func SetMetadataType(p *commonpb.Payload, t enumspb.IndexedValueType) { // nolint: forbidigo panic(fmt.Sprintf("unknown index value type %v", t)) } - p.Metadata[MetadataType] = []byte(enumspb.IndexedValueType(t).String()) + p.Metadata[MetadataType] = []byte(t.String()) } diff --git a/common/searchattribute/search_attirbute.go b/common/searchattribute/search_attirbute.go index b12e80e1e53..2a906687f9b 100644 --- a/common/searchattribute/search_attirbute.go +++ b/common/searchattribute/search_attirbute.go @@ -4,7 +4,6 @@ package searchattribute import ( "context" - "errors" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -26,11 +25,6 @@ type ( } ) -var ( - ErrInvalidName = errors.New("invalid search attribute name") - ErrInvalidType = errors.New("invalid search attribute type") -) - // ApplyTypeMap set type for all valid search attributes which don't have it. // It doesn't do any validation and just skip invalid or already set search attributes. func ApplyTypeMap(searchAttributes *commonpb.SearchAttributes, typeMap NameTypeMap) { diff --git a/common/searchattribute/stringify.go b/common/searchattribute/stringify.go index ce1af251ee1..32a92063c5f 100644 --- a/common/searchattribute/stringify.go +++ b/common/searchattribute/stringify.go @@ -33,7 +33,7 @@ func Stringify(searchAttributes *commonpb.SearchAttributes, typeMap *NameTypeMap if typeMap != nil { saType, _ = typeMap.getType(saName, customCategory|predefinedCategory) } - saValue, err := DecodeValue(saPayload, saType, true) + saValue, err := sadefs.DecodeValue(saPayload, saType, true) if err != nil { // If DecodeValue failed, save error and use raw JSON from Data field. result[saName] = string(saPayload.GetData()) @@ -105,11 +105,11 @@ func Parse(searchAttributesStr map[string]string, typeMap *NameTypeMap) (*common } func parseValueOrArray(valStr string, t enumspb.IndexedValueType) (*commonpb.Payload, error) { - var val interface{} + var val any - if isJsonArray(valStr) { + if isJSONArray(valStr) { var err error - val, err = parseJsonArray(valStr, t) + val, err = parseJSONArray(valStr, t) if err != nil { return nil, err } @@ -130,8 +130,8 @@ func parseValueOrArray(valStr string, t enumspb.IndexedValueType) (*commonpb.Pay return valPayload, nil } -func parseValueTyped(valStr string, t enumspb.IndexedValueType) (interface{}, error) { - var val interface{} +func parseValueTyped(valStr string, t enumspb.IndexedValueType) (any, error) { + var val any var err error switch t { @@ -150,22 +150,22 @@ func parseValueTyped(valStr string, t enumspb.IndexedValueType) (interface{}, er case enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED: val = parseValueUnspecified(valStr) default: - err = fmt.Errorf("%w: %v", ErrInvalidType, t) + err = fmt.Errorf("%w: %v", sadefs.ErrInvalidType, t) } return val, err } -func parseValueUnspecified(valStr string) interface{} { - var val interface{} +func parseValueUnspecified(valStr string) any { + var val any var err error if val, err = strconv.ParseInt(valStr, 10, 64); err == nil { } else if val, err = strconv.ParseBool(valStr); err == nil { } else if val, err = strconv.ParseFloat(valStr, 64); err == nil { } else if val, err = time.Parse(time.RFC3339Nano, valStr); err == nil { - } else if isJsonArray(valStr) { - arr, err := parseJsonArray(valStr, enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED) + } else if isJSONArray(valStr) { + arr, err := parseJSONArray(valStr, enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED) if err != nil { val = valStr } else { @@ -178,12 +178,12 @@ func parseValueUnspecified(valStr string) interface{} { return val } -func isJsonArray(str string) bool { +func isJSONArray(str string) bool { str = strings.TrimSpace(str) return strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]") } -func parseJsonArray(str string, t enumspb.IndexedValueType) (interface{}, error) { +func parseJSONArray(str string, t enumspb.IndexedValueType) (any, error) { switch t { case enumspb.INDEXED_VALUE_TYPE_TEXT, enumspb.INDEXED_VALUE_TYPE_KEYWORD, @@ -208,10 +208,10 @@ func parseJsonArray(str string, t enumspb.IndexedValueType) (interface{}, error) err := json.Unmarshal([]byte(str), &result) return result, err case enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED: - var result []interface{} + var result []any err := json.Unmarshal([]byte(str), &result) return result, err default: - return nil, fmt.Errorf("%w: %v", ErrInvalidType, t) + return nil, fmt.Errorf("%w: %v", sadefs.ErrInvalidType, t) } } diff --git a/common/searchattribute/stringify_test.go b/common/searchattribute/stringify_test.go index 1148f86dbf4..e4ac2b5f2ec 100644 --- a/common/searchattribute/stringify_test.go +++ b/common/searchattribute/stringify_test.go @@ -1,21 +1,25 @@ package searchattribute import ( - "errors" "testing" "time" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/common/searchattribute/sadefs" ) type StringifySuite struct { suite.Suite + *require.Assertions } func TestStringifySuite(t *testing.T) { - s := &StringifySuite{} + s := &StringifySuite{ + Assertions: require.New(t), + } suite.Run(t, s) } @@ -37,7 +41,7 @@ func (s *StringifySuite) Test_Stringify() { }, } - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": "val1", "key2": 2, "key3": true, @@ -66,7 +70,7 @@ func (s *StringifySuite) Test_Stringify() { // Even w/o typeMap error is returned but string values are set with raw JSON from GetData(). saStr, err = Stringify(sa, nil) s.Error(err) - s.True(errors.Is(err, ErrInvalidType)) + s.ErrorIs(err, sadefs.ErrInvalidType) s.Len(saStr, 3) s.Equal(`"val1"`, saStr["key1"]) s.Equal("2", saStr["key2"]) @@ -82,7 +86,7 @@ func (s *StringifySuite) Test_Stringify_Array() { }, } - sa, err := Encode(map[string]interface{}{ + sa, err := Encode(map[string]any{ "key1": []string{"val1", "val2"}, "key2": []int64{2, 3, 4}, "key3": []bool{true, false, true}, @@ -111,7 +115,7 @@ func (s *StringifySuite) Test_Stringify_Array() { // Even w/o typeMap error is returned but string values are set with raw JSON from GetData(). saStr, err = Stringify(sa, nil) s.Error(err) - s.True(errors.Is(err, ErrInvalidType)) + s.ErrorIs(err, sadefs.ErrInvalidType) s.Len(saStr, 3) s.Equal(`["val1","val2"]`, saStr["key1"]) s.Equal("[2,3,4]", saStr["key2"]) @@ -230,7 +234,7 @@ func (s *StringifySuite) Test_parseValueOrArray() { } func (s *StringifySuite) Test_parseValueTyped() { - var res interface{} + var res any var err error // int @@ -283,7 +287,7 @@ func (s *StringifySuite) Test_parseValueTyped() { } func (s *StringifySuite) Test_parseValueUnspecified() { - var res interface{} + var res any // int res = parseValueUnspecified("1") @@ -305,32 +309,32 @@ func (s *StringifySuite) Test_parseValueUnspecified() { // array res = parseValueUnspecified(`["a", "b", "c"]`) - s.Equal([]interface{}{"a", "b", "c"}, res) + s.Equal([]any{"a", "b", "c"}, res) // string res = parseValueUnspecified("test string") s.Equal("test string", res) } -func (s *StringifySuite) Test_isJsonArray() { - s.True(isJsonArray("[1,2,3]")) - s.True(isJsonArray(" [1,2,3] ")) - s.True(isJsonArray(` ["1","2","3"] `)) - s.True(isJsonArray("[]")) - s.False(isJsonArray("[")) - s.False(isJsonArray("]")) - s.False(isJsonArray("qwe")) - s.False(isJsonArray("123")) +func (s *StringifySuite) Test_isJSONArray() { + s.True(isJSONArray("[1,2,3]")) + s.True(isJSONArray(" [1,2,3] ")) + s.True(isJSONArray(` ["1","2","3"] `)) + s.True(isJSONArray("[]")) + s.False(isJSONArray("[")) + s.False(isJSONArray("]")) + s.False(isJSONArray("qwe")) + s.False(isJSONArray("123")) } -func (s *StringifySuite) Test_parseJsonArray() { +func (s *StringifySuite) Test_parseJSONArray() { t1, _ := time.Parse(time.RFC3339Nano, "2019-06-07T16:16:34-08:00") t2, _ := time.Parse(time.RFC3339Nano, "2019-06-07T17:16:34-08:00") testCases := []struct { name string indexedValueType enumspb.IndexedValueType input string - expected interface{} + expected any }{ { name: "string", @@ -366,12 +370,12 @@ func (s *StringifySuite) Test_parseJsonArray() { name: "unspecified", indexedValueType: enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, input: `["a", "b", "c"]`, - expected: []interface{}{"a", "b", "c"}, + expected: []any{"a", "b", "c"}, }, } for _, testCase := range testCases { s.Run(testCase.name, func() { - res, err := parseJsonArray(testCase.input, testCase.indexedValueType) + res, err := parseJSONArray(testCase.input, testCase.indexedValueType) s.NoError(err) s.Equal(testCase.expected, res) }) @@ -400,7 +404,7 @@ func (s *StringifySuite) Test_parseJsonArray() { }, } for _, testCase := range testCases2 { - res, err := parseJsonArray(testCase.input, testCase.indexedValueType) + res, err := parseJSONArray(testCase.input, testCase.indexedValueType) s.NotNil(err) s.Nil(res) } diff --git a/common/searchattribute/system_provider.go b/common/searchattribute/system_provider.go index 482af9c3267..dbb914e8e29 100644 --- a/common/searchattribute/system_provider.go +++ b/common/searchattribute/system_provider.go @@ -9,5 +9,5 @@ func NewSystemProvider() *SystemProvider { } func (s *SystemProvider) GetSearchAttributes(_ string, _ bool) (NameTypeMap, error) { - return NameTypeMap{}, nil + return NewNameTypeMap(nil), nil } diff --git a/common/searchattribute/test_provider.go b/common/searchattribute/test_provider.go index ee7cd7ddc33..dc2bf1b76c7 100644 --- a/common/searchattribute/test_provider.go +++ b/common/searchattribute/test_provider.go @@ -7,6 +7,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" + "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/searchattribute/sadefs" ) @@ -15,6 +16,10 @@ type ( es bool } + testMapperProvider struct { + mapper Mapper + } + TestMapper struct { Namespace string WithCustomScheduleID bool @@ -23,6 +28,7 @@ type ( var _ Provider = (*TestProvider)(nil) var _ Mapper = (*TestMapper)(nil) +var _ MapperProvider = (*testMapperProvider)(nil) var ( esCustomSearchAttributes = map[string]enumspb.IndexedValueType{ @@ -53,16 +59,16 @@ var ( func TestNameTypeMap() NameTypeMap { csa := maps.Clone(sqlCustomSearchAttributes) - return NameTypeMap{ - customSearchAttributes: csa, - } + return NewNameTypeMap(csa) } func TestEsNameTypeMap() NameTypeMap { csa := maps.Clone(esCustomSearchAttributes) - return NameTypeMap{ - customSearchAttributes: csa, - } + return NewNameTypeMap(csa) +} + +func TestSearchAttributesToRegister() map[string]enumspb.IndexedValueType { + return maps.Clone(esCustomSearchAttributes) } func TestEsNameTypeMapWithScheduleID() NameTypeMap { @@ -138,14 +144,14 @@ func (t *TestMapper) GetFieldName(alias string, namespace string) (string, error return "", serviceerror.NewInvalidArgument("unknown namespace") } -func NewNoopMapper() Mapper { - return &noopMapper{} +func NewTestMapperProvider(customMapper Mapper) MapperProvider { + return &testMapperProvider{mapper: customMapper} } -func NewTestMapperProvider(customMapper Mapper) MapperProvider { - return NewMapperProvider(customMapper, nil, NewTestProvider(), false) +func (p *testMapperProvider) GetMapper(namespace.Name) (Mapper, error) { + return p.mapper, nil } func NewNameTypeMapStub(attributes map[string]enumspb.IndexedValueType) NameTypeMap { - return NameTypeMap{customSearchAttributes: attributes} + return NewNameTypeMap(attributes) } diff --git a/common/searchattribute/validator.go b/common/searchattribute/validator.go index 1b0f93162c8..5f4ad06f2ec 100644 --- a/common/searchattribute/validator.go +++ b/common/searchattribute/validator.go @@ -96,7 +96,7 @@ func (v *Validator) Validate(searchAttributes *commonpb.SearchAttributes, namesp saType, err := saTypeMap.getType(saFieldName, customCategory|predefinedCategory) if err != nil { - if errors.Is(err, ErrInvalidName) { + if errors.Is(err, sadefs.ErrInvalidName) { return v.validationError( "search attribute %s is not defined", saFieldName, @@ -120,9 +120,9 @@ func (v *Validator) Validate(searchAttributes *commonpb.SearchAttributes, namesp ) } } - saValue, err := DecodeValue(saPayload, saType, v.allowList(namespace)) + saValue, err := sadefs.DecodeValue(saPayload, saType, v.allowList(namespace)) if err != nil { - var invalidValue interface{} + var invalidValue any if err = payload.Decode(saPayload, &invalidValue); err != nil { invalidValue = fmt.Sprintf("value from <%s>", saPayload.String()) } diff --git a/common/serviceerror/convert.go b/common/serviceerror/convert.go index 0f223b23a92..f848af009c1 100644 --- a/common/serviceerror/convert.go +++ b/common/serviceerror/convert.go @@ -54,7 +54,7 @@ func FromStatus(st *status.Status) error { return serviceerror.FromStatus(st) } -func extractErrorDetails(st *status.Status) interface{} { +func extractErrorDetails(st *status.Status) any { details := st.Details() if len(details) > 0 { return details[0] diff --git a/common/sqlquery/query.go b/common/sqlquery/query.go index f42330e64bf..3fbd2d01365 100644 --- a/common/sqlquery/query.go +++ b/common/sqlquery/query.go @@ -48,7 +48,7 @@ func ExtractIntValue(s string) (int, error) { } // ParseValue returns a string, int64 or float64 if the parsing succeeds. -func ParseValue(sqlValue string) (interface{}, error) { +func ParseValue(sqlValue string) (any, error) { if sqlValue == "" { return "", nil } diff --git a/common/taskqueue/stats.go b/common/taskqueue/stats.go new file mode 100644 index 00000000000..823778e06b4 --- /dev/null +++ b/common/taskqueue/stats.go @@ -0,0 +1,43 @@ +package taskqueue + +import ( + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +// MergeStats merges from into into. Mutates into. +func MergeStats(into, from *taskqueuepb.TaskQueueStats) { + if from == nil { + return + } + into.ApproximateBacklogCount += from.ApproximateBacklogCount + into.ApproximateBacklogAge = oldestBacklogAge(into.ApproximateBacklogAge, from.ApproximateBacklogAge) + into.TasksAddRate += from.TasksAddRate + into.TasksDispatchRate += from.TasksDispatchRate +} + +// DedupPollers removes duplicate pollers by identity. +func DedupPollers(pollerInfos []*taskqueuepb.PollerInfo) []*taskqueuepb.PollerInfo { + allKeys := make(map[string]bool) + var list []*taskqueuepb.PollerInfo + for _, item := range pollerInfos { + if _, value := allKeys[item.GetIdentity()]; !value { + allKeys[item.GetIdentity()] = true + list = append(list, item) + } + } + return list +} + +func oldestBacklogAge(left, right *durationpb.Duration) *durationpb.Duration { + if left == nil { + left = durationpb.New(0) + } + if right == nil { + right = durationpb.New(0) + } + if left.AsDuration() > right.AsDuration() { + return left + } + return right +} diff --git a/common/taskqueue/stats_test.go b/common/taskqueue/stats_test.go new file mode 100644 index 00000000000..9f8d52ff7aa --- /dev/null +++ b/common/taskqueue/stats_test.go @@ -0,0 +1,111 @@ +package taskqueue + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +func TestMergeStats(t *testing.T) { + into := &taskqueuepb.TaskQueueStats{ + ApproximateBacklogCount: 10, + ApproximateBacklogAge: durationpb.New(100 * time.Second), + TasksAddRate: 5, + TasksDispatchRate: 3, + } + from := &taskqueuepb.TaskQueueStats{ + ApproximateBacklogCount: 20, + ApproximateBacklogAge: durationpb.New(50 * time.Second), + TasksAddRate: 2, + TasksDispatchRate: 1, + } + + MergeStats(into, from) + + require.Equal(t, int64(30), into.ApproximateBacklogCount) + require.Equal(t, 100*time.Second, into.ApproximateBacklogAge.AsDuration()) + require.InDelta(t, 7, into.TasksAddRate, 1e-9) + require.InDelta(t, 4, into.TasksDispatchRate, 1e-9) +} + +func TestMergeStats_NilFrom(t *testing.T) { + into := &taskqueuepb.TaskQueueStats{ + ApproximateBacklogCount: 10, + ApproximateBacklogAge: durationpb.New(100 * time.Second), + TasksAddRate: 5, + TasksDispatchRate: 3, + } + + MergeStats(into, nil) + + require.Equal(t, int64(10), into.ApproximateBacklogCount) + require.Equal(t, 100*time.Second, into.ApproximateBacklogAge.AsDuration()) + require.InDelta(t, 5, into.TasksAddRate, 1e-9) + require.InDelta(t, 3, into.TasksDispatchRate, 1e-9) +} + +func TestMergeStats_NilBacklogAges(t *testing.T) { + into := &taskqueuepb.TaskQueueStats{ + ApproximateBacklogCount: 5, + } + from := &taskqueuepb.TaskQueueStats{ + ApproximateBacklogCount: 3, + ApproximateBacklogAge: durationpb.New(10 * time.Second), + } + + MergeStats(into, from) + + require.Equal(t, int64(8), into.ApproximateBacklogCount) + require.Equal(t, 10*time.Second, into.ApproximateBacklogAge.AsDuration()) +} + +func TestMergeStats_RightOlderAge(t *testing.T) { + into := &taskqueuepb.TaskQueueStats{ + ApproximateBacklogAge: durationpb.New(10 * time.Second), + } + from := &taskqueuepb.TaskQueueStats{ + ApproximateBacklogAge: durationpb.New(50 * time.Second), + } + + MergeStats(into, from) + + require.Equal(t, 50*time.Second, into.ApproximateBacklogAge.AsDuration()) +} + +func TestDedupPollers(t *testing.T) { + pollers := []*taskqueuepb.PollerInfo{ + {Identity: "worker-1"}, + {Identity: "worker-2"}, + {Identity: "worker-1"}, + {Identity: "worker-3"}, + } + + result := DedupPollers(pollers) + + require.Len(t, result, 3) + idents := make(map[string]bool) + for _, p := range result { + idents[p.GetIdentity()] = true + } + require.True(t, idents["worker-1"]) + require.True(t, idents["worker-2"]) + require.True(t, idents["worker-3"]) +} + +func TestDedupPollers_Empty(t *testing.T) { + result := DedupPollers(nil) + require.Empty(t, result) +} + +func TestDedupPollers_NoDuplicates(t *testing.T) { + pollers := []*taskqueuepb.PollerInfo{ + {Identity: "worker-1"}, + {Identity: "worker-2"}, + } + + result := DedupPollers(pollers) + require.Len(t, result, 2) +} diff --git a/common/tasks/execution_aware_scheduler.go b/common/tasks/execution_aware_scheduler.go new file mode 100644 index 00000000000..a8f397aa736 --- /dev/null +++ b/common/tasks/execution_aware_scheduler.go @@ -0,0 +1,129 @@ +package tasks + +import ( + "time" + + "go.temporal.io/server/common/clock" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" +) + +var _ Scheduler[Task] = (*ExecutionAwareScheduler[Task])(nil) + +type ( + // ExecutionAwareSchedulerOptions contains configuration for the ExecutionAwareScheduler. + ExecutionAwareSchedulerOptions struct { + // Enabled controls whether the executionQueueScheduler is active. + Enabled func() bool + // MaxQueues is the maximum number of concurrent execution queues. + // When this limit is reached, new queues are rejected and tasks fall back to the base scheduler. + MaxQueues func() int + // QueueTTL is how long an idle queue stays in the map before being swept. + QueueTTL func() time.Duration + // QueueConcurrency is the max number of worker goroutines per queue. + // Values <= 0 are capped to 1 (strictly sequential). + QueueConcurrency func() int + } + + // ExecutionAwareScheduler is a scheduler that wraps a base scheduler and adds + // an executionQueueScheduler for handling execution contention. + // + // By default, tasks are processed by the base scheduler. When an execution experiences + // contention (e.g., busy workflow error), it gets routed to the executionQueueScheduler + // which ensures tasks are processed sequentially per execution. + ExecutionAwareScheduler[T Task] struct { + baseScheduler Scheduler[T] + executionQueueScheduler *executionQueueScheduler[T] + + queueKeyFn QueueKeyFn[T] + options ExecutionAwareSchedulerOptions + logger log.Logger + } +) + +// NewExecutionAwareScheduler creates a new ExecutionAwareScheduler. +func NewExecutionAwareScheduler[T Task]( + baseScheduler Scheduler[T], + options ExecutionAwareSchedulerOptions, + queueKeyFn QueueKeyFn[T], + logger log.Logger, + metricsHandler metrics.Handler, + timeSource clock.TimeSource, +) *ExecutionAwareScheduler[T] { + return &ExecutionAwareScheduler[T]{ + baseScheduler: baseScheduler, + executionQueueScheduler: newExecutionQueueScheduler( + options.MaxQueues, + options.QueueTTL, + options.QueueConcurrency, + queueKeyFn, + logger, + metricsHandler, + timeSource, + ), + queueKeyFn: queueKeyFn, + options: options, + logger: logger, + } +} + +func (s *ExecutionAwareScheduler[T]) Start() { + s.baseScheduler.Start() + // Always start the executionQueueScheduler regardless of current config. + // The Enabled check gates task routing, so an idle scheduler has minimal + // overhead. This ensures if the config changes from disabled to enabled, + // tasks will be processed correctly. + s.executionQueueScheduler.Start() +} + +func (s *ExecutionAwareScheduler[T]) Stop() { + s.baseScheduler.Stop() + s.executionQueueScheduler.Stop() +} + +func (s *ExecutionAwareScheduler[T]) Submit(task T) { + if s.shouldRouteToExecutionQueueScheduler(task) { + if s.executionQueueScheduler.TrySubmit(task) { + return + } + // executionQueueScheduler is full, fall through to base scheduler. + } + s.baseScheduler.Submit(task) +} + +func (s *ExecutionAwareScheduler[T]) TrySubmit(task T) bool { + if s.shouldRouteToExecutionQueueScheduler(task) { + if s.executionQueueScheduler.TrySubmit(task) { + return true + } + // executionQueueScheduler is full, fall through to base scheduler. + } + return s.baseScheduler.TrySubmit(task) +} + +// HandleBusyWorkflow routes a task to the executionQueueScheduler when it +// encounters a contention error. Returns true if the task was handled +// (submitted to EQS), false if the caller should handle it (e.g., feature +// disabled or EQS at max capacity). +func (s *ExecutionAwareScheduler[T]) HandleBusyWorkflow(task T) bool { + if !s.options.Enabled() { + return false + } + return s.executionQueueScheduler.TrySubmit(task) +} + +// HasExecutionQueue returns true if the task's execution has an active queue +// in the executionQueueScheduler. +func (s *ExecutionAwareScheduler[T]) HasExecutionQueue(task T) bool { + if !s.options.Enabled() { + return false + } + return s.executionQueueScheduler.HasQueue(s.queueKeyFn(task)) +} + +func (s *ExecutionAwareScheduler[T]) shouldRouteToExecutionQueueScheduler(task T) bool { + if !s.options.Enabled() { + return false + } + return s.executionQueueScheduler.HasQueue(s.queueKeyFn(task)) +} diff --git a/common/tasks/execution_aware_scheduler_test.go b/common/tasks/execution_aware_scheduler_test.go new file mode 100644 index 00000000000..ba0faa8d396 --- /dev/null +++ b/common/tasks/execution_aware_scheduler_test.go @@ -0,0 +1,440 @@ +package tasks + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.temporal.io/server/common/backoff" + "go.temporal.io/server/common/clock" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.uber.org/mock/gomock" +) + +type ( + executionAwareSchedulerSuite struct { + *require.Assertions + suite.Suite + + controller *gomock.Controller + logger log.Logger + } +) + +func TestExecutionAwareSchedulerSuite(t *testing.T) { + s := new(executionAwareSchedulerSuite) + suite.Run(t, s) +} + +func (s *executionAwareSchedulerSuite) SetupTest() { + s.Assertions = require.New(s.T()) + s.controller = gomock.NewController(s.T()) + s.logger = log.NewNoopLogger() +} + +func (s *executionAwareSchedulerSuite) TearDownTest() { + s.controller.Finish() +} + +func (s *executionAwareSchedulerSuite) TestStartStop_Enabled() { + scheduler, _ := s.createSchedulerWithMock(true) + scheduler.Start() + scheduler.Stop() +} + +func (s *executionAwareSchedulerSuite) TestStartStop_Disabled() { + scheduler, _ := s.createSchedulerWithMock(false) + scheduler.Start() + scheduler.Stop() +} + +func (s *executionAwareSchedulerSuite) TestSubmit_DelegatesToBaseWhenDisabled() { + scheduler, mockBaseScheduler := s.createSchedulerWithMock(false) + mockTask := s.createTestTask("wf1", "run1") + mockBaseScheduler.EXPECT().Submit(mockTask).Times(1) + + scheduler.Start() + defer scheduler.Stop() + + scheduler.Submit(mockTask) +} + +func (s *executionAwareSchedulerSuite) TestSubmit_RoutesToBaseWhenNoActiveQueue() { + scheduler, mockBaseScheduler := s.createSchedulerWithMock(true) + mockTask := s.createTestTask("wf1", "run1") + mockBaseScheduler.EXPECT().Submit(mockTask).Times(1) + + scheduler.Start() + defer scheduler.Stop() + + scheduler.Submit(mockTask) +} + +func (s *executionAwareSchedulerSuite) TestSubmit_RoutesToExecutionQueueSchedulerWhenActiveQueue() { + scheduler, _ := s.createSchedulerWithMock(true) + scheduler.Start() + defer scheduler.Stop() + + // Create an active queue by submitting via HandleBusyWorkflow + task1 := s.createTestTask("wf1", "run1") + task1.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + + execStarted := make(chan struct{}) + execContinue := make(chan struct{}) + var completionWG sync.WaitGroup + completionWG.Add(3) // 3 tasks total + task1.EXPECT().Execute().DoAndReturn(func() error { + close(execStarted) + <-execContinue + return nil + }).Times(1) + task1.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + + s.True(scheduler.HandleBusyWorkflow(task1)) + <-execStarted + + // Add task 2 via HandleBusyWorkflow + task2 := s.createTestTask("wf1", "run1") + task2.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task2.EXPECT().Execute().Return(nil).Times(1) + task2.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + s.True(scheduler.HandleBusyWorkflow(task2)) + + // Verify queue exists + task3 := s.createTestTask("wf1", "run1") + s.True(scheduler.HasExecutionQueue(task3)) + + // Add a third task via Submit - should route to executionQueueScheduler since queue exists + task4 := s.createTestTask("wf1", "run1") + task4.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task4.EXPECT().Execute().Return(nil).Times(1) + task4.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + scheduler.Submit(task4) + + close(execContinue) + completionWG.Wait() +} + +func (s *executionAwareSchedulerSuite) TestTrySubmit_DelegatesToBaseWhenDisabled() { + scheduler, mockBaseScheduler := s.createSchedulerWithMock(false) + mockTask := s.createTestTask("wf1", "run1") + mockBaseScheduler.EXPECT().TrySubmit(mockTask).Return(true).Times(1) + + scheduler.Start() + defer scheduler.Stop() + + s.True(scheduler.TrySubmit(mockTask)) +} + +func (s *executionAwareSchedulerSuite) TestTrySubmit_RoutesToBaseWhenNoActiveQueue() { + scheduler, mockBaseScheduler := s.createSchedulerWithMock(true) + mockTask := s.createTestTask("wf1", "run1") + mockBaseScheduler.EXPECT().TrySubmit(mockTask).Return(true).Times(1) + + scheduler.Start() + defer scheduler.Stop() + + s.True(scheduler.TrySubmit(mockTask)) +} + +func (s *executionAwareSchedulerSuite) TestTrySubmit_AddsToExistingQueueSuccessfully() { + scheduler, _ := s.createSchedulerWithMock(true) + scheduler.Start() + defer scheduler.Stop() + + // First task blocks execution - this creates a queue + task1 := s.createTestTask("wf1", "run1") + task1.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + execStarted := make(chan struct{}) + execContinue := make(chan struct{}) + var completionWG sync.WaitGroup + completionWG.Add(3) + task1.EXPECT().Execute().DoAndReturn(func() error { + close(execStarted) + <-execContinue + return nil + }).Times(1) + task1.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + s.True(scheduler.HandleBusyWorkflow(task1)) + + <-execStarted + + // Add second task to keep queue active + task2 := s.createTestTask("wf1", "run1") + task2.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task2.EXPECT().Execute().Return(nil).Times(1) + task2.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + scheduler.HandleBusyWorkflow(task2) + + // TrySubmit for same key which has an active queue - should succeed + task3 := s.createTestTask("wf1", "run1") + task3.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task3.EXPECT().Execute().Return(nil).Times(1) + task3.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + s.True(scheduler.TrySubmit(task3)) + + close(execContinue) + completionWG.Wait() +} + +func (s *executionAwareSchedulerSuite) TestTrySubmit_RoutesToBaseWhenMaxQueuesReached() { + scheduler, mockBaseScheduler := s.createSchedulerWithMaxQueues(1) + scheduler.Start() + defer scheduler.Stop() + + // First task blocks execution - fills the single queue slot + blockCh := make(chan struct{}) + execStarted := make(chan struct{}) + var completionWG sync.WaitGroup + completionWG.Add(1) + task1 := s.createTestTask("wf1", "run1") + task1.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task1.EXPECT().Execute().DoAndReturn(func() error { + close(execStarted) + <-blockCh + return nil + }).MaxTimes(1) + task1.EXPECT().Ack().Do(func() { completionWG.Done() }).MaxTimes(1) + task1.EXPECT().Abort().MaxTimes(1) + s.True(scheduler.HandleBusyWorkflow(task1)) + + <-execStarted + + // TrySubmit for a different key - queue is full, should go to base scheduler + task2 := s.createTestTask("wf2", "run2") + mockBaseScheduler.EXPECT().TrySubmit(task2).Return(true).Times(1) + s.True(scheduler.TrySubmit(task2)) + + close(blockCh) + completionWG.Wait() +} + +func (s *executionAwareSchedulerSuite) TestHandleBusyWorkflow_ReturnsFalseWhenDisabled() { + scheduler, _ := s.createSchedulerWithoutLifecycle(false) + mockTask := s.createTestTask("wf1", "run1") + s.False(scheduler.HandleBusyWorkflow(mockTask)) +} + +func (s *executionAwareSchedulerSuite) TestHandleBusyWorkflow_SubmitsToExecutionQueueScheduler() { + scheduler, _ := s.createSchedulerWithMock(true) + scheduler.Start() + defer scheduler.Stop() + + var completionWG sync.WaitGroup + completionWG.Add(1) + task := s.createTestTask("wf1", "run1") + task.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task.EXPECT().Execute().Return(nil).Times(1) + task.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + + s.True(scheduler.HandleBusyWorkflow(task)) + completionWG.Wait() +} + +func (s *executionAwareSchedulerSuite) TestHandleBusyWorkflow_ReturnsFalseWhenMaxQueuesReached() { + scheduler, _ := s.createSchedulerWithMaxQueues(1) + scheduler.Start() + defer scheduler.Stop() + + // First task blocks execution so queue stays occupied + blockCh := make(chan struct{}) + execStarted := make(chan struct{}) + var completionWG sync.WaitGroup + completionWG.Add(1) + task1 := s.createTestTask("wf1", "run1") + task1.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task1.EXPECT().Execute().DoAndReturn(func() error { + close(execStarted) + <-blockCh + return nil + }).MaxTimes(1) + task1.EXPECT().Ack().Do(func() { completionWG.Done() }).MaxTimes(1) + task1.EXPECT().Abort().MaxTimes(1) + s.True(scheduler.HandleBusyWorkflow(task1)) + + <-execStarted + + // Second task for a DIFFERENT key should return false (MaxQueues reached) + task2 := s.createTestTask("wf2", "run2") + s.False(scheduler.HandleBusyWorkflow(task2)) + + close(blockCh) + completionWG.Wait() +} + +func (s *executionAwareSchedulerSuite) TestHasExecutionQueue_ReturnsFalseWhenDisabled() { + scheduler, _ := s.createSchedulerWithoutLifecycle(false) + mockTask := s.createTestTask("wf1", "run1") + s.False(scheduler.HasExecutionQueue(mockTask)) +} + +func (s *executionAwareSchedulerSuite) TestHasExecutionQueue_ReturnsFalseWhenNoQueue() { + scheduler, _ := s.createSchedulerWithMock(true) + scheduler.Start() + defer scheduler.Stop() + + mockTask := s.createTestTask("wf1", "run1") + s.False(scheduler.HasExecutionQueue(mockTask)) +} + +func (s *executionAwareSchedulerSuite) TestHasExecutionQueue_ReturnsTrueWhenQueueExists() { + scheduler, _ := s.createSchedulerWithMock(true) + scheduler.Start() + defer scheduler.Stop() + + // Create a queue by submitting a task + task1 := s.createTestTask("wf1", "run1") + task1.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + + execStarted := make(chan struct{}) + execContinue := make(chan struct{}) + var completionWG sync.WaitGroup + completionWG.Add(2) + task1.EXPECT().Execute().DoAndReturn(func() error { + close(execStarted) + <-execContinue + return nil + }).Times(1) + task1.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + + scheduler.HandleBusyWorkflow(task1) + <-execStarted + + // Add another task to ensure queue stays alive + task2 := s.createTestTask("wf1", "run1") + task2.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + task2.EXPECT().Execute().Return(nil).Times(1) + task2.EXPECT().Ack().Do(func() { completionWG.Done() }).Times(1) + scheduler.HandleBusyWorkflow(task2) + + // Check that queue exists for this key + task3 := s.createTestTask("wf1", "run1") + s.True(scheduler.HasExecutionQueue(task3)) + + // Check that queue doesn't exist for different key + task4 := s.createTestTask("wf2", "run2") + s.False(scheduler.HasExecutionQueue(task4)) + + close(execContinue) + completionWG.Wait() +} + +func (s *executionAwareSchedulerSuite) TestConcurrentSubmit() { + scheduler, mockBaseScheduler := s.createSchedulerWithMock(true) + mockBaseScheduler.EXPECT().Submit(gomock.Any()).AnyTimes() + + scheduler.Start() + defer scheduler.Stop() + + var wg sync.WaitGroup + numTasks := 100 + wg.Add(numTasks) + + for range numTasks { + go func() { + defer wg.Done() + mockTask := s.createTestTask("wf1", "run1") + scheduler.Submit(mockTask) + }() + } + + wg.Wait() +} + +func (s *executionAwareSchedulerSuite) TestConcurrentHandleBusyWorkflow() { + scheduler, _ := s.createSchedulerWithMock(true) + scheduler.Start() + defer scheduler.Stop() + + var submitWG sync.WaitGroup + var completionWG sync.WaitGroup + numTasks := 50 + submitWG.Add(numTasks) + completionWG.Add(numTasks) + + for range numTasks { + go func() { + defer submitWG.Done() + mockTask := s.createTestTask("wf1", "run1") + mockTask.EXPECT().RetryPolicy().Return(backoff.NewExponentialRetryPolicy(time.Millisecond)).AnyTimes() + mockTask.EXPECT().Execute().Return(nil).MaxTimes(1) + mockTask.EXPECT().Ack().Do(func() { completionWG.Done() }).MaxTimes(1) + mockTask.EXPECT().Abort().Do(func() { completionWG.Done() }).MaxTimes(1) + scheduler.HandleBusyWorkflow(mockTask) + }() + } + + submitWG.Wait() + completionWG.Wait() +} + +// Helper functions + +func (s *executionAwareSchedulerSuite) createTestTask(workflowID, runID string) *testExecutionTask { + mockTask := NewMockTask(s.controller) + return &testExecutionTask{ + MockTask: mockTask, + workflowID: workflowID, + runID: runID, + } +} + +func (s *executionAwareSchedulerSuite) defaultSchedulerOptions(enabled bool) ExecutionAwareSchedulerOptions { + return ExecutionAwareSchedulerOptions{ + Enabled: func() bool { return enabled }, + MaxQueues: func() int { return 500 }, + QueueTTL: func() time.Duration { return 5 * time.Second }, + QueueConcurrency: func() int { return 1 }, + } +} + +func (s *executionAwareSchedulerSuite) createSchedulerWithMock(enabled bool) (*ExecutionAwareScheduler[*testExecutionTask], *MockScheduler[*testExecutionTask]) { + mockBaseScheduler := NewMockScheduler[*testExecutionTask](s.controller) + mockBaseScheduler.EXPECT().Start().Times(1) + mockBaseScheduler.EXPECT().Stop().Times(1) + + scheduler := NewExecutionAwareScheduler[*testExecutionTask]( + mockBaseScheduler, + s.defaultSchedulerOptions(enabled), + executionKeyFn, + s.logger, + metrics.NoopMetricsHandler, + clock.NewRealTimeSource(), + ) + return scheduler, mockBaseScheduler +} + +func (s *executionAwareSchedulerSuite) createSchedulerWithoutLifecycle(enabled bool) (*ExecutionAwareScheduler[*testExecutionTask], *MockScheduler[*testExecutionTask]) { + mockBaseScheduler := NewMockScheduler[*testExecutionTask](s.controller) + scheduler := NewExecutionAwareScheduler[*testExecutionTask]( + mockBaseScheduler, + s.defaultSchedulerOptions(enabled), + executionKeyFn, + s.logger, + metrics.NoopMetricsHandler, + clock.NewRealTimeSource(), + ) + return scheduler, mockBaseScheduler +} + +func (s *executionAwareSchedulerSuite) createSchedulerWithMaxQueues(maxQueues int) (*ExecutionAwareScheduler[*testExecutionTask], *MockScheduler[*testExecutionTask]) { + mockBaseScheduler := NewMockScheduler[*testExecutionTask](s.controller) + mockBaseScheduler.EXPECT().Start().Times(1) + mockBaseScheduler.EXPECT().Stop().Times(1) + + opts := s.defaultSchedulerOptions(true) + opts.MaxQueues = func() int { return maxQueues } + + scheduler := NewExecutionAwareScheduler[*testExecutionTask]( + mockBaseScheduler, + opts, + executionKeyFn, + s.logger, + metrics.NoopMetricsHandler, + clock.NewRealTimeSource(), + ) + return scheduler, mockBaseScheduler +} diff --git a/common/tasks/execution_queue_scheduler.go b/common/tasks/execution_queue_scheduler.go new file mode 100644 index 00000000000..8433125a757 --- /dev/null +++ b/common/tasks/execution_queue_scheduler.go @@ -0,0 +1,285 @@ +package tasks + +import ( + "sync" + "sync/atomic" + "time" + + "go.temporal.io/server/common" + "go.temporal.io/server/common/backoff" + "go.temporal.io/server/common/clock" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" +) + +type ( + // QueueKeyFn extracts a queue key from a task for queue routing. + QueueKeyFn[T Task] func(T) any + + // taskEntry wraps a task with its submit timestamp for latency tracking. + taskEntry[T Task] struct { + task T + submitTime time.Time + } + + // executionQueue represents a single execution's task queue. + // Tasks are stored in a slice protected by the scheduler's mutex. + // Worker goroutines pop tasks and execute them (up to QueueConcurrency workers). + executionQueue[T Task] struct { + tasks []taskEntry[T] + activeWorkers int + lastActive time.Time + } + + // executionQueueScheduler is a scheduler that ensures sequential task execution + // per execution using dedicated goroutines per queue. + // + // Key characteristics: + // - Tasks for the same execution are executed sequentially + // - Each execution gets its own goroutines that processes tasks from a slice + // - Worker goroutines exit when the queue is empty + // - A background sweeper removes idle queue entries after QueueTTL + // - Maximum number of concurrent queues is capped at MaxQueues + executionQueueScheduler[T Task] struct { + status atomic.Int32 + shutdownChan chan struct{} + shutdownWG sync.WaitGroup + + maxQueues func() int + queueTTL func() time.Duration + queueConcurrency func() int + queueKeyFn QueueKeyFn[T] + logger log.Logger + metricsHandler metrics.Handler + timeSource clock.TimeSource + + // mu protects the queues map, sweeperRunning, and all executionQueue fields. + // All task submissions and worker pops are serialized through this lock. + mu sync.Mutex + queues map[any]*executionQueue[T] + sweeperRunning bool + } +) + +// newExecutionQueueScheduler creates a new executionQueueScheduler. +func newExecutionQueueScheduler[T Task]( + maxQueues func() int, + queueTTL func() time.Duration, + queueConcurrency func() int, + queueKeyFn QueueKeyFn[T], + logger log.Logger, + metricsHandler metrics.Handler, + timeSource clock.TimeSource, +) *executionQueueScheduler[T] { + s := &executionQueueScheduler[T]{ + shutdownChan: make(chan struct{}), + maxQueues: maxQueues, + queueTTL: queueTTL, + queueConcurrency: queueConcurrency, + queueKeyFn: queueKeyFn, + logger: logger, + metricsHandler: metricsHandler, + timeSource: timeSource, + queues: make(map[any]*executionQueue[T]), + } + s.status.Store(common.DaemonStatusInitialized) + return s +} + +func (s *executionQueueScheduler[T]) Start() { + if !s.status.CompareAndSwap(common.DaemonStatusInitialized, common.DaemonStatusStarted) { + return + } + s.logger.Info("execution queue scheduler started") +} + +func (s *executionQueueScheduler[T]) Stop() { + if !s.status.CompareAndSwap(common.DaemonStatusStarted, common.DaemonStatusStopped) { + return + } + + close(s.shutdownChan) + + go func() { + if success := common.AwaitWaitGroup(&s.shutdownWG, time.Minute); !success { + s.logger.Warn("execution queue scheduler timed out waiting for goroutines") + } + }() + + s.logger.Info("execution queue scheduler stopped") +} + +// HasQueue returns true if a queue exists for the given execution key. +// This is used by other schedulers to check if they should route tasks here. +func (s *executionQueueScheduler[T]) HasQueue(key any) bool { + s.mu.Lock() + defer s.mu.Unlock() + _, exists := s.queues[key] + return exists +} + +// TrySubmit adds a task to its execution's queue without blocking. +// Returns true if the task was accepted, false if at max queue capacity for new queues. +func (s *executionQueueScheduler[T]) TrySubmit(task T) bool { + if s.isStopped() { + task.Abort() + + return true + } + + key := s.queueKeyFn(task) + entry := taskEntry[T]{task: task, submitTime: s.timeSource.Now()} + + s.mu.Lock() + if s.isStopped() { + s.mu.Unlock() + task.Abort() + + return true + } + + q, exists := s.queues[key] + if !exists && len(s.queues) >= s.maxQueues() { + s.mu.Unlock() + metrics.ExecutionQueueSchedulerSubmitRejected.With(s.metricsHandler).Record(1) + return false + } + s.appendTaskLocked(key, q, entry) + s.mu.Unlock() + metrics.ExecutionQueueSchedulerTasksSubmitted.With(s.metricsHandler).Record(1) + return true +} + +// appendTaskLocked appends a task to the queue for the given key, creating the +// queue and spawning a worker if needed. Must be called with s.mu held. +// q is the existing queue for the key, or nil if one needs to be created. +func (s *executionQueueScheduler[T]) appendTaskLocked(key any, q *executionQueue[T], entry taskEntry[T]) { + if q == nil { + q = &executionQueue[T]{} + s.queues[key] = q + metrics.ExecutionQueueSchedulerQueueCount.With(s.metricsHandler).Record(float64(len(s.queues))) + } + q.tasks = append(q.tasks, entry) + if q.activeWorkers < max(1, s.queueConcurrency()) { + q.activeWorkers++ + s.shutdownWG.Add(1) + go s.runWorker(q) + } + if !s.sweeperRunning { + s.sweeperRunning = true + s.shutdownWG.Add(1) + go s.runSweeper() + } +} + +// runWorker is the goroutine that processes tasks for a single execution queue. +// It pops tasks from the slice under the lock and executes them without the lock. +// When the queue is empty, the worker marks itself inactive and exits. +// A new worker is spawned by Submit/TrySubmit when tasks arrive for an inactive queue. +func (s *executionQueueScheduler[T]) runWorker(q *executionQueue[T]) { + defer s.shutdownWG.Done() + + for { + s.mu.Lock() + if s.isStopped() { + // Abort all remaining tasks. + tasks := q.tasks + q.tasks = nil + q.activeWorkers-- + s.mu.Unlock() + for _, entry := range tasks { + entry.task.Abort() + } + return + } + if len(q.tasks) == 0 { + q.activeWorkers-- + if q.activeWorkers == 0 { + q.lastActive = s.timeSource.Now() + } + s.mu.Unlock() + return + } + entry := q.tasks[0] + var zero taskEntry[T] + q.tasks[0] = zero // clear reference for GC + q.tasks = q.tasks[1:] + s.mu.Unlock() + + queueWaitTime := s.timeSource.Now().Sub(entry.submitTime) + metrics.ExecutionQueueSchedulerQueueWaitTime.With(s.metricsHandler).Record(queueWaitTime) + s.executeTask(entry.task, entry.submitTime) + } +} + +// runSweeper periodically removes idle queue entries from the map. +func (s *executionQueueScheduler[T]) runSweeper() { + defer s.shutdownWG.Done() + + ch, t := s.timeSource.NewTimer(s.queueTTL()) + defer t.Stop() + + for { + select { + case <-ch: + if s.sweepIdleQueues() { + return + } + t.Reset(s.queueTTL()) + case <-s.shutdownChan: + return + } + } +} + +// sweepIdleQueues removes queues that have been idle for longer than QueueTTL. +// Returns true if no queues remain (sweeper should exit). +func (s *executionQueueScheduler[T]) sweepIdleQueues() bool { + s.mu.Lock() + defer s.mu.Unlock() + + now := s.timeSource.Now() + for key, q := range s.queues { + if q.activeWorkers == 0 && len(q.tasks) == 0 && now.Sub(q.lastActive) > s.queueTTL() { + delete(s.queues, key) + } + } + + metrics.ExecutionQueueSchedulerQueueCount.With(s.metricsHandler).Record(float64(len(s.queues))) + s.sweeperRunning = len(s.queues) != 0 + return !s.sweeperRunning +} + +func (s *executionQueueScheduler[T]) executeTask(task T, submitTime time.Time) { + operation := func() error { + if err := task.Execute(); err != nil { + return task.HandleErr(err) + } + return nil + } + + isRetryable := func(err error) bool { + return !s.isStopped() && task.IsRetryableError(err) + } + + if err := backoff.ThrottleRetry(operation, task.RetryPolicy(), isRetryable); err != nil { + if s.isStopped() { + task.Abort() + return + } + + metrics.ExecutionQueueSchedulerTasksFailed.With(s.metricsHandler).Record(1) + task.Nack(err) + return + } + + metrics.ExecutionQueueSchedulerTasksCompleted.With(s.metricsHandler).Record(1) + task.Ack() + + taskLatency := s.timeSource.Now().Sub(submitTime) + metrics.ExecutionQueueSchedulerTaskLatency.With(s.metricsHandler).Record(taskLatency) +} + +func (s *executionQueueScheduler[T]) isStopped() bool { + return s.status.Load() == common.DaemonStatusStopped +} diff --git a/common/tasks/execution_queue_scheduler_test.go b/common/tasks/execution_queue_scheduler_test.go new file mode 100644 index 00000000000..dd9606b8b8e --- /dev/null +++ b/common/tasks/execution_queue_scheduler_test.go @@ -0,0 +1,595 @@ +package tasks + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.temporal.io/server/common/backoff" + "go.temporal.io/server/common/clock" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.uber.org/mock/gomock" +) + +type ( + executionQueueSchedulerSuite struct { + *require.Assertions + suite.Suite + + controller *gomock.Controller + + scheduler *executionQueueScheduler[*MockTask] + retryPolicy backoff.RetryPolicy + } + + // testExecutionTask is a wrapper that provides an execution key for queue routing + testExecutionTask struct { + *MockTask + workflowID string + runID string + } + + testExecutionKey struct { + workflowID string + runID string + } +) + +func executionKeyFn(task *testExecutionTask) any { + return testExecutionKey{workflowID: task.workflowID, runID: task.runID} +} + +func TestExecutionQueueSchedulerSuite(t *testing.T) { + s := new(executionQueueSchedulerSuite) + suite.Run(t, s) +} + +func (s *executionQueueSchedulerSuite) SetupTest() { + s.Assertions = require.New(s.T()) + s.controller = gomock.NewController(s.T()) + s.retryPolicy = backoff.NewExponentialRetryPolicy(time.Millisecond) + s.scheduler = s.newScheduler() + s.scheduler.Start() +} + +func (s *executionQueueSchedulerSuite) TearDownTest() { + s.scheduler.Stop() + s.controller.Finish() +} + +// ============================================================================= +// Basic Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestTrySubmit_Failure() { + testWG := sync.WaitGroup{} + testWG.Add(1) + + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + executionErr := errors.New("execution error") + mockTask.EXPECT().Execute().Return(executionErr).Times(1) + mockTask.EXPECT().HandleErr(executionErr).Return(executionErr).Times(1) + mockTask.EXPECT().IsRetryableError(executionErr).Return(false).MaxTimes(1) + mockTask.EXPECT().Nack(executionErr).Do(func(_ error) { testWG.Done() }).Times(1) + + s.scheduler.TrySubmit(mockTask) + + testWG.Wait() +} + +func (s *executionQueueSchedulerSuite) TestTrySubmit_RetryThenSuccess() { + testWG := sync.WaitGroup{} + testWG.Add(1) + + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + + executionErr := errors.New("transient error") + callCount := 0 + mockTask.EXPECT().Execute().DoAndReturn(func() error { + callCount++ + if callCount < 3 { + return executionErr + } + return nil + }).Times(3) + mockTask.EXPECT().HandleErr(executionErr).Return(executionErr).Times(2) + mockTask.EXPECT().IsRetryableError(executionErr).Return(true).Times(2) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + s.scheduler.TrySubmit(mockTask) + + testWG.Wait() +} + +// ============================================================================= +// Sequential Execution Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestSequentialExecution_SameWorkflow() { + scheduler := s.newSchedulerWithExecution(500, 5*time.Second) + scheduler.Start() + defer scheduler.Stop() + + numTasks := 10 + executionOrder := make([]int, 0, numTasks) + var orderMu sync.Mutex + testWG := sync.WaitGroup{} + testWG.Add(numTasks) + + for i := range numTasks { + taskIndex := i + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().DoAndReturn(func() error { + orderMu.Lock() + executionOrder = append(executionOrder, taskIndex) + orderMu.Unlock() + return nil + }).Times(1) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + // All tasks have same workflow ID (1) + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: "wf1", runID: "run1"}) + } + + testWG.Wait() + + // Verify tasks executed in order + s.Len(executionOrder, numTasks) + for i := range numTasks { + s.Equal(i, executionOrder[i], "Tasks should execute in submission order") + } +} + +func (s *executionQueueSchedulerSuite) TestSequentialExecution_DifferentWorkflows() { + scheduler := s.newSchedulerWithExecution(500, 5*time.Second) + scheduler.Start() + defer scheduler.Stop() + + numWorkflows := 4 + tasksPerWorkflow := 5 + + // Track execution order per workflow - use separate slices to avoid map access races + type workflowOrders struct { + mu sync.Mutex + orders []int + } + executionOrders := make([]*workflowOrders, numWorkflows) + for i := range numWorkflows { + executionOrders[i] = &workflowOrders{orders: make([]int, 0, tasksPerWorkflow)} + } + + testWG := sync.WaitGroup{} + testWG.Add(numWorkflows * tasksPerWorkflow) + + for wf := range numWorkflows { + for t := range tasksPerWorkflow { + workflowID := wf + taskIndex := t + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().DoAndReturn(func() error { + executionOrders[workflowID].mu.Lock() + executionOrders[workflowID].orders = append(executionOrders[workflowID].orders, taskIndex) + executionOrders[workflowID].mu.Unlock() + return nil + }).Times(1) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: fmt.Sprintf("%d", workflowID), runID: "1"}) + } + } + + testWG.Wait() + + // Verify each workflow's tasks executed in order + for wf := range numWorkflows { + s.Len(executionOrders[wf].orders, tasksPerWorkflow) + for i := range tasksPerWorkflow { + s.Equal(i, executionOrders[wf].orders[i], "Workflow %d tasks should execute in order", wf) + } + } +} + +// ============================================================================= +// TrySubmit Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestTrySubmit_Success() { + testWG := sync.WaitGroup{} + testWG.Add(1) + + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().Return(nil).Times(1) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + result := s.scheduler.TrySubmit(mockTask) + s.True(result) + + testWG.Wait() +} + +func (s *executionQueueSchedulerSuite) TestTrySubmit_ExistingQueue() { + // Create a scheduler that blocks task execution so we can add to existing queue + scheduler := s.newSchedulerWithExecution(500, 5*time.Second) + scheduler.Start() + defer scheduler.Stop() + + blockCh := make(chan struct{}) + blockStarted := make(chan struct{}) + testWG := sync.WaitGroup{} + testWG.Add(2) + + // First task - will block + mockTask1 := NewMockTask(s.controller) + mockTask1.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask1.EXPECT().Execute().DoAndReturn(func() error { + close(blockStarted) + <-blockCh // Block until signaled + return nil + }).Times(1) + mockTask1.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + // Second task - should be added to existing queue + mockTask2 := NewMockTask(s.controller) + mockTask2.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask2.EXPECT().Execute().Return(nil).Times(1) + mockTask2.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + // Submit first task + result1 := scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask1, workflowID: "wf1", runID: "run1"}) + s.True(result1) + + // Wait for first task to start executing + <-blockStarted + + // Submit second task to same workflow - should succeed (existing queue) + result2 := scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask2, workflowID: "wf1", runID: "run1"}) + s.True(result2) + + // Verify queue exists + s.True(scheduler.HasQueue(testExecutionKey{workflowID: "wf1", runID: "run1"})) + + // Unblock first task + close(blockCh) + + testWG.Wait() +} + +// ============================================================================= +// Shutdown Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestShutdown_DrainTasks() { + // Test that tasks submitted after Stop() are aborted + scheduler := s.newSchedulerWithExecution(500, 5*time.Second) + scheduler.Start() + + testWG := sync.WaitGroup{} + testWG.Add(1) + + // Submit a task that will complete + mockTask1 := NewMockTask(s.controller) + mockTask1.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask1.EXPECT().Execute().Return(nil).Times(1) + mockTask1.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask1, workflowID: "wf1", runID: "run1"}) + testWG.Wait() + + // Stop the scheduler + scheduler.Stop() + + // Tasks submitted after stop should be aborted + mockTask2 := NewMockTask(s.controller) + mockTask2.EXPECT().Abort().Times(1) + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask2, workflowID: "wf1", runID: "run1"}) +} + +// ============================================================================= +// HasQueue Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestHasQueue() { + scheduler := s.newSchedulerWithExecution(500, 5*time.Second) + scheduler.Start() + defer scheduler.Stop() + + // Initially no queues + s.False(scheduler.HasQueue(testExecutionKey{workflowID: "wf1", runID: "run1"})) + + blockCh := make(chan struct{}) + executingCh := make(chan struct{}) + testWG := sync.WaitGroup{} + testWG.Add(1) + + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().DoAndReturn(func() error { + close(executingCh) // Signal that we're executing + <-blockCh // Wait to complete + return nil + }).Times(1) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + // Submit task + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: "wf1", runID: "run1"}) + + // Wait until task is actually executing + <-executingCh + + // Queue should exist while task is executing + s.True(scheduler.HasQueue(testExecutionKey{workflowID: "wf1", runID: "run1"})) + + // Complete task + close(blockCh) + testWG.Wait() + + // Queue still exists within TTL + s.True(scheduler.HasQueue(testExecutionKey{workflowID: "wf1", runID: "run1"})) +} + +// ============================================================================= +// Queue TTL Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestQueueTTL_ExpiresAfterIdle() { + // Use very short TTL for testing. + // The background sweeper runs every QueueTTL, so a queue may linger + // up to 2x QueueTTL after becoming idle. + ttl := 50 * time.Millisecond + scheduler := s.newSchedulerWithExecution(500, ttl) + scheduler.Start() + defer scheduler.Stop() + + testWG := sync.WaitGroup{} + testWG.Add(1) + + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().Return(nil).Times(1) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: "wf1", runID: "run1"}) + testWG.Wait() + + // Queue exists immediately after task completes + s.True(scheduler.HasQueue(testExecutionKey{workflowID: "wf1", runID: "run1"})) + + // Wait for sweeper to remove the idle queue + s.Eventually(func() bool { + return !scheduler.HasQueue(testExecutionKey{workflowID: "wf1", runID: "run1"}) + }, 5*time.Second, 10*time.Millisecond, "Queue should be removed after TTL") +} + +func (s *executionQueueSchedulerSuite) TestTTLExpiryRace_NoTaskOrphaning() { + // Regression test for Issue #3: tasks submitted near TTL expiry must not be orphaned. + // Uses a very short TTL and repeatedly submits tasks right as queues expire, + // verifying that every submitted task is eventually executed (Ack'd). + ttl := 20 * time.Millisecond + scheduler := s.newSchedulerWithExecution(500, ttl) + scheduler.Start() + defer scheduler.Stop() + + const iterations = 50 + var processedCount int32 + testWG := sync.WaitGroup{} + + for range iterations { + testWG.Add(1) + + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().Return(nil).Times(1) + mockTask.EXPECT().Ack().Do(func() { + atomic.AddInt32(&processedCount, 1) + testWG.Done() + }).Times(1) + + // Submit, then wait for the queue to be removed by the sweeper before next submit. + // This maximizes the chance of hitting the TTL expiry race. + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: "wf1", runID: "run1"}) + + // Wait for sweeper to remove the queue before next iteration + s.Eventually(func() bool { + return !scheduler.HasQueue(testExecutionKey{workflowID: "wf1", runID: "run1"}) + }, 5*time.Second, time.Millisecond) + } + + // All tasks must be processed — no orphaning allowed + done := make(chan struct{}) + go func() { + testWG.Wait() + close(done) + }() + + select { + case <-done: + // Success + case <-time.After(30 * time.Second): + s.Fail("Timed out waiting for tasks — possible task orphaning", + "processed %d/%d tasks", atomic.LoadInt32(&processedCount), iterations) + } + + s.Equal(int32(iterations), atomic.LoadInt32(&processedCount), + "All tasks must be processed, none orphaned") +} + +// ============================================================================= +// Max Queues Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestMaxQueues_RejectsNewQueues() { + // Create scheduler with max 2 queues + scheduler := newExecutionQueueScheduler( + func() int { return 2 }, + func() time.Duration { return time.Hour }, + func() int { return 1 }, + executionKeyFn, + log.NewNoopLogger(), + metrics.NoopMetricsHandler, + clock.NewRealTimeSource(), + ) + scheduler.Start() + defer scheduler.Stop() + + // Block tasks so queues stay active + blockCh := make(chan struct{}) + defer close(blockCh) + + for i := range 2 { + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().DoAndReturn(func() error { + <-blockCh + return nil + }).MaxTimes(1) + mockTask.EXPECT().Ack().MaxTimes(1) + mockTask.EXPECT().Abort().MaxTimes(1) + + result := scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: fmt.Sprintf("%d", i), runID: "1"}) + s.True(result, "First 2 queues should be created") + } + + // Third queue should be rejected + mockTask3 := NewMockTask(s.controller) + result := scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask3, workflowID: "wf99", runID: "run1"}) + s.False(result, "Third queue should be rejected") +} + +// ============================================================================= +// Concurrency Tests +// ============================================================================= + +func (s *executionQueueSchedulerSuite) TestParallelTrySubmit_DifferentWorkflows() { + scheduler := s.newSchedulerWithExecution(500, 5*time.Second) + scheduler.Start() + defer scheduler.Stop() + + numSubmitters := 100 + tasksPerSubmitter := 50 + + testWG := sync.WaitGroup{} + testWG.Add(numSubmitters * tasksPerSubmitter) + + startWG := sync.WaitGroup{} + startWG.Add(numSubmitters) + + for i := range numSubmitters { + submitterID := i + go func() { + startWG.Done() + startWG.Wait() // Sync all submitters to start at once + + for range tasksPerSubmitter { + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().Return(nil).Times(1) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: fmt.Sprintf("%d", submitterID), runID: "1"}) + } + }() + } + + // Wait for all tasks to complete + done := make(chan struct{}) + go func() { + testWG.Wait() + close(done) + }() + + select { + case <-done: + // Success + case <-time.After(30 * time.Second): + s.Fail("Timed out waiting for parallel tasks") + } +} + +func (s *executionQueueSchedulerSuite) TestParallelTrySubmit_SameWorkflow() { + scheduler := s.newSchedulerWithExecution(500, 5*time.Second) + scheduler.Start() + defer scheduler.Stop() + + numSubmitters := 50 + tasksPerSubmitter := 20 + + var executionCount int32 + testWG := sync.WaitGroup{} + testWG.Add(numSubmitters * tasksPerSubmitter) + + startWG := sync.WaitGroup{} + startWG.Add(numSubmitters) + + for range numSubmitters { + go func() { + startWG.Done() + startWG.Wait() + + for range tasksPerSubmitter { + mockTask := NewMockTask(s.controller) + mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() + mockTask.EXPECT().Execute().DoAndReturn(func() error { + atomic.AddInt32(&executionCount, 1) + return nil + }).Times(1) + mockTask.EXPECT().Ack().Do(func() { testWG.Done() }).Times(1) + + // All tasks go to same workflow + scheduler.TrySubmit(&testExecutionTask{MockTask: mockTask, workflowID: "wf1", runID: "run1"}) + } + }() + } + + done := make(chan struct{}) + go func() { + testWG.Wait() + close(done) + }() + + select { + case <-done: + // Success + case <-time.After(30 * time.Second): + s.Fail("Timed out waiting for parallel tasks to same workflow") + } + + s.Equal(int32(numSubmitters*tasksPerSubmitter), atomic.LoadInt32(&executionCount)) +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +func (s *executionQueueSchedulerSuite) newScheduler() *executionQueueScheduler[*MockTask] { + return newExecutionQueueScheduler( + func() int { return 500 }, + func() time.Duration { return 5 * time.Second }, + func() int { return 1 }, + func(task *MockTask) any { return 1 }, // All tasks to same key + log.NewNoopLogger(), + metrics.NoopMetricsHandler, + clock.NewRealTimeSource(), + ) +} + +func (s *executionQueueSchedulerSuite) newSchedulerWithExecution(maxQueues int, queueTTL time.Duration) *executionQueueScheduler[*testExecutionTask] { + return newExecutionQueueScheduler( + func() int { return maxQueues }, + func() time.Duration { return queueTTL }, + func() int { return 1 }, + executionKeyFn, + log.NewNoopLogger(), + metrics.NoopMetricsHandler, + clock.NewRealTimeSource(), + ) +} diff --git a/common/tasks/fifo_scheduler.go b/common/tasks/fifo_scheduler.go index 44f57f84992..5aa7153cb14 100644 --- a/common/tasks/fifo_scheduler.go +++ b/common/tasks/fifo_scheduler.go @@ -12,11 +12,6 @@ import ( "go.temporal.io/server/common/log/tag" ) -const ( - defaultMonitorTickerDuration = time.Minute - defaultMonitorTickerJitter = 0.15 -) - var _ Scheduler[Task] = (*FIFOScheduler[Task])(nil) type ( @@ -146,7 +141,7 @@ func (f *FIFOScheduler[T]) updateWorkerCount(targetWorkerNum int) { func (f *FIFOScheduler[T]) startWorkers( count int, ) { - for i := 0; i < count; i++ { + for range count { shutdownCh := make(chan struct{}) f.workerShutdownCh = append(f.workerShutdownCh, shutdownCh) diff --git a/common/tasks/fifo_scheduler_test.go b/common/tasks/fifo_scheduler_test.go index 2fb6b12fec7..a90c8e56680 100644 --- a/common/tasks/fifo_scheduler_test.go +++ b/common/tasks/fifo_scheduler_test.go @@ -129,9 +129,9 @@ func (s *fifoSchedulerSuite) TestParallelSubmitProcess() { startWaitGroup.Add(numSubmitter) - for i := 0; i < numSubmitter; i++ { + for range numSubmitter { channel := make(chan *MockTask, numTasks) - for j := 0; j < numTasks; j++ { + for j := range numTasks { mockTask := NewMockTask(s.controller) mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() switch j % 2 { diff --git a/common/tasks/interleaved_weighted_round_robin_test.go b/common/tasks/interleaved_weighted_round_robin_test.go index 7cb08d2c76a..193787e68a7 100644 --- a/common/tasks/interleaved_weighted_round_robin_test.go +++ b/common/tasks/interleaved_weighted_round_robin_test.go @@ -280,9 +280,9 @@ func (s *interleavedWeightedRoundRobinSchedulerSuite) TestParallelSubmitSchedule testWaitGroup.Done() }).AnyTimes() - for i := 0; i < numSubmitter; i++ { + for range numSubmitter { channel := make(chan *testTask, numTasks) - for j := 0; j < numTasks; j++ { + for range numTasks { channel <- newTestTask(s.controller, rand.Intn(4)) } close(channel) @@ -523,12 +523,19 @@ func (s *interleavedWeightedRoundRobinSchedulerSuite) TestInactiveChannelDeletio ChannelWeightFn: func(key int) int { return s.channelKeyToWeight[key] }, ChannelWeightUpdateCh: s.channelWeightUpdateCh, InactiveChannelDeletionDelay: func() time.Duration { - return 0 // Setting cleanup delay to 0 to continuously delete channels. + // Use a small positive delay (not 0) so that cleanupLoop blocks on the + // timer channel instead of spinning in a tight loop. Cleanup is triggered + // by advancing the fake time source below. + return time.Nanosecond }, }, Scheduler[*testTask](s.mockFIFOScheduler), log.NewTestLogger(), ) + // Use the suite's EventTimeSource so cleanup timers only fire when we + // explicitly advance fake time, preventing the cleanup goroutine from + // spinning in a tight loop with a real-time zero-duration timer. + s.scheduler.ts = s.ts s.mockFIFOScheduler.EXPECT().Start() s.scheduler.Start() s.mockFIFOScheduler.EXPECT().Stop() @@ -550,24 +557,32 @@ func (s *interleavedWeightedRoundRobinSchedulerSuite) TestInactiveChannelDeletio mockTask2 := newTestTask(s.controller, 2) mockTask3 := newTestTask(s.controller, 3) - for i := 0; i < 1000; i++ { + for range 1000 { taskWG.Add(1) s.scheduler.Submit(mockTask0) taskWG.Wait() + // Advance past the 1ns delay to trigger cleanup asynchronously, creating + // a concurrent race between cleanup and the next Submit call. + s.ts.Advance(2 * time.Nanosecond) taskWG.Add(1) s.scheduler.Submit(mockTask1) taskWG.Wait() + s.ts.Advance(2 * time.Nanosecond) taskWG.Add(1) s.scheduler.Submit(mockTask2) taskWG.Wait() + s.ts.Advance(2 * time.Nanosecond) taskWG.Add(1) s.scheduler.Submit(mockTask3) taskWG.Wait() + s.ts.Advance(2 * time.Nanosecond) } + // Trigger a final cleanup and give the cleanup goroutine time to run. + s.ts.Advance(2 * time.Nanosecond) time.Sleep(100 * time.Millisecond) //nolint:forbidigo s.Empty(s.scheduler.channels()) } diff --git a/common/tasks/sequential_scheduler.go b/common/tasks/sequential_scheduler.go index 8c9d101ea90..bf6447b2071 100644 --- a/common/tasks/sequential_scheduler.go +++ b/common/tasks/sequential_scheduler.go @@ -111,7 +111,7 @@ func (s *SequentialScheduler[T]) Submit(task T) { _, fnEvaluated, err := s.queues.PutOrDo( queue.ID(), queue, - func(key interface{}, value interface{}) error { + func(key any, value any) error { value.(SequentialTaskQueue[T]).Add(task) return nil }, @@ -172,7 +172,7 @@ func (s *SequentialScheduler[T]) TrySubmit(task T) bool { _, fnEvaluated, err := s.queues.PutOrDo( queue.ID(), queue, - func(key interface{}, value interface{}) error { + func(key any, value any) error { value.(SequentialTaskQueue[T]).Add(task) return nil }, @@ -231,7 +231,7 @@ func (s *SequentialScheduler[T]) updateWorkerCount(targetWorkerNum int) { func (s *SequentialScheduler[T]) startWorkers( count int, ) { - for i := 0; i < count; i++ { + for range count { shutdownCh := make(chan struct{}) s.workerShutdownCh = append(s.workerShutdownCh, shutdownCh) @@ -293,7 +293,7 @@ func (s *SequentialScheduler[T]) processTaskQueue( if !queue.IsEmpty() { s.executeTask(queue) } else { - deleted := s.queues.RemoveIf(queue.ID(), func(key interface{}, value interface{}) bool { + deleted := s.queues.RemoveIf(queue.ID(), func(key any, value any) bool { return value.(SequentialTaskQueue[T]).IsEmpty() }) if deleted { @@ -354,7 +354,7 @@ LoopDrainQueues: for !queue.IsEmpty() { queue.Remove().Abort() } - deleted := s.queues.RemoveIf(queue.ID(), func(key interface{}, value interface{}) bool { + deleted := s.queues.RemoveIf(queue.ID(), func(key any, value any) bool { return value.(SequentialTaskQueue[T]).IsEmpty() }) if deleted { diff --git a/common/tasks/sequential_scheduler_test.go b/common/tasks/sequential_scheduler_test.go index ee2e468921d..1ae6f7fb276 100644 --- a/common/tasks/sequential_scheduler_test.go +++ b/common/tasks/sequential_scheduler_test.go @@ -69,7 +69,7 @@ func (s *sequentialSchedulerSuite) TestSubmitProcess_Running_Panic_ShouldCapture mockTask.EXPECT().Execute().DoAndReturn(func() { panic("random panic") }).Times(1) - mockTask.EXPECT().Nack(gomock.Any()).Do(func(arg interface{}) { testWaitGroup.Done() }).Times(1) + mockTask.EXPECT().Nack(gomock.Any()).Do(func(arg any) { testWaitGroup.Done() }).Times(1) s.scheduler.Submit(mockTask) @@ -146,9 +146,9 @@ func (s *sequentialSchedulerSuite) TestParallelSubmitProcess() { startWaitGroup.Add(numSubmitter) - for i := 0; i < numSubmitter; i++ { + for range numSubmitter { channel := make(chan *MockTask, numTasks) - for j := 0; j < numTasks; j++ { + for j := range numTasks { mockTask := NewMockTask(s.controller) mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() switch j % 2 { @@ -300,14 +300,14 @@ func (s *sequentialSchedulerSuite) TestTrySubmitConcurrent() { goroutineStartWG.Add(numGoroutines) goroutineEndWG.Add(numGoroutines) - for goroutineID := 0; goroutineID < numGoroutines; goroutineID++ { + for goroutineID := range numGoroutines { go func(gID int) { defer goroutineEndWG.Done() // Create tasks with mock expectations tasks := make([]*MockTask, tasksPerGoroutine) - for taskIdx := 0; taskIdx < tasksPerGoroutine; taskIdx++ { + for taskIdx := range tasksPerGoroutine { mockTask := NewMockTask(s.controller) mockTask.EXPECT().RetryPolicy().Return(s.retryPolicy).AnyTimes() mockTask.EXPECT().Execute().DoAndReturn(func() error { @@ -383,7 +383,7 @@ assertionsLabel: } func (s *sequentialSchedulerSuite) newTestProcessor() *SequentialScheduler[*MockTask] { - hashFn := func(key interface{}) uint32 { + hashFn := func(key any) uint32 { return 1 } factory := func(task *MockTask) SequentialTaskQueue[*MockTask] { @@ -403,7 +403,7 @@ func (s *sequentialSchedulerSuite) newTestProcessor() *SequentialScheduler[*Mock } func (s *sequentialSchedulerSuite) newTestProcessorWithQueueSize(queueSize int) *SequentialScheduler[*MockTask] { - hashFn := func(key interface{}) uint32 { + hashFn := func(key any) uint32 { return 1 } factory := func(task *MockTask) SequentialTaskQueue[*MockTask] { diff --git a/common/tasks/sequential_task_queue.go b/common/tasks/sequential_task_queue.go index d966d445b2e..c219cbfe1d4 100644 --- a/common/tasks/sequential_task_queue.go +++ b/common/tasks/sequential_task_queue.go @@ -5,7 +5,7 @@ type ( SequentialTaskQueue[T Task] interface { // ID return the ID of the queue, as well as the tasks inside (same) - ID() interface{} + ID() any // Add push a task to the task set Add(T) // Remove pop a task from the task set diff --git a/common/tasks/sequential_task_queue_test.go b/common/tasks/sequential_task_queue_test.go index d98ae15a3e7..72c37cbcc0a 100644 --- a/common/tasks/sequential_task_queue_test.go +++ b/common/tasks/sequential_task_queue_test.go @@ -12,7 +12,7 @@ func newTestSequentialTaskQueue[T Task](id, capacity int) SequentialTaskQueue[T] } } -func (s *testSequentialTaskQueue[T]) ID() interface{} { +func (s *testSequentialTaskQueue[T]) ID() any { return s.id } diff --git a/common/telemetry/config.go b/common/telemetry/config.go index d47bfcfa856..c4af9bef49d 100644 --- a/common/telemetry/config.go +++ b/common/telemetry/config.go @@ -55,7 +55,7 @@ type ( connection struct { Kind string Metadata metadata - Spec interface{} `yaml:"-"` + Spec any `yaml:"-"` } grpcconn struct { @@ -86,7 +86,7 @@ type ( Protocol string } Metadata metadata - Spec interface{} `yaml:"-"` + Spec any `yaml:"-"` } otlpGrpcExporter struct { diff --git a/common/testing/event_generator.go b/common/testing/event_generator.go index 6f26012c712..c0498232833 100644 --- a/common/testing/event_generator.go +++ b/common/testing/event_generator.go @@ -21,8 +21,8 @@ type ( name string isStrictOnNextVertex bool maxNextGeneration int - dataFunc func(...interface{}) interface{} - data interface{} + dataFunc func(...any) any + data any } // HistoryEventModel is a graph represents relationships among history event types @@ -50,7 +50,7 @@ type ( HistoryEventEdge struct { startVertex Vertex endVertex Vertex - condition func(...interface{}) bool + condition func(...any) bool action func() } @@ -303,7 +303,7 @@ func (g *EventGenerator) randomNextVertex( count := g.dice.Intn(nextVertex.GetMaxNextVertex()) + 1 res := make([]Vertex, 0) latestVertex := g.previousVertices[len(g.previousVertices)-1] - for i := 0; i < count; i++ { + for range count { endVertex := g.pickRandomVertex(nextVertex) endVertex.GenerateData(nextVertex.GetData(), latestVertex.GetData(), g.version) latestVertex = endVertex @@ -374,14 +374,14 @@ func (c HistoryEventEdge) GetEndVertex() Vertex { // SetCondition sets the condition to access this edge func (c *HistoryEventEdge) SetCondition( - condition func(...interface{}) bool, + condition func(...any) bool, ) { c.condition = condition } // GetCondition returns the condition -func (c HistoryEventEdge) GetCondition() func(...interface{}) bool { +func (c HistoryEventEdge) GetCondition() func(...any) bool { return c.condition } @@ -476,22 +476,22 @@ func (he HistoryEventVertex) GetMaxNextVertex() int { // SetDataFunc sets the data generation function func (he *HistoryEventVertex) SetDataFunc( - dataFunc func(...interface{}) interface{}, + dataFunc func(...any) any, ) { he.dataFunc = dataFunc } // GetDataFunc returns the data generation function -func (he HistoryEventVertex) GetDataFunc() func(...interface{}) interface{} { +func (he HistoryEventVertex) GetDataFunc() func(...any) any { return he.dataFunc } // GenerateData generates the data and return func (he *HistoryEventVertex) GenerateData( - input ...interface{}, -) interface{} { + input ...any, +) any { if he.dataFunc == nil { return nil @@ -502,7 +502,7 @@ func (he *HistoryEventVertex) GenerateData( } // GetData returns the vertex data -func (he HistoryEventVertex) GetData() interface{} { +func (he HistoryEventVertex) GetData() any { return he.data } diff --git a/common/testing/fakedata/fakedata.go b/common/testing/fakedata/fakedata.go index ddda15eabcd..80598e0543f 100644 --- a/common/testing/fakedata/fakedata.go +++ b/common/testing/fakedata/fakedata.go @@ -23,6 +23,6 @@ func init() { // // var shardInfo persistencespb.ShardInfo // _ = fakedata.FakeStruct(&shardInfo) -func FakeStruct(a interface{}) error { +func FakeStruct(a any) error { return faker.FakeData(a) } diff --git a/common/testing/generator_interface.go b/common/testing/generator_interface.go index 70a77031d83..4fef2fb46f0 100644 --- a/common/testing/generator_interface.go +++ b/common/testing/generator_interface.go @@ -57,10 +57,10 @@ type ( GetMaxNextVertex() int // SetVertexDataFunc sets a function to generate end vertex data - SetDataFunc(func(...interface{}) interface{}) - GetDataFunc() func(...interface{}) interface{} - GenerateData(...interface{}) interface{} - GetData() interface{} + SetDataFunc(func(...any) any) + GetDataFunc() func(...any) any + GenerateData(...any) any + GetData() any DeepCopy() Vertex } @@ -73,8 +73,8 @@ type ( SetEndVertex(Vertex) GetEndVertex() Vertex // Condition defines a function to determine if this connection is accessible - SetCondition(func(...interface{}) bool) - GetCondition() func(...interface{}) bool + SetCondition(func(...any) bool) + GetCondition() func(...any) bool // Action defines function to perform when the end vertex reached SetAction(func()) GetAction() func() diff --git a/common/testing/grpcinject/grpcinject.go b/common/testing/grpcinject/grpcinject.go index 4500ad3bff8..e14ada865e4 100644 --- a/common/testing/grpcinject/grpcinject.go +++ b/common/testing/grpcinject/grpcinject.go @@ -33,7 +33,7 @@ func (i *Interceptor) Unary() grpc.UnaryClientInterceptor { return func( ctx context.Context, method string, - req, reply interface{}, + req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, diff --git a/common/testing/history_event_util.go b/common/testing/history_event_util.go index 5ca6ca1b9db..84a9944436f 100644 --- a/common/testing/history_event_util.go +++ b/common/testing/history_event_util.go @@ -43,7 +43,7 @@ func InitializeHistoryEventGenerator( generator := NewEventGenerator(time.Now().UnixNano()) generator.SetVersion(defaultVersion) // Functions - notPendingWorkflowTask := func(input ...interface{}) bool { + notPendingWorkflowTask := func(input ...any) bool { count := 0 history := input[0].([]Vertex) for _, e := range history { @@ -58,7 +58,7 @@ func InitializeHistoryEventGenerator( } return count <= 0 } - containActivityComplete := func(input ...interface{}) bool { + containActivityComplete := func(input ...any) bool { history := input[0].([]Vertex) for _, e := range history { if e.GetName() == enumspb.EVENT_TYPE_ACTIVITY_TASK_COMPLETED.String() { @@ -67,7 +67,7 @@ func InitializeHistoryEventGenerator( } return false } - hasPendingActivity := func(input ...interface{}) bool { + hasPendingActivity := func(input ...any) bool { count := 0 history := input[0].([]Vertex) for _, e := range history { @@ -114,7 +114,7 @@ func InitializeHistoryEventGenerator( // Setup workflow task model historyEventModel := NewHistoryEventModel() workflowTaskSchedule := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED.String()) - workflowTaskSchedule.SetDataFunc(func(input ...interface{}) interface{} { + workflowTaskSchedule.SetDataFunc(func(input ...any) any { lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 version := input[2].(int64) @@ -132,7 +132,7 @@ func InitializeHistoryEventGenerator( }) workflowTaskStart := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED.String()) workflowTaskStart.SetIsStrictOnNextVertex(true) - workflowTaskStart.SetDataFunc(func(input ...interface{}) interface{} { + workflowTaskStart.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -147,7 +147,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowTaskFail := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_TASK_FAILED.String()) - workflowTaskFail.SetDataFunc(func(input ...interface{}) interface{} { + workflowTaskFail.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -164,7 +164,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowTaskTimedOut := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_TASK_TIMED_OUT.String()) - workflowTaskTimedOut.SetDataFunc(func(input ...interface{}) interface{} { + workflowTaskTimedOut.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -179,7 +179,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowTaskComplete := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED.String()) - workflowTaskComplete.SetDataFunc(func(input ...interface{}) interface{} { + workflowTaskComplete.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -211,7 +211,7 @@ func InitializeHistoryEventGenerator( workflowModel := NewHistoryEventModel() workflowStart := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED.String()) - workflowStart.SetDataFunc(func(input ...interface{}) interface{} { + workflowStart.SetDataFunc(func(input ...any) any { historyEvent := getDefaultHistoryEvent(1, defaultVersion) historyEvent.EventType = enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED historyEvent.Attributes = &historypb.HistoryEvent_WorkflowExecutionStartedEventAttributes{WorkflowExecutionStartedEventAttributes: &historypb.WorkflowExecutionStartedEventAttributes{ @@ -232,7 +232,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowSignal := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED.String()) - workflowSignal.SetDataFunc(func(input ...interface{}) interface{} { + workflowSignal.SetDataFunc(func(input ...any) any { lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 version := input[2].(int64) @@ -245,7 +245,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowComplete := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED.String()) - workflowComplete.SetDataFunc(func(input ...interface{}) interface{} { + workflowComplete.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) eventID := lastEvent.GetEventId() + 1 version := input[2].(int64) @@ -257,7 +257,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) continueAsNew := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW.String()) - continueAsNew.SetDataFunc(func(input ...interface{}) interface{} { + continueAsNew.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) eventID := lastEvent.GetEventId() + 1 version := input[2].(int64) @@ -280,7 +280,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowFail := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_FAILED.String()) - workflowFail.SetDataFunc(func(input ...interface{}) interface{} { + workflowFail.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) eventID := lastEvent.GetEventId() + 1 version := input[2].(int64) @@ -292,7 +292,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowCancel := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED.String()) - workflowCancel.SetDataFunc(func(input ...interface{}) interface{} { + workflowCancel.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -305,7 +305,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowCancelRequest := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED.String()) - workflowCancelRequest.SetDataFunc(func(input ...interface{}) interface{} { + workflowCancelRequest.SetDataFunc(func(input ...any) any { lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 version := input[2].(int64) @@ -323,7 +323,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowTerminate := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED.String()) - workflowTerminate.SetDataFunc(func(input ...interface{}) interface{} { + workflowTerminate.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) eventID := lastEvent.GetEventId() + 1 version := input[2].(int64) @@ -336,7 +336,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) workflowTimedOut := NewHistoryEventVertex(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT.String()) - workflowTimedOut.SetDataFunc(func(input ...interface{}) interface{} { + workflowTimedOut.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) eventID := lastEvent.GetEventId() + 1 version := input[2].(int64) @@ -365,7 +365,7 @@ func InitializeHistoryEventGenerator( // Setup activity model activityModel := NewHistoryEventModel() activitySchedule := NewHistoryEventVertex(enumspb.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED.String()) - activitySchedule.SetDataFunc(func(input ...interface{}) interface{} { + activitySchedule.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -387,7 +387,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) activityStart := NewHistoryEventVertex(enumspb.EVENT_TYPE_ACTIVITY_TASK_STARTED.String()) - activityStart.SetDataFunc(func(input ...interface{}) interface{} { + activityStart.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -403,7 +403,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) activityComplete := NewHistoryEventVertex(enumspb.EVENT_TYPE_ACTIVITY_TASK_COMPLETED.String()) - activityComplete.SetDataFunc(func(input ...interface{}) interface{} { + activityComplete.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -418,7 +418,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) activityFail := NewHistoryEventVertex(enumspb.EVENT_TYPE_ACTIVITY_TASK_FAILED.String()) - activityFail.SetDataFunc(func(input ...interface{}) interface{} { + activityFail.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -434,7 +434,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) activityTimedOut := NewHistoryEventVertex(enumspb.EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT.String()) - activityTimedOut.SetDataFunc(func(input ...interface{}) interface{} { + activityTimedOut.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -453,7 +453,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) activityCancelRequest := NewHistoryEventVertex(enumspb.EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED.String()) - activityCancelRequest.SetDataFunc(func(input ...interface{}) interface{} { + activityCancelRequest.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -467,7 +467,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) activityCancel := NewHistoryEventVertex(enumspb.EVENT_TYPE_ACTIVITY_TASK_CANCELED.String()) - activityCancel.SetDataFunc(func(input ...interface{}) interface{} { + activityCancel.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -519,7 +519,7 @@ func InitializeHistoryEventGenerator( // Setup timer model timerModel := NewHistoryEventModel() timerStart := NewHistoryEventVertex(enumspb.EVENT_TYPE_TIMER_STARTED.String()) - timerStart.SetDataFunc(func(input ...interface{}) interface{} { + timerStart.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -534,7 +534,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) timerFired := NewHistoryEventVertex(enumspb.EVENT_TYPE_TIMER_FIRED.String()) - timerFired.SetDataFunc(func(input ...interface{}) interface{} { + timerFired.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -548,7 +548,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) timerCancel := NewHistoryEventVertex(enumspb.EVENT_TYPE_TIMER_CANCELED.String()) - timerCancel.SetDataFunc(func(input ...interface{}) interface{} { + timerCancel.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -576,7 +576,7 @@ func InitializeHistoryEventGenerator( // Setup child workflow model childWorkflowModel := NewHistoryEventModel() childWorkflowInitial := NewHistoryEventVertex(enumspb.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED.String()) - childWorkflowInitial.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowInitial.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -601,7 +601,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) childWorkflowInitialFail := NewHistoryEventVertex(enumspb.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_FAILED.String()) - childWorkflowInitialFail.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowInitialFail.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -620,7 +620,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) childWorkflowStart := NewHistoryEventVertex(enumspb.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED.String()) - childWorkflowStart.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowStart.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -640,7 +640,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) childWorkflowCancel := NewHistoryEventVertex(enumspb.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_CANCELED.String()) - childWorkflowCancel.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowCancel.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -661,7 +661,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) childWorkflowComplete := NewHistoryEventVertex(enumspb.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED.String()) - childWorkflowComplete.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowComplete.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -682,7 +682,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) childWorkflowFail := NewHistoryEventVertex(enumspb.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_FAILED.String()) - childWorkflowFail.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowFail.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -703,7 +703,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) childWorkflowTerminate := NewHistoryEventVertex(enumspb.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TERMINATED.String()) - childWorkflowTerminate.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowTerminate.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -724,7 +724,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) childWorkflowTimedOut := NewHistoryEventVertex(enumspb.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TIMED_OUT.String()) - childWorkflowTimedOut.SetDataFunc(func(input ...interface{}) interface{} { + childWorkflowTimedOut.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -774,7 +774,7 @@ func InitializeHistoryEventGenerator( // Setup external workflow model externalWorkflowModel := NewHistoryEventModel() externalWorkflowSignal := NewHistoryEventVertex(enumspb.EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED.String()) - externalWorkflowSignal.SetDataFunc(func(input ...interface{}) interface{} { + externalWorkflowSignal.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -795,7 +795,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) externalWorkflowSignalFailed := NewHistoryEventVertex(enumspb.EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED.String()) - externalWorkflowSignalFailed.SetDataFunc(func(input ...interface{}) interface{} { + externalWorkflowSignalFailed.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -816,7 +816,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) externalWorkflowSignaled := NewHistoryEventVertex(enumspb.EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED.String()) - externalWorkflowSignaled.SetDataFunc(func(input ...interface{}) interface{} { + externalWorkflowSignaled.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -835,7 +835,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) externalWorkflowCancel := NewHistoryEventVertex(enumspb.EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED.String()) - externalWorkflowCancel.SetDataFunc(func(input ...interface{}) interface{} { + externalWorkflowCancel.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -856,7 +856,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) externalWorkflowCancelFail := NewHistoryEventVertex(enumspb.EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED.String()) - externalWorkflowCancelFail.SetDataFunc(func(input ...interface{}) interface{} { + externalWorkflowCancelFail.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 @@ -877,7 +877,7 @@ func InitializeHistoryEventGenerator( return historyEvent }) externalWorkflowCanceled := NewHistoryEventVertex(enumspb.EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_CANCEL_REQUESTED.String()) - externalWorkflowCanceled.SetDataFunc(func(input ...interface{}) interface{} { + externalWorkflowCanceled.SetDataFunc(func(input ...any) any { lastEvent := input[0].(*historypb.HistoryEvent) lastGeneratedEvent := input[1].(*historypb.HistoryEvent) eventID := lastGeneratedEvent.GetEventId() + 1 diff --git a/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go b/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go index 0ff2f78a3a9..babc78844f3 100644 --- a/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go +++ b/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go @@ -122,6 +122,46 @@ func (mr *MockWorkflowServiceClientMockRecorder) CreateSchedule(ctx, in any, opt return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSchedule", reflect.TypeOf((*MockWorkflowServiceClient)(nil).CreateSchedule), varargs...) } +// CreateWorkerDeployment mocks base method. +func (m *MockWorkflowServiceClient) CreateWorkerDeployment(ctx context.Context, in *workflowservice.CreateWorkerDeploymentRequest, opts ...grpc.CallOption) (*workflowservice.CreateWorkerDeploymentResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateWorkerDeployment", varargs...) + ret0, _ := ret[0].(*workflowservice.CreateWorkerDeploymentResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateWorkerDeployment indicates an expected call of CreateWorkerDeployment. +func (mr *MockWorkflowServiceClientMockRecorder) CreateWorkerDeployment(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorkerDeployment", reflect.TypeOf((*MockWorkflowServiceClient)(nil).CreateWorkerDeployment), varargs...) +} + +// CreateWorkerDeploymentVersion mocks base method. +func (m *MockWorkflowServiceClient) CreateWorkerDeploymentVersion(ctx context.Context, in *workflowservice.CreateWorkerDeploymentVersionRequest, opts ...grpc.CallOption) (*workflowservice.CreateWorkerDeploymentVersionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateWorkerDeploymentVersion", varargs...) + ret0, _ := ret[0].(*workflowservice.CreateWorkerDeploymentVersionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateWorkerDeploymentVersion indicates an expected call of CreateWorkerDeploymentVersion. +func (mr *MockWorkflowServiceClientMockRecorder) CreateWorkerDeploymentVersion(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorkerDeploymentVersion", reflect.TypeOf((*MockWorkflowServiceClient)(nil).CreateWorkerDeploymentVersion), varargs...) +} + // CreateWorkflowRule mocks base method. func (m *MockWorkflowServiceClient) CreateWorkflowRule(ctx context.Context, in *workflowservice.CreateWorkflowRuleRequest, opts ...grpc.CallOption) (*workflowservice.CreateWorkflowRuleResponse, error) { m.ctrl.T.Helper() @@ -2062,6 +2102,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) UpdateWorkerConfig(ctx, in any, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkerConfig", reflect.TypeOf((*MockWorkflowServiceClient)(nil).UpdateWorkerConfig), varargs...) } +// UpdateWorkerDeploymentVersionComputeConfig mocks base method. +func (m *MockWorkflowServiceClient) UpdateWorkerDeploymentVersionComputeConfig(ctx context.Context, in *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest, opts ...grpc.CallOption) (*workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpdateWorkerDeploymentVersionComputeConfig", varargs...) + ret0, _ := ret[0].(*workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateWorkerDeploymentVersionComputeConfig indicates an expected call of UpdateWorkerDeploymentVersionComputeConfig. +func (mr *MockWorkflowServiceClientMockRecorder) UpdateWorkerDeploymentVersionComputeConfig(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkerDeploymentVersionComputeConfig", reflect.TypeOf((*MockWorkflowServiceClient)(nil).UpdateWorkerDeploymentVersionComputeConfig), varargs...) +} + // UpdateWorkerDeploymentVersionMetadata mocks base method. func (m *MockWorkflowServiceClient) UpdateWorkerDeploymentVersionMetadata(ctx context.Context, in *workflowservice.UpdateWorkerDeploymentVersionMetadataRequest, opts ...grpc.CallOption) (*workflowservice.UpdateWorkerDeploymentVersionMetadataResponse, error) { m.ctrl.T.Helper() @@ -2141,3 +2201,23 @@ func (mr *MockWorkflowServiceClientMockRecorder) UpdateWorkflowExecutionOptions( varargs := append([]any{ctx, in}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkflowExecutionOptions", reflect.TypeOf((*MockWorkflowServiceClient)(nil).UpdateWorkflowExecutionOptions), varargs...) } + +// ValidateWorkerDeploymentVersionComputeConfig mocks base method. +func (m *MockWorkflowServiceClient) ValidateWorkerDeploymentVersionComputeConfig(ctx context.Context, in *workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest, opts ...grpc.CallOption) (*workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ValidateWorkerDeploymentVersionComputeConfig", varargs...) + ret0, _ := ret[0].(*workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateWorkerDeploymentVersionComputeConfig indicates an expected call of ValidateWorkerDeploymentVersionComputeConfig. +func (mr *MockWorkflowServiceClientMockRecorder) ValidateWorkerDeploymentVersionComputeConfig(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateWorkerDeploymentVersionComputeConfig", reflect.TypeOf((*MockWorkflowServiceClient)(nil).ValidateWorkerDeploymentVersionComputeConfig), varargs...) +} diff --git a/common/testing/mocksdk/client_mock.go b/common/testing/mocksdk/client_mock.go index d9e9d46c0e3..83d786a7997 100644 --- a/common/testing/mocksdk/client_mock.go +++ b/common/testing/mocksdk/client_mock.go @@ -100,6 +100,20 @@ func (mr *MockClientMockRecorder) CompleteActivity(ctx, taskToken, result, err a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteActivity", reflect.TypeOf((*MockClient)(nil).CompleteActivity), ctx, taskToken, result, err) } +// CompleteActivityByActivityID mocks base method. +func (m *MockClient) CompleteActivityByActivityID(ctx context.Context, namespace, activityID, activityRunID string, result any, err error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CompleteActivityByActivityID", ctx, namespace, activityID, activityRunID, result, err) + ret0, _ := ret[0].(error) + return ret0 +} + +// CompleteActivityByActivityID indicates an expected call of CompleteActivityByActivityID. +func (mr *MockClientMockRecorder) CompleteActivityByActivityID(ctx, namespace, activityID, activityRunID, result, err any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteActivityByActivityID", reflect.TypeOf((*MockClient)(nil).CompleteActivityByActivityID), ctx, namespace, activityID, activityRunID, result, err) +} + // CompleteActivityByID mocks base method. func (m *MockClient) CompleteActivityByID(ctx context.Context, namespace, workflowID, runID, activityID string, result any, err error) error { m.ctrl.T.Helper() @@ -114,6 +128,21 @@ func (mr *MockClientMockRecorder) CompleteActivityByID(ctx, namespace, workflowI return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteActivityByID", reflect.TypeOf((*MockClient)(nil).CompleteActivityByID), ctx, namespace, workflowID, runID, activityID, result, err) } +// CountActivities mocks base method. +func (m *MockClient) CountActivities(ctx context.Context, options client.CountActivitiesOptions) (*client.CountActivitiesResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CountActivities", ctx, options) + ret0, _ := ret[0].(*client.CountActivitiesResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountActivities indicates an expected call of CountActivities. +func (mr *MockClientMockRecorder) CountActivities(ctx, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountActivities", reflect.TypeOf((*MockClient)(nil).CountActivities), ctx, options) +} + // CountWorkflow mocks base method. func (m *MockClient) CountWorkflow(ctx context.Context, request *workflowservice.CountWorkflowExecutionsRequest) (*workflowservice.CountWorkflowExecutionsResponse, error) { m.ctrl.T.Helper() @@ -203,6 +232,26 @@ func (mr *MockClientMockRecorder) DescribeWorkflowExecution(ctx, workflowID, run return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeWorkflowExecution", reflect.TypeOf((*MockClient)(nil).DescribeWorkflowExecution), ctx, workflowID, runID) } +// ExecuteActivity mocks base method. +func (m *MockClient) ExecuteActivity(ctx context.Context, options client.StartActivityOptions, activity any, args ...any) (client.ActivityHandle, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, options, activity} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExecuteActivity", varargs...) + ret0, _ := ret[0].(client.ActivityHandle) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteActivity indicates an expected call of ExecuteActivity. +func (mr *MockClientMockRecorder) ExecuteActivity(ctx, options, activity any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, options, activity}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteActivity", reflect.TypeOf((*MockClient)(nil).ExecuteActivity), varargs...) +} + // ExecuteWorkflow mocks base method. func (m *MockClient) ExecuteWorkflow(ctx context.Context, options client.StartWorkflowOptions, workflow any, args ...any) (client.WorkflowRun, error) { m.ctrl.T.Helper() @@ -223,6 +272,20 @@ func (mr *MockClientMockRecorder) ExecuteWorkflow(ctx, options, workflow any, ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteWorkflow", reflect.TypeOf((*MockClient)(nil).ExecuteWorkflow), varargs...) } +// GetActivityHandle mocks base method. +func (m *MockClient) GetActivityHandle(options client.GetActivityHandleOptions) client.ActivityHandle { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActivityHandle", options) + ret0, _ := ret[0].(client.ActivityHandle) + return ret0 +} + +// GetActivityHandle indicates an expected call of GetActivityHandle. +func (mr *MockClientMockRecorder) GetActivityHandle(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivityHandle", reflect.TypeOf((*MockClient)(nil).GetActivityHandle), options) +} + // GetSearchAttributes mocks base method. func (m *MockClient) GetSearchAttributes(ctx context.Context) (*workflowservice.GetSearchAttributesResponse, error) { m.ctrl.T.Helper() @@ -325,6 +388,21 @@ func (mr *MockClientMockRecorder) GetWorkflowUpdateHandle(ref any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkflowUpdateHandle", reflect.TypeOf((*MockClient)(nil).GetWorkflowUpdateHandle), ref) } +// ListActivities mocks base method. +func (m *MockClient) ListActivities(ctx context.Context, options client.ListActivitiesOptions) (client.ListActivitiesResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListActivities", ctx, options) + ret0, _ := ret[0].(client.ListActivitiesResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListActivities indicates an expected call of ListActivities. +func (mr *MockClientMockRecorder) ListActivities(ctx, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListActivities", reflect.TypeOf((*MockClient)(nil).ListActivities), ctx, options) +} + // ListArchivedWorkflow mocks base method. func (m *MockClient) ListArchivedWorkflow(ctx context.Context, request *workflowservice.ListArchivedWorkflowExecutionsRequest) (*workflowservice.ListArchivedWorkflowExecutionsResponse, error) { m.ctrl.T.Helper() diff --git a/common/testing/nettest/pipe_benchmark_test.go b/common/testing/nettest/pipe_benchmark_test.go index 6b1d72d2e0d..136c516d6d4 100644 --- a/common/testing/nettest/pipe_benchmark_test.go +++ b/common/testing/nettest/pipe_benchmark_test.go @@ -168,7 +168,7 @@ func (bi socketBenchmark) runClient(b *testing.B) { buf := make([]byte, len(msg)) - for i := 0; i < numMessages; i++ { + for range numMessages { _, err = c.Write([]byte(msg)) if err != nil { b.Fatal(err) diff --git a/common/testing/parallelsuite/guard.go b/common/testing/parallelsuite/guard.go new file mode 100644 index 00000000000..2bdb95c9973 --- /dev/null +++ b/common/testing/parallelsuite/guard.go @@ -0,0 +1,58 @@ +package parallelsuite + +import ( + "fmt" + "sync/atomic" + "testing" +) + +// guardT is a [require.TestingT] wrapper that detects mixing of assertions and Run. +// +// Before markHasSubtests: tracks assertion usage via Helper() (sets asserted flag). +// After markHasSubtests: panics on any assertion via Helper()/Errorf()/FailNow(). +type guardT struct { + *testing.T + name string + asserted atomic.Bool + hasSubtests atomic.Bool +} + +func (g *guardT) Helper() { + if g.hasSubtests.Load() { + panic(fmt.Sprintf( + "parallelsuite: assertion called on %q after Run() was called; "+ + "a test must either use assertions OR call Run(), not both — "+ + "use the callback parameter's assertions inside Run() instead", + g.name, + )) + } + g.asserted.Store(true) + g.T.Helper() +} + +func (g *guardT) Errorf(format string, args ...any) { + if g.hasSubtests.Load() { + g.Helper() // panics with clear message + } + g.T.Errorf(format, args...) +} + +func (g *guardT) FailNow() { + if g.hasSubtests.Load() { + g.Helper() // panics with clear message + } + g.T.FailNow() +} + +func (g *guardT) markHasSubtests() { + if g.hasSubtests.Swap(true) { + return + } + if g.asserted.Load() { + panic(fmt.Sprintf( + "parallelsuite: Run() called on %q after assertions were already used; "+ + "a test must either use assertions OR call Run(), not both", + g.name, + )) + } +} diff --git a/common/testing/parallelsuite/suite.go b/common/testing/parallelsuite/suite.go new file mode 100644 index 00000000000..027d80e2f4a --- /dev/null +++ b/common/testing/parallelsuite/suite.go @@ -0,0 +1,194 @@ +package parallelsuite + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" + testifysuite "github.com/stretchr/testify/suite" + "go.temporal.io/server/common/testing/historyrequire" + "go.temporal.io/server/common/testing/protorequire" +) + +// testingSuite is the constraint for suite types. +type testingSuite interface { + testifysuite.TestingSuite + copySuite(t *testing.T) testingSuite + initSuite(t *testing.T) +} + +// Suite provides parallel test execution with require-style (fail-fast) assertions. +// +// It enforces a strict rule: a test method (or subtest) must either use assertions +// directly OR create subtests via Run — not both. +type Suite[T testingSuite] struct { + testifyBase + *require.Assertions + protorequire.ProtoAssertions + historyrequire.HistoryRequire + + guardT guardT +} + +// copySuite creates a fresh suite instance initialized for the given *testing.T. +func (s *Suite[T]) copySuite(t *testing.T) testingSuite { + cp := reflect.New(reflect.TypeFor[T]().Elem()).Interface().(T) + cp.initSuite(t) + return cp +} + +func (s *Suite[T]) initSuite(t *testing.T) { + g := &s.guardT + g.name = t.Name() + g.T = t + g.asserted.Store(false) + g.hasSubtests.Store(false) + s.Assertions = require.New(g) + s.ProtoAssertions = protorequire.New(g) + s.HistoryRequire = historyrequire.New(g) +} + +// T returns the *testing.T, panicking if the guard has been sealed. +func (s *Suite[T]) T() *testing.T { + if s.guardT.hasSubtests.Load() { + panic("parallelsuite: do not call T() after Run(); use the subtest callback's parameter instead") + } + return s.guardT.T +} + +// Run creates a parallel subtest. The callback receives a fresh copy of the +// concrete suite type, initialized for the subtest's *testing.T. +func (s *Suite[T]) Run(name string, fn func(T)) bool { + pt := s.guardT.T // grab T before sealing + s.guardT.markHasSubtests() + return pt.Run(name, func(t *testing.T) { + t.Parallel() //nolint:testifylint // parallelsuite intentionally supports parallel subtests + fn(s.copySuite(t).(T)) + }) +} + +// Run discovers and runs all exported Test* methods on the given suite in parallel. +// +// Each method gets its own fresh suite instance initialized for the subtest's +// *testing.T. Both the suite-level test and each method subtest are marked as +// parallel. Any sequential setup must happen before calling Run. +// +// The suite must embed [Suite] and have no other fields. +func Run[T testingSuite](t *testing.T, s T, args ...any) { + t.Helper() + + typ := reflect.TypeOf(s) + if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct { + panic(fmt.Sprintf("parallelsuite.Run: suite must be a pointer to a struct, got %v", typ)) + } + structType := typ.Elem() + + validateSuiteStruct(structType) + + methods := discoverTestMethods(typ, structType, args) + if len(methods) == 0 { + panic(fmt.Sprintf("parallelsuite.Run: suite %s has no Test* methods", structType.Name())) + } + + argVals := make([]reflect.Value, len(args)) + for i, a := range args { + argVals[i] = reflect.ValueOf(a) + } + + t.Parallel() + + for _, method := range methods { + t.Run(method.Name, func(t *testing.T) { + t.Parallel() + + cpS := s.copySuite(t) + callArgs := append([]reflect.Value{reflect.ValueOf(cpS)}, argVals...) + method.Func.Call(callArgs) + }) + } +} + +var inheritedMethods map[string]bool + +func init() { + type ds struct{ Suite[*ds] } + ptrType := reflect.TypeOf(&ds{}) + inheritedMethods = make(map[string]bool, ptrType.NumMethod()) + for i := 0; i < ptrType.NumMethod(); i++ { + inheritedMethods[ptrType.Method(i).Name] = true + } +} + +func validateSuiteStruct(structType reflect.Type) { + if !strings.HasSuffix(structType.Name(), "Suite") { + panic(fmt.Sprintf("parallelsuite.Run: struct name %q must end with \"Suite\"", structType.Name())) + } + + if structType.NumField() != 1 { + panic(fmt.Sprintf( + "parallelsuite.Run: suite %s must have no fields besides the embedded parallelsuite.Suite; "+ + "pass parameters as extra args to Run instead (got %d fields)", + structType.Name(), structType.NumField(), + )) + } + f := structType.Field(0) + if !f.Anonymous { + panic(fmt.Sprintf( + "parallelsuite.Run: suite %s must embed parallelsuite.Suite, found named field %q", + structType.Name(), f.Name, + )) + } +} + +func discoverTestMethods(ptrType, structType reflect.Type, args []any) []reflect.Method { + expectedNumIn := 1 + len(args) + + for i := 0; i < ptrType.NumMethod(); i++ { + name := ptrType.Method(i).Name + if !strings.HasPrefix(name, "Test") && !inheritedMethods[name] { + panic(fmt.Sprintf( + "parallelsuite.Run: suite %s has exported method %s that does not start with Test; "+ + "use a package-level function instead", + structType.Name(), name, + )) + } + } + + var methods []reflect.Method + for i := 0; i < ptrType.NumMethod(); i++ { + method := ptrType.Method(i) + if !strings.HasPrefix(method.Name, "Test") { + continue + } + + mt := method.Type + if mt.NumOut() != 0 { + panic(fmt.Sprintf( + "parallelsuite.Run: method %s.%s must not have return values, got %v", + structType.Name(), method.Name, mt, + )) + } + if mt.NumIn() != expectedNumIn { + panic(fmt.Sprintf( + "parallelsuite.Run: method %s.%s has wrong number of parameters: expected %d, got %d (%v)", + structType.Name(), method.Name, expectedNumIn, mt.NumIn(), mt, + )) + } + + for j, a := range args { + paramType := mt.In(1 + j) + argType := reflect.TypeOf(a) + if !argType.AssignableTo(paramType) { + panic(fmt.Sprintf( + "parallelsuite.Run: method %s.%s parameter %d has type %v but Run arg has type %v", + structType.Name(), method.Name, j+1, paramType, argType, + )) + } + } + + methods = append(methods, method) + } + return methods +} diff --git a/common/testing/parallelsuite/suite_test.go b/common/testing/parallelsuite/suite_test.go new file mode 100644 index 00000000000..7bc90322759 --- /dev/null +++ b/common/testing/parallelsuite/suite_test.go @@ -0,0 +1,119 @@ +package parallelsuite + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type validSuite struct{ Suite[*validSuite] } + +func (s *validSuite) TestA() { + s.NotNil(s.T()) +} + +type validWithArgsSuite struct{ Suite[*validWithArgsSuite] } + +func (s *validWithArgsSuite) TestA(name string, count int) { + s.Equal("hello", name) + s.Equal(42, count) +} + +type noTestMethodsSuite struct{ Suite[*noTestMethodsSuite] } + +type wrongSigSuite struct{ Suite[*wrongSigSuite] } + +func (s *wrongSigSuite) TestBad(t *testing.T) {} //nolint:unused + +type badNameTests struct{ Suite[*badNameTests] } + +func (s *badNameTests) TestA() {} //nolint:unused + +type exportedNonTestSuite struct{ Suite[*exportedNonTestSuite] } + +func (s *exportedNonTestSuite) TestA() {} +func (s *exportedNonTestSuite) Helper() {} //nolint:unused + +type hasExtraFieldsSuite struct { + Suite[*hasExtraFieldsSuite] + x int //nolint:unused +} + +func (s *hasExtraFieldsSuite) TestA() {} //nolint:unused + +type setupTestSuite struct{ Suite[*setupTestSuite] } + +func (s *setupTestSuite) TestA() {} +func (s *setupTestSuite) SetupTest() {} //nolint:unused + +type sealAfterRunSuite struct{ Suite[*sealAfterRunSuite] } + +func (s *sealAfterRunSuite) TestAssertionAfterRun() { + // Calling Run seals the parent's assertions and T(). + s.Run("subtest", func(s *sealAfterRunSuite) { + s.NotNil(s.T()) // subtest assertions work fine + }) + + t := s.guardT.T + + // After Run: even passing assertions panic. + require.Panics(t, func() { s.NotNil(t) }) + + // T() also panics after Run. + require.Panics(t, func() { s.T() }) +} + +type sealRunAfterAssertSuite struct { + Suite[*sealRunAfterAssertSuite] +} + +func (s *sealRunAfterAssertSuite) TestRunAfterAssertion() { + // Use an assertion first. + s.NotNil(s.T()) + + t := s.guardT.T + + // Calling Run after assertions panics. + require.Panics(t, func() { + s.Run("should-not-run", func(*sealRunAfterAssertSuite) {}) + }) +} + +func TestRun_AcceptsSuite(t *testing.T) { + t.Run("no args", func(t *testing.T) { + require.NotPanics(t, func() { Run(t, &validSuite{}) }) + }) + t.Run("with args", func(t *testing.T) { + require.NotPanics(t, func() { Run(t, &validWithArgsSuite{}, "hello", 42) }) + }) +} + +func TestRun_RejectsSuite(t *testing.T) { + t.Run("no Test methods", func(t *testing.T) { + require.Panics(t, func() { Run(t, &noTestMethodsSuite{}) }) + }) + t.Run("wrong method signature", func(t *testing.T) { + require.Panics(t, func() { Run(t, &wrongSigSuite{}) }) + }) + t.Run("extra fields", func(t *testing.T) { + require.Panics(t, func() { Run(t, &hasExtraFieldsSuite{}) }) + }) + t.Run("name not ending in Suite", func(t *testing.T) { + require.Panics(t, func() { Run(t, &badNameTests{}) }) + }) + t.Run("non-Test exported method", func(t *testing.T) { + require.Panics(t, func() { Run(t, &exportedNonTestSuite{}) }) + }) + t.Run("SetupTest forbidden", func(t *testing.T) { + require.Panics(t, func() { Run(t, &setupTestSuite{}) }) + }) +} + +func TestGuardSeal(t *testing.T) { + t.Run("assertion after Run", func(t *testing.T) { + Run(t, &sealAfterRunSuite{}) + }) + t.Run("Run after assertion", func(t *testing.T) { + Run(t, &sealRunAfterAssertSuite{}) + }) +} diff --git a/common/testing/parallelsuite/testify.go b/common/testing/parallelsuite/testify.go new file mode 100644 index 00000000000..67d46967045 --- /dev/null +++ b/common/testing/parallelsuite/testify.go @@ -0,0 +1,37 @@ +package parallelsuite + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + testifysuite "github.com/stretchr/testify/suite" +) + +// testifyBase wraps testify's suite.Suite so that Suite's *require.Assertions +// shadows testify's *assert.Assertions in Go's embedding resolution. +// It disables all testify suite methods with panicking stubs. +type testifyBase struct { + testifysuite.Suite +} + +// Deprecated: SetT is managed internally by [Run]. Do not call directly. +func (b *testifyBase) SetT(_ *testing.T) { + panic("parallelsuite: do not call SetT directly; it is managed by parallelsuite.Run") +} + +// Deprecated: SetS is managed internally by [Run]. Do not call directly. +func (b *testifyBase) SetS(_ testifysuite.TestingSuite) { + panic("parallelsuite: do not call SetS directly; it is managed by parallelsuite.Run") +} + +// Deprecated: Assert returns non-fatal assertions which are not supported. +// Use s.NoError, s.Equal, etc. directly (require semantics). +func (b *testifyBase) Assert() *assert.Assertions { + panic("parallelsuite: do not use Assert(); use s.NoError, s.Equal, etc. directly") +} + +// Deprecated: Require bypasses the guard mechanism. Use s.NoError, s.Equal, etc. directly. +func (b *testifyBase) Require() *require.Assertions { + panic("parallelsuite: do not use Require(); use s.NoError, s.Equal, etc. directly") +} diff --git a/common/testing/protoassert/assert.go b/common/testing/protoassert/assert.go index 62a0325329d..5de8acc84e0 100644 --- a/common/testing/protoassert/assert.go +++ b/common/testing/protoassert/assert.go @@ -55,7 +55,7 @@ func ProtoSliceEqual[T proto.Message](t assert.TestingT, a []T, b []T) bool { if len(a) != len(b) { return false } - for i := 0; i < len(a); i++ { + for i := range a { if diff := cmp.Diff(a[i], b[i], protocmp.Transform()); diff != "" { return assert.Fail(t, fmt.Sprintf("Proto mismatch at index %d (-want +got):\n%v", i, diff)) } diff --git a/common/testing/protoassert/pretty_print.go b/common/testing/protoassert/pretty_print.go index 56dccda56b5..9ea7ccea2d9 100644 --- a/common/testing/protoassert/pretty_print.go +++ b/common/testing/protoassert/pretty_print.go @@ -119,7 +119,7 @@ func prettyPrintPointer(b *strings.Builder, v reflect.Value, depth int) { func indent(b *strings.Builder, depth int) { b.WriteByte('\n') - for i := 0; i < depth; i++ { + for range depth { b.WriteString(" ") } } diff --git a/common/testing/protoassert/testify_assert.go b/common/testing/protoassert/testify_assert.go index 9063e5d2bcd..2b6a6bc1f9c 100644 --- a/common/testing/protoassert/testify_assert.go +++ b/common/testing/protoassert/testify_assert.go @@ -30,7 +30,7 @@ func formatListDiff(listA, listB any, extraA, extraB any) string { return msg.String() } -func diffLists(listA, listB interface{}) (extraA, extraB []interface{}) { +func diffLists(listA, listB any) (extraA, extraB []any) { aValue := reflect.ValueOf(listA) bValue := reflect.ValueOf(listB) @@ -39,10 +39,10 @@ func diffLists(listA, listB interface{}) (extraA, extraB []interface{}) { // Mark indexes in bValue that we already used visited := make([]bool, bLen) - for i := 0; i < aLen; i++ { + for i := range aLen { element := aValue.Index(i).Interface() found := false - for j := 0; j < bLen; j++ { + for j := range bLen { if visited[j] { continue } @@ -57,7 +57,7 @@ func diffLists(listA, listB interface{}) (extraA, extraB []interface{}) { } } - for j := 0; j < bLen; j++ { + for j := range bLen { if visited[j] { continue } @@ -67,7 +67,7 @@ func diffLists(listA, listB interface{}) (extraA, extraB []interface{}) { return } -func isEmpty(object interface{}) bool { +func isEmpty(object any) bool { // get nil case out of the way if object == nil { @@ -96,7 +96,7 @@ func isEmpty(object interface{}) bool { } // isList checks that the provided value is array or slice. -func isList(t assert.TestingT, list interface{}, msgAndArgs ...interface{}) (ok bool) { +func isList(t assert.TestingT, list any, msgAndArgs ...any) (ok bool) { kind := reflect.TypeOf(list).Kind() if kind != reflect.Array && kind != reflect.Slice { return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s, expecting array or slice", list, kind), diff --git a/common/testing/taskpoller/taskpoller.go b/common/testing/taskpoller/taskpoller.go index fd975933fdb..7bb9c72cfc0 100644 --- a/common/testing/taskpoller/taskpoller.go +++ b/common/testing/taskpoller/taskpoller.go @@ -9,6 +9,7 @@ import ( "github.com/nexus-rpc/sdk-go/nexus" nexuspb "go.temporal.io/api/nexus/v1" + commandpb "go.temporal.io/api/command/v1" enumspb "go.temporal.io/api/enums/v1" failurepb "go.temporal.io/api/failure/v1" historypb "go.temporal.io/api/history/v1" @@ -55,6 +56,17 @@ var ( DrainWorkflowTask = func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { return &workflowservice.RespondWorkflowTaskCompletedRequest{}, nil } + // CompleteWorkflowHandler is a workflow task handler that always completes the workflow. + CompleteWorkflowHandler = func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, + }, + }}, + }, nil + } // CompleteActivityTask returns a RespondActivityTaskCompletedRequest with an auto-generated `Result` from `tv.Any().Payloads()`. CompleteActivityTask = func(tv *testvars.TestVars) func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) { return func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) { diff --git a/common/testing/testhooks/hooks.go b/common/testing/testhooks/hooks.go new file mode 100644 index 00000000000..8cbb2545e44 --- /dev/null +++ b/common/testing/testhooks/hooks.go @@ -0,0 +1,43 @@ +package testhooks + +import ( + "time" + + "go.temporal.io/server/common/namespace" +) + +// Test hook keys with their return type and scope. +// Try to avoid global scope as it requires a dedicated test cluster. +var ( + MatchingDisableSyncMatch = newKey[bool, namespace.ID]() + MatchingLBForceReadPartition = newKey[int, namespace.ID]() + MatchingLBForceWritePartition = newKey[int, namespace.ID]() + UpdateWithStartInBetweenLockAndStart = newKey[func(), namespace.ID]() + UpdateWithStartOnClosingWorkflowRetry = newKey[func(), namespace.ID]() + TaskQueuesInDeploymentSyncBatchSize = newKey[int, global]() + MatchingIgnoreRoutingConfigRevisionCheck = newKey[bool, namespace.ID]() + MatchingDeploymentRegisterErrorBackoff = newKey[time.Duration, namespace.ID]() + MatchingForwardTaskDelay = newKey[time.Duration, namespace.ID]() +) + +// keyID is a unique identifier for a key, used as a map key. +type keyID = int64 + +// global is the scope type for global hooks. +type global struct{} + +// GlobalScope is the singleton value for global hooks. +var GlobalScope = global{} + +// ScopeType indicates the scope of a hook at runtime. +type ScopeType int + +const ( + ScopeNamespace ScopeType = iota + ScopeGlobal +) + +type Key[T any, S any] struct { + id keyID + scopeType ScopeType +} diff --git a/common/testing/testhooks/key.go b/common/testing/testhooks/key.go deleted file mode 100644 index 43df33b8383..00000000000 --- a/common/testing/testhooks/key.go +++ /dev/null @@ -1,14 +0,0 @@ -package testhooks - -type Key int - -const ( - MatchingDisableSyncMatch Key = iota - MatchingLBForceReadPartition - MatchingLBForceWritePartition - UpdateWithStartInBetweenLockAndStart - UpdateWithStartOnClosingWorkflowRetry - TaskQueuesInDeploymentSyncBatchSize - MatchingIgnoreRoutingConfigRevisionCheck - MatchingDeploymentRegisterErrorBackoff -) diff --git a/common/testing/testhooks/noop_impl.go b/common/testing/testhooks/noop_impl.go index a83cbf97ca4..e2ae8eafdaf 100644 --- a/common/testing/testhooks/noop_impl.go +++ b/common/testing/testhooks/noop_impl.go @@ -26,7 +26,7 @@ func NewTestHooks() TestHooks { // false, which hopefully the compiler will inline and remove the hook as dead code. // // TestHooks should be used very sparingly, see comment on TestHooks. -func Get[T any](_ TestHooks, _ Key) (T, bool) { +func Get[T any, S any](_ TestHooks, _ Key[T, S], _ S) (T, bool) { var zero T return zero, false } @@ -34,10 +34,28 @@ func Get[T any](_ TestHooks, _ Key) (T, bool) { // Call calls a func() hook if present. // // TestHooks should be used very sparingly, see comment on TestHooks. -func Call(_ TestHooks, _ Key) { +func Call[S any](_ TestHooks, _ Key[func(), S], _ S) {} + +// Hook is an empty stub in production mode. NewHook and its methods are only available with -tags=test_dep. +type Hook struct{} + +func NewHook[T any, S any](_ Key[T, S], _ T) Hook { + panic("testhooks.NewHook called but TestHooks are not enabled: use -tags=test_dep when running `go test`") +} + +func (h Hook) Scope() ScopeType { + panic("testhooks.Hook used but TestHooks are not enabled: use -tags=test_dep when running `go test`") +} + +func (h Hook) Apply(_ TestHooks, _ any) func() { + panic("testhooks.Hook used but TestHooks are not enabled: use -tags=test_dep when running `go test`") } // Set is only to be used by test code together with the test_dep build tag. -func Set[T any](_ TestHooks, _ Key, _ T) func() { +func Set[T any, S any](_ TestHooks, _ Key[T, S], _ T, _ any) func() { panic("testhooks.Set called but TestHooks are not enabled: use -tags=test_dep when running `go test`") } + +func newKey[T any, S any]() Key[T, S] { + return Key[T, S]{} +} diff --git a/common/testing/testhooks/test_impl.go b/common/testing/testhooks/test_impl.go index f5dac36a2c4..b7a5c28d8f2 100644 --- a/common/testing/testhooks/test_impl.go +++ b/common/testing/testhooks/test_impl.go @@ -4,7 +4,9 @@ package testhooks import ( "sync" + "sync/atomic" + "go.temporal.io/server/common/namespace" "go.uber.org/fx" ) @@ -12,6 +14,11 @@ var Module = fx.Options( fx.Provide(NewTestHooks), ) +type hookKey struct { + id keyID + scope any +} + // TestHooks holds a registry of active test hooks. It should be obtained through fx and // used with Get and Set. // @@ -29,14 +36,14 @@ func NewTestHooks() TestHooks { // Get gets the value of a test hook from the registry. // // TestHooks should be used sparingly, see comment on TestHooks. -func Get[T any](th TestHooks, key Key) (T, bool) { +func Get[T any, S any](th TestHooks, key Key[T, S], scope S) (T, bool) { var zero T if th.data == nil { + // This means TestHooks wasn't created via NewTestHooks. Ignore. return zero, false } - if val, ok := th.data.Load(key); ok { - // this is only used in test so we want to panic on type mismatch: - return val.(T), ok //nolint:revive + if val, ok := th.data.Load(hookKey{key.id, scope}); ok { + return val.(T), true //nolint:revive } return zero, false } @@ -44,15 +51,53 @@ func Get[T any](th TestHooks, key Key) (T, bool) { // Call calls a func() hook if present. // // TestHooks should be used sparingly, see comment on TestHooks. -func Call(th TestHooks, key Key) { - if hook, ok := Get[func()](th, key); ok { +func Call[S any](th TestHooks, key Key[func(), S], scope S) { + if hook, ok := Get(th, key, scope); ok { hook() } } -// Set sets a test hook to a value and returns a cleanup function to unset it. -// Calls to Set and the cleanup function should form a stack. -func Set[T any](th TestHooks, key Key, val T) func() { - th.data.Store(key, val) - return func() { th.data.Delete(key) } +// Hook bundles a key and value for type-erased use in InjectHook. +type Hook struct { + scopeType ScopeType + apply func(TestHooks, any) func() +} + +func (h Hook) Scope() ScopeType { return h.scopeType } +func (h Hook) Apply(th TestHooks, scope any) func() { return h.apply(th, scope) } + +func NewHook[T any, S any](key Key[T, S], value T) Hook { + return Hook{ + scopeType: key.scopeType, + apply: func(th TestHooks, scope any) func() { + if _, ok := scope.(S); !ok { + panic("testhooks: scope type mismatch") + } + return Set(th, key, value, scope) + }, + } +} + +// Set sets a test hook to a value with the given scope and returns a cleanup function to unset it. +func Set[T any, S any](th TestHooks, key Key[T, S], val T, scope any) func() { + mk := hookKey{key.id, scope} + th.data.Store(mk, val) + return func() { th.data.Delete(mk) } +} + +// keyCounter provides unique IDs for keys. +var keyCounter atomic.Int64 + +func newKey[T any, S any]() Key[T, S] { + var zero S + var s ScopeType + switch any(zero).(type) { + case namespace.ID: + s = ScopeNamespace + case global: + s = ScopeGlobal + default: + panic("testhooks: unknown scope type") + } + return Key[T, S]{id: keyCounter.Add(1), scopeType: s} } diff --git a/common/testing/testhooks/test_impl_test.go b/common/testing/testhooks/test_impl_test.go index f6d17309f9b..b35d1335a5d 100644 --- a/common/testing/testhooks/test_impl_test.go +++ b/common/testing/testhooks/test_impl_test.go @@ -6,30 +6,31 @@ import ( "testing" "github.com/stretchr/testify/require" + "go.temporal.io/server/common/namespace" ) func TestGet(t *testing.T) { th := NewTestHooks() t.Run("returns false for unset key", func(t *testing.T) { - _, ok := Get[int](th, MatchingDisableSyncMatch) + _, ok := Get(th, MatchingDisableSyncMatch, namespace.ID("ns1")) require.False(t, ok) }) t.Run("returns set value", func(t *testing.T) { - cleanup := Set(th, MatchingDisableSyncMatch, 42) + cleanup := Set(th, MatchingLBForceReadPartition, 42, namespace.ID("ns1")) defer cleanup() - v, ok := Get[int](th, MatchingDisableSyncMatch) + v, ok := Get(th, MatchingLBForceReadPartition, namespace.ID("ns1")) require.True(t, ok) require.Equal(t, 42, v) }) t.Run("cleanup removes value", func(t *testing.T) { - cleanup := Set(th, MatchingDisableSyncMatch, 42) + cleanup := Set(th, MatchingLBForceReadPartition, 42, namespace.ID("ns1")) cleanup() - _, ok := Get[int](th, MatchingDisableSyncMatch) + _, ok := Get(th, MatchingLBForceReadPartition, namespace.ID("ns1")) require.False(t, ok) }) } @@ -38,15 +39,70 @@ func TestCall(t *testing.T) { th := NewTestHooks() t.Run("does nothing for unset key", func(t *testing.T) { - Call(th, MatchingDisableSyncMatch) // should not panic + Call(th, UpdateWithStartInBetweenLockAndStart, namespace.ID("ns1")) // should not panic }) t.Run("calls set function", func(t *testing.T) { called := false - cleanup := Set(th, MatchingDisableSyncMatch, func() { called = true }) + cleanup := Set(th, UpdateWithStartInBetweenLockAndStart, func() { called = true }, namespace.ID("ns1")) defer cleanup() - Call(th, MatchingDisableSyncMatch) + Call(th, UpdateWithStartInBetweenLockAndStart, namespace.ID("ns1")) require.True(t, called) }) } + +func TestSet(t *testing.T) { + th := NewTestHooks() + + t.Run("namespace-scoped keys are isolated", func(t *testing.T) { + cleanup1 := Set(th, MatchingDisableSyncMatch, true, namespace.ID("ns1")) + defer cleanup1() + + cleanup2 := Set(th, MatchingDisableSyncMatch, false, namespace.ID("ns2")) + + v, ok := Get(th, MatchingDisableSyncMatch, namespace.ID("ns1")) + require.True(t, ok) + require.True(t, v) + + v, ok = Get(th, MatchingDisableSyncMatch, namespace.ID("ns2")) + require.True(t, ok) + require.False(t, v) + + _, ok = Get(th, MatchingDisableSyncMatch, namespace.ID("other")) + require.False(t, ok) + + // Cleanup only affects specific namespace + cleanup2() + _, ok = Get(th, MatchingDisableSyncMatch, namespace.ID("ns2")) + require.False(t, ok) + + v, ok = Get(th, MatchingDisableSyncMatch, namespace.ID("ns1")) + require.True(t, ok) + require.True(t, v) + }) + + t.Run("global-scoped keys", func(t *testing.T) { + cleanup := Set(th, TaskQueuesInDeploymentSyncBatchSize, 42, GlobalScope) + defer cleanup() + + v, ok := Get(th, TaskQueuesInDeploymentSyncBatchSize, GlobalScope) + require.True(t, ok) + require.Equal(t, 42, v) + }) + + t.Run("overwrites previous value for same scope", func(t *testing.T) { + cleanup1 := Set(th, MatchingLBForceReadPartition, 1, namespace.ID("ns1")) + cleanup2 := Set(th, MatchingLBForceReadPartition, 2, namespace.ID("ns1")) + + v, ok := Get(th, MatchingLBForceReadPartition, namespace.ID("ns1")) + require.True(t, ok) + require.Equal(t, 2, v) + + cleanup2() + _, ok = Get(th, MatchingLBForceReadPartition, namespace.ID("ns1")) + require.False(t, ok) + + cleanup1() // should not panic + }) +} diff --git a/common/testing/testlogger/testlogger.go b/common/testing/testlogger/testlogger.go index 4a3b66a3509..228536519be 100644 --- a/common/testing/testlogger/testlogger.go +++ b/common/testing/testlogger/testlogger.go @@ -5,6 +5,7 @@ import ( "container/list" "fmt" "os" + "path/filepath" "regexp" "runtime" "slices" @@ -199,10 +200,10 @@ func WithoutCaller() LoggerOption { // SetLogLevel overrides the temporal test log level during this test. func SetLogLevel(tt CleanupCapableT, level zapcore.Level) LoggerOption { return func(t *TestLogger) { - oldLevel := os.Getenv("TEMPORAL_TEST_LOG_LEVEL") - _ = os.Setenv("TEMPORAL_TEST_LOG_LEVEL", level.String()) + oldLevel := os.Getenv(log.TestLogLevelEnvVar) + _ = os.Setenv(log.TestLogLevelEnvVar, level.String()) tt.Cleanup(func() { - _ = os.Setenv("TEMPORAL_TEST_LOG_LEVEL", oldLevel) + _ = os.Setenv(log.TestLogLevelEnvVar, oldLevel) }) t.state.level = level @@ -211,6 +212,46 @@ func SetLogLevel(tt CleanupCapableT, level zapcore.Level) LoggerOption { var _ log.Logger = (*TestLogger)(nil) +// globalFileCore is a process-wide singleton zapcore.Core that writes to the file specified +// by TEMPORAL_TEST_LOG_FILE. It is a NopCore when the env var is unset. +var ( + globalFileCore zapcore.Core = zapcore.NewNopCore() + globalFileCoreOnce sync.Once +) + +func getGlobalFileCore() zapcore.Core { + globalFileCoreOnce.Do(func() { + logFile := os.Getenv(log.TestLogFileEnvVar) + if logFile == "" { + return + } + if err := os.MkdirAll(filepath.Dir(logFile), 0o755); err != nil { + fmt.Fprintf(os.Stderr, "testlogger: failed to create log file dir %s: %v\n", filepath.Dir(logFile), err) + return + } + f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + fmt.Fprintf(os.Stderr, "testlogger: failed to open log file %s: %v\n", logFile, err) + return + } + format := cmp.Or(os.Getenv(log.TestLogFileFormatEnvVar), "json") + var enc zapcore.Encoder + switch strings.ToLower(format) { + case "console": + enc = zapcore.NewConsoleEncoder(log.DefaultZapEncoderConfig) + default: // "json" and anything unrecognized + enc = zapcore.NewJSONEncoder(log.DefaultZapEncoderConfig) + } + level := zapcore.DebugLevel + if levelV := os.Getenv(log.TestLogFileLevelEnvVar); levelV != "" { + level = log.ParseZapLevel(levelV) + } + globalFileCore = zapcore.NewCore(enc, zapcore.AddSync(f), level) + fmt.Fprintf(os.Stderr, "testlogger: file logging enabled → %s (format=%s level=%s)\n", logFile, format, level) + }) + return globalFileCore +} + // NewTestLogger creates a new TestLogger that logs to the provided testing.T. // Mode controls the behavior of the logger for when an expected or unexpected error is encountered. func NewTestLogger(t TestingT, mode Mode, opts ...LoggerOption) *TestLogger { @@ -232,21 +273,26 @@ func NewTestLogger(t TestingT, mode Mode, opts ...LoggerOption) *TestLogger { } if tl.wrapped == nil { writer := zaptest.NewTestingWriter(t) - var enc zapcore.Encoder + + // Console core: format and level controlled by TEMPORAL_TEST_LOG_FORMAT / TEMPORAL_TEST_LOG_LEVEL. + var consoleEnc zapcore.Encoder format := cmp.Or(os.Getenv(log.TestLogFormatEnvVar), "console") switch strings.ToLower(format) { case "console": - enc = zapcore.NewConsoleEncoder(log.DefaultZapEncoderConfig) + consoleEnc = zapcore.NewConsoleEncoder(log.DefaultZapEncoderConfig) case "json": - enc = zapcore.NewJSONEncoder(log.DefaultZapEncoderConfig) + consoleEnc = zapcore.NewJSONEncoder(log.DefaultZapEncoderConfig) default: t.Fatalf("unknown log encoding %q", format) } - level := tl.state.level + consoleLevel := tl.state.level if levelV := os.Getenv(log.TestLogLevelEnvVar); levelV != "" { - level = log.ParseZapLevel(levelV) + consoleLevel = log.ParseZapLevel(levelV) } - core := zapcore.NewCore(enc, writer, level) + core := zapcore.NewTee( + zapcore.NewCore(consoleEnc, writer, consoleLevel), + getGlobalFileCore()) + zapOptions := []zap.Option{ // Send zap errors to the same writer and mark the test as failed if // that happens. diff --git a/common/testing/testvars/test_vars.go b/common/testing/testvars/test_vars.go index 1e1ab4ec6c5..f41bb050d50 100644 --- a/common/testing/testvars/test_vars.go +++ b/common/testing/testvars/test_vars.go @@ -282,20 +282,14 @@ func (tv *TestVars) DeploymentVersionTransition() *workflowpb.DeploymentVersionT return ret } -func (tv *TestVars) VersioningOverridePinned(useV32 bool) *workflowpb.VersioningOverride { - if useV32 { - return &workflowpb.VersioningOverride{ - Override: &workflowpb.VersioningOverride_Pinned{ - Pinned: &workflowpb.VersioningOverride_PinnedOverride{ - Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, - Version: tv.ExternalDeploymentVersion(), - }, - }, - } - } +func (tv *TestVars) VersioningOverridePinned() *workflowpb.VersioningOverride { return &workflowpb.VersioningOverride{ - Behavior: enumspb.VERSIONING_BEHAVIOR_PINNED, - PinnedVersion: tv.DeploymentVersionString(), + Override: &workflowpb.VersioningOverride_Pinned{ + Pinned: &workflowpb.VersioningOverride_PinnedOverride{ + Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, + Version: tv.ExternalDeploymentVersion(), + }, + }, } } diff --git a/common/tqid/task_queue_validator.go b/common/tqid/task_queue_validator.go index 7a226f6c46e..db59b999b95 100644 --- a/common/tqid/task_queue_validator.go +++ b/common/tqid/task_queue_validator.go @@ -67,7 +67,7 @@ func NormalizeAndValidate( // Parameters: // - taskQueue: The TaskQueue to validate and normalize. If nil, returns an error. // - defaultName: Default name to use if taskQueue name is empty. -// - parentTaskQueue: The TaskQueue of the parent component, if any. Can be nil. +// - parentTaskQueue: The TaskQueue of the parent component, if any. Can be empty. // - maxIDLengthLimit: Maximum allowed length for the TaskQueue name. // // Returns an error if validation fails, nil otherwise. @@ -77,10 +77,10 @@ func NormalizeAndValidateUserDefined( parentTaskQueue string, maxIDLengthLimit int, ) error { - if err := normalizeAndValidate(taskQueue, defaultName, maxIDLengthLimit, false); err != nil { + if err := normalizeAndValidate(taskQueue, defaultName, maxIDLengthLimit, true); err != nil { return err } - // reminder: if this check goes first, taskQueue.GetName() is not ready to use directly + // reminder: if this check goes first, taskQueue.GetName() may not be normalized yet. return primitives.CheckInternalPerNsTaskQueueAllowed(taskQueue.GetName(), parentTaskQueue) } diff --git a/common/tqid/task_queue_validator_test.go b/common/tqid/task_queue_validator_test.go index 155f222f3db..836e8f3f634 100644 --- a/common/tqid/task_queue_validator_test.go +++ b/common/tqid/task_queue_validator_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/server/common/primitives" ) @@ -188,7 +189,7 @@ func TestNormalizeAndValidateUserDefined(t *testing.T) { parentTaskQueue: nil, defaultVal: "", maxIDLengthLimit: 100, - expectedError: "cannot use internal per namespace task queue", + expectedError: "cannot use internal per-namespace task queue", }, { name: "Internal per-ns task queue with non-internal parent", @@ -196,7 +197,15 @@ func TestNormalizeAndValidateUserDefined(t *testing.T) { parentTaskQueue: &taskqueuepb.TaskQueue{Name: "user-parent-tq"}, defaultVal: "", maxIDLengthLimit: 100, - expectedError: "cannot use internal per namespace task queue", + expectedError: "cannot use internal per-namespace task queue", + }, + { + name: "Reserved /_sys/ prefix task queue with non-internal parent", + taskQueue: &taskqueuepb.TaskQueue{Name: "/_sys/my-task-queue"}, + parentTaskQueue: &taskqueuepb.TaskQueue{Name: "user-parent-tq"}, + defaultVal: "", + maxIDLengthLimit: 100, + expectedError: "task queue name cannot start with reserved prefix /_sys/", }, { name: "Internal per-ns task queue with internal per-ns parent", @@ -254,8 +263,9 @@ func TestNormalizeAndValidateUserDefined(t *testing.T) { require.NotEqual(t, enumspb.TASK_QUEUE_KIND_UNSPECIFIED, tt.taskQueue.GetKind(), "Kind should be normalized") } } else { - require.Error(t, err) - require.Contains(t, err.Error(), tt.expectedError) + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, invalidArgErr.Error(), tt.expectedError) } if tt.taskQueue != nil && tt.taskQueue.GetName() == "" && tt.defaultVal != "" && tt.expectedError == "" { diff --git a/common/util.go b/common/util.go index cb376028d96..20226f188c6 100644 --- a/common/util.go +++ b/common/util.go @@ -690,7 +690,7 @@ func DiscardUnknownProto(m proto.Message) error { // MergeProtoExcludingFields merges fields from source into target, excluding specific fields. // The fields to exclude are specified as pointers to fields in the target struct. -func MergeProtoExcludingFields(target, source proto.Message, doNotSyncFunc func(v any) []interface{}) error { +func MergeProtoExcludingFields(target, source proto.Message, doNotSyncFunc func(v any) []any) error { if target == nil || source == nil { return serviceerror.NewInvalidArgument("target and source cannot be nil") } @@ -725,7 +725,7 @@ func MergeProtoExcludingFields(target, source proto.Message, doNotSyncFunc func( return nil } -func getFieldNameFromStruct(structPtr interface{}, fieldPtr interface{}) (string, error) { +func getFieldNameFromStruct(structPtr any, fieldPtr any) (string, error) { structVal := reflect.ValueOf(structPtr).Elem() for i := 0; i < structVal.NumField(); i++ { field := structVal.Field(i) diff --git a/common/util/util.go b/common/util/util.go index 85014361490..d47907e5244 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -172,7 +172,7 @@ func RepeatSlice[T any](xs []T, n int) []T { return nil } ys := make([]T, n*len(xs)) - for i := 0; i < n; i++ { + for i := range n { copy(ys[i*len(xs):], xs) } return ys diff --git a/common/util/wildcard.go b/common/util/wildcard.go index 80c74aa465d..c1cdb95efb5 100644 --- a/common/util/wildcard.go +++ b/common/util/wildcard.go @@ -39,3 +39,12 @@ func WildCardStringsToRegexp(patterns []string) (*regexp.Regexp, error) { result.WriteRune('$') return regexp.Compile(result.String()) } + +// MustWildCardStringsToRegexp is like WildCardStringsToRegexp but panics on error. +func MustWildCardStringsToRegexp(patterns []string) *regexp.Regexp { + re, err := WildCardStringsToRegexp(patterns) + if err != nil { + panic(err) //nolint:forbidigo // Must* functions conventionally panic on error. + } + return re +} diff --git a/common/util_test.go b/common/util_test.go index c87dd31a859..ed91c25836e 100644 --- a/common/util_test.go +++ b/common/util_test.go @@ -420,12 +420,12 @@ func TestMergeProtoExcludingFields(t *testing.T) { WorkflowId: source.WorkflowId + "_target", } - doNotSync := func(v any) []interface{} { + doNotSync := func(v any) []any { info, ok := v.(*persistencespb.WorkflowExecutionInfo) if !ok || info == nil { return nil } - return []interface{}{ + return []any{ &info.NamespaceId, } } @@ -445,12 +445,12 @@ func TestMergeProtoExcludingFields(t *testing.T) { require.Error(t, err) source, target = generateExecutionInfo() - doNotSync = func(v any) []interface{} { + doNotSync = func(v any) []any { info, ok := v.(*persistencespb.WorkflowExecutionInfo) if !ok || info == nil { return nil } - return []interface{}{ + return []any{ &info.WorkflowTaskVersion, &info.WorkflowTaskScheduledEventId, &info.WorkflowTaskStartedEventId, diff --git a/common/worker_versioning/routing_info_cache_test.go b/common/worker_versioning/routing_info_cache_test.go index 349aca03563..96dc766bd82 100644 --- a/common/worker_versioning/routing_info_cache_test.go +++ b/common/worker_versioning/routing_info_cache_test.go @@ -255,12 +255,12 @@ func TestRoutingInfoCache_Concurrent(t *testing.T) { numOperations := 100 // Concurrent writes - for i := 0; i < numGoroutines; i++ { + for i := range numGoroutines { idx := i wg.Add(1) //nolint:revive // use-waitgroup-go: standard sync.WaitGroup doesn't have Go() method go func() { defer wg.Done() - for j := 0; j < numOperations; j++ { + for j := range numOperations { version := &deploymentspb.WorkerDeploymentVersion{ DeploymentName: "deployment", BuildId: "build", @@ -280,11 +280,11 @@ func TestRoutingInfoCache_Concurrent(t *testing.T) { } // Concurrent reads - for i := 0; i < numGoroutines; i++ { + for range numGoroutines { wg.Add(1) //nolint:revive // use-waitgroup-go: standard sync.WaitGroup doesn't have Go() method go func() { defer wg.Done() - for j := 0; j < numOperations; j++ { + for range numOperations { routingCache.Get(namespace, taskQueue, taskQueueType) } }() diff --git a/common/worker_versioning/worker_versioning.go b/common/worker_versioning/worker_versioning.go index 5ebcaeb16a1..80930e41a65 100644 --- a/common/worker_versioning/worker_versioning.go +++ b/common/worker_versioning/worker_versioning.go @@ -370,7 +370,7 @@ func checkVersionMembershipViaUserData( return HasDeploymentVersion(tqData.GetDeploymentData(), DeploymentVersionFromDeployment(DeploymentFromExternalDeploymentVersion(version))), nil } -func FindDeploymentVersion(deployments *persistencespb.DeploymentData, v *deploymentspb.WorkerDeploymentVersion) int { +func FindOldDeploymentVersion(deployments *persistencespb.DeploymentData, v *deploymentspb.WorkerDeploymentVersion) int { for i, vd := range deployments.GetVersions() { if proto.Equal(v, vd.GetVersion()) { return i @@ -485,16 +485,20 @@ func MakeBuildIdDirective(buildId string) *taskqueuespb.TaskVersionDirective { return &taskqueuespb.TaskVersionDirective{BuildId: &taskqueuespb.TaskVersionDirective_AssignedBuildId{AssignedBuildId: buildId}} } -func StampFromCapabilities(cap *commonpb.WorkerVersionCapabilities) *commonpb.WorkerVersionStamp { - if cap.GetUseVersioning() && cap.GetDeploymentSeriesName() != "" { +func StampFromCapabilities(capabilities *commonpb.WorkerVersionCapabilities, options *deploymentpb.WorkerDeploymentOptions) *commonpb.WorkerVersionStamp { + if options.GetWorkerVersioningMode() == enumspb.WORKER_VERSIONING_MODE_VERSIONED && options.GetDeploymentName() != "" { // Versioning 3, do not return stamp. return nil } - // TODO: remove `cap.BuildId != ""` condition after old versioning cleanup. this condition is used to differentiate + if capabilities.GetUseVersioning() && capabilities.GetDeploymentSeriesName() != "" { + // Versioning 3, do not return stamp. + return nil + } + // TODO: remove `capabilities.BuildId != ""` condition after old versioning cleanup. this condition is used to differentiate // between old and new versioning in Record*TaskStart calls. [cleanup-old-wv] // we don't want to add stamp for task started events in old versioning - if cap.GetBuildId() != "" { - return &commonpb.WorkerVersionStamp{UseVersioning: cap.UseVersioning, BuildId: cap.BuildId} + if capabilities.GetBuildId() != "" { + return &commonpb.WorkerVersionStamp{UseVersioning: capabilities.UseVersioning, BuildId: capabilities.BuildId} } return nil } @@ -1139,6 +1143,8 @@ func WorkerDeploymentVersionFromStringV32(s string) (*deploymentspb.WorkerDeploy // CleanupOldDeletedVersions removes versions deleted more than 7 days ago. Also removes more deleted versions if // the limit is being exceeded. Never removes undeleted versions. +// Deprecated. Versions now are deleted serially without using the deleted flag in versionData. +// TODO: remove this cleanup logic after next major release. func CleanupOldDeletedVersions(deploymentData *persistencespb.WorkerDeploymentData, maxVersions int) bool { now := time.Now() aWeekAgo := now.Add(-time.Hour * 24 * 7) diff --git a/common/worker_versioning/worker_versioning_test.go b/common/worker_versioning/worker_versioning_test.go index 940c83bedca..6949426fcd5 100644 --- a/common/worker_versioning/worker_versioning_test.go +++ b/common/worker_versioning/worker_versioning_test.go @@ -650,7 +650,7 @@ func TestFindDeploymentVersionForWorkflowID_PartialRamp(t *testing.T) { } histogram := make(map[string]int) runs := 1000000 - for i := 0; i < runs; i++ { + for i := range runs { v, _ := FindTargetDeploymentVersionAndRevisionNumberForWorkflowID(current.GetVersion(), 0, ramping.GetVersion(), ramping.GetRampPercentage(), 0, "wf-"+strconv.Itoa(i)) histogram[v.GetBuildId()]++ } diff --git a/components/callbacks/fx.go b/components/callbacks/fx.go index 17b55d50065..1d9d894fafb 100644 --- a/components/callbacks/fx.go +++ b/components/callbacks/fx.go @@ -8,6 +8,8 @@ import ( "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/collection" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" queuescommon "go.temporal.io/server/service/history/queues/common" "go.uber.org/fx" ) @@ -23,6 +25,7 @@ var Module = fx.Module( func HTTPCallerProviderProvider( clusterMetadata cluster.Metadata, + namespaceRegistry namespace.Registry, rpcFactory common.RPCFactory, httpClientCache *cluster.FrontendHTTPClientCache, logger log.Logger, @@ -32,17 +35,19 @@ func HTTPCallerProviderProvider( return nil, fmt.Errorf("cannot create local frontend HTTP client: %w", err) } defaultClient := &http.Client{} + callbackTokenGenerator := commonnexus.NewCallbackTokenGenerator() m := collection.NewOnceMap(func(queuescommon.NamespaceIDAndDestination) HTTPCaller { return func(r *http.Request) (*http.Response, error) { return routeRequest(r, clusterMetadata, + namespaceRegistry, httpClientCache, + callbackTokenGenerator, defaultClient, localClient, logger, ) - } }) return m.Get, nil diff --git a/components/callbacks/request.go b/components/callbacks/request.go index cfdaaf435cf..4bb4e60caae 100644 --- a/components/callbacks/request.go +++ b/components/callbacks/request.go @@ -1,29 +1,98 @@ package callbacks import ( + "errors" "net/http" + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/api/serviceerror" "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" - "go.temporal.io/server/common/nexus" + "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" ) // Header key used to identify callbacks that originate from and target the same cluster. // Note: this is the nexusoperations.NexusCallbackSourceHeader stripped of Nexus-Callback- const callbackSourceHeader = "source" +// routeSystemCallbackRequest routes a system callback request to the appropriate frontend client +// based on the callback token's namespace and active cluster. +func routeSystemCallbackRequest( + r *http.Request, + clusterMetadata cluster.Metadata, + namespaceRegistry namespace.Registry, + httpClientCache *cluster.FrontendHTTPClientCache, + callbackTokenGenerator *commonnexus.CallbackTokenGenerator, + localClient *common.FrontendHTTPClient, + logger log.Logger, +) (*http.Response, error) { + var frontendClient *common.FrontendHTTPClient + if r.Header != nil { + token, err := commonnexus.DecodeCallbackToken(r.Header.Get(commonnexus.CallbackTokenHeader)) + if err != nil { + logger.Error("failed to decode callback token", tag.Error(err)) + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid callback token") + } + + completion, err := callbackTokenGenerator.DecodeCompletion(token) + if err != nil { + logger.Error("failed to decode completion from token", tag.Error(err)) + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid callback token") + } + ns, err := namespaceRegistry.GetNamespaceByID(namespace.ID(completion.NamespaceId)) + if err != nil { + logger.Error("failed to get namespace for nexus completion request", tag.WorkflowNamespaceID(completion.NamespaceId), tag.Error(err)) + var nfe *serviceerror.NamespaceNotFound + if errors.As(err, &nfe) { + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "namespace %q not found", completion.NamespaceId) + } + return nil, commonnexus.ConvertGRPCError(err, false) + } + clusterName := ns.ActiveClusterName(completion.GetWorkflowId()) + if clusterMetadata.GetCurrentClusterName() == clusterName { + frontendClient = localClient + } else { + fec, err := httpClientCache.Get(clusterName) + if err != nil { + logger.Warn( + "HTTPCallerProvider unable to get FrontendHTTPClient for callback target cluster. Using local HTTP Client.", + tag.SourceCluster(clusterMetadata.GetCurrentClusterName()), + tag.TargetCluster(clusterName), + tag.Error(err), + ) + frontendClient = localClient + } else { + frontendClient = fec + } + } + } else { + frontendClient = localClient + } + r.URL.Path = commonnexus.PathCompletionCallbackNoIdentifier + r.URL.Scheme = frontendClient.Scheme + r.URL.Host = frontendClient.Address + r.Host = frontendClient.Address + return frontendClient.Do(r) +} + func routeRequest( r *http.Request, clusterMetadata cluster.Metadata, + namespaceRegistry namespace.Registry, httpClientCache *cluster.FrontendHTTPClientCache, + callbackTokenGenerator *commonnexus.CallbackTokenGenerator, defaultClient *http.Client, localClient *common.FrontendHTTPClient, logger log.Logger, ) (*http.Response, error) { + if r.URL.String() == commonnexus.SystemCallbackURL { + return routeSystemCallbackRequest(r, clusterMetadata, namespaceRegistry, httpClientCache, callbackTokenGenerator, localClient, logger) + } // This source header is populated in nexusoperations/executors (via the ClientProvider) for worker targets - // if this header is not populated then we assume it's and external target. + // if this header is not populated then we assume it's an external target. if r.Header == nil || r.Header.Get(callbackSourceHeader) == "" { return defaultClient.Do(r) } @@ -61,9 +130,6 @@ func routeRequest( frontendClient = localClient } - if r.URL.String() == nexus.SystemCallbackURL { - r.URL.Path = nexus.PathCompletionCallbackNoIdentifier - } r.URL.Scheme = frontendClient.Scheme r.URL.Host = frontendClient.Address r.Host = frontendClient.Address diff --git a/components/callbacks/request_test.go b/components/callbacks/request_test.go new file mode 100644 index 00000000000..7a7b9bbbce4 --- /dev/null +++ b/components/callbacks/request_test.go @@ -0,0 +1,324 @@ +package callbacks + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.temporal.io/api/serviceerror" + persistencespb "go.temporal.io/server/api/persistence/v1" + tokenspb "go.temporal.io/server/api/token/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/cluster" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" + "go.uber.org/mock/gomock" +) + +func newTestFrontendHTTPClient(ts *httptest.Server) *common.FrontendHTTPClient { + u, _ := url.Parse(ts.URL) + return &common.FrontendHTTPClient{ + Client: *ts.Client(), + Address: u.Host, + Scheme: u.Scheme, + } +} + +func TestRouteRequest_ExternalTarget(t *testing.T) { + // When no source header is set, the request should be sent via the default client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + + r, err := http.NewRequest(http.MethodPost, ts.URL+"/some/path", nil) + require.NoError(t, err) + + resp, err := routeRequest( + r, + clusterMeta, + nil, // namespaceRegistry not needed for external targets + nil, // httpClientCache not needed for external targets + nil, // callbackTokenGenerator not needed for external targets + ts.Client(), + nil, // localClient not needed for external targets + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRouteRequest_SourceHeaderLocal(t *testing.T) { + // When the source header matches the local cluster, the request should be routed to the local client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + clusterMeta.EXPECT().GetAllClusterInfo().Return(map[string]cluster.ClusterInformation{ + "cluster-A": {ClusterID: "cluster-id-A"}, + }) + clusterMeta.EXPECT().GetCurrentClusterName().Return("cluster-A") + + localClient := newTestFrontendHTTPClient(ts) + + r, err := http.NewRequest(http.MethodPost, "http://original-host/some/path", nil) + require.NoError(t, err) + r.Header.Set(callbackSourceHeader, "cluster-id-A") + + resp, err := routeRequest( + r, + clusterMeta, + nil, // namespaceRegistry + nil, // httpClientCache - not used since it's the local cluster + nil, // callbackTokenGenerator + &http.Client{}, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusAccepted, resp.StatusCode) +} + +func TestRouteRequest_SourceHeaderUnknownCluster(t *testing.T) { + // When the source header doesn't match any known cluster, falls back to local client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + clusterMeta.EXPECT().GetAllClusterInfo().Return(map[string]cluster.ClusterInformation{ + "cluster-A": {ClusterID: "cluster-id-A"}, + }) + clusterMeta.EXPECT().GetCurrentClusterName().Return("cluster-A") + + localClient := newTestFrontendHTTPClient(ts) + + r, err := http.NewRequest(http.MethodPost, "http://original-host/some/path", nil) + require.NoError(t, err) + r.Header.Set(callbackSourceHeader, "unknown-cluster-id") + + resp, err := routeRequest( + r, + clusterMeta, + nil, + nil, + nil, + &http.Client{}, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusAccepted, resp.StatusCode) +} + +func TestRouteSystemCallbackRequest_NilHeaders(t *testing.T) { + // When the request has nil headers, it should fall back to the local client. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, commonnexus.PathCompletionCallbackNoIdentifier, r.URL.Path) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + localClient := newTestFrontendHTTPClient(ts) + + r := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{Path: "/"}, + Header: nil, + } + + resp, err := routeSystemCallbackRequest( + r, + nil, // clusterMetadata - not needed for nil headers path + nil, // namespaceRegistry + nil, // httpClientCache + nil, // callbackTokenGenerator + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRouteSystemCallbackRequest_InvalidToken(t *testing.T) { + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, "not-valid-json") + + _, err = routeSystemCallbackRequest( + r, + nil, + nil, + nil, + nil, + nil, + log.NewNoopLogger(), + ) + require.Error(t, err) + var handlerErr *nexus.HandlerError + require.ErrorAs(t, err, &handlerErr) + require.Equal(t, nexus.HandlerErrorTypeBadRequest, handlerErr.Type) + require.Contains(t, handlerErr.Error(), "invalid callback token") +} + +func TestRouteSystemCallbackRequest_InvalidTokenData(t *testing.T) { + // Valid token structure but invalid data field. + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, `{"v":1,"d":"!!!invalid-base64"}`) + + tokenGen := commonnexus.NewCallbackTokenGenerator() + + _, err = routeSystemCallbackRequest( + r, + nil, + nil, + nil, + tokenGen, + nil, + log.NewNoopLogger(), + ) + require.Error(t, err) + var handlerErr *nexus.HandlerError + require.ErrorAs(t, err, &handlerErr) + require.Equal(t, nexus.HandlerErrorTypeBadRequest, handlerErr.Type) +} + +func TestRouteSystemCallbackRequest_NamespaceNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + nsRegistry := namespace.NewMockRegistry(ctrl) + + tokenGen := commonnexus.NewCallbackTokenGenerator() + tokenStr, err := tokenGen.Tokenize(&tokenspb.NexusOperationCompletion{ + NamespaceId: "ns-id-1", + WorkflowId: "wf-1", + }) + require.NoError(t, err) + + nsRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id-1")).Return( + nil, serviceerror.NewNamespaceNotFound("ns-id-1"), + ) + + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, tokenStr) + + _, err = routeSystemCallbackRequest( + r, + nil, + nsRegistry, + nil, + tokenGen, + nil, + log.NewNoopLogger(), + ) + require.Error(t, err) + var handlerErr *nexus.HandlerError + require.ErrorAs(t, err, &handlerErr) + require.Equal(t, nexus.HandlerErrorTypeNotFound, handlerErr.Type) +} + +func TestRouteSystemCallbackRequest_Success(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, commonnexus.PathCompletionCallbackNoIdentifier, r.URL.Path) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + ctrl := gomock.NewController(t) + clusterMeta := cluster.NewMockMetadata(ctrl) + nsRegistry := namespace.NewMockRegistry(ctrl) + + tokenGen := commonnexus.NewCallbackTokenGenerator() + tokenStr, err := tokenGen.Tokenize(&tokenspb.NexusOperationCompletion{ + NamespaceId: "ns-id-1", + WorkflowId: "wf-1", + }) + require.NoError(t, err) + + testNS := namespace.NewLocalNamespaceForTest( + &persistencespb.NamespaceInfo{Id: "ns-id-1", Name: "test-ns"}, + nil, + "cluster-A", + ) + nsRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id-1")).Return(testNS, nil) + + // httpClientCache.Get will fail for "cluster-A", so it falls back to localClient. + clusterMeta.EXPECT().GetCurrentClusterName().Return("cluster-A").AnyTimes() + clusterMeta.EXPECT().GetAllClusterInfo().Return(map[string]cluster.ClusterInformation{}).AnyTimes() + clusterMeta.EXPECT().RegisterMetadataChangeCallback(gomock.Any(), gomock.Any()) + + localClient := newTestFrontendHTTPClient(ts) + + // Create a cache that will fail for the requested cluster since we don't set up metadata fully. + httpClientCache := cluster.NewFrontendHTTPClientCache(clusterMeta, nil) + + r, err := http.NewRequest(http.MethodPost, commonnexus.SystemCallbackURL, nil) + require.NoError(t, err) + r.Header.Set(commonnexus.CallbackTokenHeader, tokenStr) + + resp, err := routeSystemCallbackRequest( + r, + clusterMeta, + nsRegistry, + httpClientCache, + tokenGen, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRouteRequest_SystemCallback(t *testing.T) { + // Verify that routeRequest delegates to routeSystemCallbackRequest for system callback URLs. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, commonnexus.PathCompletionCallbackNoIdentifier, r.URL.Path) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + localClient := newTestFrontendHTTPClient(ts) + + // Use nil headers to take the simplest path through routeSystemCallbackRequest. + r := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "temporal", + Host: "system", + }, + Header: nil, + } + + resp, err := routeRequest( + r, + nil, + nil, + nil, + nil, + &http.Client{}, + localClient, + log.NewNoopLogger(), + ) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/components/nexusoperations/config.go b/components/nexusoperations/config.go index ec41f910ef8..722b8c23ec6 100644 --- a/components/nexusoperations/config.go +++ b/components/nexusoperations/config.go @@ -107,6 +107,8 @@ var DisallowedOperationHeaders = dynamicconfig.NewGlobalTypedSettingWithConverte headers.CallerNameHeaderName, headers.CallerTypeHeaderName, headers.CallOriginHeaderName, + headers.PrincipalTypeHeaderName, + headers.PrincipalNameHeaderName, }, `Case insensitive list of disallowed header keys for Nexus Operations. ScheduleNexusOperation commands with a "nexus_header" field that contains any of these disallowed keys will be @@ -163,7 +165,6 @@ NexusOperationCancelRequestFailed events. Default true.`, type Config struct { NumHistoryShards int32 - Enabled dynamicconfig.BoolPropertyFn RequestTimeout dynamicconfig.DurationPropertyFnWithDestinationFilter MinRequestTimeout dynamicconfig.DurationPropertyFnWithNamespaceFilter MaxConcurrentOperations dynamicconfig.IntPropertyFnWithNamespaceFilter @@ -183,7 +184,6 @@ type Config struct { func ConfigProvider(dc *dynamicconfig.Collection, cfg *config.Persistence) *Config { return &Config{ - Enabled: dynamicconfig.EnableNexus.Get(dc), RequestTimeout: RequestTimeout.Get(dc), MinRequestTimeout: MinRequestTimeout.Get(dc), MaxConcurrentOperations: MaxConcurrentOperations.Get(dc), diff --git a/components/nexusoperations/events_test.go b/components/nexusoperations/events_test.go index 1da26dff450..3a5cfc8a97d 100644 --- a/components/nexusoperations/events_test.go +++ b/components/nexusoperations/events_test.go @@ -128,7 +128,7 @@ func TestTerminalStatesDeletion(t *testing.T) { testCases := []struct { name string def hsm.EventDefinition - attributes interface{} + attributes any }{ { name: "CompletedDeletesStateMachine", diff --git a/components/nexusoperations/executors_test.go b/components/nexusoperations/executors_test.go index 3b903883263..344f1e06310 100644 --- a/components/nexusoperations/executors_test.go +++ b/components/nexusoperations/executors_test.go @@ -239,6 +239,7 @@ func TestProcessInvocationTask(t *testing.T) { Message: "cause", FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + Type: "NexusFailure", Details: &commonpb.Payloads{ Payloads: []*commonpb.Payload{ mustToPayload(t, nexus.Failure{ @@ -296,6 +297,7 @@ func TestProcessInvocationTask(t *testing.T) { Message: "cause", FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + Type: "NexusFailure", Details: &commonpb.Payloads{ Payloads: []*commonpb.Payload{ mustToPayload(t, nexus.Failure{ @@ -579,7 +581,6 @@ func TestProcessInvocationTask(t *testing.T) { } require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{ Config: &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(true), RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(tc.requestTimeout), MaxOperationTokenLength: dynamicconfig.GetIntPropertyFnFilteredByNamespace(10), MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond), @@ -1015,7 +1016,6 @@ func TestProcessCancelationTask(t *testing.T) { require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{ Config: &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(true), RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(tc.requestTimeout), MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond), RecordCancelRequestCompletionEvents: dynamicconfig.GetBoolPropertyFn(true), @@ -1094,7 +1094,6 @@ func TestProcessCancelationTask_OperationCompleted(t *testing.T) { require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{ Config: &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(true), RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Hour), UseNewFailureWireFormat: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true), RetryPolicy: func() backoff.RetryPolicy { @@ -1303,7 +1302,6 @@ func TestProcessCancelationTask_SystemEndpoint(t *testing.T) { require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{ Config: &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(true), RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Hour), MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond), RecordCancelRequestCompletionEvents: dynamicconfig.GetBoolPropertyFn(true), @@ -1688,7 +1686,6 @@ func TestProcessInvocationTask_SystemEndpoint(t *testing.T) { require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{ Config: &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(true), RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Hour), MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond), PayloadSizeLimit: dynamicconfig.GetIntPropertyFnFilteredByNamespace(2 * 1024 * 1024), diff --git a/components/nexusoperations/frontend/fx.go b/components/nexusoperations/frontend/fx.go index 402740c2ed9..77e5a7d4573 100644 --- a/components/nexusoperations/frontend/fx.go +++ b/components/nexusoperations/frontend/fx.go @@ -24,7 +24,6 @@ var Module = fx.Module( func ConfigProvider(coll *dynamicconfig.Collection) *Config { return &Config{ - Enabled: dynamicconfig.EnableNexus.Get(coll), PayloadSizeLimit: dynamicconfig.BlobSizeLimitError.Get(coll), ForwardingEnabledForNamespace: dynamicconfig.EnableNamespaceNotActiveAutoForwarding.Get(coll), MaxOperationTokenLength: nexusoperations.MaxOperationTokenLength.Get(coll), diff --git a/components/nexusoperations/frontend/handler.go b/components/nexusoperations/frontend/handler.go index 341553d622f..9bab7e006be 100644 --- a/components/nexusoperations/frontend/handler.go +++ b/components/nexusoperations/frontend/handler.go @@ -47,7 +47,6 @@ const ( ) type Config struct { - Enabled dynamicconfig.BoolPropertyFn MaxOperationTokenLength dynamicconfig.IntPropertyFnWithNamespaceFilter PayloadSizeLimit dynamicconfig.IntPropertyFnWithNamespaceFilter ForwardingEnabledForNamespace dynamicconfig.BoolPropertyFnWithNamespaceFilter @@ -85,10 +84,6 @@ type completionHandler struct { // nolint:revive // (cyclomatic complexity) This function is long but the complexity is justified. func (h *completionHandler) CompleteOperation(ctx context.Context, r *nexusrpc.CompletionRequest) (retErr error) { startTime := time.Now() - if !h.Config.Enabled() { - h.preProcessErrorsCounter.Record(1) - return nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "Nexus APIs are disabled") - } token, err := commonnexus.DecodeCallbackToken(r.HTTPRequest.Header.Get(commonnexus.CallbackTokenHeader)) if err != nil { h.Logger.Error("failed to decode callback token", tag.Error(err)) @@ -419,7 +414,7 @@ func (c *requestContext) interceptRequest(ctx context.Context, request *nexusrpc ctx = c.AuthInterceptor.EnhanceContext(ctx, authInfo, claims) } - err = c.AuthInterceptor.Authorize(ctx, claims, &authorization.CallTarget{ + _, err = c.AuthInterceptor.Authorize(ctx, claims, &authorization.CallTarget{ APIName: apiName, Namespace: c.namespace.Name().String(), Request: request, @@ -438,7 +433,7 @@ func (c *requestContext) interceptRequest(ctx context.Context, request *nexusrpc return commonnexus.ConvertGRPCError(err, false) } - if err := c.NamespaceValidationInterceptor.ValidateState(c.namespace, apiName); err != nil { + if err := c.NamespaceValidationInterceptor.ValidateState(c.namespace, apiName, c.workflowID); err != nil { c.outcomeTag = metrics.OutcomeTag("invalid_namespace_state") return commonnexus.ConvertGRPCError(err, false) } diff --git a/components/nexusoperations/workflow/commands.go b/components/nexusoperations/workflow/commands.go index f24b669715c..b443d083a6e 100644 --- a/components/nexusoperations/workflow/commands.go +++ b/components/nexusoperations/workflow/commands.go @@ -40,13 +40,6 @@ func (ch *commandHandler) HandleScheduleCommand( ns := ms.GetNamespaceEntry() nsName := ms.GetNamespaceEntry().Name().String() - if !ch.config.Enabled() { - return workflow.FailWorkflowTaskError{ - Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_FEATURE_DISABLED, - Message: "Nexus operations disabled", - } - } - attrs := command.GetScheduleNexusOperationCommandAttributes() if attrs == nil { return workflow.FailWorkflowTaskError{ @@ -148,7 +141,7 @@ func (ch *commandHandler) HandleScheduleCommand( } } - if !validator.IsValidPayloadSize(attrs.Input.Size()) { + if attrs.Endpoint != commonnexus.SystemEndpoint && !validator.IsValidPayloadSize(attrs.Input.Size()) { return workflow.FailWorkflowTaskError{ Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES, Message: "ScheduleNexusOperationCommandAttributes.Input exceeds size limit", @@ -243,13 +236,6 @@ func (ch *commandHandler) HandleCancelCommand( workflowTaskCompletedEventID int64, command *commandpb.Command, ) error { - if !ch.config.Enabled() { - return workflow.FailWorkflowTaskError{ - Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_FEATURE_DISABLED, - Message: "Nexus operations disabled", - } - } - attrs := command.GetRequestCancelNexusOperationCommandAttributes() if attrs == nil { return workflow.FailWorkflowTaskError{ diff --git a/components/nexusoperations/workflow/commands_test.go b/components/nexusoperations/workflow/commands_test.go index 42c758c7ec4..2566d9a5225 100644 --- a/components/nexusoperations/workflow/commands_test.go +++ b/components/nexusoperations/workflow/commands_test.go @@ -50,7 +50,6 @@ type testContext struct { } var defaultConfig = &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(true), MaxServiceNameLength: dynamicconfig.GetIntPropertyFnFilteredByNamespace(len("service")), MaxOperationNameLength: dynamicconfig.GetIntPropertyFnFilteredByNamespace(len("op")), MaxConcurrentOperations: dynamicconfig.GetIntPropertyFnFilteredByNamespace(2), @@ -118,18 +117,6 @@ func newTestContext(t *testing.T, cfg *nexusoperations.Config) testContext { } func TestHandleScheduleCommand(t *testing.T) { - t.Run("feature disabled", func(t *testing.T) { - tcx := newTestContext(t, &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(false), - }) - err := tcx.scheduleHandler(context.Background(), tcx.ms, commandValidator{maxPayloadSize: 1}, 1, &commandpb.Command{}) - var failWFTErr workflow.FailWorkflowTaskError - require.ErrorAs(t, err, &failWFTErr) - require.False(t, failWFTErr.TerminateWorkflow) - require.Equal(t, enumspb.WORKFLOW_TASK_FAILED_CAUSE_FEATURE_DISABLED, failWFTErr.Cause) - require.Equal(t, 0, len(tcx.history.Events)) - }) - t.Run("empty attributes", func(t *testing.T) { tcx := newTestContext(t, defaultConfig) err := tcx.scheduleHandler(context.Background(), tcx.ms, commandValidator{maxPayloadSize: 1}, 1, &commandpb.Command{}) @@ -275,9 +262,32 @@ func TestHandleScheduleCommand(t *testing.T) { require.Equal(t, 0, len(tcx.history.Events)) }) + t.Run("system endpoint skips payload size validation", func(t *testing.T) { + tcx := newTestContext(t, defaultConfig) + err := tcx.scheduleHandler(context.Background(), tcx.ms, commandValidator{maxPayloadSize: 1}, 1, &commandpb.Command{ + Attributes: &commandpb.Command_ScheduleNexusOperationCommandAttributes{ + ScheduleNexusOperationCommandAttributes: &commandpb.ScheduleNexusOperationCommandAttributes{ + Endpoint: commonnexus.SystemEndpoint, + Service: "service", + Operation: "op", + Input: &commonpb.Payload{ + Data: []byte("ab"), + }, + }, + }, + }) + // Should NOT get payload size error; instead gets ProcessInput validation error (service not found). + var failWFTErr workflow.FailWorkflowTaskError + require.ErrorAs(t, err, &failWFTErr) + require.False(t, failWFTErr.TerminateWorkflow) + require.Equal(t, enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES, failWFTErr.Cause) + require.NotContains(t, failWFTErr.Message, "Input exceeds size limit") + require.Empty(t, tcx.history.Events) + }) + t.Run("exceeds max concurrent operations", func(t *testing.T) { tcx := newTestContext(t, defaultConfig) - for i := 0; i < 2; i++ { + for range 2 { err := tcx.scheduleHandler(context.Background(), tcx.ms, commandValidator{maxPayloadSize: 1}, 1, &commandpb.Command{ Attributes: &commandpb.Command_ScheduleNexusOperationCommandAttributes{ ScheduleNexusOperationCommandAttributes: &commandpb.ScheduleNexusOperationCommandAttributes{ @@ -602,18 +612,6 @@ func TestHandleScheduleCommand(t *testing.T) { } func TestHandleCancelCommand(t *testing.T) { - t.Run("feature disabled", func(t *testing.T) { - tcx := newTestContext(t, &nexusoperations.Config{ - Enabled: dynamicconfig.GetBoolPropertyFn(false), - }) - err := tcx.cancelHandler(context.Background(), tcx.ms, commandValidator{maxPayloadSize: 1}, 1, &commandpb.Command{}) - var failWFTErr workflow.FailWorkflowTaskError - require.ErrorAs(t, err, &failWFTErr) - require.False(t, failWFTErr.TerminateWorkflow) - require.Equal(t, enumspb.WORKFLOW_TASK_FAILED_CAUSE_FEATURE_DISABLED, failWFTErr.Cause) - require.Equal(t, 0, len(tcx.history.Events)) - }) - t.Run("empty attributes", func(t *testing.T) { tcx := newTestContext(t, defaultConfig) err := tcx.cancelHandler(context.Background(), tcx.ms, commandValidator{maxPayloadSize: 1}, 1, &commandpb.Command{}) @@ -807,7 +805,7 @@ func TestOperationNodeDeletionOnTerminalEvents(t *testing.T) { tcx testContext, scheduledEventID int64, eventType enumspb.EventType, - eventAttr interface{}, + eventAttr any, def hsm.EventDefinition, ) { coll := nexusoperations.MachineCollection(tcx.ms.HSM()) @@ -864,7 +862,7 @@ func TestOperationNodeDeletionOnTerminalEvents(t *testing.T) { cases := []struct { name string eventType enumspb.EventType - eventAttr interface{} + eventAttr any eventDef hsm.EventDefinition }{ { diff --git a/config/development-cluster-a.yaml b/config/development-cluster-a.yaml index 488148a3f41..2d764623e0c 100644 --- a/config/development-cluster-a.yaml +++ b/config/development-cluster-a.yaml @@ -71,13 +71,13 @@ clusterMetadata: initialFailoverVersion: 1 rpcName: "frontend" rpcAddress: "localhost:7233" -# Use tctl --ad 127.0.0.1:7233 adm cl upsert-remote-cluster --frontend_address "127.0.0.1:8233" +# Use temporal --address 127.0.0.1:7233 operator cluster upsert --frontend-address "127.0.0.1:8233 --enable-connection --enable-replication" # cluster-b: # enabled: true # initialFailoverVersion: 2 # rpcName: "frontend" # rpcAddress: "localhost:8233" -# Use tctl --ad 127.0.0.1:7233 adm cl upsert-remote-cluster --frontend_address "127.0.0.1:9233" +# Use temporal --address 127.0.0.1:7233 operator cluster upsert --frontend-address "127.0.0.1:9233 --enable-connection --enable-replication" # cluster-c: # enabled: false # initialFailoverVersion: 3 diff --git a/config/development-cluster-b.yaml b/config/development-cluster-b.yaml index bb6aad3f590..9ac837cffc2 100644 --- a/config/development-cluster-b.yaml +++ b/config/development-cluster-b.yaml @@ -62,26 +62,21 @@ services: clusterMetadata: enableGlobalNamespace: true failoverVersionIncrement: 100 - masterClusterName: "cluster-a" + masterClusterName: "cluster-b" currentClusterName: "cluster-b" clusterInformation: - cluster-a: - enabled: true - initialFailoverVersion: 1 - rpcName: "frontend" - rpcAddress: "localhost:7233" cluster-b: enabled: true initialFailoverVersion: 2 rpcName: "frontend" rpcAddress: "localhost:8233" -# Use tctl --ad 127.0.0.1:8233 adm cl upsert-remote-cluster --frontend_address "127.0.0.1:7233" +# Use temporal --address 127.0.0.1:8233 operator cluster upsert --frontend-address "127.0.0.1:7233 --enable-connection --enable-replication" # active: # enabled: true # initialFailoverVersion: 1 # rpcName: "frontend" # rpcAddress: "localhost:7233" -# Use tctl --ad 127.0.0.1:8233 adm cl upsert-remote-cluster --frontend_address "127.0.0.1:9233" +# Use temporal --address 127.0.0.1:8233 operator cluster upsert --frontend-address "127.0.0.1:9233 --enable-connection --enable-replication" # other: # enabled: false # initialFailoverVersion: 3 diff --git a/config/development-cluster-c.yaml b/config/development-cluster-c.yaml index 9561ec24aae..5fff9e760c4 100644 --- a/config/development-cluster-c.yaml +++ b/config/development-cluster-c.yaml @@ -62,26 +62,21 @@ services: clusterMetadata: enableGlobalNamespace: true failoverVersionIncrement: 100 - masterClusterName: "cluster-a" + masterClusterName: "cluster-c" currentClusterName: "cluster-c" clusterInformation: - cluster-a: - enabled: true - initialFailoverVersion: 1 - rpcName: "frontend" - rpcAddress: "localhost:7233" cluster-c: enabled: true initialFailoverVersion: 3 rpcName: "frontend" rpcAddress: "localhost:9233" -# Use tctl --ad 127.0.0.1:9233 adm cl upsert-remote-cluster --frontend_address "127.0.0.1:7233" +# Use temporal --address 127.0.0.1:9233 operator cluster upsert --frontend-address "127.0.0.1:7233 --enable-connection --enable-replication" # active: # enabled: true # initialFailoverVersion: 1 # rpcName: "frontend" # rpcAddress: "localhost:7233" -# Use tctl --ad 127.0.0.1:9233 adm cl upsert-remote-cluster --frontend_address "127.0.0.1:8233" +# Use temporal --address 127.0.0.1:9233 operator cluster upsert --frontend-address "127.0.0.1:8233 --enable-connection --enable-replication" # standby: # enabled: true # initialFailoverVersion: 2 diff --git a/config/dynamicconfig/development-cass.yaml b/config/dynamicconfig/development-cass.yaml index 553d27716aa..ab9998f7af5 100644 --- a/config/dynamicconfig/development-cass.yaml +++ b/config/dynamicconfig/development-cass.yaml @@ -37,8 +37,6 @@ system.enableDeploymentVersions: - value: true frontend.workerVersioningRuleAPIs: - value: true -system.enableNexus: - - value: true component.nexusoperations.callback.endpoint.template: - value: http://localhost:7243/namespaces/{{.NamespaceName}}/nexus/callback component.callbacks.allowedAddresses: diff --git a/config/dynamicconfig/development-sql.yaml b/config/dynamicconfig/development-sql.yaml index 11583f931cb..85a3c355591 100644 --- a/config/dynamicconfig/development-sql.yaml +++ b/config/dynamicconfig/development-sql.yaml @@ -55,8 +55,6 @@ system.enableDeploymentVersions: - value: true system.enableDeployments: - value: true -system.enableNexus: - - value: true component.nexusoperations.callback.endpoint.template: - value: http://localhost:7243/namespaces/{{.NamespaceName}}/nexus/callback component.callbacks.allowedAddresses: diff --git a/config/dynamicconfig/development-xdc.yaml b/config/dynamicconfig/development-xdc.yaml index e148320fe41..fb5d13c7f94 100644 --- a/config/dynamicconfig/development-xdc.yaml +++ b/config/dynamicconfig/development-xdc.yaml @@ -31,8 +31,6 @@ frontend.workerVersioningDataAPIs: - value: true frontend.workerVersioningWorkflowAPIs: - value: true -system.enableNexus: - - value: true component.nexusoperations.callback.endpoint.template: - value: http://localhost:7243/namespaces/{{.NamespaceName}}/nexus/callback component.callbacks.allowedAddresses: diff --git a/develop/docker-compose/docker-compose.yml b/develop/docker-compose/docker-compose.yml index c25fa0f4641..031b08323ac 100644 --- a/develop/docker-compose/docker-compose.yml +++ b/develop/docker-compose/docker-compose.yml @@ -15,7 +15,7 @@ services: networks: - temporal-dev-network cassandra: - image: cassandra:3.11 + image: cassandra:5.0 container_name: temporal-dev-cassandra ports: - "9042:9042" diff --git a/develop/github/docker-compose.yml b/develop/github/docker-compose.yml index 6cf535596ed..754641d2010 100644 --- a/develop/github/docker-compose.yml +++ b/develop/github/docker-compose.yml @@ -1,11 +1,11 @@ x-healthcheck-defaults: &healthcheck-defaults interval: 3s timeout: 3s - retries: 30 + retries: 60 services: cassandra: - image: cassandra:3.11 + image: cassandra:5.0 ports: - "9042:9042" environment: diff --git a/docker/docker-bake.hcl b/docker/docker-bake.hcl index ee4471af1b8..2e918cb597e 100644 --- a/docker/docker-bake.hcl +++ b/docker/docker-bake.hcl @@ -10,7 +10,11 @@ variable "IMAGE_REPO" { default = "temporaliotest" } -variable "IMAGE_SHA_TAG" { +variable "IMAGE_SHA_SHORT_TAG" { + default = "" +} + +variable "IMAGE_SHA_FULL_TAG" { default = "" } @@ -30,9 +34,7 @@ variable "TAG_LATEST" { default = false } -# IMPORTANT: When updating ALPINE_TAG, also update the default value in: -# - docker/targets/admin-tools.Dockerfile -# - docker/targets/server.Dockerfile +# ALPINE_TAG is the single source of truth for the Alpine base image version. # NOTE: We use just the tag without a digest pin because digest-pinned manifest lists # cause platform resolution issues in multi-arch buildx builds (InvalidBaseImagePlatform warnings). variable "ALPINE_TAG" { @@ -46,7 +48,8 @@ target "admin-tools" { ALPINE_TAG = "${ALPINE_TAG}" } tags = compact([ - "${IMAGE_REPO}/admin-tools:${IMAGE_SHA_TAG}", + "${IMAGE_REPO}/admin-tools:${IMAGE_SHA_SHORT_TAG}", + "${IMAGE_REPO}/admin-tools:${IMAGE_SHA_FULL_TAG}", "${IMAGE_REPO}/admin-tools:${SAFE_IMAGE_BRANCH_TAG}", TAG_LATEST ? "${IMAGE_REPO}/admin-tools:latest" : "", ]) @@ -71,7 +74,8 @@ target "server" { ALPINE_TAG = "${ALPINE_TAG}" } tags = compact([ - "${IMAGE_REPO}/server:${IMAGE_SHA_TAG}", + "${IMAGE_REPO}/server:${IMAGE_SHA_SHORT_TAG}", + "${IMAGE_REPO}/server:${IMAGE_SHA_FULL_TAG}", "${IMAGE_REPO}/server:${SAFE_IMAGE_BRANCH_TAG}", TAG_LATEST ? "${IMAGE_REPO}/server:latest" : "", ]) diff --git a/docker/targets/admin-tools.Dockerfile b/docker/targets/admin-tools.Dockerfile index 0568ea114af..f821ee29853 100644 --- a/docker/targets/admin-tools.Dockerfile +++ b/docker/targets/admin-tools.Dockerfile @@ -1,9 +1,4 @@ -# IMPORTANT: When updating ALPINE_TAG, also update the default value in: -# - docker/docker-bake.hcl (variable "ALPINE_TAG") -# - docker/targets/server.Dockerfile (ARG ALPINE_TAG) -# NOTE: We use just the tag without a digest pin because digest-pinned manifest lists -# cause platform resolution issues in multi-arch buildx builds (InvalidBaseImagePlatform warnings). -ARG ALPINE_TAG=3.23.3 +ARG ALPINE_TAG FROM alpine:${ALPINE_TAG} @@ -11,7 +6,9 @@ ARG TARGETARCH RUN apk add --no-cache \ ca-certificates \ - tzdata && addgroup -g 1000 temporal && \ + tzdata && \ + apk upgrade --no-cache zlib && \ + addgroup -g 1000 temporal && \ adduser -u 1000 -G temporal -D temporal # Copy all admin tool binaries: diff --git a/docker/targets/server.Dockerfile b/docker/targets/server.Dockerfile index d3fe286bd8f..026f9e16bbf 100644 --- a/docker/targets/server.Dockerfile +++ b/docker/targets/server.Dockerfile @@ -1,9 +1,4 @@ -# IMPORTANT: When updating ALPINE_TAG, also update the default value in: -# - docker/docker-bake.hcl (variable "ALPINE_TAG") -# - docker/targets/admin-tools.Dockerfile (ARG ALPINE_TAG) -# NOTE: We use just the tag without a digest pin because digest-pinned manifest lists -# cause platform resolution issues in multi-arch buildx builds (InvalidBaseImagePlatform warnings). -ARG ALPINE_TAG=3.23.3 +ARG ALPINE_TAG FROM alpine:${ALPINE_TAG} @@ -11,7 +6,9 @@ ARG TARGETARCH RUN apk add --no-cache \ ca-certificates \ - tzdata && addgroup -g 1000 temporal && \ + tzdata && \ + apk upgrade --no-cache zlib && \ + addgroup -g 1000 temporal && \ adduser -u 1000 -G temporal -D temporal COPY --chmod=755 ./build/${TARGETARCH}/temporal-server /usr/local/bin/ diff --git a/docs/Makefile b/docs/Makefile index 5f5ae551b11..2282f897186 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,11 +1,20 @@ .DEFAULT_GOAL := update-diagrams COLOR := "\e[1;36m%s\e[0m\n" -DIAGRAMS := $(wildcard ./_assets/*.d2) +ASSETS_DIR := _assets +D2_GLOB := $(ASSETS_DIR)/*.d2 +DIAGRAMS := $(wildcard $(D2_GLOB)) +D2_FLAGS := --theme 7 --dark-theme 200 install-d2: @printf $(COLOR) "Install d2..." - @go install oss.terrastruct.com/d2@v0.6.3 + @go install oss.terrastruct.com/d2@v0.7.1 update-diagrams: install-d2 @printf $(COLOR) "Update diagrams" - $(foreach DIAGRAM, $(DIAGRAMS), $(shell d2 --layout elk --theme 7 --dark-theme 200 $(DIAGRAM) $(NEWLINE))) + $(foreach DIAGRAM, $(DIAGRAMS), $(shell d2 $(D2_FLAGS) $(DIAGRAM) $(NEWLINE))) + +install-reflex: + @go install github.com/cespare/reflex@v0.3.2 + +watch-diagrams: install-d2 install-reflex + reflex -r '^$(ASSETS_DIR)/.*\.d2$$' -- d2 $(D2_FLAGS) '{}' diff --git a/docs/_assets/chasm-asm.d2 b/docs/_assets/chasm-asm.d2 new file mode 100644 index 00000000000..50bb73a2955 --- /dev/null +++ b/docs/_assets/chasm-asm.d2 @@ -0,0 +1,27 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +Registry + +ASM: { + Library + "Component Types" + Tasks + Fields + "Search Attributes" + + Library -> "Component Types": "1:n" + Library -> Tasks: "1:n" + "Component Types" -> Fields: "1:n" + "Component Types" -> "Search Attributes": "0:n" +} + +Registry -> ASM.Library: "1:n" diff --git a/docs/_assets/chasm-asm.svg b/docs/_assets/chasm-asm.svg new file mode 100644 index 00000000000..105a504fc22 --- /dev/null +++ b/docs/_assets/chasm-asm.svg @@ -0,0 +1,185 @@ +RegistryASMLibraryComponent TypesTasksFieldsSearch Attributes 1:n1:n1:n0:n1:n + + + + + + + + + + + + + + diff --git a/docs/_assets/chasm-component-state.d2 b/docs/_assets/chasm-component-state.d2 new file mode 100644 index 00000000000..d261809bab2 --- /dev/null +++ b/docs/_assets/chasm-component-state.d2 @@ -0,0 +1,19 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.font-size: 18 +*.style.bold: true + +"Component Type": { + grid-rows: 4 + "Field[T]" + "Map[K, T]" + ParentPtr + Transient +} diff --git a/docs/_assets/chasm-component-state.svg b/docs/_assets/chasm-component-state.svg new file mode 100644 index 00000000000..69329831bee --- /dev/null +++ b/docs/_assets/chasm-component-state.svg @@ -0,0 +1,171 @@ +Component TypeField[T]Map[K, T]ParentPtrTransient + + + + + + + diff --git a/docs/_assets/chasm-componentref.d2 b/docs/_assets/chasm-componentref.d2 new file mode 100644 index 00000000000..2c7b1a6cda2 --- /dev/null +++ b/docs/_assets/chasm-componentref.d2 @@ -0,0 +1,18 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +ComponentRef: { + grid-rows: 4 + ExecutionKey + ComponentPath + "initial VersionedTransition" + "lastUpdate VersionedTransition" +} diff --git a/docs/_assets/chasm-componentref.svg b/docs/_assets/chasm-componentref.svg new file mode 100644 index 00000000000..1ee49267c38 --- /dev/null +++ b/docs/_assets/chasm-componentref.svg @@ -0,0 +1,171 @@ +ComponentRefExecutionKeyComponentPathinitial VersionedTransitionlastUpdate VersionedTransition + + + + + + + diff --git a/docs/_assets/chasm-context.d2 b/docs/_assets/chasm-context.d2 new file mode 100644 index 00000000000..4236a1e34af --- /dev/null +++ b/docs/_assets/chasm-context.d2 @@ -0,0 +1,13 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +MutableContext -> Context: extends +MutableContext -> "Scheduled Tasks": "AddTask()" diff --git a/docs/_assets/chasm-context.svg b/docs/_assets/chasm-context.svg new file mode 100644 index 00000000000..6ac69c20096 --- /dev/null +++ b/docs/_assets/chasm-context.svg @@ -0,0 +1,178 @@ +MutableContextContextScheduled Tasks extendsAddTask() + + + + + + + diff --git a/docs/_assets/chasm-engine-read.d2 b/docs/_assets/chasm-engine-read.d2 new file mode 100644 index 00000000000..e14902633ee --- /dev/null +++ b/docs/_assets/chasm-engine-read.d2 @@ -0,0 +1,24 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +shape: sequence_diagram + +Caller.style.bold: true +Engine.style.bold: true +Storage.style.bold: true +fn.style.bold: true +Context.style.bold: true + +Caller -> Engine: "ReadComponent" +Engine -> Storage: "load" +Storage -> Engine: "Execution" +Engine -> fn: "fn(component, ctx)" +fn -> Context: "read" +fn -> Engine: "result" +Engine -> Caller: "result" diff --git a/docs/_assets/chasm-engine-read.svg b/docs/_assets/chasm-engine-read.svg new file mode 100644 index 00000000000..bb8354e9724 --- /dev/null +++ b/docs/_assets/chasm-engine-read.svg @@ -0,0 +1,185 @@ +CallerEngineStoragefnContext ReadComponentloadExecutionfn(component, ctx)readresultresult + + + + + + + + + + + + + + diff --git a/docs/_assets/chasm-engine.d2 b/docs/_assets/chasm-engine.d2 new file mode 100644 index 00000000000..9af32e45ac4 --- /dev/null +++ b/docs/_assets/chasm-engine.d2 @@ -0,0 +1,24 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +shape: sequence_diagram + +Caller.style.bold: true +Engine.style.bold: true +Storage.style.bold: true +fn.style.bold: true +MutableContext.style.bold: true + +Caller -> Engine: "UpdateComponent" +Engine -> Storage: "load" +Storage -> Engine: "Execution" +Engine -> fn: "fn(component, ctx)" +fn -> MutableContext: "write / schedule" +fn -> Engine: "" +Engine -> Storage: "commit" diff --git a/docs/_assets/chasm-engine.svg b/docs/_assets/chasm-engine.svg new file mode 100644 index 00000000000..7e7572cf111 --- /dev/null +++ b/docs/_assets/chasm-engine.svg @@ -0,0 +1,184 @@ +CallerEngineStoragefnMutableContext UpdateComponentloadExecutionfn(component, ctx)write / schedulecommit + + + + + + + + + + + + + diff --git a/docs/_assets/chasm-execution.d2 b/docs/_assets/chasm-execution.d2 new file mode 100644 index 00000000000..577120df28a --- /dev/null +++ b/docs/_assets/chasm-execution.d2 @@ -0,0 +1,19 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: elk + } +} + +*.style.bold: true + +Node: { + grid-rows: 5 + Name + Component + "initial VersionedTransition" + "lastUpdate VersionedTransition" + "Children (0..n Node)" +} diff --git a/docs/_assets/chasm-execution.svg b/docs/_assets/chasm-execution.svg new file mode 100644 index 00000000000..fe928dc00ee --- /dev/null +++ b/docs/_assets/chasm-execution.svg @@ -0,0 +1,172 @@ +NodeNameComponentinitial VersionedTransitionlastUpdate VersionedTransitionChildren (0..n Node) + + + + + + + + diff --git a/docs/_assets/chasm-executionkey.d2 b/docs/_assets/chasm-executionkey.d2 new file mode 100644 index 00000000000..86aee90e143 --- /dev/null +++ b/docs/_assets/chasm-executionkey.d2 @@ -0,0 +1,17 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +ExecutionKey: { + grid-rows: 3 + NamespaceID + BusinessID + RunID +} diff --git a/docs/_assets/chasm-executionkey.svg b/docs/_assets/chasm-executionkey.svg new file mode 100644 index 00000000000..1ea96e9e3e5 --- /dev/null +++ b/docs/_assets/chasm-executionkey.svg @@ -0,0 +1,170 @@ +ExecutionKeyNamespaceIDBusinessIDRunID + + + + + + diff --git a/docs/_assets/chasm-lifecycle.d2 b/docs/_assets/chasm-lifecycle.d2 new file mode 100644 index 00000000000..8b0e5f6ef19 --- /dev/null +++ b/docs/_assets/chasm-lifecycle.d2 @@ -0,0 +1,21 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +start: "⬤" { + style.stroke: transparent + style.fill: transparent + style.bold: false + style.font-size: 12 +} + +start -> Running +Running -> Completed +Running -> Failed diff --git a/docs/_assets/chasm-lifecycle.svg b/docs/_assets/chasm-lifecycle.svg new file mode 100644 index 00000000000..73614e5112f --- /dev/null +++ b/docs/_assets/chasm-lifecycle.svg @@ -0,0 +1,177 @@ +RunningCompletedFailed + + + + + + diff --git a/docs/_assets/chasm-search-attributes.d2 b/docs/_assets/chasm-search-attributes.d2 new file mode 100644 index 00000000000..6b104197415 --- /dev/null +++ b/docs/_assets/chasm-search-attributes.d2 @@ -0,0 +1,13 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +Component -> "Search Attributes": "updates in Apply Logic" +"Search Attributes" -> Visibility: "indexed in" diff --git a/docs/_assets/chasm-search-attributes.svg b/docs/_assets/chasm-search-attributes.svg new file mode 100644 index 00000000000..df65cdfae69 --- /dev/null +++ b/docs/_assets/chasm-search-attributes.svg @@ -0,0 +1,178 @@ +ComponentSearch AttributesVisibility updates in Apply Logicindexed in + + + + + + + diff --git a/docs/_assets/chasm-task-handler.d2 b/docs/_assets/chasm-task-handler.d2 new file mode 100644 index 00000000000..a213e2e29cb --- /dev/null +++ b/docs/_assets/chasm-task-handler.d2 @@ -0,0 +1,20 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +direction: down + +*.style.bold: true + +drop.shape: circle +done.shape: circle + +Task -> "Task Handler".Validator +"Task Handler".Validator -> drop: "false" +"Task Handler".Validator -> "Task Handler".Executor: "true" +"Task Handler".Executor -> done: "success" diff --git a/docs/_assets/chasm-task-handler.svg b/docs/_assets/chasm-task-handler.svg new file mode 100644 index 00000000000..b378a981688 --- /dev/null +++ b/docs/_assets/chasm-task-handler.svg @@ -0,0 +1,182 @@ +dropdoneTaskTask HandlerValidatorExecutor falsetruesuccess + + + + + + + + + + + diff --git a/docs/_assets/chasm-tasks.d2 b/docs/_assets/chasm-tasks.d2 new file mode 100644 index 00000000000..96b8279012b --- /dev/null +++ b/docs/_assets/chasm-tasks.d2 @@ -0,0 +1,54 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +Rows: { + label: "" + style.stroke-width: 0 + style.fill: transparent + grid-columns: 2 + horizontal-gap: 32 + vertical-gap: 32 + + "In Transaction": { + grid-rows: 2 + horizontal-gap: 32 + vertical-gap: 64 + "Pure Task": { + style.bold: true + grid-rows: 2 + horizontal-gap: 32 + vertical-gap: 48 + + Validator + Executor + } + "Component State" + } + + "After Commit": { + grid-rows: 2 + horizontal-gap: 32 + vertical-gap: 64 + "Side Effect Task": { + style.bold: true + grid-rows: 2 + horizontal-gap: 32 + vertical-gap: 48 + + Validator + Executor + } + "Engine \nor\n External Service" + } +} + +Rows."In Transaction"."Pure Task" -> Rows."In Transaction"."Component State": "read/write" +Rows."After Commit"."Side Effect Task" -> Rows."After Commit"."Engine \nor\n External Service": "invoke" diff --git a/docs/_assets/chasm-tasks.svg b/docs/_assets/chasm-tasks.svg new file mode 100644 index 00000000000..71270e5421d --- /dev/null +++ b/docs/_assets/chasm-tasks.svg @@ -0,0 +1,192 @@ +In TransactionAfter CommitPure TaskComponent StateSide Effect TaskEngine or External ServiceValidatorExecutorValidatorExecutor read/writeinvoke + + + + + + + + + + + + + + diff --git a/docs/_assets/chasm-transition.d2 b/docs/_assets/chasm-transition.d2 new file mode 100644 index 00000000000..4e458b305f4 --- /dev/null +++ b/docs/_assets/chasm-transition.d2 @@ -0,0 +1,16 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +direction: down + +*.style.bold: true + +Event -> "Transition Fn" +"Transition Fn" -> Fields: "write" +"Transition Fn" -> Tasks: "schedule" diff --git a/docs/_assets/chasm-transition.svg b/docs/_assets/chasm-transition.svg new file mode 100644 index 00000000000..b015ede8fd0 --- /dev/null +++ b/docs/_assets/chasm-transition.svg @@ -0,0 +1,179 @@ +EventTransition FnFieldsTasks writeschedule + + + + + + + + diff --git a/docs/_assets/chasm-transitions.d2 b/docs/_assets/chasm-transitions.d2 new file mode 100644 index 00000000000..0119b64bff4 --- /dev/null +++ b/docs/_assets/chasm-transitions.d2 @@ -0,0 +1,13 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +"Source State" -> "Destination State": Event +"Destination State" -> "Apply Logic" diff --git a/docs/_assets/chasm-transitions.svg b/docs/_assets/chasm-transitions.svg new file mode 100644 index 00000000000..29e6a6967e2 --- /dev/null +++ b/docs/_assets/chasm-transitions.svg @@ -0,0 +1,177 @@ +Source StateDestination StateApply Logic Event + + + + + + diff --git a/docs/_assets/chasm-visibility.d2 b/docs/_assets/chasm-visibility.d2 new file mode 100644 index 00000000000..197123191a6 --- /dev/null +++ b/docs/_assets/chasm-visibility.d2 @@ -0,0 +1,16 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +"Visibility Component": { + grid-rows: 2 + SearchAttributes + Memo +} diff --git a/docs/_assets/chasm-visibility.svg b/docs/_assets/chasm-visibility.svg new file mode 100644 index 00000000000..0c2d4aee734 --- /dev/null +++ b/docs/_assets/chasm-visibility.svg @@ -0,0 +1,169 @@ +Visibility ComponentSearchAttributesMemo + + + + + diff --git a/docs/_assets/chasm-vt.d2 b/docs/_assets/chasm-vt.d2 new file mode 100644 index 00000000000..b96c2f54ae5 --- /dev/null +++ b/docs/_assets/chasm-vt.d2 @@ -0,0 +1,16 @@ +# https://d2lang.com +# Generate SVG by running `make` inside of `docs/`. + +vars: { + d2-config: { + layout-engine: dagre + } +} + +*.style.bold: true + +VersionedTransition: { + grid-rows: 2 + FailoverVersion + TransitionCount +} diff --git a/docs/_assets/chasm-vt.svg b/docs/_assets/chasm-vt.svg new file mode 100644 index 00000000000..de2a3bac341 --- /dev/null +++ b/docs/_assets/chasm-vt.svg @@ -0,0 +1,169 @@ +VersionedTransitionFailoverVersionTransitionCount + + + + + diff --git a/docs/_assets/matching-context.d2 b/docs/_assets/matching-context.d2 index 66337ec9c3c..40ead8ccd89 100644 --- a/docs/_assets/matching-context.d2 +++ b/docs/_assets/matching-context.d2 @@ -1,6 +1,12 @@ # https://d2lang.com # Generate SVG by running `make` inside of `docs/`. +vars: { + d2-config: { + layout-engine: elk + } +} + Cluster: { grid-rows: 2 vertical-gap: 10 diff --git a/docs/_assets/retries.d2 b/docs/_assets/retries.d2 index 2cda5dba3ec..a4dbaba3f66 100644 --- a/docs/_assets/retries.d2 +++ b/docs/_assets/retries.d2 @@ -1,6 +1,12 @@ # https://d2lang.com # Generate SVG by running `make` inside of `docs/`. +vars: { + d2-config: { + layout-engine: elk + } +} + classes: { invisible: { style.opacity: 0 diff --git a/docs/architecture/chasm.md b/docs/architecture/chasm.md index 904de184f91..a4ffe734c0d 100644 --- a/docs/architecture/chasm.md +++ b/docs/architecture/chasm.md @@ -1,15 +1,373 @@ -# Terminology +# CHASM: Coordinated Heterogeneous Application State Machines -- The Temporal server implements abstractions such as `Workflow`, `Activity`, and `Scheduler`. +This document is a step-by-step introduction to the core architecture and domain entities of the CHASM framework. -- Each of these abstractions is an `Archetype`. +--- -- An instance of an `Archetype` is an `Execution` (e.g. a workflow execution, an activity execution). +## Why CHASM? -- An execution possesses a `BusinessID` that should typically be meaningful in the user’s own systems. It is guaranteed to be unique among executions within a namespace that are in a non-terminal state. +Temporal Workflows are powerful, but they have real limits: too slow or heavyweight for some problems, unable to scale in every dimension (e.g. millions of signals, large payloads), and overly complex when a purpose-built solution would be simpler. -- A single `Execution` may consist of multiple non-overlapping runs (e.g. successive non-overlapping invocations of an `Activity` or `Workflow` that share the same `businessID`, successive non-overlapping invocations of a `Workflow` by a `Scheduler`). +CHASM addresses this by treating Workflow as just one **Application State Machine (ASM)** among many. An ASM is a specialized state machine that leverages Temporal infrastructure like sharding, routing, atomic storage, failure recovery without the full cost of a Workflow — and hides those distributed systems details behind a clean, typed API so developers can focus on business logic. -- An `Execution` has a tree structure in which each subtree is a `Component`. A component, and thus an execution, may be a tree comprising one node only. +--- -- A `ComponentRef` contains an `ExecutionKey`, a `ComponentPath`, and additional information identifying (uniquely across all clusters when there is a multi-cluster configuration with failovers) a specific transition in execution history. Together these identify a component within an execution. \ No newline at end of file +## Application State Machine (ASM) + +An **ASM** is a registered state machine type, composed of a Library, Component types, and Tasks. + + + + + + +
+ + + + + +### Registry +The global catalog of all registered Libraries. + +### Library +A Library groups components, tasks, and service handlers into a namespace. + +Examples: [**`workflow`**](../../chasm/lib/workflow/library.go), [**`scheduler`**](../../chasm/lib/scheduler/library.go) and [**`nexusoperation`**](../../chasm/lib/nexusoperation/library.go) + +### Component type +A registered type that defines **state** (Fields) and **behavior**. +Each Component type is identified by a name (aka **Archetype**), which CHASM converts to a stable ID (aka **Archetype ID**) for storage. + +> [!NOTE] +> At runtime, a Component type is instantiated as a **Component** living inside a Node — see [Execution](#execution). + +### Tasks +Asynchronous work units (e.g. network calls, timers) that a Component type can schedule. Registered in Library alongside Component types. + +### Fields +The framework-managed containers for a component's persisted state. + +
+ +--- + +## Component State + +A Component's state is made up of typed Fields. CHASM uses these to persist and replicate data. + + + + + + +
+ + + + + +### Field types +- **`Field[T]`** — stores a single value. +- **`Map[K, T]`** — stores a keyed collection. +- **`ParentPtr`** — a typed reference to the parent Component. Allows a child to read its parent's state and call its methods. +- Transient fields — plain Go fields (not wrapped in a `Field`) that hold in-memory derived state. Not persisted; invalidated and recomputed as needed. + +`T` determines the field kind: +- **Data Field** — `T` is a Protobuf message; stores serialized data. +- **Component Field** — `T` is a child Component; stores a nested component subtree. + +> [!NOTE] +> `Field` and `Map` children are each persisted as separate nodes. Use a separate field for data that changes at a different frequency, is large, or is only read in certain operations. + +
+ +--- + +## Execution + +An **Execution** is a runtime instance of an ASM. Namespace retention, visibility records, and ID reuse policies all apply at the Execution level. + + + + + + +
+ + + + + +### ExecutionKey (*identity*) +The unique identifier of an Execution, composed of: +- **NamespaceID** +- **BusinessID** — user-defined name (e.g., a Workflow ID). Persists across resets. +- **RunID** — a single instance. Changes on reset or when a follow-up run is started under the same BusinessID; the BusinessID stays the same. + +
+ + + + + + +
+ + + + + +### Node (*state*) +The state of the Execution where [`ExecutionKey`](#executionkey-identity) identifies the **Root Component**. +- **Name** — the child's key in its parent: a sub-component field name or a `Map` key. +- **Component** — the runtime instance of a Component type. The Node handles storage; the Component handles behavior. +- **Children** — 0 or more child Nodes, each keyed by name. +- **initialVT / lastUpdateVT** — [VersionedTransition](#versionedtransition) stamps. + +The root Node and its descendants form the **CHASM Tree** of the Execution. + +### ComponentPath +The sequence of Names from the root to a child Node within an Execution (e.g. `["callbacks", "cb-1"]`). + +
+ +--- + +## Component Lifecycle + +Every Component implements a lifecycle method that CHASM uses to determine whether it has reached a terminal state. + + + + + + +
+ + + + + +### States +- **Running** — the component is active and accepting transitions. +- **Completed** — the component finished successfully; a terminal state. +- **Failed** — the component finished with an error; a terminal state. + +### Terminal State + +When the **Root Component** reaches a terminal state, the Execution closes: its BusinessID becomes available for reuse and it is eventually deleted according to the namespace's retention policy. + +
+ +--- + +## Engine + +The **Engine** is the entry point for interacting with Executions. + + + + + + +
+ + + + + +### Read +- **ReadComponent** — reads a Component's state without triggering a Transition. +- **PollComponent** — reads a Component's state and waits (long-polls) until a condition is met. +- **NotifyExecution** — notifies any `PollComponent` callers waiting on the Execution. + +### Context +Read-only. Provided during reads and task validation. + +
+ + + + + + +
+ + + + + +### Write +- **StartExecution** — creates a new Execution. +- **UpdateWithStartExecution** — creates a new Execution or updates an existing one atomically. +- **UpdateComponent** — delivers an event to a Component, triggering a [Transition](#transition). +- **DeleteExecution** — deletes an Execution. + +### MutableContext +Provided during write operations. Allows writing Fields and scheduling Tasks. + +
+ +--- + +## Transition + +A **Transition** is the atomic unit of state change in CHASM. It operates on the entire Execution — any Node can be read or mutated. + + + + + + +
+ + + + + +### Event +Any external trigger delivered to a Component — e.g. a network callback, timer firing, or incoming signal. + +### Transition Fn +Developer-provided code that runs when the Event is delivered. May write Fields and schedule Tasks. + +### Atomicity +All Field writes and Task schedules from a transition commit together as a single database write, or roll back entirely. On commit, every modified Node is stamped with a new [**VersionedTransition**](#versionedtransition). + +
+ +--- + +## Tasks + +Transitions can schedule Tasks for deferred work. There are two kinds, differing in when they run and what they can access. Tasks are written atomically with the component state ([transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html)), guaranteeing no work is silently lost on crash. + + + + + + +
+ + + + + +### Pure Task +Runs within a transaction with full read/write access to component state. A Pure Task scheduled without a deadline runs immediately in the same transaction as the scheduling Transition. One scheduled with a deadline runs in a new transaction when the deadline fires. + +### Side Effect Task +Runs asynchronously after the transaction commits. Can call external services but cannot mutate state directly — any state change must go through the same API as any external caller. + +### Task Executor +Each task type is processed by a handler with two methods: +- **Validator** — runs before execution; discards the task when it is no longer relevant (e.g. a newer attempt has superseded an older timer). +- **Executor** — carries out the task's work. + +### Retries +If the Validator or Executor returns an error, the framework retries the task until it succeeds or the Validator discards it. + +Tasks are processed in FIFO order in general, but strict ordering is not guaranteed. + +
+ +--- + +## VersionedTransition + +Every transition is stamped with a **VersionedTransition** — the logical clock of CHASM. + + + + + + +
+ + + + + +Provides a total ordering of all state changes, even across data centers. + +- **FailoverVersion** — increments when the owning cluster changes (cross-DC failover). +- **TransitionCount** — increments with every state update within the Execution. + +
+ +--- + +## ComponentRef + +When a component starts an external task and needs the result routed back as a **callback**, it creates a **ComponentRef** — a serialized token that acts as a return address. + + + + + + +
+ + + + + +Contains the full [**ExecutionKey**](#executionkey-identity) and [**ComponentPath**](#componentpath) so the callback can find the right node. + +Also carries two [VersionedTransition](#versionedtransition) values: + +- **initialVT** — the VersionedTransition when this node was *created*. Guards against a callback for a deleted-and-recreated node hitting the wrong instance at the same path. +- **lastUpdateVT** — the VersionedTransition when the token was *issued*. Guards against a stale callback updating a node that has already moved past the expected state. + +### Consistency Check +When a callback arrives, the Engine loads the Node at the given ComponentPath and verifies both VT values before allowing the transition to proceed. A mismatch on either value causes the callback to be rejected. + +
+ +--- + +## Visibility + +**Visibility** is a built-in child Component that manages an Execution's visibility record. + + + + + + +
+ + + + + +### Search Attributes +Indexed key-value pairs that make Executions queryable. There are two sources: +- **User-defined** custom Search Attributes stored by the archetype user (e.g. custom tags on a workflow execution). +- **Component-defined** Search Attributes computed dynamically from the root component's state, provided by implementing the search attributes provider interface. + +### Memo +Unindexed key-value metadata attached to an Execution. There are two sources: +- **User-defined** custom Memo stored by the archetype user. +- **Component-defined** Memo computed dynamically from the root component's state, provided by implementing the memo provider interface. + +
+ +--- + +## Typical Package Layout + +Each ASM is a **self-contained package**. + +``` +my_asm/ +├── proto/ +│ └── my_asm.proto # Service API (gRPC) and state/field message types +├── .go # Component struct, Field declarations, lifecycle method etc. +├── config.go # Dynamic config settings for the ASM +├── frontend.go # Frontend gRPC handler to map API calls to Engine operations +├── fx.go # fx module to wire the Library into the application +├── library.go # Registers component and task types with the Library +└── statemachine.go # (optional) Transition declarations using the statemachine framework +``` diff --git a/docs/architecture/nexus.md b/docs/architecture/nexus.md index 34b68f333cc..4e1025b5332 100644 --- a/docs/architecture/nexus.md +++ b/docs/architecture/nexus.md @@ -92,22 +92,6 @@ the 1.31.0 release and will be made the default. AllowInsecure: true # In production, set to false and ensure traffic is HTTPS/TLS encrypted ``` - -## Disabling Nexus - -To disable Nexus completely a server restart is required as the outbound queue processor (detailed below) is started -if `system.enableNexus` is on, but does not shut itself down when this config is disabled. See [Disabling -the Outbound Queue Processor](#disabling-the-outbound-queue-processor) for shutting off processing on a running server. - -## Downgrading to a Pre-Nexus Server Release - -In order to safely downgrade the server version to `1.24.x`, first disable nexus via dynamic config -(`system.enableNexus`). This ensures that no experimental functionality while Nexus was still being developed is -triggered. - -After disabling Nexus, outbound tasks currently scheduled will not be run and timer tasks will immediately go to the -[DLQ](../admin/dlq.md) without any retries. Workflows with pending Nexus operations will be stuck. - # Components ## Nexus Endpoint Registry diff --git a/docs/architecture/speculative-workflow-task.md b/docs/architecture/speculative-workflow-task.md index e109d223313..2aa45ff96f8 100644 --- a/docs/architecture/speculative-workflow-task.md +++ b/docs/architecture/speculative-workflow-task.md @@ -49,12 +49,14 @@ a special [in-memory-queue](./in-memory-queue.md) is used for speculative Workfl > #### TODO > It is important to point out that the `WorkflowTaskScheduled` and `WorkflowTaskStarted` events > for transient and speculative Workflow Task are only added to the `PollWorkflowTask` response - and -> not to the `GetWorkflowExecutionHistory` response. This has an unfortunate consequence: when the +> not to the `GetWorkflowExecutionHistory` response. This has an unfortunate consequence: when the > worker receives a speculative Workflow Task on a sticky task queue, but the Workflow is already -> evicted from its cache, it issues a `GetWorkflowExecutionHistory` request, which returns the -> history *without* speculative events. This leads to a `premature end of stream` error on the +> evicted from its cache, it issues a `GetWorkflowExecutionHistory` request, which returns the +> history *without* speculative events. This leads to a `premature end of stream` error on the > worker side. The worker fails the Workflow Task, clears stickiness, and everything works fine > after that - but a failed Workflow Task appears in the history. Fortunately, it doesn't happen often. +> +> See PR #9325 for related work on ensuring transient events are not incorrectly returned to CLI/UI clients. ## Speculative Workflow Task & Workflow Update Speculative Workflow Task was introduced to make it possible for Workflow Update to have zero writes diff --git a/docs/development/macos/cassandra.md b/docs/development/macos/cassandra.md index c8d2917d916..684562438ea 100644 --- a/docs/development/macos/cassandra.md +++ b/docs/development/macos/cassandra.md @@ -1,22 +1,17 @@ -# Run Cassandra v3.11 on macOS +# Run Cassandra v5.0 on macOS ### Install ```bash -brew install cassandra@3.11 +brew install cassandra ``` -For MacBook with ARM chip, you need to replace the JNA library with a recent version. -1. Locate the JNA file (`find $(brew --prefix) -name "jna-*.jar"`) and remove it. -2. Download a recent version of JNA jar file (5.8+) from [Maven](https://search.maven.org/artifact/net.java.dev.jna/jna). -3. Move the file you downloaded to the folder in step 1. - ### Start ```bash cassandra -f ``` ### Post Installation -Verify Cassandra v3.11 is running and accessible: +Verify Cassandra v5.0 is running and accessible: ```bash cqlsh ``` \ No newline at end of file diff --git a/docs/development/testing.md b/docs/development/testing.md index de6c36d045c..ca74abbd93c 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -11,11 +11,16 @@ This document describes the project's testing setup, utilities and best practice ### Environment variables - `CGO_ENABLED`: Set to `0` to disable CGO, which can significantly speed up compilation time. -- `TEMPORAL_TEST_LOG_FORMAT`: Controls the output format for test logs. Available options: `json` or `console` -- `TEMPORAL_TEST_LOG_LEVEL`: Sets the verbosity level for test logging. Available levels: `debug`, `info`, `warn`, `error`, `fatal` +- `TEMPORAL_TEST_LOG_FORMAT`: Controls the console output format for test logs. Available options: `json` or `console` (default) +- `TEMPORAL_TEST_LOG_LEVEL`: Sets the minimum verbosity level written to console output. Available levels: `debug` (default), `info`, `warn`, `error`, `fatal` +- `TEMPORAL_TEST_LOG_FILE`: Path to a file that receives a separate copy of test logs. When unset (default), file logging is disabled. Useful in CI to route debug-level logs to a downloadable artifact without flooding the job log. +- `TEMPORAL_TEST_LOG_FILE_FORMAT`: Output format for the log file. Available options: `json` (default) or `console` +- `TEMPORAL_TEST_LOG_FILE_LEVEL`: Minimum verbosity level written to the log file. Available levels: `debug` (default), `info`, `warn`, `error`, `fatal` - `TEMPORAL_TEST_OTEL_OUTPUT`: Enables OpenTelemetry (OTEL) trace output for failed tests to the provided file path. - `TEMPORAL_TEST_SHARED_CLUSTERS`: Number of shared clusters in the pool. Each can be used by multiple tests simultaneously. - `TEMPORAL_TEST_DEDICATED_CLUSTERS`: Number of dedicated clusters in the pool. Each can be used by one test only at a time. +- `TEMPORAL_TEST_TIMEOUT`: Sets the duration timeout per test (e.g., `90s` for 90 seconds). This can be overridden per-test using `testcore.WithTimeout()`. The timeout is multiplied by `debug.TimeoutMultiplier` when debugging. +- `TEMPORAL_TEST_DATA_ENCODING`: Controls the encoding used for persistence DataBlobs. Available options: `proto3` (default) or `json`. ### Debugging via IDE @@ -28,10 +33,33 @@ To pass in the required build tags, add them to the "Go tool arguments" field in -tags disable_grpc_modules,test_dep ``` +## Best Practices + +### Use `require` instead of `assert` + +Always use `require.X` (and `protorequire.X`) instead of `assert.X` (and `protoassert.X`). +`assert` records a failure but lets the test continue, which often leads to confusing +cascading errors. + +### Parallelization + +All tests (and subtests!) should use `t.Parallel()` to be run concurrently; +unless there is a reason not to. + +`make parallelize-tests` can be used to automatically add `t.Parallel()`. +Use `//parallelize:ignore` to opt your test out of it. + ## Test helpers Test helpers can be found in the [common/testing](../../common/testing) package. +### parallelsuite package + +Use `parallelsuite.Suite` to ensure your test suite is fast and safe: it runs all test methods and sub-tests in parallel by default; +and provides assertion helpers and safety mechanisms. + +It replaces all use of `testify`'s `Suite`. + ### testvars package Instead of creating identifiers like task queue name, namespace or worker identity by hand, @@ -56,7 +84,7 @@ func TestFoo(t *testing.T) { Later you can assert on the generated values. `testvars` guarantees to provide the same value every time you call the same method. ```go -assert.Equal(t, tv.WorkflowID(), startedWorkflow.WorkflowId) +require.Equal(t, tv.WorkflowID(), startedWorkflow.WorkflowId) ``` If you need more than one value for the same entity in one test, you can use `WithEntityNumber()` method to @@ -108,6 +136,20 @@ You'll find a fully initialized task poller in any functional test suite, look f _NOTE: The previous `testcore.TaskPoller` has been deprecated and should not be used in new code._ +### testhooks package + +The `testhooks` package injects test-specific behavior into production code paths that are otherwise +difficult to test. This is a **last resort** - prefer mocking and dependency injection when possible. + +**Example:** + +The UpdateWithStart API has a race window between releasing a lock and starting a workflow where +another request could create the same workflow first. The `UpdateWithStartInBetweenLockAndStart` +hook lets tests inject a callback at this exact point, making it possible to reliably test +conflict handling. + +_NOTE: Tests using testhooks must be run with `-tags=test_dep`._ + ### softassert package `softassert.That` is a "soft" assertion that logs an error if the given condition is false. @@ -123,11 +165,10 @@ will ultimately fail the test. Use `testcore.NewEnv(t)` to create a test environment with access to a Temporal cluster for end-to-end testing. ```go -func TestMyFeatureSuite(t *testing.T) { - t.Run("scenario one", func(t *testing.T) { - s := testcore.NewEnv(t) - // ... - })} +func (s* TestMyFeatureSuite) func TestXYZ(t *testing.T) { + s := testcore.NewEnv(t) + // ... +} ``` Note that each test has its own namespace (`s.Namespace()`) for isolation. diff --git a/go.mod b/go.mod index 339588fc0b9..82469f91fc5 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ retract ( ) require ( - cloud.google.com/go/storage v1.51.0 + cloud.google.com/go/storage v1.56.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/aws/aws-sdk-go v1.55.8 github.com/blang/semver/v4 v4.0.0 @@ -18,14 +18,14 @@ require ( github.com/emirpasic/gods v1.18.1 github.com/fatih/color v1.18.0 github.com/go-faker/faker/v4 v4.6.0 - github.com/go-jose/go-jose/v4 v4.0.5 + github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-sql-driver/mysql v1.9.0 github.com/gocql/gocql v1.7.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 github.com/iancoleman/strcase v0.3.0 github.com/jackc/pgx/v5 v5.7.2 github.com/jmoiron/sqlx v1.4.0 @@ -33,91 +33,135 @@ require ( github.com/lib/pq v1.10.9 github.com/maruel/panicparse/v2 v2.4.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/nexus-rpc/sdk-go v0.5.2-0.20260211051645-26b0b4c584e5 + github.com/nexus-rpc/sdk-go v0.6.0 github.com/olekukonko/tablewriter v0.0.5 github.com/olivere/elastic/v7 v7.0.32 - github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.21.0 - github.com/prometheus/client_model v0.6.1 + github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.62.0 github.com/robfig/cron/v3 v3.0.1 github.com/sony/gobreaker v1.0.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 github.com/tidwall/btree v1.8.1 github.com/uber-go/tally/v4 v4.1.17 github.com/urfave/cli v1.22.16 - github.com/urfave/cli/v2 v2.27.5 + github.com/urfave/cli/v2 v2.27.7 go.opentelemetry.io/collector/pdata v1.34.0 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 - go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 + go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 go.opentelemetry.io/otel/exporters/prometheus v0.56.0 - go.opentelemetry.io/otel/metric v1.34.0 - go.opentelemetry.io/otel/sdk v1.34.0 - go.opentelemetry.io/otel/sdk/metric v1.34.0 - go.opentelemetry.io/otel/trace v1.34.0 - go.temporal.io/api v1.62.2 - go.temporal.io/sdk v1.38.0 + go.opentelemetry.io/otel/metric v1.40.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/sdk/metric v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 + go.temporal.io/api v1.62.8-0.20260407190616-8574d6aa8b01 + go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 + go.temporal.io/sdk v1.41.1 go.uber.org/fx v1.24.0 go.uber.org/mock v0.6.0 go.uber.org/multierr v1.11.0 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 - golang.org/x/oauth2 v0.28.0 - golang.org/x/sync v0.18.0 - golang.org/x/text v0.31.0 - golang.org/x/time v0.10.0 - google.golang.org/api v0.224.0 - google.golang.org/grpc v1.72.2 - google.golang.org/protobuf v1.36.6 + golang.org/x/oauth2 v0.34.0 + golang.org/x/sync v0.19.0 + golang.org/x/text v0.32.0 + golang.org/x/time v0.14.0 + google.golang.org/api v0.256.0 + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.10 gopkg.in/validator.v2 v2.0.1 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.44.3 ) -require github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect +require ( + cloud.google.com/go/longrunning v0.7.0 // indirect + cloud.google.com/go/run v1.15.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.72.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect + github.com/aws/smithy-go v1.24.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/term v0.38.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + k8s.io/api v0.35.1 // indirect + k8s.io/apimachinery v0.35.1 // indirect + k8s.io/client-go v0.35.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) require ( - cel.dev/expr v0.23.1 // indirect - cloud.google.com/go v0.118.3 // indirect; indirect e - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.4.2 // indirect - cloud.google.com/go/monitoring v1.24.1 // indirect + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.121.6 // indirect; indirect e + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect dario.cat/mergo v1.0.1 // indirect - filippo.io/edwards25519 v1.1.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + filippo.io/edwards25519 v1.1.1 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/apache/thrift v0.21.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -134,12 +178,12 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -149,29 +193,26 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twmb/murmur3 v1.1.8 // indirect github.com/uber-common/bark v1.3.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - github.com/zeebo/errs v1.4.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 - go.opentelemetry.io/proto/otlp v1.5.0 + go.opentelemetry.io/proto/otlp v1.7.1 go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.19.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.40.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/inf.v0 v0.9.1 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) - -replace go.temporal.io/api => github.com/temporalio/api-go v1.62.8-0.20260407190616-8574d6aa8b01 diff --git a/go.sum b/go.sum index ca2528a51e9..d1ea3ed8d09 100644 --- a/go.sum +++ b/go.sum @@ -1,46 +1,49 @@ -cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= -cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= -cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q= -cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= -cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= -cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0= -cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0= -cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= -cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= -cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= -cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/run v1.15.0 h1:4cwyNv9SUQEsQOf5/DfPKyMWYSA52p38/o119BgMhO4= +cloud.google.com/go/run v1.15.0/go.mod h1:rgFHMdAopLl++57vzeqA+a1o2x0/ILZnEacRD6nC0EA= +cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI= +cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -49,6 +52,40 @@ github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= +github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/ecs v1.72.1 h1:Pciw9l/TbLpOjvTT9vm4IzHAyl2xQMBGlS44d0TvXXE= +github.com/aws/aws-sdk-go-v2/service/ecs v1.72.1/go.mod h1:DdtkqcURi9GM8f9HVLzJLTvS0h0k1qYg39vKQFmeR/k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0 h1:u66DMbJWDFXs9458RAHNtq2d0gyqcZFV4mzRwfjM358= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0/go.mod h1:ogjbkxFgFOjG3dYFQ8irC92gQfpfMDcy1RDKNSZWXNU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= +github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/benbjohnson/clock v0.0.0-20160125162948-a620c1cc9866/go.mod h1:UMqtWQTnOe4byzwe7Zhwh8f8s+36uszN51sJrSIZlTE= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -72,31 +109,34 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b/go.mod h1:v9FBN7gdVTpiD/+LZ7Po0UKvROyT87uLVxTHVky/dlQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-farm v0.0.0-20140601200337-fc41e106ee0e/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -108,19 +148,31 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs= github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -136,6 +188,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -143,23 +197,23 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= -github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -198,6 +252,7 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -208,6 +263,7 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/panicparse/v2 v2.4.0 h1:yQKMIbQ0DKfinzVkTkcUzQyQ60UCiNnYfR7PWwTs2VI= @@ -230,19 +286,24 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nexus-rpc/sdk-go v0.5.2-0.20260211051645-26b0b4c584e5 h1:Van9KGGs8lcDgxzSNFbDhEMNeJ80TbBxwZ45f9iBk9U= -github.com/nexus-rpc/sdk-go v0.5.2-0.20260211051645-26b0b4c584e5/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= +github.com/nexus-rpc/sdk-go v0.6.0 h1:QRgnP2zTbxEbiyWG/aXH8uSC5LV/Mg1fqb19jb4DBlo= +github.com/nexus-rpc/sdk-go v0.6.0/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -251,14 +312,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a h1:AA9vgIBDjMHPC2McaGPojgV2dcI78ZC0TLNhYCXEKH8= github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a/go.mod h1:lzZQ3Noex5pfAy7mkAeCjcBDteYU85uWWnJ/y6gKU8k= github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= @@ -276,8 +338,8 @@ github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfm github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samuel/go-thrift v0.0.0-20190219015601-e8b6b52668fe/go.mod h1:Vrkh1pnjV9Bl8c3P9zH0/D4NlOHWP5d4/hF4YTULaec= @@ -291,8 +353,10 @@ github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -306,12 +370,11 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/temporalio/api-go v1.62.8-0.20260407190616-8574d6aa8b01 h1:Rtf7bXBmXzNuUgJS+PGIVgvp/qRhIbNKqz9nrBynDQM= -github.com/temporalio/api-go v1.62.8-0.20260407190616-8574d6aa8b01/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w= github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4= github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04= @@ -335,28 +398,28 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector/pdata v1.34.0 h1:2vwYftckXe7pWxI9mfSo+tw3wqdGNrYpMbDx/5q6rw8= go.opentelemetry.io/collector/pdata v1.34.0/go.mod h1:StPHMFkhLBellRWrULq0DNjv4znCDJZP6La4UuC+JHI= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= @@ -365,20 +428,24 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0u go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/exporters/prometheus v0.56.0 h1:GnCIi0QyG0yy2MrJLzVrIM7laaJstj//flf1zEJCG+E= go.opentelemetry.io/otel/exporters/prometheus v0.56.0/go.mod h1:JQcVZtbIIPM+7SWBB+T6FK+xunlyidwLp++fN0sUaOk= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/sdk v1.38.0 h1:4Bok5LEdED7YKpsSjIa3dDqram5VOq+ydBf4pyx0Wo4= -go.temporal.io/sdk v1.38.0/go.mod h1:a+R2Ej28ObvHoILbHaxMyind7M6D+W0L7edt5UJF4SE= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.temporal.io/api v1.62.8-0.20260407190616-8574d6aa8b01 h1:YZf5B/BjOlNm4h2TtYVXvDrZURRxMIAPmXzAqKKBK34= +go.temporal.io/api v1.62.8-0.20260407190616-8574d6aa8b01/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ= +go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2/go.mod h1:T8dnzVPeO+gaUTj9eDgm/lT2lZH4+JXNvrGaQGyVi50= +go.temporal.io/sdk v1.41.1 h1:yOpvsHyDD1lNuwlGBv/SUodCPhjv9nDeC9lLHW/fJUA= +go.temporal.io/sdk v1.41.1/go.mod h1:/InXQT5guZ6AizYzpmzr5avQ/GMgq1ZObcKlKE2AhTc= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -397,16 +464,20 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -426,8 +497,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -440,18 +511,18 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -472,13 +543,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -486,10 +559,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -504,34 +577,38 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= -google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= @@ -545,6 +622,18 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= @@ -574,3 +663,11 @@ modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/proto/internal/buf.yaml b/proto/internal/buf.yaml index 1c717d85b94..0625f0cffae 100644 --- a/proto/internal/buf.yaml +++ b/proto/internal/buf.yaml @@ -9,15 +9,9 @@ deps: breaking: use: - WIRE - # Uncomment this to temporarily ignore specific files or directories: - ignore: + # Uncomment this to temporarily ignore specific files or directories: + # ignore: # example: - temporal/server/api/.../message.proto - - temporal/server/api/persistence/v1/chasm.proto - - temporal/server/api/token/v1/message.proto - # TODO: (Chetan) Remove this after merging #8500 - - temporal/server/api/persistence/v1/executions.proto - # Temporary ignore for revert of #9138 - - temporal/server/api/historyservice/v1/request_response.proto lint: use: - DEFAULT diff --git a/proto/internal/temporal/server/api/adminservice/v1/request_response.proto b/proto/internal/temporal/server/api/adminservice/v1/request_response.proto index 04a95cd0d07..8240afb3926 100644 --- a/proto/internal/temporal/server/api/adminservice/v1/request_response.proto +++ b/proto/internal/temporal/server/api/adminservice/v1/request_response.proto @@ -1,43 +1,42 @@ syntax = "proto3"; package temporal.server.api.adminservice.v1; -option go_package = "go.temporal.io/server/api/adminservice/v1;adminservice"; -import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; - +import "google/protobuf/timestamp.proto"; +import "temporal/api/common/v1/message.proto"; import "temporal/api/enums/v1/common.proto"; import "temporal/api/enums/v1/task_queue.proto"; -import "temporal/api/common/v1/message.proto"; -import "temporal/api/version/v1/message.proto"; -import "temporal/api/workflow/v1/message.proto"; import "temporal/api/namespace/v1/message.proto"; import "temporal/api/replication/v1/message.proto"; import "temporal/api/taskqueue/v1/message.proto"; - +import "temporal/api/version/v1/message.proto"; +import "temporal/api/workflow/v1/message.proto"; import "temporal/server/api/cluster/v1/message.proto"; import "temporal/server/api/common/v1/dlq.proto"; -import "temporal/server/api/enums/v1/common.proto"; import "temporal/server/api/enums/v1/cluster.proto"; -import "temporal/server/api/enums/v1/task.proto"; +import "temporal/server/api/enums/v1/common.proto"; import "temporal/server/api/enums/v1/dlq.proto"; +import "temporal/server/api/enums/v1/task.proto"; +import "temporal/server/api/health/v1/message.proto"; import "temporal/server/api/history/v1/message.proto"; import "temporal/server/api/namespace/v1/message.proto"; -import "temporal/server/api/replication/v1/message.proto"; import "temporal/server/api/persistence/v1/cluster_metadata.proto"; import "temporal/server/api/persistence/v1/executions.proto"; -import "temporal/server/api/persistence/v1/workflow_mutable_state.proto"; -import "temporal/server/api/persistence/v1/tasks.proto"; import "temporal/server/api/persistence/v1/hsm.proto"; +import "temporal/server/api/persistence/v1/tasks.proto"; +import "temporal/server/api/persistence/v1/workflow_mutable_state.proto"; +import "temporal/server/api/replication/v1/message.proto"; import "temporal/server/api/taskqueue/v1/message.proto"; +option go_package = "go.temporal.io/server/api/adminservice/v1;adminservice"; + message RebuildMutableStateRequest { string namespace = 1; temporal.api.common.v1.WorkflowExecution execution = 2; } -message RebuildMutableStateResponse { -} +message RebuildMutableStateResponse {} message ImportWorkflowExecutionRequest { string namespace = 1; @@ -56,6 +55,8 @@ message DescribeMutableStateRequest { temporal.api.common.v1.WorkflowExecution execution = 2; bool skip_force_reload = 3; string archetype = 4; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 5; } message DescribeMutableStateResponse { @@ -63,7 +64,7 @@ message DescribeMutableStateResponse { string history_addr = 2; // CacheMutableState is only available when mutable state is in cache. temporal.server.api.persistence.v1.WorkflowMutableState cache_mutable_state = 3; - // DatabaseMutableState is always available, + // DatabaseMutableState is always available, // but only loaded from database when mutable state is NOT in cache or skip_force_reload is false. temporal.server.api.persistence.v1.WorkflowMutableState database_mutable_state = 4; } @@ -89,8 +90,7 @@ message CloseShardRequest { int32 shard_id = 1; } -message CloseShardResponse { -} +message CloseShardResponse {} message GetShardRequest { int32 shard_id = 1; @@ -132,8 +132,7 @@ message RemoveTaskRequest { google.protobuf.Timestamp visibility_time = 4; } -message RemoveTaskResponse { -} +message RemoveTaskResponse {} /** * StartEventId defines the beginning of the event to fetch. The first event is exclusive. @@ -215,8 +214,7 @@ message ReapplyEventsRequest { temporal.api.common.v1.DataBlob events = 3; } -message ReapplyEventsResponse { -} +message ReapplyEventsResponse {} message AddSearchAttributesRequest { map search_attributes = 1; @@ -225,8 +223,7 @@ message AddSearchAttributesRequest { string namespace = 4; } -message AddSearchAttributesResponse { -} +message AddSearchAttributesResponse {} message RemoveSearchAttributesRequest { repeated string search_attributes = 1; @@ -234,8 +231,7 @@ message RemoveSearchAttributesRequest { string namespace = 3; } -message RemoveSearchAttributesResponse { -} +message RemoveSearchAttributesResponse {} message GetSearchAttributesRequest { string index_name = 1; @@ -289,15 +285,13 @@ message AddOrUpdateRemoteClusterRequest { bool enable_replication = 4; } -message AddOrUpdateRemoteClusterResponse { -} +message AddOrUpdateRemoteClusterResponse {} message RemoveRemoteClusterRequest { string cluster_name = 1; } -message RemoveRemoteClusterResponse { -} +message RemoveRemoteClusterResponse {} message ListClusterMembersRequest { // (-- api-linter: core::0140::prepositions=disabled @@ -341,8 +335,7 @@ message PurgeDLQMessagesRequest { int64 inclusive_end_message_id = 4; } -message PurgeDLQMessagesResponse { -} +message PurgeDLQMessagesResponse {} message MergeDLQMessagesRequest { temporal.server.api.enums.v1.DeadLetterQueueType type = 1; @@ -362,10 +355,11 @@ message RefreshWorkflowTasksRequest { string namespace_id = 3; temporal.api.common.v1.WorkflowExecution execution = 2; string archetype = 4; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 5; } -message RefreshWorkflowTasksResponse { -} +message RefreshWorkflowTasksResponse {} message ResendReplicationTasksRequest { string namespace_id = 1; @@ -378,8 +372,7 @@ message ResendReplicationTasksRequest { int64 end_version = 8; } -message ResendReplicationTasksResponse { -} +message ResendReplicationTasksResponse {} message GetTaskQueueTasksRequest { string namespace = 1; @@ -402,6 +395,8 @@ message DeleteWorkflowExecutionRequest { string namespace = 1; temporal.api.common.v1.WorkflowExecution execution = 2; string archetype = 3; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 4; } message DeleteWorkflowExecutionResponse { @@ -454,7 +449,7 @@ message GetDLQTasksResponse { message PurgeDLQTasksRequest { temporal.server.api.common.v1.HistoryDLQKey dlq_key = 1; - temporal.server.api.common.v1. HistoryDLQTaskMetadata inclusive_max_task_metadata = 2; + temporal.server.api.common.v1.HistoryDLQTaskMetadata inclusive_max_task_metadata = 2; } message PurgeDLQTasksResponse { @@ -547,11 +542,12 @@ message ListQueuesResponse { bytes next_page_token = 2; } -message DeepHealthCheckRequest { -} +message DeepHealthCheckRequest {} message DeepHealthCheckResponse { temporal.server.api.enums.v1.HealthState state = 1; + // Per-service diagnostic details including per-host breakdown. + repeated temporal.server.api.health.v1.ServiceHealthDetail services = 2; } message SyncWorkflowStateRequest { @@ -577,6 +573,8 @@ message GenerateLastHistoryReplicationTasksRequest { temporal.api.common.v1.WorkflowExecution execution = 2; repeated string target_clusters = 3; string archetype = 4; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 5; } message GenerateLastHistoryReplicationTasksResponse { @@ -634,40 +632,36 @@ message StartAdminBatchOperationRequest { } } -message StartAdminBatchOperationResponse { -} +message StartAdminBatchOperationResponse {} // BatchOperationRefreshTasks refreshes tasks for batch executions. // This regenerates all pending tasks for each execution. -message BatchOperationRefreshTasks { -} - +message BatchOperationRefreshTasks {} message MigrateScheduleRequest { - // Target scheduler implementation for migration. - enum SchedulerTarget { - SCHEDULER_TARGET_UNSPECIFIED = 0; - // Migrate to CHASM-backed scheduler (V2). - SCHEDULER_TARGET_CHASM = 1; - // Migrate to workflow-backed scheduler (V1). - SCHEDULER_TARGET_WORKFLOW = 2; - } - - // Namespace name. - string namespace = 1; + // Target scheduler implementation for migration. + enum SchedulerTarget { + SCHEDULER_TARGET_UNSPECIFIED = 0; + // Migrate to CHASM-backed scheduler (V2). + SCHEDULER_TARGET_CHASM = 1; + // Migrate to workflow-backed scheduler (V1). + SCHEDULER_TARGET_WORKFLOW = 2; + } - // Schedule ID. - string schedule_id = 2; + // Namespace name. + string namespace = 1; + + // Schedule ID. + string schedule_id = 2; - // Target scheduler implementation. - SchedulerTarget target = 3; + // Target scheduler implementation. + SchedulerTarget target = 3; - // Identity of the caller. - string identity = 4; + // Identity of the caller. + string identity = 4; - // Used for request deduplication. - string request_id = 5; + // Used for request deduplication. + string request_id = 5; } message MigrateScheduleResponse {} - diff --git a/proto/internal/temporal/server/api/adminservice/v1/service.proto b/proto/internal/temporal/server/api/adminservice/v1/service.proto index 1830a158353..40e052fd0ce 100644 --- a/proto/internal/temporal/server/api/adminservice/v1/service.proto +++ b/proto/internal/temporal/server/api/adminservice/v1/service.proto @@ -1,171 +1,230 @@ syntax = "proto3"; package temporal.server.api.adminservice.v1; -option go_package = "go.temporal.io/server/api/adminservice/v1;adminservice"; import "temporal/server/api/adminservice/v1/request_response.proto"; +import "temporal/server/api/common/v1/api_category.proto"; + +option go_package = "go.temporal.io/server/api/adminservice/v1;adminservice"; // AdminService provides advanced APIs for debugging and analysis with admin privilege service AdminService { - // RebuildMutableState attempts to rebuild mutable state according to persisted history events. - // NOTE: this is experimental API - rpc RebuildMutableState (RebuildMutableStateRequest) returns (RebuildMutableStateResponse) { - } - - // ImportWorkflowExecution attempts to import workflow according to persisted history events. - // NOTE: this is experimental API - rpc ImportWorkflowExecution (ImportWorkflowExecutionRequest) returns (ImportWorkflowExecutionResponse) { - } - - // DescribeWorkflowExecution returns information about the internal states of workflow execution. - rpc DescribeMutableState (DescribeMutableStateRequest) returns (DescribeMutableStateResponse) { - } - - // DescribeHistoryHost returns information about the internal states of a history host - rpc DescribeHistoryHost (DescribeHistoryHostRequest) returns (DescribeHistoryHostResponse) { - } - - rpc GetShard (GetShardRequest) returns (GetShardResponse) { - } - - rpc CloseShard (CloseShardRequest) returns (CloseShardResponse) { - } - - rpc ListHistoryTasks (ListHistoryTasksRequest) returns (ListHistoryTasksResponse) { - } - - rpc RemoveTask (RemoveTaskRequest) returns (RemoveTaskResponse) { - } - - // Returns the raw history of specified workflow execution. It fails with 'NotFound' if specified workflow - // execution in unknown to the service. - // StartEventId defines the beginning of the event to fetch. The first event is inclusive. - // EndEventId and EndEventVersion defines the end of the event to fetch. The end event is exclusive. - rpc GetWorkflowExecutionRawHistoryV2 (GetWorkflowExecutionRawHistoryV2Request) returns (GetWorkflowExecutionRawHistoryV2Response) { - } - - // StartEventId defines the beginning of the event to fetch. The first event is inclusive. - // EndEventId and EndEventVersion defines the end of the event to fetch. The end event is inclusive. - rpc GetWorkflowExecutionRawHistory (GetWorkflowExecutionRawHistoryRequest) returns (GetWorkflowExecutionRawHistoryResponse) { - } - - // GetReplicationMessages returns new replication tasks since the read level provided in the token. - rpc GetReplicationMessages (GetReplicationMessagesRequest) returns (GetReplicationMessagesResponse) { - } - - // GetNamespaceReplicationMessages returns new namespace replication tasks since last retrieved task Id. - rpc GetNamespaceReplicationMessages (GetNamespaceReplicationMessagesRequest) returns (GetNamespaceReplicationMessagesResponse) { - } - - // GetDLQReplicationMessages return replication messages based on DLQ info. - rpc GetDLQReplicationMessages(GetDLQReplicationMessagesRequest) returns (GetDLQReplicationMessagesResponse){ - } - - // ReapplyEvents applies stale events to the current workflow and current run. - rpc ReapplyEvents (ReapplyEventsRequest) returns (ReapplyEventsResponse) { - } - - // AddSearchAttributes add custom search attributes and returns comprehensive information about them. - // Deprecated. Use operatorservice instead. - rpc AddSearchAttributes (AddSearchAttributesRequest) returns (AddSearchAttributesResponse) { - } - - // RemoveSearchAttributes removes custom search attributes and returns comprehensive information about them. - // Deprecated. Use operatorservice instead. - rpc RemoveSearchAttributes (RemoveSearchAttributesRequest) returns (RemoveSearchAttributesResponse) { - } - - // GetSearchAttributes returns comprehensive information about search attributes. - // Deprecated. Use operatorservice instead. - rpc GetSearchAttributes (GetSearchAttributesRequest) returns (GetSearchAttributesResponse) { - } - - // DescribeCluster returns information about Temporal cluster. - rpc DescribeCluster(DescribeClusterRequest) returns (DescribeClusterResponse) { - } - - // ListClusters returns information about Temporal clusters. - rpc ListClusters(ListClustersRequest) returns (ListClustersResponse) { - } - - // ListClusterMembers returns information about Temporal cluster members. - rpc ListClusterMembers(ListClusterMembersRequest) returns (ListClusterMembersResponse) { - } - - // AddOrUpdateRemoteCluster adds or updates remote cluster. - rpc AddOrUpdateRemoteCluster(AddOrUpdateRemoteClusterRequest) returns (AddOrUpdateRemoteClusterResponse) { - } - - // RemoveRemoteCluster removes remote cluster. - rpc RemoveRemoteCluster(RemoveRemoteClusterRequest) returns (RemoveRemoteClusterResponse) { - } - - // GetDLQMessages returns messages from DLQ. - rpc GetDLQMessages(GetDLQMessagesRequest) returns (GetDLQMessagesResponse) { - } - - // (-- api-linter: core::0165::response-message-name=disabled - // aip.dev/not-precedent: --) - // PurgeDLQMessages purges messages from DLQ. - rpc PurgeDLQMessages(PurgeDLQMessagesRequest) returns (PurgeDLQMessagesResponse) { - } - - // MergeDLQMessages merges messages from DLQ. - rpc MergeDLQMessages(MergeDLQMessagesRequest) returns (MergeDLQMessagesResponse) { - } - - // RefreshWorkflowTasks refreshes all tasks of a workflow. - rpc RefreshWorkflowTasks(RefreshWorkflowTasksRequest) returns (RefreshWorkflowTasksResponse) { - } - - // StartAdminBatchOperation starts an admin batch operation. Supports internal operations like RefreshWorkflowTasks. - rpc StartAdminBatchOperation(StartAdminBatchOperationRequest) returns (StartAdminBatchOperationResponse) { - } - - // ResendReplicationTasks requests replication tasks from remote cluster and apply tasks to current cluster. - rpc ResendReplicationTasks(ResendReplicationTasksRequest) returns (ResendReplicationTasksResponse) { - } - - // GetTaskQueueTasks returns tasks from task queue. - rpc GetTaskQueueTasks(GetTaskQueueTasksRequest) returns (GetTaskQueueTasksResponse) { - } - - // DeleteWorkflowExecution force deletes a workflow's visibility record, current & concrete execution record and history if possible - rpc DeleteWorkflowExecution(DeleteWorkflowExecutionRequest) returns (DeleteWorkflowExecutionResponse) { - } - - rpc StreamWorkflowReplicationMessages(stream StreamWorkflowReplicationMessagesRequest) returns (stream StreamWorkflowReplicationMessagesResponse) { - } - - rpc GetNamespace(GetNamespaceRequest) returns (GetNamespaceResponse) { - } - - rpc GetDLQTasks (GetDLQTasksRequest) returns (GetDLQTasksResponse) { - } - // (-- api-linter: core::0165::response-message-name=disabled - // aip.dev/not-precedent: --) - rpc PurgeDLQTasks (PurgeDLQTasksRequest) returns (PurgeDLQTasksResponse) {} - - rpc MergeDLQTasks (MergeDLQTasksRequest) returns (MergeDLQTasksResponse) {} - - rpc DescribeDLQJob (DescribeDLQJobRequest) returns (DescribeDLQJobResponse) {} - - rpc CancelDLQJob (CancelDLQJobRequest) returns (CancelDLQJobResponse) {} - - rpc AddTasks (AddTasksRequest) returns (AddTasksResponse) {} - - rpc ListQueues (ListQueuesRequest) returns (ListQueuesResponse) {} - - rpc DeepHealthCheck (DeepHealthCheckRequest) returns (DeepHealthCheckResponse) {} - - rpc SyncWorkflowState (SyncWorkflowStateRequest) returns (SyncWorkflowStateResponse) {} - - rpc GenerateLastHistoryReplicationTasks(GenerateLastHistoryReplicationTasksRequest) returns (GenerateLastHistoryReplicationTasksResponse) {} - - rpc DescribeTaskQueuePartition (DescribeTaskQueuePartitionRequest) returns (DescribeTaskQueuePartitionResponse) {} - - rpc ForceUnloadTaskQueuePartition (ForceUnloadTaskQueuePartitionRequest) returns (ForceUnloadTaskQueuePartitionResponse) {} - - // MigrateSchedule migrates a schedule between V1 (workflow-backed) and V2 (CHASM-backed) implementations. - rpc MigrateSchedule (MigrateScheduleRequest) returns (MigrateScheduleResponse) {} + // RebuildMutableState attempts to rebuild mutable state according to persisted history events. + // NOTE: this is experimental API + rpc RebuildMutableState(RebuildMutableStateRequest) returns (RebuildMutableStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // ImportWorkflowExecution attempts to import workflow according to persisted history events. + // NOTE: this is experimental API + rpc ImportWorkflowExecution(ImportWorkflowExecutionRequest) returns (ImportWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // DescribeWorkflowExecution returns information about the internal states of workflow execution. + rpc DescribeMutableState(DescribeMutableStateRequest) returns (DescribeMutableStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // DescribeHistoryHost returns information about the internal states of a history host + rpc DescribeHistoryHost(DescribeHistoryHostRequest) returns (DescribeHistoryHostResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc GetShard(GetShardRequest) returns (GetShardResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc CloseShard(CloseShardRequest) returns (CloseShardResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc ListHistoryTasks(ListHistoryTasksRequest) returns (ListHistoryTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc RemoveTask(RemoveTaskRequest) returns (RemoveTaskResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // Returns the raw history of specified workflow execution. It fails with 'NotFound' if specified workflow + // execution in unknown to the service. + // StartEventId defines the beginning of the event to fetch. The first event is inclusive. + // EndEventId and EndEventVersion defines the end of the event to fetch. The end event is exclusive. + rpc GetWorkflowExecutionRawHistoryV2(GetWorkflowExecutionRawHistoryV2Request) returns (GetWorkflowExecutionRawHistoryV2Response) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // StartEventId defines the beginning of the event to fetch. The first event is inclusive. + // EndEventId and EndEventVersion defines the end of the event to fetch. The end event is inclusive. + rpc GetWorkflowExecutionRawHistory(GetWorkflowExecutionRawHistoryRequest) returns (GetWorkflowExecutionRawHistoryResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // GetReplicationMessages returns new replication tasks since the read level provided in the token. + rpc GetReplicationMessages(GetReplicationMessagesRequest) returns (GetReplicationMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // GetNamespaceReplicationMessages returns new namespace replication tasks since last retrieved task Id. + rpc GetNamespaceReplicationMessages(GetNamespaceReplicationMessagesRequest) returns (GetNamespaceReplicationMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // GetDLQReplicationMessages return replication messages based on DLQ info. + rpc GetDLQReplicationMessages(GetDLQReplicationMessagesRequest) returns (GetDLQReplicationMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // ReapplyEvents applies stale events to the current workflow and current run. + rpc ReapplyEvents(ReapplyEventsRequest) returns (ReapplyEventsResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // AddSearchAttributes add custom search attributes and returns comprehensive information about them. + // Deprecated. Use operatorservice instead. + rpc AddSearchAttributes(AddSearchAttributesRequest) returns (AddSearchAttributesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // RemoveSearchAttributes removes custom search attributes and returns comprehensive information about them. + // Deprecated. Use operatorservice instead. + rpc RemoveSearchAttributes(RemoveSearchAttributesRequest) returns (RemoveSearchAttributesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // GetSearchAttributes returns comprehensive information about search attributes. + // Deprecated. Use operatorservice instead. + rpc GetSearchAttributes(GetSearchAttributesRequest) returns (GetSearchAttributesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // DescribeCluster returns information about Temporal cluster. + rpc DescribeCluster(DescribeClusterRequest) returns (DescribeClusterResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // ListClusters returns information about Temporal clusters. + rpc ListClusters(ListClustersRequest) returns (ListClustersResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // ListClusterMembers returns information about Temporal cluster members. + rpc ListClusterMembers(ListClusterMembersRequest) returns (ListClusterMembersResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // AddOrUpdateRemoteCluster adds or updates remote cluster. + rpc AddOrUpdateRemoteCluster(AddOrUpdateRemoteClusterRequest) returns (AddOrUpdateRemoteClusterResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // RemoveRemoteCluster removes remote cluster. + rpc RemoveRemoteCluster(RemoveRemoteClusterRequest) returns (RemoveRemoteClusterResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // GetDLQMessages returns messages from DLQ. + rpc GetDLQMessages(GetDLQMessagesRequest) returns (GetDLQMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // (-- api-linter: core::0165::response-message-name=disabled + // aip.dev/not-precedent: --) + // PurgeDLQMessages purges messages from DLQ. + rpc PurgeDLQMessages(PurgeDLQMessagesRequest) returns (PurgeDLQMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // MergeDLQMessages merges messages from DLQ. + rpc MergeDLQMessages(MergeDLQMessagesRequest) returns (MergeDLQMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // RefreshWorkflowTasks refreshes all tasks of a workflow. + rpc RefreshWorkflowTasks(RefreshWorkflowTasksRequest) returns (RefreshWorkflowTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // StartAdminBatchOperation starts an admin batch operation. Supports internal operations like RefreshWorkflowTasks. + rpc StartAdminBatchOperation(StartAdminBatchOperationRequest) returns (StartAdminBatchOperationResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // ResendReplicationTasks requests replication tasks from remote cluster and apply tasks to current cluster. + rpc ResendReplicationTasks(ResendReplicationTasksRequest) returns (ResendReplicationTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // GetTaskQueueTasks returns tasks from task queue. + rpc GetTaskQueueTasks(GetTaskQueueTasksRequest) returns (GetTaskQueueTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // DeleteWorkflowExecution force deletes a workflow's visibility record, current & concrete execution record and history if possible + rpc DeleteWorkflowExecution(DeleteWorkflowExecutionRequest) returns (DeleteWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc StreamWorkflowReplicationMessages(stream StreamWorkflowReplicationMessagesRequest) returns (stream StreamWorkflowReplicationMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc GetNamespace(GetNamespaceRequest) returns (GetNamespaceResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc GetDLQTasks(GetDLQTasksRequest) returns (GetDLQTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + // (-- api-linter: core::0165::response-message-name=disabled + // aip.dev/not-precedent: --) + rpc PurgeDLQTasks(PurgeDLQTasksRequest) returns (PurgeDLQTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc MergeDLQTasks(MergeDLQTasksRequest) returns (MergeDLQTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc DescribeDLQJob(DescribeDLQJobRequest) returns (DescribeDLQJobResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc CancelDLQJob(CancelDLQJobRequest) returns (CancelDLQJobResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc AddTasks(AddTasksRequest) returns (AddTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc ListQueues(ListQueuesRequest) returns (ListQueuesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc DeepHealthCheck(DeepHealthCheckRequest) returns (DeepHealthCheckResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc SyncWorkflowState(SyncWorkflowStateRequest) returns (SyncWorkflowStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc GenerateLastHistoryReplicationTasks(GenerateLastHistoryReplicationTasksRequest) returns (GenerateLastHistoryReplicationTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc DescribeTaskQueuePartition(DescribeTaskQueuePartitionRequest) returns (DescribeTaskQueuePartitionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc ForceUnloadTaskQueuePartition(ForceUnloadTaskQueuePartitionRequest) returns (ForceUnloadTaskQueuePartitionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // MigrateSchedule migrates a schedule between V1 (workflow-backed) and V2 (CHASM-backed) implementations. + rpc MigrateSchedule(MigrateScheduleRequest) returns (MigrateScheduleResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } } diff --git a/proto/internal/temporal/server/api/archiver/v1/message.proto b/proto/internal/temporal/server/api/archiver/v1/message.proto index ead6f57eb52..41680046eee 100644 --- a/proto/internal/temporal/server/api/archiver/v1/message.proto +++ b/proto/internal/temporal/server/api/archiver/v1/message.proto @@ -2,47 +2,46 @@ syntax = "proto3"; package temporal.server.api.archiver.v1; -option go_package = "go.temporal.io/server/api/archiver/v1;archiver"; - import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; - import "temporal/api/common/v1/message.proto"; -import "temporal/api/history/v1/message.proto"; import "temporal/api/enums/v1/workflow.proto"; +import "temporal/api/history/v1/message.proto"; + +option go_package = "go.temporal.io/server/api/archiver/v1;archiver"; message HistoryBlobHeader { - string namespace = 1; - string namespace_id = 2; - string workflow_id = 3; - string run_id = 4; - bool is_last = 5; - int64 first_failover_version = 6; - int64 last_failover_version = 7; - int64 first_event_id = 8; - int64 last_event_id = 9; - int64 event_count = 10; + string namespace = 1; + string namespace_id = 2; + string workflow_id = 3; + string run_id = 4; + bool is_last = 5; + int64 first_failover_version = 6; + int64 last_failover_version = 7; + int64 first_event_id = 8; + int64 last_event_id = 9; + int64 event_count = 10; } -message HistoryBlob { - HistoryBlobHeader header = 1; - repeated temporal.api.history.v1.History body = 2; +message HistoryBlob { + HistoryBlobHeader header = 1; + repeated temporal.api.history.v1.History body = 2; } // VisibilityRecord is a single workflow visibility record in archive. message VisibilityRecord { - string namespace_id = 1; - string namespace = 2; - string workflow_id = 3; - string run_id = 4; - string workflow_type_name = 5; - google.protobuf.Timestamp start_time = 6; - google.protobuf.Timestamp execution_time = 7; - google.protobuf.Timestamp close_time = 8; - temporal.api.enums.v1.WorkflowExecutionStatus status = 9; - int64 history_length = 10; - temporal.api.common.v1.Memo memo = 11; - map search_attributes = 12; - string history_archival_uri = 13; - google.protobuf.Duration execution_duration = 14; + string namespace_id = 1; + string namespace = 2; + string workflow_id = 3; + string run_id = 4; + string workflow_type_name = 5; + google.protobuf.Timestamp start_time = 6; + google.protobuf.Timestamp execution_time = 7; + google.protobuf.Timestamp close_time = 8; + temporal.api.enums.v1.WorkflowExecutionStatus status = 9; + int64 history_length = 10; + temporal.api.common.v1.Memo memo = 11; + map search_attributes = 12; + string history_archival_uri = 13; + google.protobuf.Duration execution_duration = 14; } diff --git a/proto/internal/temporal/server/api/batch/v1/request_response.proto b/proto/internal/temporal/server/api/batch/v1/request_response.proto index e30f2456fa8..cb4542e05dd 100644 --- a/proto/internal/temporal/server/api/batch/v1/request_response.proto +++ b/proto/internal/temporal/server/api/batch/v1/request_response.proto @@ -1,16 +1,17 @@ syntax = "proto3"; package temporal.server.api.batch.v1; -option go_package = "go.temporal.io/server/api/batch/v1;batch"; -import "temporal/api/workflowservice/v1/request_response.proto"; +import "google/protobuf/duration.proto"; import "temporal/api/enums/v1/batch_operation.proto"; +import "temporal/api/workflowservice/v1/request_response.proto"; import "temporal/server/api/adminservice/v1/request_response.proto"; -import "google/protobuf/duration.proto"; -message BatchOperationInput { - string namespace_id = 1; - +option go_package = "go.temporal.io/server/api/batch/v1;batch"; + +message BatchOperationInput { + string namespace_id = 1; + int64 concurrency = 2; int64 attempts_on_retryable_error = 3; @@ -29,4 +30,4 @@ message BatchOperationInput { // The request to start an admin batch operation. // Mutually exclusive with StartBatchOperationRequest request. temporal.server.api.adminservice.v1.StartAdminBatchOperationRequest admin_request = 8; -} \ No newline at end of file +} diff --git a/proto/internal/temporal/server/api/chasm/v1/message.proto b/proto/internal/temporal/server/api/chasm/v1/message.proto new file mode 100644 index 00000000000..83a8e5d2730 --- /dev/null +++ b/proto/internal/temporal/server/api/chasm/v1/message.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package temporal.server.api.chasm.v1; + +import "google/protobuf/timestamp.proto"; +import "temporal/api/common/v1/message.proto"; + +option go_package = "go.temporal.io/server/api/chasm/v1;chasm"; + +message VisibilityExecutionInfo { + string business_id = 1; + string run_id = 2; + google.protobuf.Timestamp start_time = 3; + google.protobuf.Timestamp close_time = 4; + int64 history_length = 5; + int64 history_size_bytes = 6; + int64 state_transition_count = 7; + temporal.api.common.v1.SearchAttributes chasm_search_attributes = 8; + temporal.api.common.v1.SearchAttributes custom_search_attributes = 9; + temporal.api.common.v1.Memo memo = 10; + temporal.api.common.v1.Payload chasm_memo = 11; +} diff --git a/proto/internal/temporal/server/api/checksum/v1/message.proto b/proto/internal/temporal/server/api/checksum/v1/message.proto index 8a48a8e2009..d766dad78b7 100644 --- a/proto/internal/temporal/server/api/checksum/v1/message.proto +++ b/proto/internal/temporal/server/api/checksum/v1/message.proto @@ -2,44 +2,42 @@ syntax = "proto3"; package temporal.server.api.checksum.v1; -option go_package = "go.temporal.io/server/api/checksum/v1;checksum"; - import "temporal/api/enums/v1/workflow.proto"; - -import "temporal/server/api/history/v1/message.proto"; import "temporal/server/api/enums/v1/workflow.proto"; +import "temporal/server/api/history/v1/message.proto"; -message MutableStateChecksumPayload { - bool cancel_requested = 1; - temporal.server.api.enums.v1.WorkflowExecutionState state = 2; - temporal.api.enums.v1.WorkflowExecutionStatus status = 3; - - int64 last_write_version = 4; - int64 last_write_event_id = 5; - int64 last_first_event_id = 6; - int64 next_event_id = 7; - int64 last_processed_event_id = 8; - - int64 signal_count = 9; - int64 activity_count = 21; - int64 child_execution_count = 22; - int64 user_timer_count = 23; - int64 request_cancel_external_count = 24; - int64 signal_external_count = 25; - - int32 workflow_task_attempt = 10; - int64 workflow_task_version = 11; - int64 workflow_task_scheduled_event_id = 12; - int64 workflow_task_started_event_id = 13; - - repeated int64 pending_timer_started_event_ids = 14; - repeated int64 pending_activity_scheduled_event_ids = 15; - repeated int64 pending_signal_initiated_event_ids = 16; - repeated int64 pending_req_cancel_initiated_event_ids = 17; - repeated int64 pending_child_initiated_event_ids = 18; - repeated string pending_chasm_node_paths = 26; - - string sticky_task_queue_name = 19; - temporal.server.api.history.v1.VersionHistories version_histories = 20; +option go_package = "go.temporal.io/server/api/checksum/v1;checksum"; +message MutableStateChecksumPayload { + bool cancel_requested = 1; + temporal.server.api.enums.v1.WorkflowExecutionState state = 2; + temporal.api.enums.v1.WorkflowExecutionStatus status = 3; + + int64 last_write_version = 4; + int64 last_write_event_id = 5; + int64 last_first_event_id = 6; + int64 next_event_id = 7; + int64 last_processed_event_id = 8; + + int64 signal_count = 9; + int64 activity_count = 21; + int64 child_execution_count = 22; + int64 user_timer_count = 23; + int64 request_cancel_external_count = 24; + int64 signal_external_count = 25; + + int32 workflow_task_attempt = 10; + int64 workflow_task_version = 11; + int64 workflow_task_scheduled_event_id = 12; + int64 workflow_task_started_event_id = 13; + + repeated int64 pending_timer_started_event_ids = 14; + repeated int64 pending_activity_scheduled_event_ids = 15; + repeated int64 pending_signal_initiated_event_ids = 16; + repeated int64 pending_req_cancel_initiated_event_ids = 17; + repeated int64 pending_child_initiated_event_ids = 18; + repeated string pending_chasm_node_paths = 26; + + string sticky_task_queue_name = 19; + temporal.server.api.history.v1.VersionHistories version_histories = 20; } diff --git a/proto/internal/temporal/server/api/cli/v1/message.proto b/proto/internal/temporal/server/api/cli/v1/message.proto index f633ec6564b..c8cc0dc9438 100644 --- a/proto/internal/temporal/server/api/cli/v1/message.proto +++ b/proto/internal/temporal/server/api/cli/v1/message.proto @@ -2,71 +2,70 @@ syntax = "proto3"; package temporal.server.api.cli.v1; -option go_package = "go.temporal.io/server/api/cli/v1;cli"; - import "google/protobuf/timestamp.proto"; - import "temporal/api/common/v1/message.proto"; import "temporal/api/enums/v1/workflow.proto"; import "temporal/api/workflow/v1/message.proto"; +option go_package = "go.temporal.io/server/api/cli/v1;cli"; + message DescribeWorkflowExecutionResponse { - temporal.api.workflow.v1.WorkflowExecutionConfig execution_config = 1; - WorkflowExecutionInfo workflow_execution_info = 2; - repeated PendingActivityInfo pending_activities = 3; - repeated temporal.api.workflow.v1.PendingChildExecutionInfo pending_children = 4; - temporal.api.workflow.v1.PendingWorkflowTaskInfo pending_workflow_task = 5; + temporal.api.workflow.v1.WorkflowExecutionConfig execution_config = 1; + WorkflowExecutionInfo workflow_execution_info = 2; + repeated PendingActivityInfo pending_activities = 3; + repeated temporal.api.workflow.v1.PendingChildExecutionInfo pending_children = 4; + temporal.api.workflow.v1.PendingWorkflowTaskInfo pending_workflow_task = 5; } message WorkflowExecutionInfo { - temporal.api.common.v1.WorkflowExecution execution = 1; - temporal.api.common.v1.WorkflowType type = 2; - google.protobuf.Timestamp start_time = 3; - google.protobuf.Timestamp close_time = 4; - temporal.api.enums.v1.WorkflowExecutionStatus status = 5; - int64 history_length = 6; - string parent_namespace_id = 7; - temporal.api.common.v1.WorkflowExecution parent_execution = 8; - google.protobuf.Timestamp execution_time = 9; - temporal.api.common.v1.Memo memo = 10; - SearchAttributes search_attributes = 11; - temporal.api.workflow.v1.ResetPoints auto_reset_points = 12; - int64 state_transition_count = 13; - int64 history_size_bytes = 14; - temporal.api.common.v1.WorkerVersionStamp most_recent_worker_version_stamp = 15; + temporal.api.common.v1.WorkflowExecution execution = 1; + temporal.api.common.v1.WorkflowType type = 2; + google.protobuf.Timestamp start_time = 3; + google.protobuf.Timestamp close_time = 4; + temporal.api.enums.v1.WorkflowExecutionStatus status = 5; + int64 history_length = 6; + string parent_namespace_id = 7; + temporal.api.common.v1.WorkflowExecution parent_execution = 8; + google.protobuf.Timestamp execution_time = 9; + temporal.api.common.v1.Memo memo = 10; + SearchAttributes search_attributes = 11; + temporal.api.workflow.v1.ResetPoints auto_reset_points = 12; + int64 state_transition_count = 13; + int64 history_size_bytes = 14; + temporal.api.common.v1.WorkerVersionStamp most_recent_worker_version_stamp = 15; } message PendingActivityInfo { - string activity_id = 1; - temporal.api.common.v1.ActivityType activity_type = 2; - temporal.api.enums.v1.PendingActivityState state = 3; - string heartbeat_details = 4; - google.protobuf.Timestamp last_heartbeat_time = 5; - google.protobuf.Timestamp last_started_time = 6; - int32 attempt = 7; - int32 maximum_attempts = 8; - google.protobuf.Timestamp scheduled_time = 9; - google.protobuf.Timestamp expiration_time = 10; - Failure last_failure = 11; - string last_worker_identity = 12; + string activity_id = 1; + temporal.api.common.v1.ActivityType activity_type = 2; + temporal.api.enums.v1.PendingActivityState state = 3; + string heartbeat_details = 4; + google.protobuf.Timestamp last_heartbeat_time = 5; + google.protobuf.Timestamp last_started_time = 6; + int32 attempt = 7; + int32 maximum_attempts = 8; + google.protobuf.Timestamp scheduled_time = 9; + google.protobuf.Timestamp expiration_time = 10; + Failure last_failure = 11; + string last_worker_identity = 12; } message SearchAttributes { - map indexed_fields = 1; + map indexed_fields = 1; } message Failure { - string message = 1; - string source = 2; - string stack_trace = 3; - Failure cause = 4; - string failure_type = 5; + string message = 1; + string source = 2; + string stack_trace = 3; + Failure cause = 4; + string failure_type = 5; } message AddSearchAttributesResponse { - string index_name = 1; - map custom_search_attributes = 2; - map system_search_attributes = 3; - map mapping = 4; - WorkflowExecutionInfo add_workflow_execution_info = 5; + string index_name = 1; + map custom_search_attributes = 2; + map system_search_attributes = 3; + map mapping = 4; + WorkflowExecutionInfo add_workflow_execution_info = 5; } diff --git a/proto/internal/temporal/server/api/clock/v1/message.proto b/proto/internal/temporal/server/api/clock/v1/message.proto index 8063d8b6b83..d2a58293a59 100644 --- a/proto/internal/temporal/server/api/clock/v1/message.proto +++ b/proto/internal/temporal/server/api/clock/v1/message.proto @@ -5,21 +5,21 @@ package temporal.server.api.clock.v1; option go_package = "go.temporal.io/server/api/clock/v1;clock"; message VectorClock { - int32 shard_id = 1; - int64 clock = 2; - int64 cluster_id = 3; + int32 shard_id = 1; + int64 clock = 2; + int64 cluster_id = 3; } // A Hybrid Logical Clock timestamp. // Guarantees strict total ordering for conflict resolution purposes. message HybridLogicalClock { - // Wall clock - A single time source MUST guarantee that 2 consecutive timestamps are monotonically non-decreasing. - // e.g. by storing the last wall clock and returning max(gettimeofday(), lastWallClock). - int64 wall_clock = 1; - // Incremental sequence that is reset every time the system's wallclock moves forward. - // Ensures the clock generates monotonically increasing timestamps. - int32 version = 2; - // The cluster version ID as described in the XDC docs - used as a tie breaker. - // See: https://github.com/uber/cadence/blob/master/docs/design/2290-cadence-ndc.md - int64 cluster_id = 3; + // Wall clock - A single time source MUST guarantee that 2 consecutive timestamps are monotonically non-decreasing. + // e.g. by storing the last wall clock and returning max(gettimeofday(), lastWallClock). + int64 wall_clock = 1; + // Incremental sequence that is reset every time the system's wallclock moves forward. + // Ensures the clock generates monotonically increasing timestamps. + int32 version = 2; + // The cluster version ID as described in the XDC docs - used as a tie breaker. + // See: https://github.com/uber/cadence/blob/master/docs/design/2290-cadence-ndc.md + int64 cluster_id = 3; } diff --git a/proto/internal/temporal/server/api/cluster/v1/message.proto b/proto/internal/temporal/server/api/cluster/v1/message.proto index 4247fff5fb8..06e95b5b6cc 100644 --- a/proto/internal/temporal/server/api/cluster/v1/message.proto +++ b/proto/internal/temporal/server/api/cluster/v1/message.proto @@ -2,34 +2,33 @@ syntax = "proto3"; package temporal.server.api.cluster.v1; -option go_package = "go.temporal.io/server/api/cluster/v1;cluster"; - import "google/protobuf/timestamp.proto"; - import "temporal/server/api/enums/v1/cluster.proto"; +option go_package = "go.temporal.io/server/api/cluster/v1;cluster"; + message HostInfo { - string identity = 1; + string identity = 1; } message RingInfo { - string role = 1; - int32 member_count = 2; - repeated HostInfo members = 3; + string role = 1; + int32 member_count = 2; + repeated HostInfo members = 3; } message MembershipInfo { - HostInfo current_host = 1; - repeated string reachable_members = 2; - repeated RingInfo rings = 3; + HostInfo current_host = 1; + repeated string reachable_members = 2; + repeated RingInfo rings = 3; } message ClusterMember { - temporal.server.api.enums.v1.ClusterMemberRole role = 1; - string host_id = 2; - string rpc_address = 3; - int32 rpc_port = 4; - google.protobuf.Timestamp session_start_time = 5; - google.protobuf.Timestamp last_heartbit_time = 6; - google.protobuf.Timestamp record_expiry_time = 7; + temporal.server.api.enums.v1.ClusterMemberRole role = 1; + string host_id = 2; + string rpc_address = 3; + int32 rpc_port = 4; + google.protobuf.Timestamp session_start_time = 5; + google.protobuf.Timestamp last_heartbit_time = 6; + google.protobuf.Timestamp record_expiry_time = 7; } diff --git a/proto/internal/temporal/server/api/common/v1/api_category.proto b/proto/internal/temporal/server/api/common/v1/api_category.proto new file mode 100644 index 00000000000..3ac0fb25dd0 --- /dev/null +++ b/proto/internal/temporal/server/api/common/v1/api_category.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package temporal.server.api.common.v1; + +import "google/protobuf/descriptor.proto"; + +option go_package = "go.temporal.io/server/api/common/v1;commonspb"; + +extend google.protobuf.MethodOptions { + optional ApiCategoryOptions api_category = 50001; +} + +message ApiCategoryOptions { + // The category of this API for health and observability purposes. + ApiCategory category = 1; +} + +enum ApiCategory { + // Unspecified API category. Treated as standard API. + API_CATEGORY_UNSPECIFIED = 0; + + // Standard API with typical request/response patterns. + API_CATEGORY_STANDARD = 1; + + // Long-polling API that intentionally waits for state changes or external events. + // These APIs should be excluded from health signal tracking because their latency + // reflects client wait times and event availability rather than server health. + // Including them in health metrics would skew the data and could cause healthy + // nodes to appear unhealthy. + // + // Examples: PollMutableState, PollWorkflowExecutionUpdate, QueryWorkflow + API_CATEGORY_LONG_POLL = 2; + + API_CATEGORY_SYSTEM = 3; +} diff --git a/proto/internal/temporal/server/api/common/v1/dlq.proto b/proto/internal/temporal/server/api/common/v1/dlq.proto index 5328b2120e5..f3ed852a10c 100644 --- a/proto/internal/temporal/server/api/common/v1/dlq.proto +++ b/proto/internal/temporal/server/api/common/v1/dlq.proto @@ -1,10 +1,11 @@ syntax = "proto3"; package temporal.server.api.common.v1; -option go_package = "go.temporal.io/server/api/common/v1;commonspb"; import "temporal/api/common/v1/message.proto"; +option go_package = "go.temporal.io/server/api/common/v1;commonspb"; + message HistoryTask { // shard_id is included to avoid having to deserialize the task blob. int32 shard_id = 1; @@ -37,4 +38,3 @@ message HistoryDLQKey { string source_cluster = 2; string target_cluster = 3; } - diff --git a/proto/internal/temporal/server/api/deployment/v1/message.proto b/proto/internal/temporal/server/api/deployment/v1/message.proto index 513069a3986..cb253f47e08 100644 --- a/proto/internal/temporal/server/api/deployment/v1/message.proto +++ b/proto/internal/temporal/server/api/deployment/v1/message.proto @@ -2,50 +2,51 @@ syntax = "proto3"; package temporal.server.api.deployment.v1; -option go_package = "go.temporal.io/server/api/deployment/v1;deployment"; - -import "temporal/api/enums/v1/task_queue.proto"; -import "temporal/api/enums/v1/deployment.proto"; import "google/protobuf/timestamp.proto"; -import "temporal/api/deployment/v1/message.proto"; import "temporal/api/common/v1/message.proto"; +import "temporal/api/compute/v1/config.proto"; +import "temporal/api/deployment/v1/message.proto"; +import "temporal/api/enums/v1/deployment.proto"; +import "temporal/api/enums/v1/task_queue.proto"; + +option go_package = "go.temporal.io/server/api/deployment/v1;deployment"; // Identifies a Worker Deployment Version. The combination of `deployment_name` and `build_id` // serve as the identifier. message WorkerDeploymentVersion { - // The name of the Deployment this version belongs too. - string deployment_name = 1; - // Build ID uniquely identifies the Deployment Version within a Deployment, but the same Build - // ID can be used in multiple Deployments. - string build_id = 2; + // The name of the Deployment this version belongs too. + string deployment_name = 1; + // Build ID uniquely identifies the Deployment Version within a Deployment, but the same Build + // ID can be used in multiple Deployments. + string build_id = 2; } // The source of truth for this data is in the WorkerDeployment entity workflows, which is // synced to all TQs whenever the source changes. // Deprecated. message DeploymentVersionData { - // Nil means unversioned. - WorkerDeploymentVersion version = 1; + // Nil means unversioned. + WorkerDeploymentVersion version = 1; - // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. - google.protobuf.Timestamp routing_update_time = 2; + // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. + google.protobuf.Timestamp routing_update_time = 2; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not current. - google.protobuf.Timestamp current_since_time = 3; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not current. + google.protobuf.Timestamp current_since_time = 3; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. - google.protobuf.Timestamp ramping_since_time = 4; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. + google.protobuf.Timestamp ramping_since_time = 4; - // Range: [0, 100]. Must be zero if the version is not ramping (i.e. `ramping_since_time` is nil). - // Can be in the range [0, 100] if the version is ramping. - float ramp_percentage = 5; + // Range: [0, 100]. Must be zero if the version is not ramping (i.e. `ramping_since_time` is nil). + // Can be in the range [0, 100] if the version is ramping. + float ramp_percentage = 5; - // Status of the Worker Deployment Version. - temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 6; + // Status of the Worker Deployment Version. + temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 6; } // Information that a TQ should know about a particular Deployment Version. This info is not part of @@ -53,395 +54,453 @@ message DeploymentVersionData { // As of Workflow Version `VersionDataRevisionNumber`, version specific data has its own revision // number, which makes async propagations safer and allows async registration. message WorkerDeploymentVersionData { - // Incremented everytime version data changes. Updates with lower revision number than what is - // already in the TQ will be ignored to avoid stale writes. - int64 revision_number = 1; - // Last update time. Used for garbage collecting deleted versions from TQ user data. - google.protobuf.Timestamp update_time = 2; - // In order to protect against deletes being overwritten by delayed stale writes, we can't - // immediately delete the version data from task queues. instead, we mark them as deleted while - // keeping the revision number. - // Old enough deleted versions are GCed based on update_time. - bool deleted = 3; - - temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 6; + // Incremented everytime version data changes. Updates with lower revision number than what is + // already in the TQ will be ignored to avoid stale writes. + int64 revision_number = 1; + // Last update time. Used for garbage collecting deleted versions from TQ user data. + google.protobuf.Timestamp update_time = 2; + // In order to protect against deletes being overwritten by delayed stale writes, we can't + // immediately delete the version data from task queues. instead, we mark them as deleted while + // keeping the revision number. + // Old enough deleted versions are GCed based on update_time. + // Deprecated. This mechanism is not safe against reactivation of versions after delete. + // Use forget_version flag for synchronous deletion of the version data from TQ. + bool deleted = 3; + + temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 6; } // Local state for Worker Deployment Version message VersionLocalState { - WorkerDeploymentVersion version = 1; - google.protobuf.Timestamp create_time = 2; - - // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. - google.protobuf.Timestamp routing_update_time = 3; - - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not current. - google.protobuf.Timestamp current_since_time = 4; - - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. - google.protobuf.Timestamp ramping_since_time = 5; - - // Range: [0, 100]. Must be zero if the version is not ramping (i.e. `ramping_since_time` is nil). - // Can be in the range [0, 100] if the version is ramping. - float ramp_percentage = 6; - - // Timestamp when this version first became current or ramping. - google.protobuf.Timestamp first_activation_time = 12; - - // Timestamp when this version last became current. - // Can be used to determine whether a version has ever been Current. - google.protobuf.Timestamp last_current_time = 16; - - // Timestamp when this version last stopped being current or ramping. - google.protobuf.Timestamp last_deactivation_time = 13; - - // Helps user determine when it is safe to decommission the workers of this - // Version. Not present when version is current or ramping. - // Current limitations: - // - Not supported for Unversioned mode. - // - Periodically refreshed, may have delays up to few minutes (consult the - // last_checked_time value). - // - Refreshed only when version is not current or ramping AND the status is not - // "drained" yet. - // - Once the status is changed to "drained", it is not changed until the Version - // becomes Current or Ramping again, at which time the drainage info is cleared. - // This means if the Version is "drained" but new workflows are sent to it via - // Pinned Versioning Override, the status does not account for those Pinned-override - // executions and remains "drained". - temporal.api.deployment.v1.VersionDrainageInfo drainage_info = 7; - - // Arbitrary user-provided metadata attached to this version. - temporal.api.deployment.v1.VersionMetadata metadata = 8; + WorkerDeploymentVersion version = 1; + google.protobuf.Timestamp create_time = 2; + + // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. + google.protobuf.Timestamp routing_update_time = 3; + + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not current. + google.protobuf.Timestamp current_since_time = 4; + + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. + google.protobuf.Timestamp ramping_since_time = 5; + + // Range: [0, 100]. Must be zero if the version is not ramping (i.e. `ramping_since_time` is nil). + // Can be in the range [0, 100] if the version is ramping. + float ramp_percentage = 6; + + // Timestamp when this version first became current or ramping. + google.protobuf.Timestamp first_activation_time = 12; + + // Timestamp when this version last became current. + // Can be used to determine whether a version has ever been Current. + google.protobuf.Timestamp last_current_time = 16; + + // Timestamp when this version last stopped being current or ramping. + google.protobuf.Timestamp last_deactivation_time = 13; + + // Helps user determine when it is safe to decommission the workers of this + // Version. Not present when version is current or ramping. + // Current limitations: + // - Not supported for Unversioned mode. + // - Periodically refreshed, may have delays up to few minutes (consult the + // last_checked_time value). + // - Refreshed only when version is not current or ramping AND the status is not + // "drained" yet. + // - Once the status is changed to "drained", it is not changed until the Version + // becomes Current or Ramping again, at which time the drainage info is cleared. + // This means if the Version is "drained" but new workflows are sent to it via + // Pinned Versioning Override, the status does not account for those Pinned-override + // executions and remains "drained". + temporal.api.deployment.v1.VersionDrainageInfo drainage_info = 7; + + // Arbitrary user-provided metadata attached to this version. + temporal.api.deployment.v1.VersionMetadata metadata = 8; // Deployment workflow should always be running before starting the version workflow. // We should not start the deployment workflow. If we cannot find the deployment workflow when signaling, it means a bug and we should fix it. // Deprecated. bool started_deployment_workflow = 9 [deprecated = true]; - // Key: Task Queue Name - map task_queue_families = 10; + // Key: Task Queue Name + map task_queue_families = 10; - // Number of task queues which will be synced in a single batch. - int32 sync_batch_size = 11; + // Number of task queues which will be synced in a single batch. + int32 sync_batch_size = 11; - message TaskQueueFamilyData { - // Key: Task Queue Type - map task_queues = 1; - } + message TaskQueueFamilyData { + // Key: Task Queue Type + map task_queues = 1; + } - // Status of the Worker Deployment Version. - temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 14; + // Status of the Worker Deployment Version. + temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 14; - // Incremented everytime version data synced to TQ changes. Updates with lower revision number - // than what is already in the TQ will be ignored to avoid stale writes during async operations. - int64 revision_number = 15; + // Incremented everytime version data synced to TQ changes. Updates with lower revision number + // than what is already in the TQ will be ignored to avoid stale writes during async operations. + int64 revision_number = 15; + + // Identity of the last client who modified the configuration of this Version. + // Covers changes through: CreateWorkerDeploymentVersion, UpdateWorkerDeploymentVersionComputeConfig, + // UpdateWorkerDeploymentVersionMetadata. + string last_modifier_identity = 17; + + // Cached compute config summary, kept in sync with the WCI on each compute config update. + temporal.api.compute.v1.ComputeConfigSummary compute_config = 18; } // Data specific to a task queue, from the perspective of a worker deployment version. -message TaskQueueVersionData { -} +message TaskQueueVersionData {} // used as Worker Deployment Version workflow input: message WorkerDeploymentVersionWorkflowArgs { - string namespace_name = 1; - string namespace_id = 2; - VersionLocalState version_state = 3; + string namespace_name = 1; + string namespace_id = 2; + VersionLocalState version_state = 3; } // used as Worker Deployment workflow input: message WorkerDeploymentWorkflowArgs { - string namespace_name = 1; - string namespace_id = 2; - string deployment_name = 3; - WorkerDeploymentLocalState state = 4; + string namespace_name = 1; + string namespace_id = 2; + string deployment_name = 3; + WorkerDeploymentLocalState state = 4; } // Local state for Worker Deployment message WorkerDeploymentLocalState { - google.protobuf.Timestamp create_time = 1; - // Encapsulates task routing information for this deployment. - temporal.api.deployment.v1.RoutingConfig routing_config = 2; - map versions = 3; - bytes conflict_token = 4; - string last_modifier_identity = 5; - // Number of task queues which will be synced in a single batch. - int32 sync_batch_size = 6; - string manager_identity = 7; - // Track async propagations in progress per build ID. Map: build_id -> revision numbers. - // Used to track which propagations are still pending across continue-as-new. - map propagating_revisions = 8; + google.protobuf.Timestamp create_time = 1; + // Encapsulates task routing information for this deployment. + temporal.api.deployment.v1.RoutingConfig routing_config = 2; + map versions = 3; + bytes conflict_token = 4; + string last_modifier_identity = 5; + // Number of task queues which will be synced in a single batch. + int32 sync_batch_size = 6; + string manager_identity = 7; + // Track async propagations in progress per build ID. Map: build_id -> revision numbers. + // Used to track which propagations are still pending across continue-as-new. + map propagating_revisions = 8; + // Request ID used to create this worker deployment. + string create_request_id = 9; } // Tracks revision numbers that are currently propagating for a specific build ID message PropagatingRevisions { - repeated int64 revision_numbers = 1; + repeated int64 revision_numbers = 1; } message WorkerDeploymentVersionSummary { - string version = 1; - google.protobuf.Timestamp create_time = 2; - temporal.api.enums.v1.VersionDrainageStatus drainage_status = 3 [deprecated=true]; - // Information about workflow drainage to help the user determine when it is safe - // to decommission a Version. Not present while version is current or ramping. - temporal.api.deployment.v1.VersionDrainageInfo drainage_info = 4; - // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. - google.protobuf.Timestamp routing_update_time = 5; + string version = 1; + google.protobuf.Timestamp create_time = 2; + temporal.api.enums.v1.VersionDrainageStatus drainage_status = 3 [deprecated = true]; + // Information about workflow drainage to help the user determine when it is safe + // to decommission a Version. Not present while version is current or ramping. + temporal.api.deployment.v1.VersionDrainageInfo drainage_info = 4; + // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. + google.protobuf.Timestamp routing_update_time = 5; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not current. - google.protobuf.Timestamp current_since_time = 6; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not current. + google.protobuf.Timestamp current_since_time = 6; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. - google.protobuf.Timestamp ramping_since_time = 7; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. + google.protobuf.Timestamp ramping_since_time = 7; - // Timestamp when this version first became current or ramping. - google.protobuf.Timestamp first_activation_time = 8; + // Timestamp when this version first became current or ramping. + google.protobuf.Timestamp first_activation_time = 8; - // Timestamp when this version last became current. - // Can be used to determine whether a version has ever been Current. - google.protobuf.Timestamp last_current_time = 11; + // Timestamp when this version last became current. + // Can be used to determine whether a version has ever been Current. + google.protobuf.Timestamp last_current_time = 11; - // Timestamp when this version last stopped being current or ramping. - google.protobuf.Timestamp last_deactivation_time = 9; + // Timestamp when this version last stopped being current or ramping. + google.protobuf.Timestamp last_deactivation_time = 9; - // Status of the Worker Deployment Version. - temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 10; + // Status of the Worker Deployment Version. + temporal.api.enums.v1.WorkerDeploymentVersionStatus status = 10; + + // Request ID used to create this version. Used for idempotency. + // Not synced from the version workflow; only set by the deployment workflow. + string create_request_id = 12; + + // Compute config summary for this version. Synced from the version workflow on each compute config update. + // Also set by the deployment workflow at version creation time if a compute config was provided. + temporal.api.compute.v1.ComputeConfigSummary compute_config = 13; } // used as Worker Deployment Version workflow update input: message RegisterWorkerInVersionArgs { - string task_queue_name = 1; - temporal.api.enums.v1.TaskQueueType task_queue_type = 2; - int32 max_task_queues = 3; - string version = 4; - temporal.api.deployment.v1.RoutingConfig routing_config = 5; + string task_queue_name = 1; + temporal.api.enums.v1.TaskQueueType task_queue_type = 2; + int32 max_task_queues = 3; + string version = 4; + temporal.api.deployment.v1.RoutingConfig routing_config = 5; } // used as Worker Deployment workflow update input: message RegisterWorkerInWorkerDeploymentArgs { - string task_queue_name = 1; - temporal.api.enums.v1.TaskQueueType task_queue_type = 2; - int32 max_task_queues = 3; - WorkerDeploymentVersion version = 4; + string task_queue_name = 1; + temporal.api.enums.v1.TaskQueueType task_queue_type = 2; + int32 max_task_queues = 3; + WorkerDeploymentVersion version = 4; } // used as Worker Deployment workflow activity input: message DescribeVersionFromWorkerDeploymentActivityArgs { - string version = 1; + string version = 1; } message DescribeVersionFromWorkerDeploymentActivityResult { - // All the Task Queues that have ever polled from this Deployment version. - repeated temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo task_queue_infos = 1; + // All the Task Queues that have ever polled from this Deployment version. + repeated temporal.api.deployment.v1.WorkerDeploymentVersionInfo.VersionTaskQueueInfo task_queue_infos = 1; } // used as Worker Deployment workflow update input (sent from Worker Deployment workflow): message SyncVersionStateUpdateArgs { - // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. - google.protobuf.Timestamp routing_update_time = 1 [deprecated = true]; + // Last time `current_since_time`, `ramping_since_time, or `ramp_percentage` of this version changed. + google.protobuf.Timestamp routing_update_time = 1 [deprecated = true]; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not current. - google.protobuf.Timestamp current_since_time = 2 [deprecated = true]; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not current. + google.protobuf.Timestamp current_since_time = 2 [deprecated = true]; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) - // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. - google.protobuf.Timestamp ramping_since_time = 3 [deprecated = true]; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: 'Since' captures the field semantics despite being a preposition. --) + // Nil if not ramping. Updated when the version first starts ramping, not on each ramp change. + google.protobuf.Timestamp ramping_since_time = 3 [deprecated = true]; - // Range: [0, 100]. Must be zero if the version is not ramping (i.e. `ramping_since_time` is nil). - // Can be in the range [0, 100] if the version is ramping. - float ramp_percentage = 4 [deprecated = true]; + // Range: [0, 100]. Must be zero if the version is not ramping (i.e. `ramping_since_time` is nil). + // Can be in the range [0, 100] if the version is ramping. + float ramp_percentage = 4 [deprecated = true]; - // Full routing config for async propagation mode. When present, the version workflow - // will propagate the entire routing config asynchronously. When absent, sync mode is used. - temporal.api.deployment.v1.RoutingConfig routing_config = 5; + // Full routing config for async propagation mode. When present, the version workflow + // will propagate the entire routing config asynchronously. When absent, sync mode is used. + temporal.api.deployment.v1.RoutingConfig routing_config = 5; } // used as Worker Deployment workflow update response (sent from Worker Deployment workflow): message SyncVersionStateResponse { - // Deprecated. State could be so large, no need to send it to the deployment workflow. - VersionLocalState version_state = 1 [deprecated = true]; - WorkerDeploymentVersionSummary summary = 2; + // Deprecated. State could be so large, no need to send it to the deployment workflow. + VersionLocalState version_state = 1 [deprecated = true]; + WorkerDeploymentVersionSummary summary = 2; } // Sent from Version workflow to Worker Deployment workflow message AddVersionUpdateArgs { - string version = 1; - google.protobuf.Timestamp create_time = 2; + string version = 1; + google.protobuf.Timestamp create_time = 2; } // Sent from Drainage child workflow to Version parent message SyncDrainageInfoSignalArgs { - temporal.api.deployment.v1.VersionDrainageInfo drainage_info = 1; + temporal.api.deployment.v1.VersionDrainageInfo drainage_info = 1; } // Sent from Version workflow to Worker Deployment workflow message SyncDrainageStatusSignalArgs { - string version = 1; - temporal.api.enums.v1.VersionDrainageStatus drainage_status = 2; + string version = 1; + temporal.api.enums.v1.VersionDrainageStatus drainage_status = 2; } // Sent from Version workflow to Worker Deployment workflow when async propagation completes message PropagationCompletionInfo { - int64 revision_number = 1; - string build_id = 2; + int64 revision_number = 1; + string build_id = 2; } - // used as Worker Deployment Version workflow query response: message QueryDescribeVersionResponse { - VersionLocalState version_state = 1; + VersionLocalState version_state = 1; } // used as Worker Deployment Version workflow query response: message QueryDescribeWorkerDeploymentResponse { - WorkerDeploymentLocalState state = 1; + WorkerDeploymentLocalState state = 1; +} + +// used as Worker Deployment workflow query response: +message CreateRequestIDQueryResponse { + string request_id = 1; + bytes conflict_token = 2; } // used as Worker Deployment Version workflow activity input: message StartWorkerDeploymentRequest { - string deployment_name = 1; - string request_id = 2; + string deployment_name = 1; + string request_id = 2; } // used as Worker Deployment workflow activity input: message StartWorkerDeploymentVersionRequest { - string deployment_name = 1; - string build_id = 2; - string request_id = 3; + string deployment_name = 1; + string build_id = 2; + string request_id = 3; + string identity = 4; + temporal.api.compute.v1.ComputeConfigSummary compute_config = 5; } // used as Worker Deployment Version workflow activity input: message SyncDeploymentVersionUserDataRequest { - string deployment_name = 4; - WorkerDeploymentVersion version = 1; - repeated SyncUserData sync = 2; - // if true, the version will be forgotten from the task queue user data. - bool forget_version = 3; - // Async mode: full routing config to propagate (includes revision_number) - temporal.api.deployment.v1.RoutingConfig update_routing_config = 5; - // Async mode: version-specific data to upsert - WorkerDeploymentVersionData upsert_version_data = 6; - - message SyncUserData { - string name = 1; - repeated temporal.api.enums.v1.TaskQueueType types = 2; - DeploymentVersionData data = 3; - } + string deployment_name = 4; + WorkerDeploymentVersion version = 1; + repeated SyncUserData sync = 2; + // if true, the version will be forgotten from the task queue user data. + bool forget_version = 3; + // Async mode: full routing config to propagate (includes revision_number) + temporal.api.deployment.v1.RoutingConfig update_routing_config = 5; + // Async mode: version-specific data to upsert + WorkerDeploymentVersionData upsert_version_data = 6; + + message SyncUserData { + string name = 1; + repeated temporal.api.enums.v1.TaskQueueType types = 2; + DeploymentVersionData data = 3; + } } // used as Worker Deployment Version workflow activity output: message SyncDeploymentVersionUserDataResponse { - map task_queue_max_versions = 1; + map task_queue_max_versions = 1; } // used as Worker Deployment Version workflow activity input: message CheckWorkerDeploymentUserDataPropagationRequest { - map task_queue_max_versions = 1; + map task_queue_max_versions = 1; } // used as Worker Deployment workflow activity input: message SyncUnversionedRampActivityArgs { - string current_version = 1; - SyncVersionStateUpdateArgs update_args = 2; + string current_version = 1; + SyncVersionStateUpdateArgs update_args = 2; } // used as Worker Deployment workflow activity output: message SyncUnversionedRampActivityResponse { - map task_queue_max_versions = 1; + map task_queue_max_versions = 1; } // used as Worker Deployment Version workflow update input: message UpdateVersionMetadataArgs { - map upsert_entries = 1; - repeated string remove_entries = 2; - string identity = 3; + map upsert_entries = 1; + repeated string remove_entries = 2; + string identity = 3; } // used as Worker Deployment Version workflow update response: message UpdateVersionMetadataResponse { - temporal.api.deployment.v1.VersionMetadata metadata = 1; + temporal.api.deployment.v1.VersionMetadata metadata = 1; } // used as Worker Deployment workflow update input: message SetCurrentVersionArgs { - string identity = 1; - string version = 2; - bool ignore_missing_task_queues = 3; - bytes conflict_token = 4; - bool allow_no_pollers = 5; + string identity = 1; + string version = 2; + bool ignore_missing_task_queues = 3; + bytes conflict_token = 4; + bool allow_no_pollers = 5; } // used as Worker Deployment update response: message SetCurrentVersionResponse { - string previous_version = 1; - bytes conflict_token = 2; + string previous_version = 1; + bytes conflict_token = 2; +} + +// used as Worker Deployment workflow update input: +message CreateWorkerDeploymentArgs { + string identity = 1; + + // Retrying with same request id is a successful no-op. + // Retrying with different request id is an error. + // One deployment is deleted, same or different request id will re-create it. + string request_id = 2; } +// used as Worker Deployment update response: +message CreateWorkerDeploymentResponse { + bytes conflict_token = 1; +} + +// used as Worker Deployment workflow update input: +message CreateWorkerDeploymentVersionArgs { + string identity = 1; + + // Retrying with same request id is a successful no-op. + // Retrying with different request id (including auto-created) is an error. + string request_id = 2; + + // Version string (.) + string version = 3; + + temporal.api.compute.v1.ComputeConfig compute_config = 4; +} + +// used as Worker Deployment update response: +message CreateWorkerDeploymentVersionResponse {} + // used as Worker Deployment workflow update input: message DeleteVersionArgs { - string identity = 1; - string version = 2; - bool skip_drainage = 3; - // If true, it would mean that the delete operation is initiated by the server internally. This is done on the - // event that the addition of a version exceeds the max number of versions allowed in a worker-deployment (defaultMaxVersions). - // False elsewhere. - bool server_delete = 4; - // version workflow does not block the update for tq propagation - bool async_propagation = 5; + string identity = 1; + string version = 2; + bool skip_drainage = 3; + // If true, it would mean that the delete operation is initiated by the server internally. This is done on the + // event that the addition of a version exceeds the max number of versions allowed in a worker-deployment (defaultMaxVersions). + // False elsewhere. + bool server_delete = 4; + // version workflow does not block the update for tq propagation + bool async_propagation = 5; } // used as Worker Deployment Activity input: message DeleteVersionActivityArgs { - string identity = 1; - string deployment_name = 2; - string version = 3; - string request_id = 4; - bool skip_drainage = 5; - bool async_propagation = 6; + string identity = 1; + string deployment_name = 2; + string version = 3; + string request_id = 4; + bool skip_drainage = 5; + bool async_propagation = 6; } // used as Worker Deployment Activity input: message CheckTaskQueuesHavePollersActivityArgs { - // Key: Task Queue Name - map task_queues_and_types = 1; + // Key: Task Queue Name + map task_queues_and_types = 1; - message TaskQueueTypes { - repeated temporal.api.enums.v1.TaskQueueType types = 1; - } + message TaskQueueTypes { + repeated temporal.api.enums.v1.TaskQueueType types = 1; + } - WorkerDeploymentVersion worker_deployment_version = 2; + WorkerDeploymentVersion worker_deployment_version = 2; } // used as Worker Deployment workflow update input: message DeleteDeploymentArgs { - string identity = 1; + string identity = 1; } // used as Worker Deployment update response: message SetRampingVersionResponse { - string previous_version = 1; - float previous_percentage = 2; - bytes conflict_token = 3; + string previous_version = 1; + float previous_percentage = 2; + bytes conflict_token = 3; } // used as Worker Deployment workflow update input: message SetRampingVersionArgs { - string identity = 1; - string version = 2; - float percentage = 3; - bool ignore_missing_task_queues = 4; - bytes conflict_token = 5; - bool allow_no_pollers = 6; + string identity = 1; + string version = 2; + float percentage = 3; + bool ignore_missing_task_queues = 4; + bytes conflict_token = 5; + bool allow_no_pollers = 6; } // used as Worker Deployment workflow update input: @@ -461,60 +520,95 @@ message SetManagerIdentityResponse { // used as Worker Deployment activity input: message SyncVersionStateActivityArgs { - string deployment_name = 1; - // . or possibly just in the future - string version = 2; - SyncVersionStateUpdateArgs update_args = 3; - string request_id = 4; + string deployment_name = 1; + // . or possibly just in the future + string version = 2; + SyncVersionStateUpdateArgs update_args = 3; + string request_id = 4; } // used as Worker Deployment activity result: message SyncVersionStateActivityResult { - VersionLocalState version_state = 1 [deprecated = true]; - WorkerDeploymentVersionSummary summary = 2; + VersionLocalState version_state = 1 [deprecated = true]; + WorkerDeploymentVersionSummary summary = 2; } // used as Worker Deployment activity input: message IsVersionMissingTaskQueuesArgs { - string prev_current_version = 1; - string new_current_version = 2; + string prev_current_version = 1; + string new_current_version = 2; } // used as Worker Deployment activity output: message IsVersionMissingTaskQueuesResult { - bool is_missing_task_queues = 1; + bool is_missing_task_queues = 1; } // used as Worker Deployment workflow memo: message WorkerDeploymentWorkflowMemo { - string deployment_name = 1; - google.protobuf.Timestamp create_time = 2; - temporal.api.deployment.v1.RoutingConfig routing_config = 3; - temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary latest_version_summary = 4; - temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary current_version_summary = 5; - temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary ramping_version_summary = 6; + string deployment_name = 1; + google.protobuf.Timestamp create_time = 2; + temporal.api.deployment.v1.RoutingConfig routing_config = 3; + temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary latest_version_summary = 4; + temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary current_version_summary = 5; + temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary ramping_version_summary = 6; } // Subset of fields of WorkerDeploymentInfo returned in ListWorkerDeploymentsResponse message WorkerDeploymentSummary { - string name = 1; - google.protobuf.Timestamp create_time = 2; - temporal.api.deployment.v1.RoutingConfig routing_config = 3; - temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary latest_version_summary = 4; - temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary current_version_summary = 5; - temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary ramping_version_summary = 6; + string name = 1; + google.protobuf.Timestamp create_time = 2; + temporal.api.deployment.v1.RoutingConfig routing_config = 3; + temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary latest_version_summary = 4; + temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary current_version_summary = 5; + temporal.api.deployment.v1.WorkerDeploymentInfo.WorkerDeploymentVersionSummary ramping_version_summary = 6; +} + +// Input for the activity that validates compute config scaling groups via +// the Worker Controller Instance client. +message ValidateWorkerControllerInstanceSpecInput { + map scaling_groups = 1; +} + +// used as activity input for creating or updating a Worker Controller Instance +// via the WCI client. +message UpdateWorkerControllerInstanceInput { + temporal.api.deployment.v1.WorkerDeploymentVersion version = 1; + string identity = 2; + map upsert_scaling_groups = 3; + repeated string remove_scaling_groups = 4; +} + +// used as activity input for deleting a Worker Controller Instance +// via the WCI client. +message DeleteWorkerControllerInstanceInput { + temporal.api.deployment.v1.WorkerDeploymentVersion version = 1; + string identity = 2; } +// Input for the UpdateComputeConfig workflow update on a version workflow. +message UpdateComputeConfigArgs { + string identity = 1; + string request_id = 2; + // Scaling groups to add or update. + map upsert_scaling_groups = 3; + // Names of scaling groups to remove. Names that don't match an existing group are ignored. + repeated string remove_scaling_groups = 4; +} + +// Response for the UpdateComputeConfig workflow update on a version workflow. +message UpdateComputeConfigResponse {} + // Signal input for force-continue-as-new on Deployment workflow message ForceCANDeploymentSignalArgs { - // If provided, this state will be used instead of the current state - // when performing continue-as-new. - WorkerDeploymentLocalState override_state = 1; + // If provided, this state will be used instead of the current state + // when performing continue-as-new. + WorkerDeploymentLocalState override_state = 1; } // Signal input for force-continue-as-new on Version workflow message ForceCANVersionSignalArgs { - // If provided, this state will be used instead of the current state - // when performing continue-as-new. - VersionLocalState override_state = 1; + // If provided, this state will be used instead of the current state + // when performing continue-as-new. + VersionLocalState override_state = 1; } diff --git a/proto/internal/temporal/server/api/enums/v1/cluster.proto b/proto/internal/temporal/server/api/enums/v1/cluster.proto index 97712e50443..bcd5e30f918 100644 --- a/proto/internal/temporal/server/api/enums/v1/cluster.proto +++ b/proto/internal/temporal/server/api/enums/v1/cluster.proto @@ -5,19 +5,21 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum ClusterMemberRole { - CLUSTER_MEMBER_ROLE_UNSPECIFIED = 0; - CLUSTER_MEMBER_ROLE_FRONTEND = 1; - CLUSTER_MEMBER_ROLE_HISTORY = 2; - CLUSTER_MEMBER_ROLE_MATCHING = 3; - CLUSTER_MEMBER_ROLE_WORKER = 4; + CLUSTER_MEMBER_ROLE_UNSPECIFIED = 0; + CLUSTER_MEMBER_ROLE_FRONTEND = 1; + CLUSTER_MEMBER_ROLE_HISTORY = 2; + CLUSTER_MEMBER_ROLE_MATCHING = 3; + CLUSTER_MEMBER_ROLE_WORKER = 4; } enum HealthState { - HEALTH_STATE_UNSPECIFIED = 0; - // The host is in a healthy state. - HEALTH_STATE_SERVING = 1; - // The host is unhealthy through external observation. - HEALTH_STATE_NOT_SERVING = 2; - // The host has marked itself as not ready to serve traffic. - HEALTH_STATE_DECLINED_SERVING = 3; + HEALTH_STATE_UNSPECIFIED = 0; + // The host is in a healthy state. + HEALTH_STATE_SERVING = 1; + // The host is unhealthy through external observation. + HEALTH_STATE_NOT_SERVING = 2; + // The host has marked itself as not ready to serve traffic. + HEALTH_STATE_DECLINED_SERVING = 3; + // An internal error occurred while checking health (e.g. resolver failure). + HEALTH_STATE_INTERNAL_ERROR = 4; } diff --git a/proto/internal/temporal/server/api/enums/v1/common.proto b/proto/internal/temporal/server/api/enums/v1/common.proto index 894740a58c4..9dc22ccb469 100644 --- a/proto/internal/temporal/server/api/enums/v1/common.proto +++ b/proto/internal/temporal/server/api/enums/v1/common.proto @@ -5,28 +5,28 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum DeadLetterQueueType { - DEAD_LETTER_QUEUE_TYPE_UNSPECIFIED = 0; - DEAD_LETTER_QUEUE_TYPE_REPLICATION = 1; - DEAD_LETTER_QUEUE_TYPE_NAMESPACE = 2; + DEAD_LETTER_QUEUE_TYPE_UNSPECIFIED = 0; + DEAD_LETTER_QUEUE_TYPE_REPLICATION = 1; + DEAD_LETTER_QUEUE_TYPE_NAMESPACE = 2; } enum ChecksumFlavor { - CHECKSUM_FLAVOR_UNSPECIFIED = 0; - CHECKSUM_FLAVOR_IEEE_CRC32_OVER_PROTO3_BINARY = 1; + CHECKSUM_FLAVOR_UNSPECIFIED = 0; + CHECKSUM_FLAVOR_IEEE_CRC32_OVER_PROTO3_BINARY = 1; } // State of a callback. enum CallbackState { - // Default value, unspecified state. - CALLBACK_STATE_UNSPECIFIED = 0; - // Callback is standing by, waiting to be triggered. - CALLBACK_STATE_STANDBY = 1; - // Callback is in the queue waiting to be executed or is currently executing. - CALLBACK_STATE_SCHEDULED = 2; - // Callback has failed with a retryable error and is backing off before the next attempt. - CALLBACK_STATE_BACKING_OFF = 3; - // Callback has failed. - CALLBACK_STATE_FAILED = 4; - // Callback has succeeded. - CALLBACK_STATE_SUCCEEDED = 5; + // Default value, unspecified state. + CALLBACK_STATE_UNSPECIFIED = 0; + // Callback is standing by, waiting to be triggered. + CALLBACK_STATE_STANDBY = 1; + // Callback is in the queue waiting to be executed or is currently executing. + CALLBACK_STATE_SCHEDULED = 2; + // Callback has failed with a retryable error and is backing off before the next attempt. + CALLBACK_STATE_BACKING_OFF = 3; + // Callback has failed. + CALLBACK_STATE_FAILED = 4; + // Callback has succeeded. + CALLBACK_STATE_SUCCEEDED = 5; } diff --git a/proto/internal/temporal/server/api/enums/v1/fairness_state.proto b/proto/internal/temporal/server/api/enums/v1/fairness_state.proto index 140d79a78d1..ebce3c5f71b 100644 --- a/proto/internal/temporal/server/api/enums/v1/fairness_state.proto +++ b/proto/internal/temporal/server/api/enums/v1/fairness_state.proto @@ -5,8 +5,8 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum FairnessState { - FAIRNESS_STATE_UNSPECIFIED = 0; - FAIRNESS_STATE_V0 = 1; - FAIRNESS_STATE_V1 = 2; - FAIRNESS_STATE_V2 = 3; -}; + FAIRNESS_STATE_UNSPECIFIED = 0; + FAIRNESS_STATE_V0 = 1; + FAIRNESS_STATE_V1 = 2; + FAIRNESS_STATE_V2 = 3; +} diff --git a/proto/internal/temporal/server/api/enums/v1/nexus.proto b/proto/internal/temporal/server/api/enums/v1/nexus.proto index 51a820236d1..d7c986c0137 100644 --- a/proto/internal/temporal/server/api/enums/v1/nexus.proto +++ b/proto/internal/temporal/server/api/enums/v1/nexus.proto @@ -5,23 +5,23 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum NexusOperationState { - // Default value, unspecified state. - NEXUS_OPERATION_STATE_UNSPECIFIED = 0; - // Operation is in the queue waiting to be executed or is currently executing. - NEXUS_OPERATION_STATE_SCHEDULED = 1; - // Operation has failed with a retryable error and is backing off before the next attempt. - NEXUS_OPERATION_STATE_BACKING_OFF = 2; - // Operation was started and will complete asynchronously. - NEXUS_OPERATION_STATE_STARTED = 3; - // Operation succeeded. - // This may happen either as a response to a start request or as reported via callback. - NEXUS_OPERATION_STATE_SUCCEEDED = 4; - // Operation failed either when a start request encounters a non-retryable error or as reported via callback. - NEXUS_OPERATION_STATE_FAILED = 5; - // Operation completed as canceled (may have not ever been delivered). - // This may happen either as a response to a start request or as reported via callback. - NEXUS_OPERATION_STATE_CANCELED = 6; - // Operation timed out - exceeded the user supplied schedule-to-close timeout. - // Any attempts to complete the operation in this state will be ignored. - NEXUS_OPERATION_STATE_TIMED_OUT = 7; + // Default value, unspecified state. + NEXUS_OPERATION_STATE_UNSPECIFIED = 0; + // Operation is in the queue waiting to be executed or is currently executing. + NEXUS_OPERATION_STATE_SCHEDULED = 1; + // Operation has failed with a retryable error and is backing off before the next attempt. + NEXUS_OPERATION_STATE_BACKING_OFF = 2; + // Operation was started and will complete asynchronously. + NEXUS_OPERATION_STATE_STARTED = 3; + // Operation succeeded. + // This may happen either as a response to a start request or as reported via callback. + NEXUS_OPERATION_STATE_SUCCEEDED = 4; + // Operation failed either when a start request encounters a non-retryable error or as reported via callback. + NEXUS_OPERATION_STATE_FAILED = 5; + // Operation completed as canceled (may have not ever been delivered). + // This may happen either as a response to a start request or as reported via callback. + NEXUS_OPERATION_STATE_CANCELED = 6; + // Operation timed out - exceeded the user supplied schedule-to-close timeout. + // Any attempts to complete the operation in this state will be ignored. + NEXUS_OPERATION_STATE_TIMED_OUT = 7; } diff --git a/proto/internal/temporal/server/api/enums/v1/predicate.proto b/proto/internal/temporal/server/api/enums/v1/predicate.proto index 01a3ae191a7..1e0abe4533e 100644 --- a/proto/internal/temporal/server/api/enums/v1/predicate.proto +++ b/proto/internal/temporal/server/api/enums/v1/predicate.proto @@ -5,17 +5,17 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum PredicateType { - PREDICATE_TYPE_UNSPECIFIED = 0; - PREDICATE_TYPE_UNIVERSAL = 1; - PREDICATE_TYPE_EMPTY = 2; - PREDICATE_TYPE_AND = 3; - PREDICATE_TYPE_OR = 4; - PREDICATE_TYPE_NOT = 5; - PREDICATE_TYPE_NAMESPACE_ID = 6; - PREDICATE_TYPE_TASK_TYPE = 7; - PREDICATE_TYPE_DESTINATION = 8; - PREDICATE_TYPE_OUTBOUND_TASK_GROUP = 9; - // Predicate used for grouping outbound tasks. Consists of task_group, namespace_id, and destination. - // This replaces a previous implementation which used an AND predicate over 3 separate predicate types. - PREDICATE_TYPE_OUTBOUND_TASK = 10; + PREDICATE_TYPE_UNSPECIFIED = 0; + PREDICATE_TYPE_UNIVERSAL = 1; + PREDICATE_TYPE_EMPTY = 2; + PREDICATE_TYPE_AND = 3; + PREDICATE_TYPE_OR = 4; + PREDICATE_TYPE_NOT = 5; + PREDICATE_TYPE_NAMESPACE_ID = 6; + PREDICATE_TYPE_TASK_TYPE = 7; + PREDICATE_TYPE_DESTINATION = 8; + PREDICATE_TYPE_OUTBOUND_TASK_GROUP = 9; + // Predicate used for grouping outbound tasks. Consists of task_group, namespace_id, and destination. + // This replaces a previous implementation which used an AND predicate over 3 separate predicate types. + PREDICATE_TYPE_OUTBOUND_TASK = 10; } diff --git a/proto/internal/temporal/server/api/enums/v1/replication.proto b/proto/internal/temporal/server/api/enums/v1/replication.proto index be1f2490e70..d8977df4b7f 100644 --- a/proto/internal/temporal/server/api/enums/v1/replication.proto +++ b/proto/internal/temporal/server/api/enums/v1/replication.proto @@ -5,29 +5,30 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum ReplicationTaskType { - REPLICATION_TASK_TYPE_UNSPECIFIED = 0; - REPLICATION_TASK_TYPE_NAMESPACE_TASK = 1; - REPLICATION_TASK_TYPE_HISTORY_TASK = 2; - REPLICATION_TASK_TYPE_SYNC_SHARD_STATUS_TASK = 3; - REPLICATION_TASK_TYPE_SYNC_ACTIVITY_TASK = 4; - REPLICATION_TASK_TYPE_HISTORY_METADATA_TASK = 5; - REPLICATION_TASK_TYPE_HISTORY_V2_TASK = 6; - REPLICATION_TASK_TYPE_SYNC_WORKFLOW_STATE_TASK = 7; - REPLICATION_TASK_TYPE_TASK_QUEUE_USER_DATA = 8; - REPLICATION_TASK_TYPE_SYNC_HSM_TASK = 9; - REPLICATION_TASK_TYPE_BACKFILL_HISTORY_TASK = 10; - REPLICATION_TASK_TYPE_VERIFY_VERSIONED_TRANSITION_TASK = 11; - REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK = 12; + REPLICATION_TASK_TYPE_UNSPECIFIED = 0; + REPLICATION_TASK_TYPE_NAMESPACE_TASK = 1; + REPLICATION_TASK_TYPE_HISTORY_TASK = 2; + REPLICATION_TASK_TYPE_SYNC_SHARD_STATUS_TASK = 3; + REPLICATION_TASK_TYPE_SYNC_ACTIVITY_TASK = 4; + REPLICATION_TASK_TYPE_HISTORY_METADATA_TASK = 5; + REPLICATION_TASK_TYPE_HISTORY_V2_TASK = 6; + REPLICATION_TASK_TYPE_SYNC_WORKFLOW_STATE_TASK = 7; + REPLICATION_TASK_TYPE_TASK_QUEUE_USER_DATA = 8; + REPLICATION_TASK_TYPE_SYNC_HSM_TASK = 9; + REPLICATION_TASK_TYPE_BACKFILL_HISTORY_TASK = 10; + REPLICATION_TASK_TYPE_VERIFY_VERSIONED_TRANSITION_TASK = 11; + REPLICATION_TASK_TYPE_SYNC_VERSIONED_TRANSITION_TASK = 12; + REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK = 13; } enum NamespaceOperation { - NAMESPACE_OPERATION_UNSPECIFIED = 0; - NAMESPACE_OPERATION_CREATE = 1; - NAMESPACE_OPERATION_UPDATE = 2; + NAMESPACE_OPERATION_UNSPECIFIED = 0; + NAMESPACE_OPERATION_CREATE = 1; + NAMESPACE_OPERATION_UPDATE = 2; } enum ReplicationFlowControlCommand { - REPLICATION_FLOW_CONTROL_COMMAND_UNSPECIFIED = 0; - REPLICATION_FLOW_CONTROL_COMMAND_RESUME = 1; - REPLICATION_FLOW_CONTROL_COMMAND_PAUSE = 2; + REPLICATION_FLOW_CONTROL_COMMAND_UNSPECIFIED = 0; + REPLICATION_FLOW_CONTROL_COMMAND_RESUME = 1; + REPLICATION_FLOW_CONTROL_COMMAND_PAUSE = 2; } diff --git a/proto/internal/temporal/server/api/enums/v1/task.proto b/proto/internal/temporal/server/api/enums/v1/task.proto index e50593bd76c..6d10bf6a260 100644 --- a/proto/internal/temporal/server/api/enums/v1/task.proto +++ b/proto/internal/temporal/server/api/enums/v1/task.proto @@ -6,68 +6,71 @@ option go_package = "go.temporal.io/server/api/enums/v1;enums"; // TaskSource is the source from which a task was produced. enum TaskSource { - TASK_SOURCE_UNSPECIFIED = 0; - // Task produced by history service. - TASK_SOURCE_HISTORY = 1; - // Task produced from matching db backlog. - TASK_SOURCE_DB_BACKLOG = 2; + TASK_SOURCE_UNSPECIFIED = 0; + // Task produced by history service. + TASK_SOURCE_HISTORY = 1; + // Task produced from matching db backlog. + TASK_SOURCE_DB_BACKLOG = 2; } enum TaskType { - TASK_TYPE_UNSPECIFIED = 0; - TASK_TYPE_REPLICATION_HISTORY = 1; - TASK_TYPE_REPLICATION_SYNC_ACTIVITY = 2; - TASK_TYPE_TRANSFER_WORKFLOW_TASK = 3; - TASK_TYPE_TRANSFER_ACTIVITY_TASK = 4; - TASK_TYPE_TRANSFER_CLOSE_EXECUTION = 5; - TASK_TYPE_TRANSFER_CANCEL_EXECUTION = 6; - TASK_TYPE_TRANSFER_START_CHILD_EXECUTION = 7; - TASK_TYPE_TRANSFER_SIGNAL_EXECUTION = 8; - reserved 9; // TASK_TYPE_TRANSFER_RECORD_WORKFLOW_STARTED - TASK_TYPE_TRANSFER_RESET_WORKFLOW = 10; - reserved 11; // TASK_TYPE_TRANSFER_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES - TASK_TYPE_WORKFLOW_TASK_TIMEOUT = 12; - TASK_TYPE_ACTIVITY_TIMEOUT = 13; - TASK_TYPE_USER_TIMER = 14; - TASK_TYPE_WORKFLOW_RUN_TIMEOUT = 15; - TASK_TYPE_DELETE_HISTORY_EVENT = 16; - TASK_TYPE_ACTIVITY_RETRY_TIMER = 17; - TASK_TYPE_WORKFLOW_BACKOFF_TIMER = 18; - TASK_TYPE_VISIBILITY_START_EXECUTION = 19; - TASK_TYPE_VISIBILITY_UPSERT_EXECUTION = 20; - TASK_TYPE_VISIBILITY_CLOSE_EXECUTION = 21; - TASK_TYPE_VISIBILITY_DELETE_EXECUTION = 22; - reserved 23; - TASK_TYPE_TRANSFER_DELETE_EXECUTION = 24; - TASK_TYPE_REPLICATION_SYNC_WORKFLOW_STATE = 25; - TASK_TYPE_ARCHIVAL_ARCHIVE_EXECUTION = 26; - // An immediate outbound task generated by a state machine. - // Outbound tasks specify a destination that is used to group tasks into a per namespace-and-destination - // scheduler. - TASK_TYPE_STATE_MACHINE_OUTBOUND = 27; - // A timer task generated by a state machine. - TASK_TYPE_STATE_MACHINE_TIMER = 28; + TASK_TYPE_UNSPECIFIED = 0; + TASK_TYPE_REPLICATION_HISTORY = 1; + TASK_TYPE_REPLICATION_SYNC_ACTIVITY = 2; + TASK_TYPE_TRANSFER_WORKFLOW_TASK = 3; + TASK_TYPE_TRANSFER_ACTIVITY_TASK = 4; + TASK_TYPE_TRANSFER_CLOSE_EXECUTION = 5; + TASK_TYPE_TRANSFER_CANCEL_EXECUTION = 6; + TASK_TYPE_TRANSFER_START_CHILD_EXECUTION = 7; + TASK_TYPE_TRANSFER_SIGNAL_EXECUTION = 8; + reserved 9; // TASK_TYPE_TRANSFER_RECORD_WORKFLOW_STARTED + TASK_TYPE_TRANSFER_RESET_WORKFLOW = 10; + reserved 11; // TASK_TYPE_TRANSFER_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES + TASK_TYPE_WORKFLOW_TASK_TIMEOUT = 12; + TASK_TYPE_ACTIVITY_TIMEOUT = 13; + TASK_TYPE_USER_TIMER = 14; + TASK_TYPE_WORKFLOW_RUN_TIMEOUT = 15; + TASK_TYPE_DELETE_HISTORY_EVENT = 16; + TASK_TYPE_ACTIVITY_RETRY_TIMER = 17; + TASK_TYPE_WORKFLOW_BACKOFF_TIMER = 18; + TASK_TYPE_VISIBILITY_START_EXECUTION = 19; + TASK_TYPE_VISIBILITY_UPSERT_EXECUTION = 20; + TASK_TYPE_VISIBILITY_CLOSE_EXECUTION = 21; + TASK_TYPE_VISIBILITY_DELETE_EXECUTION = 22; + reserved 23; + TASK_TYPE_TRANSFER_DELETE_EXECUTION = 24; + TASK_TYPE_REPLICATION_SYNC_WORKFLOW_STATE = 25; + TASK_TYPE_ARCHIVAL_ARCHIVE_EXECUTION = 26; + // An immediate outbound task generated by a state machine. + // Outbound tasks specify a destination that is used to group tasks into a per namespace-and-destination + // scheduler. + TASK_TYPE_STATE_MACHINE_OUTBOUND = 27; + // A timer task generated by a state machine. + TASK_TYPE_STATE_MACHINE_TIMER = 28; - // Timeout task for the entire workflow execution chain. - TASK_TYPE_WORKFLOW_EXECUTION_TIMEOUT = 29; + // Timeout task for the entire workflow execution chain. + TASK_TYPE_WORKFLOW_EXECUTION_TIMEOUT = 29; - TASK_TYPE_REPLICATION_SYNC_HSM = 30; - TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION = 31; + TASK_TYPE_REPLICATION_SYNC_HSM = 30; + TASK_TYPE_REPLICATION_SYNC_VERSIONED_TRANSITION = 31; - // A task that applies a batch of state changes to a CHASM entity. - TASK_TYPE_CHASM_PURE = 32; + // A task that applies a batch of state changes to a CHASM entity. + TASK_TYPE_CHASM_PURE = 32; - // A task with side effects generated by a CHASM component. - TASK_TYPE_CHASM = 33; + // A task with side effects generated by a CHASM component. + TASK_TYPE_CHASM = 33; - // A task to send worker commands via Nexus. - TASK_TYPE_WORKER_COMMANDS = 34; + // A replication task that deletes workflow on passive cluster(s). + TASK_TYPE_REPLICATION_DELETE_EXECUTION = 34; + + // A task to send worker commands via Nexus. + TASK_TYPE_WORKER_COMMANDS = 35; } // TaskPriority is only used for replication task as of May 2024 enum TaskPriority { - TASK_PRIORITY_UNSPECIFIED = 0; - TASK_PRIORITY_HIGH = 1; - // gap between index can be used for future priority levels if needed - TASK_PRIORITY_LOW = 10; + TASK_PRIORITY_UNSPECIFIED = 0; + TASK_PRIORITY_HIGH = 1; + // gap between index can be used for future priority levels if needed + TASK_PRIORITY_LOW = 10; } diff --git a/proto/internal/temporal/server/api/enums/v1/workflow.proto b/proto/internal/temporal/server/api/enums/v1/workflow.proto index cb74c9a0c4f..12f529df154 100644 --- a/proto/internal/temporal/server/api/enums/v1/workflow.proto +++ b/proto/internal/temporal/server/api/enums/v1/workflow.proto @@ -5,24 +5,24 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum WorkflowExecutionState { - WORKFLOW_EXECUTION_STATE_UNSPECIFIED = 0; - WORKFLOW_EXECUTION_STATE_CREATED = 1; - WORKFLOW_EXECUTION_STATE_RUNNING = 2; - WORKFLOW_EXECUTION_STATE_COMPLETED = 3; - WORKFLOW_EXECUTION_STATE_ZOMBIE = 4; - WORKFLOW_EXECUTION_STATE_VOID = 5; - WORKFLOW_EXECUTION_STATE_CORRUPTED = 6; + WORKFLOW_EXECUTION_STATE_UNSPECIFIED = 0; + WORKFLOW_EXECUTION_STATE_CREATED = 1; + WORKFLOW_EXECUTION_STATE_RUNNING = 2; + WORKFLOW_EXECUTION_STATE_COMPLETED = 3; + WORKFLOW_EXECUTION_STATE_ZOMBIE = 4; + WORKFLOW_EXECUTION_STATE_VOID = 5; + WORKFLOW_EXECUTION_STATE_CORRUPTED = 6; } enum WorkflowBackoffType { - WORKFLOW_BACKOFF_TYPE_UNSPECIFIED = 0; - WORKFLOW_BACKOFF_TYPE_RETRY = 1; - WORKFLOW_BACKOFF_TYPE_CRON = 2; - WORKFLOW_BACKOFF_TYPE_DELAY_START = 3; + WORKFLOW_BACKOFF_TYPE_UNSPECIFIED = 0; + WORKFLOW_BACKOFF_TYPE_RETRY = 1; + WORKFLOW_BACKOFF_TYPE_CRON = 2; + WORKFLOW_BACKOFF_TYPE_DELAY_START = 3; } enum PausedWorkflowEntityType { - PAUSED_WORKFLOW_ENTITY_TYPE_UNSPECIFIED = 0; - PAUSED_WORKFLOW_ENTITY_TYPE_ACTIVITY = 1; - PAUSED_WORKFLOW_ENTITY_TYPE_WORKFLOW = 2; + PAUSED_WORKFLOW_ENTITY_TYPE_UNSPECIFIED = 0; + PAUSED_WORKFLOW_ENTITY_TYPE_ACTIVITY = 1; + PAUSED_WORKFLOW_ENTITY_TYPE_WORKFLOW = 2; } diff --git a/proto/internal/temporal/server/api/enums/v1/workflow_task_type.proto b/proto/internal/temporal/server/api/enums/v1/workflow_task_type.proto index 62a2d41d93d..ab89c64abe5 100644 --- a/proto/internal/temporal/server/api/enums/v1/workflow_task_type.proto +++ b/proto/internal/temporal/server/api/enums/v1/workflow_task_type.proto @@ -5,9 +5,9 @@ package temporal.server.api.enums.v1; option go_package = "go.temporal.io/server/api/enums/v1;enums"; enum WorkflowTaskType { - WORKFLOW_TASK_TYPE_UNSPECIFIED = 0; - WORKFLOW_TASK_TYPE_NORMAL = 1; - // TODO (alex): TRANSIENT is not current used. Needs to be set when Attempt>1. - WORKFLOW_TASK_TYPE_TRANSIENT = 2; - WORKFLOW_TASK_TYPE_SPECULATIVE = 3; + WORKFLOW_TASK_TYPE_UNSPECIFIED = 0; + WORKFLOW_TASK_TYPE_NORMAL = 1; + // TODO (alex): TRANSIENT is not current used. Needs to be set when Attempt>1. + WORKFLOW_TASK_TYPE_TRANSIENT = 2; + WORKFLOW_TASK_TYPE_SPECULATIVE = 3; } diff --git a/proto/internal/temporal/server/api/errordetails/v1/message.proto b/proto/internal/temporal/server/api/errordetails/v1/message.proto index d26aca8f6c6..8d92a4e5a78 100644 --- a/proto/internal/temporal/server/api/errordetails/v1/message.proto +++ b/proto/internal/temporal/server/api/errordetails/v1/message.proto @@ -4,58 +4,53 @@ syntax = "proto3"; package temporal.server.api.errordetails.v1; -option go_package = "go.temporal.io/server/api/errordetails/v1;errordetails"; - import "temporal/server/api/history/v1/message.proto"; import "temporal/server/api/persistence/v1/hsm.proto"; -message TaskAlreadyStartedFailure { -} +option go_package = "go.temporal.io/server/api/errordetails/v1;errordetails"; + +message TaskAlreadyStartedFailure {} message CurrentBranchChangedFailure { - bytes current_branch_token = 1; - bytes request_branch_token = 2; - temporal.server.api.persistence.v1.VersionedTransition current_versioned_transition = 3; - temporal.server.api.persistence.v1.VersionedTransition request_versioned_transition = 4; + bytes current_branch_token = 1; + bytes request_branch_token = 2; + temporal.server.api.persistence.v1.VersionedTransition current_versioned_transition = 3; + temporal.server.api.persistence.v1.VersionedTransition request_versioned_transition = 4; } message ShardOwnershipLostFailure { - string owner_host = 1; - string current_host = 2; + string owner_host = 1; + string current_host = 2; } message RetryReplicationFailure { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - int64 start_event_id = 4; - int64 start_event_version = 5; - int64 end_event_id = 6; - int64 end_event_version = 7; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + int64 start_event_id = 4; + int64 start_event_version = 5; + int64 end_event_id = 6; + int64 end_event_version = 7; } message SyncStateFailure { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 4; - temporal.server.api.history.v1.VersionHistories version_histories = 5; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 6; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 4; + temporal.server.api.history.v1.VersionHistories version_histories = 5; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 6; } -message StickyWorkerUnavailableFailure { -} +message StickyWorkerUnavailableFailure {} // Deprecated. Only used in WV2. [cleanup-old-wv] -message ObsoleteDispatchBuildIdFailure { -} +message ObsoleteDispatchBuildIdFailure {} // Returned when History determines a task that Matching wants to dispatch is no longer valid. -message ObsoleteMatchingTaskFailure { -} +message ObsoleteMatchingTaskFailure {} // Returned when an activity start is rejected by History because the workflow is in a transitioning // between worker deployments. -message ActivityStartDuringTransitionFailure { -} +message ActivityStartDuringTransitionFailure {} diff --git a/proto/internal/temporal/server/api/health/v1/message.proto b/proto/internal/temporal/server/api/health/v1/message.proto new file mode 100644 index 00000000000..636c7161224 --- /dev/null +++ b/proto/internal/temporal/server/api/health/v1/message.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +package temporal.server.api.health.v1; + +import "temporal/server/api/enums/v1/cluster.proto"; + +option go_package = "go.temporal.io/server/api/health/v1;health"; + +// Individual health check result. +// The check_type field uses human-readable strings rather than an enum for extensibility. +message HealthCheck { + // Machine-readable check type identifier for programmatic matching. + // Known values defined as Go constants in api/health/v1/types.go: + // "grpc_health", "rpc_latency", "rpc_error_ratio", + // "persistence_latency", "persistence_error_ratio", + // "host_availability", "task_queue_backlog" + // We use strings instead of an enum for flexibility: new check types can be + // added without proto changes. See HealthCheck.message for human-readable details. + string check_type = 1; + temporal.server.api.enums.v1.HealthState state = 2; + // Actual observed value (0 if N/A). + double value = 3; + // Threshold that was exceeded (0 if N/A). + double threshold = 4; + // Human-readable detail describing what happened, e.g. + // "RPC latency 850.00ms exceeded 500.00ms threshold". + string message = 5; +} + +// Health details for a single host. +message HostHealthDetail { + string address = 1; + temporal.server.api.enums.v1.HealthState state = 2; + repeated HealthCheck checks = 3; +} + +// Health details for a service (history, frontend, matching). +message ServiceHealthDetail { + string service = 1; + temporal.server.api.enums.v1.HealthState state = 2; + repeated HostHealthDetail hosts = 3; + // Service-level diagnostic message (e.g. "no available hosts", "resolver error"). + string message = 4; +} diff --git a/proto/internal/temporal/server/api/history/v1/message.proto b/proto/internal/temporal/server/api/history/v1/message.proto index a90d6dbe655..c9ff8436071 100644 --- a/proto/internal/temporal/server/api/history/v1/message.proto +++ b/proto/internal/temporal/server/api/history/v1/message.proto @@ -2,54 +2,53 @@ syntax = "proto3"; package temporal.server.api.history.v1; -option go_package = "go.temporal.io/server/api/history/v1;history"; - import "google/protobuf/timestamp.proto"; - import "temporal/api/history/v1/message.proto"; +option go_package = "go.temporal.io/server/api/history/v1;history"; + message TransientWorkflowTaskInfo { - reserved 1; - reserved 2; + reserved 1; + reserved 2; - // A list of history events that are to be appended to the "real" workflow history. - repeated temporal.api.history.v1.HistoryEvent history_suffix = 3; + // A list of history events that are to be appended to the "real" workflow history. + repeated temporal.api.history.v1.HistoryEvent history_suffix = 3; } // VersionHistoryItem contains signal eventId and the corresponding version. message VersionHistoryItem { - int64 event_id = 1; - int64 version = 2; + int64 event_id = 1; + int64 version = 2; } // VersionHistory contains the version history of a branch. message VersionHistory { - bytes branch_token = 1; - repeated VersionHistoryItem items = 2; + bytes branch_token = 1; + repeated VersionHistoryItem items = 2; } // VersionHistories contains all version histories from all branches. message VersionHistories { - int32 current_version_history_index = 1; - repeated VersionHistory histories = 2; + int32 current_version_history_index = 1; + repeated VersionHistory histories = 2; } message TaskKey { - int64 task_id = 1; - google.protobuf.Timestamp fire_time = 2; + int64 task_id = 1; + google.protobuf.Timestamp fire_time = 2; } message TaskRange { - TaskKey inclusive_min_task_key = 1; - TaskKey exclusive_max_task_key = 2; + TaskKey inclusive_min_task_key = 1; + TaskKey exclusive_max_task_key = 2; } // StrippedHistoryEvent is a stripped down version of HistoryEvent that only contains the event_id and version. message StrippedHistoryEvent { - int64 event_id = 1; - int64 version = 4; + int64 event_id = 1; + int64 version = 4; } message StrippedHistoryEvents { - repeated StrippedHistoryEvent events = 1; + repeated StrippedHistoryEvent events = 1; } diff --git a/proto/internal/temporal/server/api/historyservice/v1/request_response.proto b/proto/internal/temporal/server/api/historyservice/v1/request_response.proto index 27a8593bd4b..2ed7f596737 100644 --- a/proto/internal/temporal/server/api/historyservice/v1/request_response.proto +++ b/proto/internal/temporal/server/api/historyservice/v1/request_response.proto @@ -1,28 +1,29 @@ syntax = "proto3"; package temporal.server.api.historyservice.v1; -option go_package = "go.temporal.io/server/api/historyservice/v1;historyservice"; import "google/protobuf/descriptor.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; - import "temporal/api/activity/v1/message.proto"; -import "temporal/api/deployment/v1/message.proto"; import "temporal/api/common/v1/message.proto"; -import "temporal/api/history/v1/message.proto"; -import "temporal/api/taskqueue/v1/message.proto"; +import "temporal/api/deployment/v1/message.proto"; import "temporal/api/enums/v1/workflow.proto"; -import "temporal/api/workflow/v1/message.proto"; -import "temporal/api/query/v1/message.proto"; -import "temporal/api/protocol/v1/message.proto"; import "temporal/api/failure/v1/message.proto"; +import "temporal/api/history/v1/message.proto"; import "temporal/api/nexus/v1/message.proto"; - +import "temporal/api/protocol/v1/message.proto"; +import "temporal/api/query/v1/message.proto"; +import "temporal/api/taskqueue/v1/message.proto"; +import "temporal/api/workflow/v1/message.proto"; +import "temporal/api/workflowservice/v1/request_response.proto"; +import "temporal/server/api/adminservice/v1/request_response.proto"; import "temporal/server/api/clock/v1/message.proto"; +import "temporal/server/api/common/v1/dlq.proto"; import "temporal/server/api/enums/v1/cluster.proto"; import "temporal/server/api/enums/v1/common.proto"; import "temporal/server/api/enums/v1/workflow.proto"; +import "temporal/server/api/health/v1/message.proto"; import "temporal/server/api/history/v1/message.proto"; import "temporal/server/api/namespace/v1/message.proto"; import "temporal/server/api/persistence/v1/executions.proto"; @@ -33,261 +34,267 @@ import "temporal/server/api/taskqueue/v1/message.proto"; import "temporal/server/api/token/v1/message.proto"; import "temporal/server/api/workflow/v1/message.proto"; -import "temporal/api/workflowservice/v1/request_response.proto"; -import "temporal/server/api/adminservice/v1/request_response.proto"; -import "temporal/server/api/common/v1/dlq.proto"; +option go_package = "go.temporal.io/server/api/historyservice/v1;historyservice"; extend google.protobuf.MessageOptions { - optional RoutingOptions routing = 7234; + optional RoutingOptions routing = 7234; } // RoutingOptions define how a request is routed to the appropriate host. message RoutingOptions { - // Routing is custom and implemented in the non-generated client/history/client.go. - bool custom = 1; - // Request will be routed to a random host. - bool any_host = 2; - // Request will be routed according to the specified shard ID field. - string shard_id = 3; - // Requested routed by task token or workflow ID may also specify how to obtain the namespace ID. Defaults to the - // "namespace_id" field. - string namespace_id = 4; - // Request will be routed by resolving the namespace ID and workflow ID to a given shard. - string workflow_id = 5; - // Request will be routed by resolving the namespace ID and the workflow ID from this task token to a given shard. - string task_token = 6; - // Request will be routed by resolving the namespace ID and the workflow ID from the first task info element. - string task_infos = 7; - // Request will be routed by resolving the namespace ID and the workflow ID from this chasm ref to a given shard. - string chasm_component_ref = 8; + // Routing is custom and implemented in the non-generated client/history/client.go. + bool custom = 1; + // Request will be routed to a random host. + bool any_host = 2; + // Request will be routed according to the specified shard ID field. + string shard_id = 3; + // Requested routed by task token or workflow ID may also specify how to obtain the namespace ID. Defaults to the + // "namespace_id" field. + string namespace_id = 4; + // Request will be routed by resolving the namespace ID and workflow ID to a given shard. + string workflow_id = 5; + // Request will be routed by resolving the namespace ID and the workflow ID from this task token to a given shard. + string task_token = 6; + // Request will be routed by resolving the namespace ID and the workflow ID from the first task info element. + string task_infos = 7; + // Request will be routed by resolving the namespace ID and the workflow ID from this chasm ref to a given shard. + string chasm_component_ref = 8; } message StartWorkflowExecutionRequest { - option (routing).workflow_id = "start_request.workflow_id"; - - string namespace_id = 1; - temporal.api.workflowservice.v1.StartWorkflowExecutionRequest start_request = 2; - temporal.server.api.workflow.v1.ParentExecutionInfo parent_execution_info = 3; - int32 attempt = 4; - google.protobuf.Timestamp workflow_execution_expiration_time = 5; - temporal.api.enums.v1.ContinueAsNewInitiator continue_as_new_initiator = 6; - // History service should use the values of continued_failure and last_completion_result - // here, not the ones in start_request (those are moved into here in the frontend). - temporal.api.failure.v1.Failure continued_failure = 7; - temporal.api.common.v1.Payloads last_completion_result = 8; - google.protobuf.Duration first_workflow_task_backoff = 9; - // For child or continued-as-new workflows, including a version here from the source - // (parent/previous) will set the initial version stamp of this workflow. - // Deprecated. use `inherited_build_id` - temporal.api.common.v1.WorkerVersionStamp source_version_stamp = 10; - // The root execution info of the new workflow. - // For top-level workflows (ie., without parent), this field must be nil. - temporal.server.api.workflow.v1.RootExecutionInfo root_execution_info = 11; - // inherited build ID from parent/previous execution - // Deprecated. Use behavior, version, and task queue fields in `parent_execution_info`. - string inherited_build_id = 12; - // If set, takes precedence over the Versioning Behavior sent by the SDK on Workflow Task completion. - // To unset the override after the workflow is running, use UpdateWorkflowExecutionOptions. - temporal.api.workflow.v1.VersioningOverride versioning_override = 13; - // If set, we verify the parent-child relationship before applying ID conflict policy WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING - bool child_workflow_only = 14; - // If present, the new workflow should start on this version with pinned base behavior. - temporal.api.deployment.v1.WorkerDeploymentVersion inherited_pinned_version = 15; - // Passes deployment version and revision number from a parent/previous workflow with AutoUpgrade behavior - // to its child/continued-as-new workflow. The first workflow task of the child/CAN workflow is dispatched to - // either this deployment version or the current version of the task queue, depending on which is the more recent version. - // After the first workflow task, the effective behavior of the workflow is determined by worker-sent values in - // subsequent workflow tasks. - temporal.api.deployment.v1.InheritedAutoUpgradeInfo inherited_auto_upgrade_info = 16; + option (routing).workflow_id = "start_request.workflow_id"; + + string namespace_id = 1; + temporal.api.workflowservice.v1.StartWorkflowExecutionRequest start_request = 2; + temporal.server.api.workflow.v1.ParentExecutionInfo parent_execution_info = 3; + int32 attempt = 4; + google.protobuf.Timestamp workflow_execution_expiration_time = 5; + temporal.api.enums.v1.ContinueAsNewInitiator continue_as_new_initiator = 6; + // History service should use the values of continued_failure and last_completion_result + // here, not the ones in start_request (those are moved into here in the frontend). + temporal.api.failure.v1.Failure continued_failure = 7; + temporal.api.common.v1.Payloads last_completion_result = 8; + google.protobuf.Duration first_workflow_task_backoff = 9; + // For child or continued-as-new workflows, including a version here from the source + // (parent/previous) will set the initial version stamp of this workflow. + // Deprecated. use `inherited_build_id` + temporal.api.common.v1.WorkerVersionStamp source_version_stamp = 10; + // The root execution info of the new workflow. + // For top-level workflows (ie., without parent), this field must be nil. + temporal.server.api.workflow.v1.RootExecutionInfo root_execution_info = 11; + // inherited build ID from parent/previous execution + // Deprecated. Use behavior, version, and task queue fields in `parent_execution_info`. + string inherited_build_id = 12; + // If set, takes precedence over the Versioning Behavior sent by the SDK on Workflow Task completion. + // To unset the override after the workflow is running, use UpdateWorkflowExecutionOptions. + temporal.api.workflow.v1.VersioningOverride versioning_override = 13; + // If set, we verify the parent-child relationship before applying ID conflict policy WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING + bool child_workflow_only = 14; + // If present, the new workflow should start on this version with pinned base behavior. + temporal.api.deployment.v1.WorkerDeploymentVersion inherited_pinned_version = 15; + // Passes deployment version and revision number from a parent/previous workflow with AutoUpgrade behavior + // to its child/continued-as-new workflow. The first workflow task of the child/CAN workflow is dispatched to + // either this deployment version or the current version of the task queue, depending on which is the more recent version. + // After the first workflow task, the effective behavior of the workflow is determined by worker-sent values in + // subsequent workflow tasks. + temporal.api.deployment.v1.InheritedAutoUpgradeInfo inherited_auto_upgrade_info = 16; + // The target version that the previous run implicitly declined to upgrade to. + // Computed at continue-as-new time from the previous run's last_notified_target_version + // (if set) or its existing declined value (CaN chain). For retries, passed through + // directly from the started event. Written onto the new run's + // WorkflowExecutionStartedEvent. + temporal.api.history.v1.DeclinedTargetVersionUpgrade declined_target_version_upgrade = 17; } message StartWorkflowExecutionResponse { - string run_id = 1; - temporal.server.api.clock.v1.VectorClock clock = 2; - // Set if request_eager_execution is set on the start request - temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse eager_workflow_task = 3; - bool started = 4; - temporal.api.enums.v1.WorkflowExecutionStatus status = 5; - temporal.api.common.v1.Link link = 6; + string run_id = 1; + temporal.server.api.clock.v1.VectorClock clock = 2; + // Set if request_eager_execution is set on the start request + temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse eager_workflow_task = 3; + bool started = 4; + temporal.api.enums.v1.WorkflowExecutionStatus status = 5; + temporal.api.common.v1.Link link = 6; } message GetMutableStateRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - int64 expected_next_event_id = 3; - bytes current_branch_token = 4; - temporal.server.api.history.v1.VersionHistoryItem version_history_item = 5; - temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 6; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + int64 expected_next_event_id = 3; + bytes current_branch_token = 4; + temporal.server.api.history.v1.VersionHistoryItem version_history_item = 5; + temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 6; } message GetMutableStateResponse { - temporal.api.common.v1.WorkflowExecution execution = 1; - temporal.api.common.v1.WorkflowType workflow_type = 2; - int64 next_event_id = 3; - int64 previous_started_event_id = 4; - int64 last_first_event_id = 5; - temporal.api.taskqueue.v1.TaskQueue task_queue = 6; - temporal.api.taskqueue.v1.TaskQueue sticky_task_queue = 7; - reserved 8; - reserved 9; - reserved 10; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration sticky_task_queue_schedule_to_start_timeout = 11; - reserved 12; - bytes current_branch_token = 13; - reserved 14; - temporal.server.api.enums.v1.WorkflowExecutionState workflow_state = 15; - temporal.api.enums.v1.WorkflowExecutionStatus workflow_status = 16; - temporal.server.api.history.v1.VersionHistories version_histories = 17; - bool is_sticky_task_queue_enabled = 18; - int64 last_first_event_txn_id = 19; - string first_execution_run_id = 20; - // If using build-id based versioning: version stamp of last worker to complete a workflow - // task for this workflow. - temporal.api.common.v1.WorkerVersionStamp most_recent_worker_version_stamp = 21; - // The currently assigned build ID for this execution. Presence of this value means worker versioning is used - // for this execution. - string assigned_build_id = 22; - string inherited_build_id = 23; - repeated temporal.server.api.persistence.v1.VersionedTransition transition_history = 24; - temporal.api.workflow.v1.WorkflowExecutionVersioningInfo versioning_info = 25; + temporal.api.common.v1.WorkflowExecution execution = 1; + temporal.api.common.v1.WorkflowType workflow_type = 2; + int64 next_event_id = 3; + int64 previous_started_event_id = 4; + int64 last_first_event_id = 5; + temporal.api.taskqueue.v1.TaskQueue task_queue = 6; + temporal.api.taskqueue.v1.TaskQueue sticky_task_queue = 7; + reserved 8; + reserved 9; + reserved 10; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration sticky_task_queue_schedule_to_start_timeout = 11; + reserved 12; + bytes current_branch_token = 13; + reserved 14; + temporal.server.api.enums.v1.WorkflowExecutionState workflow_state = 15; + temporal.api.enums.v1.WorkflowExecutionStatus workflow_status = 16; + temporal.server.api.history.v1.VersionHistories version_histories = 17; + bool is_sticky_task_queue_enabled = 18; + int64 last_first_event_txn_id = 19; + string first_execution_run_id = 20; + // If using build-id based versioning: version stamp of last worker to complete a workflow + // task for this workflow. + temporal.api.common.v1.WorkerVersionStamp most_recent_worker_version_stamp = 21; + // The currently assigned build ID for this execution. Presence of this value means worker versioning is used + // for this execution. + string assigned_build_id = 22; + string inherited_build_id = 23; + repeated temporal.server.api.persistence.v1.VersionedTransition transition_history = 24; + temporal.api.workflow.v1.WorkflowExecutionVersioningInfo versioning_info = 25; + // Transient or speculative workflow task events which are not yet persisted in the history. + // These events should be appended to the history when it is returned to the worker. + temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_or_speculative_tasks = 26; } message PollMutableStateRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - int64 expected_next_event_id = 3; - bytes current_branch_token = 4; - temporal.server.api.history.v1.VersionHistoryItem version_history_item = 5; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + int64 expected_next_event_id = 3; + bytes current_branch_token = 4; + temporal.server.api.history.v1.VersionHistoryItem version_history_item = 5; } message PollMutableStateResponse { - temporal.api.common.v1.WorkflowExecution execution = 1; - temporal.api.common.v1.WorkflowType workflow_type = 2; - int64 next_event_id = 3; - int64 previous_started_event_id = 4; - int64 last_first_event_id = 5; - temporal.api.taskqueue.v1.TaskQueue task_queue = 6; - temporal.api.taskqueue.v1.TaskQueue sticky_task_queue = 7; - reserved 8; - reserved 9; - reserved 10; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration sticky_task_queue_schedule_to_start_timeout = 11; - bytes current_branch_token = 12; - reserved 13; - temporal.server.api.history.v1.VersionHistories version_histories = 14; - temporal.server.api.enums.v1.WorkflowExecutionState workflow_state = 15; - temporal.api.enums.v1.WorkflowExecutionStatus workflow_status = 16; - int64 last_first_event_txn_id = 17; - string first_execution_run_id = 18; + temporal.api.common.v1.WorkflowExecution execution = 1; + temporal.api.common.v1.WorkflowType workflow_type = 2; + int64 next_event_id = 3; + int64 previous_started_event_id = 4; + int64 last_first_event_id = 5; + temporal.api.taskqueue.v1.TaskQueue task_queue = 6; + temporal.api.taskqueue.v1.TaskQueue sticky_task_queue = 7; + reserved 8; + reserved 9; + reserved 10; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration sticky_task_queue_schedule_to_start_timeout = 11; + bytes current_branch_token = 12; + reserved 13; + temporal.server.api.history.v1.VersionHistories version_histories = 14; + temporal.server.api.enums.v1.WorkflowExecutionState workflow_state = 15; + temporal.api.enums.v1.WorkflowExecutionStatus workflow_status = 16; + int64 last_first_event_txn_id = 17; + string first_execution_run_id = 18; } message ResetStickyTaskQueueRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; } -message ResetStickyTaskQueueResponse { -} +message ResetStickyTaskQueueResponse {} message ExecuteMultiOperationRequest { - option (routing).workflow_id = "workflow_id"; + option (routing).workflow_id = "workflow_id"; - string namespace_id = 1; - string workflow_id = 2; - repeated Operation operations = 3; + string namespace_id = 1; + string workflow_id = 2; + repeated Operation operations = 3; - message Operation { - oneof operation { - StartWorkflowExecutionRequest start_workflow = 1; - UpdateWorkflowExecutionRequest update_workflow = 2; - } + message Operation { + oneof operation { + StartWorkflowExecutionRequest start_workflow = 1; + UpdateWorkflowExecutionRequest update_workflow = 2; } + } } message ExecuteMultiOperationResponse { - repeated Response responses = 1; + repeated Response responses = 1; - message Response { - oneof response { - StartWorkflowExecutionResponse start_workflow = 1; - UpdateWorkflowExecutionResponse update_workflow = 2; - } + message Response { + oneof response { + StartWorkflowExecutionResponse start_workflow = 1; + UpdateWorkflowExecutionResponse update_workflow = 2; } + } } message RecordWorkflowTaskStartedRequest { - option (routing).workflow_id = "workflow_execution.workflow_id"; - - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - int64 scheduled_event_id = 3; - reserved 4; - // Unique id of each poll request. Used to ensure at most once delivery of tasks. - string request_id = 5; - temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest poll_request = 6; - temporal.server.api.clock.v1.VectorClock clock = 7; - temporal.server.api.taskqueue.v1.BuildIdRedirectInfo build_id_redirect_info = 8; - // The deployment passed by History when the task was scheduled. - // Deprecated. use `version_directive.deployment`. - temporal.api.deployment.v1.Deployment scheduled_deployment = 9; - // Versioning directive that was sent by history when scheduling the task. - temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 10; - // Stamp value from when the workflow task was scheduled. Used to validate the task is still relevant. - int32 stamp = 11; - // Revision number that was sent by matching when the task was dispatched. Used to resolve eventual consistency issues - // that may arise due to stale routing configs in task queue partitions. - int64 task_dispatch_revision_number = 12; - // Target worker deployment version according to matching when starting the task. - // Computed after matching with a poller, right before calling RecordWorkflowTaskStarted. - // Sent only if the target version is different from the poller's version. - temporal.api.deployment.v1.WorkerDeploymentVersion target_deployment_version = 13; + option (routing).workflow_id = "workflow_execution.workflow_id"; + + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + int64 scheduled_event_id = 3; + reserved 4; + // Unique id of each poll request. Used to ensure at most once delivery of tasks. + string request_id = 5; + temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest poll_request = 6; + temporal.server.api.clock.v1.VectorClock clock = 7; + temporal.server.api.taskqueue.v1.BuildIdRedirectInfo build_id_redirect_info = 8; + // The deployment passed by History when the task was scheduled. + // Deprecated. use `version_directive.deployment`. + temporal.api.deployment.v1.Deployment scheduled_deployment = 9; + // Versioning directive that was sent by history when scheduling the task. + temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 10; + // Stamp value from when the workflow task was scheduled. Used to validate the task is still relevant. + int32 stamp = 11; + // Revision number that was sent by matching when the task was dispatched. Used to resolve eventual consistency issues + // that may arise due to stale routing configs in task queue partitions. + int64 task_dispatch_revision_number = 12; + // Target worker deployment version according to matching when starting the task. + // Computed after matching with a poller, right before calling RecordWorkflowTaskStarted. + // Sent only if the target version is different from the poller's version. + temporal.api.deployment.v1.WorkerDeploymentVersion target_deployment_version = 13; } message RecordWorkflowTaskStartedResponse { - temporal.api.common.v1.WorkflowType workflow_type = 1; - int64 previous_started_event_id = 2; - int64 scheduled_event_id = 3; - int64 started_event_id = 4; - int64 next_event_id = 5; - int32 attempt = 6; - bool sticky_execution_enabled = 7; - temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 8; - temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 9; - reserved 10; - bytes branch_token = 11; - google.protobuf.Timestamp scheduled_time = 12; - google.protobuf.Timestamp started_time = 13; - map queries = 14; - temporal.server.api.clock.v1.VectorClock clock = 15; - repeated temporal.api.protocol.v1.Message messages = 16; - int64 version = 17; - temporal.api.history.v1.History history = 18; - bytes next_page_token = 19; - // Deprecated: This field is being replaced by raw_history_bytes which sends raw bytes - // instead of a proto-decoded History. This avoids matching service having to decode history. - // TODO: PRATHYUSH - // DEPRECATION PLAN: - // Two dynamic config flags control the raw history optimization: - // - history.sendRawHistoryBetweenInternalServices: enables raw history (uses field 18 when OFF, field 20/21 when ON) - // - history.sendRawHistoryBytesToMatchingService: selects field 20 (OFF) vs field 21 (ON) - // - // Version timeline (current version: v1.29): - // - v1.31: This change is released. Both flags default to false for backward compatibility. - // - v1.32: Both flags will be enabled by default in code. - // - v1.33: raw_history (field 20) and history (field 18) will be deprecated and removed, - // as raw_history_bytes (field 21) will be the only field used. - temporal.api.history.v1.History raw_history = 20 [deprecated = true]; - repeated bytes raw_history_bytes = 21; + temporal.api.common.v1.WorkflowType workflow_type = 1; + int64 previous_started_event_id = 2; + int64 scheduled_event_id = 3; + int64 started_event_id = 4; + int64 next_event_id = 5; + int32 attempt = 6; + bool sticky_execution_enabled = 7; + temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 8; + temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 9; + reserved 10; + bytes branch_token = 11; + google.protobuf.Timestamp scheduled_time = 12; + google.protobuf.Timestamp started_time = 13; + map queries = 14; + temporal.server.api.clock.v1.VectorClock clock = 15; + repeated temporal.api.protocol.v1.Message messages = 16; + int64 version = 17; + temporal.api.history.v1.History history = 18; + bytes next_page_token = 19; + // Deprecated: This field is being replaced by raw_history_bytes which sends raw bytes + // instead of a proto-decoded History. This avoids matching service having to decode history. + // TODO: PRATHYUSH + // DEPRECATION PLAN: + // Two dynamic config flags control the raw history optimization: + // - history.sendRawHistoryBetweenInternalServices: enables raw history (uses field 18 when OFF, field 20/21 when ON) + // - history.sendRawHistoryBytesToMatchingService: selects field 20 (OFF) vs field 21 (ON) + // + // Version timeline (current version: v1.29): + // - v1.31: This change is released. Both flags default to false for backward compatibility. + // - v1.32: Both flags will be enabled by default in code. + // - v1.33: raw_history (field 20) and history (field 18) will be deprecated and removed, + // as raw_history_bytes (field 21) will be the only field used. + temporal.api.history.v1.History raw_history = 20 [deprecated = true]; + repeated bytes raw_history_bytes = 21; } // RecordWorkflowTaskStartedResponseWithRawHistory is wire-compatible with RecordWorkflowTaskStartedResponse. @@ -309,284 +316,273 @@ message RecordWorkflowTaskStartedResponse { // IMPORTANT: Field numbers and all other fields must remain identical between these two messages. // Any change to RecordWorkflowTaskStartedResponse must be mirrored here. message RecordWorkflowTaskStartedResponseWithRawHistory { - temporal.api.common.v1.WorkflowType workflow_type = 1; - int64 previous_started_event_id = 2; - int64 scheduled_event_id = 3; - int64 started_event_id = 4; - int64 next_event_id = 5; - int32 attempt = 6; - bool sticky_execution_enabled = 7; - temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 8; - temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 9; - reserved 10; - bytes branch_token = 11; - google.protobuf.Timestamp scheduled_time = 12; - google.protobuf.Timestamp started_time = 13; - map queries = 14; - temporal.server.api.clock.v1.VectorClock clock = 15; - repeated temporal.api.protocol.v1.Message messages = 16; - int64 version = 17; - temporal.api.history.v1.History history = 18; - bytes next_page_token = 19; - // Deprecated: This field is being replaced by raw_history_bytes which sends raw bytes - // instead of a proto-decoded History. This avoids matching service having to decode history. - repeated bytes raw_history = 20 [deprecated = true]; - repeated bytes raw_history_bytes = 21; + temporal.api.common.v1.WorkflowType workflow_type = 1; + int64 previous_started_event_id = 2; + int64 scheduled_event_id = 3; + int64 started_event_id = 4; + int64 next_event_id = 5; + int32 attempt = 6; + bool sticky_execution_enabled = 7; + temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 8; + temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 9; + reserved 10; + bytes branch_token = 11; + google.protobuf.Timestamp scheduled_time = 12; + google.protobuf.Timestamp started_time = 13; + map queries = 14; + temporal.server.api.clock.v1.VectorClock clock = 15; + repeated temporal.api.protocol.v1.Message messages = 16; + int64 version = 17; + temporal.api.history.v1.History history = 18; + bytes next_page_token = 19; + // Deprecated: This field is being replaced by raw_history_bytes which sends raw bytes + // instead of a proto-decoded History. This avoids matching service having to decode history. + repeated bytes raw_history = 20 [deprecated = true]; + repeated bytes raw_history_bytes = 21; } message RecordActivityTaskStartedRequest { - option (routing).custom = true; - - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - int64 scheduled_event_id = 3; - reserved 4; - // Unique id of each poll request. Used to ensure at most once delivery of tasks. - string request_id = 5; - temporal.api.workflowservice.v1.PollActivityTaskQueueRequest poll_request = 6; - temporal.server.api.clock.v1.VectorClock clock = 7; - temporal.server.api.taskqueue.v1.BuildIdRedirectInfo build_id_redirect_info = 8; - - // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. - int32 stamp = 9; - // The deployment passed by History when the task was scheduled. - // Deprecated. use `version_directive.deployment`. - temporal.api.deployment.v1.Deployment scheduled_deployment = 10; - reserved 11; - // Versioning directive that was sent by history when scheduling the task. - temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 12; - // Revision number that was sent by matching when the task was dispatched. Used to resolve eventual consistency issues - // that may arise due to stale routing configs in task queue partitions. - int64 task_dispatch_revision_number = 13; - // Reference to the Chasm component for activity execution (if applicable). For standalone activities, all necessary - // start information is carried within this component, obviating the need to use the fields that apply to embedded - // activities with the exception of version_directive. - bytes component_ref = 14; + option (routing).custom = true; + + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + int64 scheduled_event_id = 3; + reserved 4; + // Unique id of each poll request. Used to ensure at most once delivery of tasks. + string request_id = 5; + temporal.api.workflowservice.v1.PollActivityTaskQueueRequest poll_request = 6; + temporal.server.api.clock.v1.VectorClock clock = 7; + temporal.server.api.taskqueue.v1.BuildIdRedirectInfo build_id_redirect_info = 8; + + // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. + int32 stamp = 9; + // The deployment passed by History when the task was scheduled. + // Deprecated. use `version_directive.deployment`. + temporal.api.deployment.v1.Deployment scheduled_deployment = 10; + reserved 11; + // Versioning directive that was sent by history when scheduling the task. + temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 12; + // Revision number that was sent by matching when the task was dispatched. Used to resolve eventual consistency issues + // that may arise due to stale routing configs in task queue partitions. + int64 task_dispatch_revision_number = 13; + // Reference to the Chasm component for activity execution (if applicable). For standalone activities, all necessary + // start information is carried within this component, obviating the need to use the fields that apply to embedded + // activities with the exception of version_directive. + bytes component_ref = 14; } message RecordActivityTaskStartedResponse { - temporal.api.history.v1.HistoryEvent scheduled_event = 1; - google.protobuf.Timestamp started_time = 2; - int32 attempt = 3; - google.protobuf.Timestamp current_attempt_scheduled_time = 4; - temporal.api.common.v1.Payloads heartbeat_details = 5; - temporal.api.common.v1.WorkflowType workflow_type = 6; - string workflow_namespace = 7; - temporal.server.api.clock.v1.VectorClock clock = 8; - int64 version = 9; - temporal.api.common.v1.Priority priority = 10; - temporal.api.common.v1.RetryPolicy retry_policy = 11; - int64 start_version = 12; - // ID of the activity run (applicable for standalone activities only) - string activity_run_id = 13; + temporal.api.history.v1.HistoryEvent scheduled_event = 1; + google.protobuf.Timestamp started_time = 2; + int32 attempt = 3; + google.protobuf.Timestamp current_attempt_scheduled_time = 4; + temporal.api.common.v1.Payloads heartbeat_details = 5; + temporal.api.common.v1.WorkflowType workflow_type = 6; + string workflow_namespace = 7; + temporal.server.api.clock.v1.VectorClock clock = 8; + int64 version = 9; + temporal.api.common.v1.Priority priority = 10; + temporal.api.common.v1.RetryPolicy retry_policy = 11; + int64 start_version = 12; + // ID of the activity run (applicable for standalone activities only) + string activity_run_id = 13; } message RespondWorkflowTaskCompletedRequest { - option (routing).task_token = "complete_request.task_token"; + option (routing).task_token = "complete_request.task_token"; - string namespace_id = 1; - temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest complete_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest complete_request = 2; } message RespondWorkflowTaskCompletedResponse { - RecordWorkflowTaskStartedResponse started_response = 1 [deprecated = true]; - repeated temporal.api.workflowservice.v1.PollActivityTaskQueueResponse activity_tasks = 2; - int64 reset_history_event_id = 3; - temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse new_workflow_task = 4; + RecordWorkflowTaskStartedResponse started_response = 1 [deprecated = true]; + repeated temporal.api.workflowservice.v1.PollActivityTaskQueueResponse activity_tasks = 2; + int64 reset_history_event_id = 3; + temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse new_workflow_task = 4; } message RespondWorkflowTaskFailedRequest { - option (routing).task_token = "failed_request.task_token"; + option (routing).task_token = "failed_request.task_token"; - string namespace_id = 1; - temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest failed_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest failed_request = 2; } -message RespondWorkflowTaskFailedResponse { -} +message RespondWorkflowTaskFailedResponse {} message IsWorkflowTaskValidRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - temporal.server.api.clock.v1.VectorClock clock = 3; - int64 scheduled_event_id = 4; - int32 stamp = 5; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + temporal.server.api.clock.v1.VectorClock clock = 3; + int64 scheduled_event_id = 4; + int32 stamp = 5; } message IsWorkflowTaskValidResponse { - // whether matching service can call history service to start the workflow task - bool is_valid = 1; + // whether matching service can call history service to start the workflow task + bool is_valid = 1; } message RecordActivityTaskHeartbeatRequest { - option (routing).task_token = "heartbeat_request.task_token"; + option (routing).task_token = "heartbeat_request.task_token"; - string namespace_id = 1; - temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest heartbeat_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest heartbeat_request = 2; } message RecordActivityTaskHeartbeatResponse { - bool cancel_requested = 1; - bool activity_paused = 2; - bool activity_reset = 3; + bool cancel_requested = 1; + bool activity_paused = 2; + bool activity_reset = 3; } message RespondActivityTaskCompletedRequest { - option (routing).task_token = "complete_request.task_token"; + option (routing).task_token = "complete_request.task_token"; - string namespace_id = 1; - temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest complete_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.RespondActivityTaskCompletedRequest complete_request = 2; } -message RespondActivityTaskCompletedResponse { -} +message RespondActivityTaskCompletedResponse {} message RespondActivityTaskFailedRequest { - option (routing).task_token = "failed_request.task_token"; + option (routing).task_token = "failed_request.task_token"; - string namespace_id = 1; - temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest failed_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.RespondActivityTaskFailedRequest failed_request = 2; } -message RespondActivityTaskFailedResponse { -} +message RespondActivityTaskFailedResponse {} message RespondActivityTaskCanceledRequest { - option (routing).task_token = "cancel_request.task_token"; + option (routing).task_token = "cancel_request.task_token"; - string namespace_id = 1; - temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest cancel_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest cancel_request = 2; } -message RespondActivityTaskCanceledResponse { -} +message RespondActivityTaskCanceledResponse {} message IsActivityTaskValidRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - temporal.server.api.clock.v1.VectorClock clock = 3; - int64 scheduled_event_id = 4; - // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. - int32 stamp = 5; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + temporal.server.api.clock.v1.VectorClock clock = 3; + int64 scheduled_event_id = 4; + // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. + int32 stamp = 5; } message IsActivityTaskValidResponse { - // whether matching service can call history service to start the activity task - bool is_valid = 1; + // whether matching service can call history service to start the activity task + bool is_valid = 1; } message SignalWorkflowExecutionRequest { - option (routing).workflow_id = "signal_request.workflow_execution.workflow_id"; + option (routing).workflow_id = "signal_request.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest signal_request = 2; - temporal.api.common.v1.WorkflowExecution external_workflow_execution = 3; - bool child_workflow_only = 4; + string namespace_id = 1; + temporal.api.workflowservice.v1.SignalWorkflowExecutionRequest signal_request = 2; + temporal.api.common.v1.WorkflowExecution external_workflow_execution = 3; + bool child_workflow_only = 4; } -message SignalWorkflowExecutionResponse { -} +message SignalWorkflowExecutionResponse {} message SignalWithStartWorkflowExecutionRequest { - option (routing).workflow_id = "signal_with_start_request.workflow_id"; + option (routing).workflow_id = "signal_with_start_request.workflow_id"; - string namespace_id = 1; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "with" is needed here. --) - temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest signal_with_start_request = 2; + string namespace_id = 1; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "with" is needed here. --) + temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest signal_with_start_request = 2; } message SignalWithStartWorkflowExecutionResponse { - string run_id = 1; - bool started = 2; + string run_id = 1; + bool started = 2; } message RemoveSignalMutableStateRequest { - option (routing).workflow_id = "workflow_execution.workflow_id"; + option (routing).workflow_id = "workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - string request_id = 3; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + string request_id = 3; } -message RemoveSignalMutableStateResponse { -} +message RemoveSignalMutableStateResponse {} message TerminateWorkflowExecutionRequest { - option (routing).workflow_id = "terminate_request.workflow_execution.workflow_id"; + option (routing).workflow_id = "terminate_request.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest terminate_request = 2; - temporal.api.common.v1.WorkflowExecution external_workflow_execution = 3; - bool child_workflow_only = 4; + string namespace_id = 1; + temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest terminate_request = 2; + temporal.api.common.v1.WorkflowExecution external_workflow_execution = 3; + bool child_workflow_only = 4; } -message TerminateWorkflowExecutionResponse { -} +message TerminateWorkflowExecutionResponse {} message DeleteWorkflowExecutionRequest { - option (routing).workflow_id = "workflow_execution.workflow_id"; + option (routing).workflow_id = "workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - reserved 3; - bool closed_workflow_only = 4; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + reserved 3; + bool closed_workflow_only = 4; } -message DeleteWorkflowExecutionResponse { -} +message DeleteWorkflowExecutionResponse {} message ResetWorkflowExecutionRequest { - option (routing).workflow_id = "reset_request.workflow_execution.workflow_id"; + option (routing).workflow_id = "reset_request.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest reset_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.ResetWorkflowExecutionRequest reset_request = 2; } message ResetWorkflowExecutionResponse { - string run_id = 1; + string run_id = 1; } message RequestCancelWorkflowExecutionRequest { - option (routing).workflow_id = "cancel_request.workflow_execution.workflow_id"; + option (routing).workflow_id = "cancel_request.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest cancel_request = 2; - int64 external_initiated_event_id = 3; - temporal.api.common.v1.WorkflowExecution external_workflow_execution = 4; - bool child_workflow_only = 5; + string namespace_id = 1; + temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest cancel_request = 2; + int64 external_initiated_event_id = 3; + temporal.api.common.v1.WorkflowExecution external_workflow_execution = 4; + bool child_workflow_only = 5; } -message RequestCancelWorkflowExecutionResponse { -} +message RequestCancelWorkflowExecutionResponse {} message ScheduleWorkflowTaskRequest { - option (routing).workflow_id = "workflow_execution.workflow_id"; + option (routing).workflow_id = "workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - bool is_first_workflow_task = 3; - temporal.server.api.clock.v1.VectorClock child_clock = 4; - temporal.server.api.clock.v1.VectorClock parent_clock = 5; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + bool is_first_workflow_task = 3; + temporal.server.api.clock.v1.VectorClock child_clock = 4; + temporal.server.api.clock.v1.VectorClock parent_clock = 5; } -message ScheduleWorkflowTaskResponse { -} +message ScheduleWorkflowTaskResponse {} message VerifyFirstWorkflowTaskScheduledRequest { - option (routing).workflow_id = "workflow_execution.workflow_id"; + option (routing).workflow_id = "workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - temporal.server.api.clock.v1.VectorClock clock = 3; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + temporal.server.api.clock.v1.VectorClock clock = 3; } -message VerifyFirstWorkflowTaskScheduledResponse { -} +message VerifyFirstWorkflowTaskScheduledResponse {} /** * RecordChildExecutionCompletedRequest is used for reporting the completion of child execution to parent workflow @@ -596,501 +592,485 @@ message VerifyFirstWorkflowTaskScheduledResponse { * child creates multiple runs through ContinueAsNew before finally completing. **/ message RecordChildExecutionCompletedRequest { - option (routing).workflow_id = "parent_execution.workflow_id"; + option (routing).workflow_id = "parent_execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution parent_execution = 2; - int64 parent_initiated_id = 3; - temporal.api.common.v1.WorkflowExecution child_execution = 4; - temporal.api.history.v1.HistoryEvent completion_event = 5; - temporal.server.api.clock.v1.VectorClock clock = 6; - int64 parent_initiated_version = 7; - string child_first_execution_run_id = 8; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution parent_execution = 2; + int64 parent_initiated_id = 3; + temporal.api.common.v1.WorkflowExecution child_execution = 4; + temporal.api.history.v1.HistoryEvent completion_event = 5; + temporal.server.api.clock.v1.VectorClock clock = 6; + int64 parent_initiated_version = 7; + string child_first_execution_run_id = 8; } -message RecordChildExecutionCompletedResponse { -} +message RecordChildExecutionCompletedResponse {} message VerifyChildExecutionCompletionRecordedRequest { - option (routing).workflow_id = "parent_execution.workflow_id"; + option (routing).workflow_id = "parent_execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution parent_execution = 2; - temporal.api.common.v1.WorkflowExecution child_execution = 3; - int64 parent_initiated_id = 4; - int64 parent_initiated_version = 5; - temporal.server.api.clock.v1.VectorClock clock = 6; - bool resend_parent = 7; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution parent_execution = 2; + temporal.api.common.v1.WorkflowExecution child_execution = 3; + int64 parent_initiated_id = 4; + int64 parent_initiated_version = 5; + temporal.server.api.clock.v1.VectorClock clock = 6; + bool resend_parent = 7; } -message VerifyChildExecutionCompletionRecordedResponse { -} +message VerifyChildExecutionCompletionRecordedResponse {} message DescribeWorkflowExecutionRequest { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest request = 2; } message DescribeWorkflowExecutionResponse { - temporal.api.workflow.v1.WorkflowExecutionConfig execution_config = 1; - temporal.api.workflow.v1.WorkflowExecutionInfo workflow_execution_info = 2; - repeated temporal.api.workflow.v1.PendingActivityInfo pending_activities = 3; - repeated temporal.api.workflow.v1.PendingChildExecutionInfo pending_children = 4; - temporal.api.workflow.v1.PendingWorkflowTaskInfo pending_workflow_task = 5; - repeated temporal.api.workflow.v1.CallbackInfo callbacks = 6; - repeated temporal.api.workflow.v1.PendingNexusOperationInfo pending_nexus_operations = 7; - temporal.api.workflow.v1.WorkflowExecutionExtendedInfo workflow_extended_info = 8; + temporal.api.workflow.v1.WorkflowExecutionConfig execution_config = 1; + temporal.api.workflow.v1.WorkflowExecutionInfo workflow_execution_info = 2; + repeated temporal.api.workflow.v1.PendingActivityInfo pending_activities = 3; + repeated temporal.api.workflow.v1.PendingChildExecutionInfo pending_children = 4; + temporal.api.workflow.v1.PendingWorkflowTaskInfo pending_workflow_task = 5; + repeated temporal.api.workflow.v1.CallbackInfo callbacks = 6; + repeated temporal.api.workflow.v1.PendingNexusOperationInfo pending_nexus_operations = 7; + temporal.api.workflow.v1.WorkflowExecutionExtendedInfo workflow_extended_info = 8; } message ReplicateEventsV2Request { - option (routing).workflow_id = "workflow_execution.workflow_id"; + option (routing).workflow_id = "workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - repeated temporal.server.api.history.v1.VersionHistoryItem version_history_items = 3; - temporal.api.common.v1.DataBlob events = 4; - // New run events does not need version history since there is no prior events. - temporal.api.common.v1.DataBlob new_run_events = 5; - temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 6; - string new_run_id = 7; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + repeated temporal.server.api.history.v1.VersionHistoryItem version_history_items = 3; + temporal.api.common.v1.DataBlob events = 4; + // New run events does not need version history since there is no prior events. + temporal.api.common.v1.DataBlob new_run_events = 5; + temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 6; + string new_run_id = 7; } -message ReplicateEventsV2Response { -} +message ReplicateEventsV2Response {} message ReplicateWorkflowStateRequest { - option (routing).workflow_id = "workflow_state.execution_info.workflow_id"; + option (routing).workflow_id = "workflow_state.execution_info.workflow_id"; - temporal.server.api.persistence.v1.WorkflowMutableState workflow_state = 1; - string remote_cluster = 2; - string namespace_id= 3; - bool is_force_replication = 4; - bool is_close_transfer_task_acked = 5; + temporal.server.api.persistence.v1.WorkflowMutableState workflow_state = 1; + string remote_cluster = 2; + string namespace_id = 3; + bool is_force_replication = 4; + bool is_close_transfer_task_acked = 5; } -message ReplicateWorkflowStateResponse { -} +message ReplicateWorkflowStateResponse {} message SyncShardStatusRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - string source_cluster = 1; - int32 shard_id = 2; - google.protobuf.Timestamp status_time = 3; + string source_cluster = 1; + int32 shard_id = 2; + google.protobuf.Timestamp status_time = 3; } -message SyncShardStatusResponse { -} +message SyncShardStatusResponse {} message SyncActivityRequest { - option (routing).workflow_id = "workflow_id"; - - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - int64 version = 4; - int64 scheduled_event_id = 5; - google.protobuf.Timestamp scheduled_time = 6; - int64 started_event_id = 7; - google.protobuf.Timestamp started_time = 8; - google.protobuf.Timestamp last_heartbeat_time = 9; - temporal.api.common.v1.Payloads details = 10; - int32 attempt = 11; - temporal.api.failure.v1.Failure last_failure = 12; - string last_worker_identity = 13; - temporal.server.api.history.v1.VersionHistory version_history = 14; - temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 15; - // build ID of the worker who received this activity last time - string last_started_build_id = 16; - // workflows redirect_counter value when this activity started last time - int64 last_started_redirect_counter = 17; - - // The first time the activity was scheduled. - google.protobuf.Timestamp first_scheduled_time = 18; - // The last time an activity attempt completion was recorded by the server. - google.protobuf.Timestamp last_attempt_complete_time = 19; - // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. - int32 stamp = 20; - // Indicates if the activity is paused. - bool paused = 21; - - // Retry policy for the activity. - google.protobuf.Duration retry_initial_interval = 22; - google.protobuf.Duration retry_maximum_interval = 23; - int32 retry_maximum_attempts = 24; - double retry_backoff_coefficient = 25; - int64 start_version = 26; + option (routing).workflow_id = "workflow_id"; + + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + int64 version = 4; + int64 scheduled_event_id = 5; + google.protobuf.Timestamp scheduled_time = 6; + int64 started_event_id = 7; + google.protobuf.Timestamp started_time = 8; + google.protobuf.Timestamp last_heartbeat_time = 9; + temporal.api.common.v1.Payloads details = 10; + int32 attempt = 11; + temporal.api.failure.v1.Failure last_failure = 12; + string last_worker_identity = 13; + temporal.server.api.history.v1.VersionHistory version_history = 14; + temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 15; + // build ID of the worker who received this activity last time + string last_started_build_id = 16; + // workflows redirect_counter value when this activity started last time + int64 last_started_redirect_counter = 17; + + // The first time the activity was scheduled. + google.protobuf.Timestamp first_scheduled_time = 18; + // The last time an activity attempt completion was recorded by the server. + google.protobuf.Timestamp last_attempt_complete_time = 19; + // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. + int32 stamp = 20; + // Indicates if the activity is paused. + bool paused = 21; + + // Retry policy for the activity. + google.protobuf.Duration retry_initial_interval = 22; + google.protobuf.Duration retry_maximum_interval = 23; + int32 retry_maximum_attempts = 24; + double retry_backoff_coefficient = 25; + int64 start_version = 26; } message SyncActivitiesRequest { - option (routing).workflow_id = "workflow_id"; + option (routing).workflow_id = "workflow_id"; - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - repeated ActivitySyncInfo activities_info = 4; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + repeated ActivitySyncInfo activities_info = 4; } message ActivitySyncInfo { - int64 version = 1; - int64 scheduled_event_id = 2; - google.protobuf.Timestamp scheduled_time = 3; - int64 started_event_id = 4; - google.protobuf.Timestamp started_time = 5 ; - google.protobuf.Timestamp last_heartbeat_time = 6; - temporal.api.common.v1.Payloads details = 7; - int32 attempt = 8; - temporal.api.failure.v1.Failure last_failure = 9; - string last_worker_identity = 10; - temporal.server.api.history.v1.VersionHistory version_history = 11; - // build ID of the worker who received this activity last time - string last_started_build_id = 12; - // workflows redirect_counter value when this activity started last time - int64 last_started_redirect_counter = 13; - - - // The first time the activity was scheduled. - google.protobuf.Timestamp first_scheduled_time = 18; - // The last time an activity attempt completion was recorded by the server. - google.protobuf.Timestamp last_attempt_complete_time = 19; - // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. - int32 stamp = 20; - // Indicates if the activity is paused. - bool paused = 21; - // Retry policy for the activity. It needs to be replicated now, since the activity properties can be updated. - google.protobuf.Duration retry_initial_interval = 22; - google.protobuf.Duration retry_maximum_interval = 23; - int32 retry_maximum_attempts = 24; - double retry_backoff_coefficient = 25; - int64 start_version = 26; - -} - -message SyncActivityResponse { -} + int64 version = 1; + int64 scheduled_event_id = 2; + google.protobuf.Timestamp scheduled_time = 3; + int64 started_event_id = 4; + google.protobuf.Timestamp started_time = 5; + google.protobuf.Timestamp last_heartbeat_time = 6; + temporal.api.common.v1.Payloads details = 7; + int32 attempt = 8; + temporal.api.failure.v1.Failure last_failure = 9; + string last_worker_identity = 10; + temporal.server.api.history.v1.VersionHistory version_history = 11; + // build ID of the worker who received this activity last time + string last_started_build_id = 12; + // workflows redirect_counter value when this activity started last time + int64 last_started_redirect_counter = 13; + + // The first time the activity was scheduled. + google.protobuf.Timestamp first_scheduled_time = 18; + // The last time an activity attempt completion was recorded by the server. + google.protobuf.Timestamp last_attempt_complete_time = 19; + // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. + int32 stamp = 20; + // Indicates if the activity is paused. + bool paused = 21; + // Retry policy for the activity. It needs to be replicated now, since the activity properties can be updated. + google.protobuf.Duration retry_initial_interval = 22; + google.protobuf.Duration retry_maximum_interval = 23; + int32 retry_maximum_attempts = 24; + double retry_backoff_coefficient = 25; + int64 start_version = 26; +} + +message SyncActivityResponse {} message DescribeMutableStateRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - bool skip_force_reload = 3; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 4; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + bool skip_force_reload = 3; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 4; } message DescribeMutableStateResponse { - // CacheMutableState is only available when mutable state is in cache. - temporal.server.api.persistence.v1.WorkflowMutableState cache_mutable_state = 1; - // DatabaseMutableState is always available, - // but only loaded from database when mutable state is NOT in cache or skip_force_reload is false. - temporal.server.api.persistence.v1.WorkflowMutableState database_mutable_state = 2; + // CacheMutableState is only available when mutable state is in cache. + temporal.server.api.persistence.v1.WorkflowMutableState cache_mutable_state = 1; + // DatabaseMutableState is always available, + // but only loaded from database when mutable state is NOT in cache or skip_force_reload is false. + temporal.server.api.persistence.v1.WorkflowMutableState database_mutable_state = 2; } // At least one of the parameters needs to be provided. message DescribeHistoryHostRequest { - option (routing).custom = true; + option (routing).custom = true; - //ip:port - string host_address = 1; - int32 shard_id = 2; - string namespace_id = 3; - temporal.api.common.v1.WorkflowExecution workflow_execution = 4; + //ip:port + string host_address = 1; + int32 shard_id = 2; + string namespace_id = 3; + temporal.api.common.v1.WorkflowExecution workflow_execution = 4; } message DescribeHistoryHostResponse { - int32 shards_number = 1; - repeated int32 - shard_ids = 2; - temporal.server.api.namespace.v1.NamespaceCacheInfo namespace_cache = 3; - reserved 4; - string address = 5; + int32 shards_number = 1; + repeated int32 shard_ids = 2; + temporal.server.api.namespace.v1.NamespaceCacheInfo namespace_cache = 3; + reserved 4; + string address = 5; } message CloseShardRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - int32 shard_id = 1; + int32 shard_id = 1; } -message CloseShardResponse { -} +message CloseShardResponse {} message GetShardRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - int32 shard_id = 1; + int32 shard_id = 1; } message GetShardResponse { - temporal.server.api.persistence.v1.ShardInfo shard_info = 1; + temporal.server.api.persistence.v1.ShardInfo shard_info = 1; } message RemoveTaskRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - int32 shard_id = 1; - // The task category. See tasks.TaskCategoryRegistry for more. - int32 category = 2; - int64 task_id = 3; - google.protobuf.Timestamp visibility_time = 4; + int32 shard_id = 1; + // The task category. See tasks.TaskCategoryRegistry for more. + int32 category = 2; + int64 task_id = 3; + google.protobuf.Timestamp visibility_time = 4; } -message RemoveTaskResponse { -} +message RemoveTaskResponse {} message GetReplicationMessagesRequest { - option (routing).custom = true; + option (routing).custom = true; - repeated temporal.server.api.replication.v1.ReplicationToken tokens = 1; - string cluster_name = 2; + repeated temporal.server.api.replication.v1.ReplicationToken tokens = 1; + string cluster_name = 2; } message GetReplicationMessagesResponse { - map shard_messages = 1; + map shard_messages = 1; } message GetDLQReplicationMessagesRequest { - option (routing).task_infos = "task_infos"; + option (routing).task_infos = "task_infos"; - repeated temporal.server.api.replication.v1.ReplicationTaskInfo task_infos = 1; + repeated temporal.server.api.replication.v1.ReplicationTaskInfo task_infos = 1; } message GetDLQReplicationMessagesResponse { - repeated temporal.server.api.replication.v1.ReplicationTask replication_tasks = 1; + repeated temporal.server.api.replication.v1.ReplicationTask replication_tasks = 1; } message QueryWorkflowRequest { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.QueryWorkflowRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.QueryWorkflowRequest request = 2; } message QueryWorkflowResponse { - temporal.api.workflowservice.v1.QueryWorkflowResponse response = 1; + temporal.api.workflowservice.v1.QueryWorkflowResponse response = 1; } message ReapplyEventsRequest { - option (routing).workflow_id = "request.workflow_execution.workflow_id"; + option (routing).workflow_id = "request.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.server.api.adminservice.v1.ReapplyEventsRequest request = 2; + string namespace_id = 1; + temporal.server.api.adminservice.v1.ReapplyEventsRequest request = 2; } -message ReapplyEventsResponse { -} +message ReapplyEventsResponse {} message GetDLQMessagesRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - temporal.server.api.enums.v1.DeadLetterQueueType type = 1; - int32 shard_id = 2; - string source_cluster = 3; - int64 inclusive_end_message_id = 4; - int32 maximum_page_size = 5; - bytes next_page_token = 6; + temporal.server.api.enums.v1.DeadLetterQueueType type = 1; + int32 shard_id = 2; + string source_cluster = 3; + int64 inclusive_end_message_id = 4; + int32 maximum_page_size = 5; + bytes next_page_token = 6; } message GetDLQMessagesResponse { - temporal.server.api.enums.v1.DeadLetterQueueType type = 1; - repeated temporal.server.api.replication.v1.ReplicationTask replication_tasks = 2; - bytes next_page_token = 3; - repeated temporal.server.api.replication.v1.ReplicationTaskInfo replication_tasks_info = 4; + temporal.server.api.enums.v1.DeadLetterQueueType type = 1; + repeated temporal.server.api.replication.v1.ReplicationTask replication_tasks = 2; + bytes next_page_token = 3; + repeated temporal.server.api.replication.v1.ReplicationTaskInfo replication_tasks_info = 4; } message PurgeDLQMessagesRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - temporal.server.api.enums.v1.DeadLetterQueueType type = 1; - int32 shard_id = 2; - string source_cluster = 3; - int64 inclusive_end_message_id = 4; + temporal.server.api.enums.v1.DeadLetterQueueType type = 1; + int32 shard_id = 2; + string source_cluster = 3; + int64 inclusive_end_message_id = 4; } -message PurgeDLQMessagesResponse { -} +message PurgeDLQMessagesResponse {} message MergeDLQMessagesRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - temporal.server.api.enums.v1.DeadLetterQueueType type = 1; - int32 shard_id = 2; - string source_cluster = 3; - int64 inclusive_end_message_id = 4; - int32 maximum_page_size = 5; - bytes next_page_token = 6; + temporal.server.api.enums.v1.DeadLetterQueueType type = 1; + int32 shard_id = 2; + string source_cluster = 3; + int64 inclusive_end_message_id = 4; + int32 maximum_page_size = 5; + bytes next_page_token = 6; } message MergeDLQMessagesResponse { - bytes next_page_token = 1; + bytes next_page_token = 1; } message RefreshWorkflowTasksRequest { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 3; - temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest request = 2; + string namespace_id = 1; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 3; + temporal.server.api.adminservice.v1.RefreshWorkflowTasksRequest request = 2; } -message RefreshWorkflowTasksResponse { -} +message RefreshWorkflowTasksResponse {} message GenerateLastHistoryReplicationTasksRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - repeated string target_clusters = 3; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 4; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + repeated string target_clusters = 3; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 4; } message GenerateLastHistoryReplicationTasksResponse { - int64 state_transition_count = 1; - int64 history_length = 2; + int64 state_transition_count = 1; + int64 history_length = 2; } message GetReplicationStatusRequest { - option (routing).custom = true; + option (routing).custom = true; - // Remote cluster names to query for. If omit, will return for all remote clusters. - repeated string remote_clusters = 1; + // Remote cluster names to query for. If omit, will return for all remote clusters. + repeated string remote_clusters = 1; } message GetReplicationStatusResponse { - repeated ShardReplicationStatus shards = 1; + repeated ShardReplicationStatus shards = 1; } message ShardReplicationStatus { - int32 shard_id = 1; - // Max replication task id of current cluster - int64 max_replication_task_id = 2; - // Local time on this shard - google.protobuf.Timestamp shard_local_time = 3; - map remote_clusters = 4; - map handover_namespaces = 5; + int32 shard_id = 1; + // Max replication task id of current cluster + int64 max_replication_task_id = 2; + // Local time on this shard + google.protobuf.Timestamp shard_local_time = 3; + map remote_clusters = 4; + map handover_namespaces = 5; - google.protobuf.Timestamp max_replication_task_visibility_time = 6; + google.protobuf.Timestamp max_replication_task_visibility_time = 6; } message HandoverNamespaceInfo { - // max replication task id when namespace transition to Handover state - int64 handover_replication_task_id = 1; + // max replication task id when namespace transition to Handover state + int64 handover_replication_task_id = 1; } message ShardReplicationStatusPerCluster { - // Acked replication task id - int64 acked_task_id = 1; - // Acked replication task creation time - google.protobuf.Timestamp acked_task_visibility_time = 2; + // Acked replication task id + int64 acked_task_id = 1; + // Acked replication task creation time + google.protobuf.Timestamp acked_task_visibility_time = 2; } message RebuildMutableStateRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; } -message RebuildMutableStateResponse { -} +message RebuildMutableStateResponse {} message ImportWorkflowExecutionRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - repeated temporal.api.common.v1.DataBlob history_batches = 3; - temporal.server.api.history.v1.VersionHistory version_history = 4; - bytes token = 5; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + repeated temporal.api.common.v1.DataBlob history_batches = 3; + temporal.server.api.history.v1.VersionHistory version_history = 4; + bytes token = 5; } message ImportWorkflowExecutionResponse { - bytes token = 1; - bool events_applied = 2; + bytes token = 1; + bool events_applied = 2; } message DeleteWorkflowVisibilityRecordRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - google.protobuf.Timestamp workflow_start_time = 3; - google.protobuf.Timestamp workflow_close_time = 4; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + google.protobuf.Timestamp workflow_start_time = 3; + google.protobuf.Timestamp workflow_close_time = 4; } -message DeleteWorkflowVisibilityRecordResponse { -} +message DeleteWorkflowVisibilityRecordResponse {} // (-- api-linter: core::0134=disabled // aip.dev/not-precedent: This service does not follow the update method AIP --) message UpdateWorkflowExecutionRequest { - option (routing).workflow_id = "request.workflow_execution.workflow_id"; + option (routing).workflow_id = "request.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest request = 2; } message UpdateWorkflowExecutionResponse { - temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse response = 1; + temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse response = 1; } message StreamWorkflowReplicationMessagesRequest { - option (routing).custom = true; + option (routing).custom = true; - oneof attributes { - temporal.server.api.replication.v1.SyncReplicationState sync_replication_state = 1; - } + oneof attributes { + temporal.server.api.replication.v1.SyncReplicationState sync_replication_state = 1; + } } message StreamWorkflowReplicationMessagesResponse { - oneof attributes { - temporal.server.api.replication.v1.WorkflowReplicationMessages messages = 1; - } + oneof attributes { + temporal.server.api.replication.v1.WorkflowReplicationMessages messages = 1; + } } message PollWorkflowExecutionUpdateRequest { - option (routing).workflow_id = "request.update_ref.workflow_execution.workflow_id"; + option (routing).workflow_id = "request.update_ref.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest request = 2; } message PollWorkflowExecutionUpdateResponse { - temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse response = 1; + temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse response = 1; } message GetWorkflowExecutionHistoryRequest { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest request = 2; } message GetWorkflowExecutionHistoryResponse { - temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse response = 1; - temporal.api.history.v1.History history = 2; + temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse response = 1; + temporal.api.history.v1.History history = 2; } // This message must be wire compatible with GetWorkflowExecutionHistoryResponse. message GetWorkflowExecutionHistoryResponseWithRaw { - temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse response = 1; - repeated bytes history = 2; + temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse response = 1; + repeated bytes history = 2; } message GetWorkflowExecutionHistoryReverseRequest { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest request = 2; } message GetWorkflowExecutionHistoryReverseResponse { - temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse response = 1; + temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse response = 1; } /** @@ -1098,330 +1078,328 @@ message GetWorkflowExecutionHistoryReverseResponse { * EndEventId and EndEventVersion defines the end of the event to fetch. The end event is exclusive. **/ message GetWorkflowExecutionRawHistoryV2Request { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request request = 2; + string namespace_id = 1; + temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Request request = 2; } message GetWorkflowExecutionRawHistoryV2Response { - temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response response = 1; + temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryV2Response response = 1; } message GetWorkflowExecutionRawHistoryRequest { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest request = 2; + string namespace_id = 1; + temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryRequest request = 2; } message GetWorkflowExecutionRawHistoryResponse { - temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse response = 1; + temporal.server.api.adminservice.v1.GetWorkflowExecutionRawHistoryResponse response = 1; } message ForceDeleteWorkflowExecutionRequest { - option (routing).workflow_id = "request.execution.workflow_id"; + option (routing).workflow_id = "request.execution.workflow_id"; - string namespace_id = 1; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 3; - temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest request = 2; + string namespace_id = 1; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 3; + temporal.server.api.adminservice.v1.DeleteWorkflowExecutionRequest request = 2; } message ForceDeleteWorkflowExecutionResponse { - temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse response = 1; + temporal.server.api.adminservice.v1.DeleteWorkflowExecutionResponse response = 1; } message GetDLQTasksRequest { - option (routing).any_host = true; + option (routing).any_host = true; - temporal.server.api.common.v1.HistoryDLQKey dlq_key = 1; - // page_size must be positive. Up to this many tasks will be returned. - int32 page_size = 2; - bytes next_page_token = 3; + temporal.server.api.common.v1.HistoryDLQKey dlq_key = 1; + // page_size must be positive. Up to this many tasks will be returned. + int32 page_size = 2; + bytes next_page_token = 3; } message GetDLQTasksResponse { - repeated temporal.server.api.common.v1.HistoryDLQTask dlq_tasks = 1; - // next_page_token is empty if there are no more results. However, the converse is not true. If there are no more - // results, this field may still be non-empty. This is to avoid having to do a count query to determine whether - // there are more results. - bytes next_page_token = 2; + repeated temporal.server.api.common.v1.HistoryDLQTask dlq_tasks = 1; + // next_page_token is empty if there are no more results. However, the converse is not true. If there are no more + // results, this field may still be non-empty. This is to avoid having to do a count query to determine whether + // there are more results. + bytes next_page_token = 2; } message DeleteDLQTasksRequest { - option (routing).any_host = true; + option (routing).any_host = true; - temporal.server.api.common.v1.HistoryDLQKey dlq_key = 1; - temporal.server.api.common.v1.HistoryDLQTaskMetadata inclusive_max_task_metadata = 2; + temporal.server.api.common.v1.HistoryDLQKey dlq_key = 1; + temporal.server.api.common.v1.HistoryDLQTaskMetadata inclusive_max_task_metadata = 2; } message DeleteDLQTasksResponse { - // messages_deleted is the total number of messages deleted in DeleteDLQTasks operation. - int64 messages_deleted = 1; + // messages_deleted is the total number of messages deleted in DeleteDLQTasks operation. + int64 messages_deleted = 1; } message ListQueuesRequest { - option (routing).any_host = true; + option (routing).any_host = true; - int32 queue_type = 1; - int32 page_size = 2; - bytes next_page_token = 3; + int32 queue_type = 1; + int32 page_size = 2; + bytes next_page_token = 3; } message ListQueuesResponse { - message QueueInfo { - string queue_name = 1; - int64 message_count = 2; - int64 last_message_id = 3; - } - repeated QueueInfo queues = 1; - bytes next_page_token = 2; + message QueueInfo { + string queue_name = 1; + int64 message_count = 2; + int64 last_message_id = 3; + } + repeated QueueInfo queues = 1; + bytes next_page_token = 2; } message AddTasksRequest { - option (routing).shard_id = "shard_id"; - - // Even though we can obtain the shard ID from the tasks, we still need the shard_id in the request for routing. If - // not, it would be possible to include tasks for shards that belong to different hosts, and we'd need to fan-out the - // request, which would be more complicated. - int32 shard_id = 1; - - message Task { - // category_id is needed to deserialize the tasks. See TaskCategory for a list of options here. However, keep in mind - // that the list of valid options is registered dynamically with the server in the history/tasks package, so that - // enum is not comprehensive. - int32 category_id = 1; - // blob is the serialized task. - temporal.api.common.v1.DataBlob blob = 2; - } + option (routing).shard_id = "shard_id"; - // A list of tasks to enqueue or re-enqueue. - repeated Task tasks = 2; + // Even though we can obtain the shard ID from the tasks, we still need the shard_id in the request for routing. If + // not, it would be possible to include tasks for shards that belong to different hosts, and we'd need to fan-out the + // request, which would be more complicated. + int32 shard_id = 1; + + message Task { + // category_id is needed to deserialize the tasks. See TaskCategory for a list of options here. However, keep in mind + // that the list of valid options is registered dynamically with the server in the history/tasks package, so that + // enum is not comprehensive. + int32 category_id = 1; + // blob is the serialized task. + temporal.api.common.v1.DataBlob blob = 2; + } + + // A list of tasks to enqueue or re-enqueue. + repeated Task tasks = 2; } message AddTasksResponse {} message ListTasksRequest { - option (routing).shard_id = "request.shard_id"; + option (routing).shard_id = "request.shard_id"; - temporal.server.api.adminservice.v1.ListHistoryTasksRequest request = 1; + temporal.server.api.adminservice.v1.ListHistoryTasksRequest request = 1; } message ListTasksResponse { - temporal.server.api.adminservice.v1.ListHistoryTasksResponse response = 1; + temporal.server.api.adminservice.v1.ListHistoryTasksResponse response = 1; } message CompleteNexusOperationChasmRequest { - option (routing).chasm_component_ref = "completion.component_ref"; - - // Completion token - holds information for locating an entity and the corresponding component. - temporal.server.api.token.v1.NexusOperationCompletion completion = 1; - oneof outcome { - // Result of a successful operation, only set if state == successful. - temporal.api.common.v1.Payload success = 2; - // Operation failure, only set if state != successful. - temporal.api.failure.v1.Failure failure = 3; - } - // Time when the operation was closed. - google.protobuf.Timestamp close_time = 4; + option (routing).chasm_component_ref = "completion.component_ref"; + + // Completion token - holds information for locating an entity and the corresponding component. + temporal.server.api.token.v1.NexusOperationCompletion completion = 1; + oneof outcome { + // Result of a successful operation, only set if state == successful. + temporal.api.common.v1.Payload success = 2; + // Operation failure, only set if state != successful. + temporal.api.failure.v1.Failure failure = 3; + } + // Time when the operation was closed. + google.protobuf.Timestamp close_time = 4; } message CompleteNexusOperationChasmResponse {} message CompleteNexusOperationRequest { - option (routing) = { - namespace_id: "completion.namespace_id" - workflow_id: "completion.workflow_id" - }; - - // Completion token - holds information for locating a run and the corresponding operation state machine. - temporal.server.api.token.v1.NexusOperationCompletion completion = 1; - // Operation state - may only be successful / failed / canceled. - string state = 2; - oneof outcome { - // Result of a successful operation, only set if state == successful. - temporal.api.common.v1.Payload success = 3; - // Operation failure, only set if state != successful. - temporal.api.nexus.v1.Failure failure = 4; - } - // Operation token - used when the completion is received before the started response. - string operation_token = 5; - // Time the operation was started. Used when completion is received before the started response. - google.protobuf.Timestamp start_time = 6; - // Links to be attached to a fabricated start event if completion is received before started response. - repeated temporal.api.common.v1.Link links = 7; -} - -message CompleteNexusOperationResponse { -} + option (routing) = { + namespace_id: "completion.namespace_id" + workflow_id: "completion.workflow_id" + }; + + // Completion token - holds information for locating a run and the corresponding operation state machine. + temporal.server.api.token.v1.NexusOperationCompletion completion = 1; + // Operation state - may only be successful / failed / canceled. + string state = 2; + oneof outcome { + // Result of a successful operation, only set if state == successful. + temporal.api.common.v1.Payload success = 3; + // Operation failure, only set if state != successful. + temporal.api.nexus.v1.Failure failure = 4; + } + // Operation token - used when the completion is received before the started response. + string operation_token = 5; + // Time the operation was started. Used when completion is received before the started response. + google.protobuf.Timestamp start_time = 6; + // Links to be attached to a fabricated start event if completion is received before started response. + repeated temporal.api.common.v1.Link links = 7; +} + +message CompleteNexusOperationResponse {} message InvokeStateMachineMethodRequest { - option (routing).workflow_id = "workflow_id"; + option (routing).workflow_id = "workflow_id"; - // TODO(Tianyu): This is the same as NexusOperationsCompletion but obviously is not about Nexus. This is because - // State machine signaling is a generalization of the Nexus mechanisms. Perhaps eventually they should be merged. - // Namespace UUID. - string namespace_id = 1; - // Workflow ID. - string workflow_id = 2; - // Run ID at the time this token was generated. - string run_id = 3; - // Reference including the path to the backing Operation state machine and a version + transition count for - // staleness checks. - temporal.server.api.persistence.v1.StateMachineRef ref = 4; + // TODO(Tianyu): This is the same as NexusOperationsCompletion but obviously is not about Nexus. This is because + // State machine signaling is a generalization of the Nexus mechanisms. Perhaps eventually they should be merged. + // Namespace UUID. + string namespace_id = 1; + // Workflow ID. + string workflow_id = 2; + // Run ID at the time this token was generated. + string run_id = 3; + // Reference including the path to the backing Operation state machine and a version + transition count for + // staleness checks. + temporal.server.api.persistence.v1.StateMachineRef ref = 4; - // The method name to invoke. Methods must be explicitly registered for the target state machine in the state - // machine registry, and accept an argument type of HistoryEvent that is the completion event of the completed - // workflow. - string method_name = 5; + // The method name to invoke. Methods must be explicitly registered for the target state machine in the state + // machine registry, and accept an argument type of HistoryEvent that is the completion event of the completed + // workflow. + string method_name = 5; - // Input, in serialized bytes, to the method. Users specify a deserializer during method registration for each state machine. - bytes input = 6; + // Input, in serialized bytes, to the method. Users specify a deserializer during method registration for each state machine. + bytes input = 6; } message InvokeStateMachineMethodResponse { - // Output, in serialized bytes, of the method. Users specify a serializer during method registration for each state machine. - bytes output = 1; + // Output, in serialized bytes, of the method. Users specify a serializer during method registration for each state machine. + bytes output = 1; } message DeepHealthCheckRequest { - option (routing).custom = true; + option (routing).custom = true; - string host_address = 1; + string host_address = 1; } message DeepHealthCheckResponse { - temporal.server.api.enums.v1.HealthState state = 1; + temporal.server.api.enums.v1.HealthState state = 1; + // Per-check diagnostic results. Populated for all checks regardless of state. + repeated temporal.server.api.health.v1.HealthCheck checks = 2; } message SyncWorkflowStateRequest { - option (routing).workflow_id = "execution.workflow_id"; + option (routing).workflow_id = "execution.workflow_id"; - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 3; - temporal.server.api.history.v1.VersionHistories version_histories = 4; - int32 target_cluster_id = 5; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 6; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 3; + temporal.server.api.history.v1.VersionHistories version_histories = 4; + int32 target_cluster_id = 5; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 6; } message SyncWorkflowStateResponse { - reserved 1; - reserved 2; - reserved 3; - reserved 4; - replication.v1.VersionedTransitionArtifact versioned_transition_artifact = 5; + reserved 1; + reserved 2; + reserved 3; + reserved 4; + replication.v1.VersionedTransitionArtifact versioned_transition_artifact = 5; } // (-- api-linter: core::0134::request-mask-required=disabled // (-- api-linter: core::0134::request-resource-required=disabled message UpdateActivityOptionsRequest { - option (routing).workflow_id = "update_request.execution.workflow_id"; + option (routing).workflow_id = "update_request.execution.workflow_id"; - // Namespace ID of the workflow which scheduled this activity - string namespace_id = 1; + // Namespace ID of the workflow which scheduled this activity + string namespace_id = 1; - temporal.api.workflowservice.v1.UpdateActivityOptionsRequest update_request = 2; + temporal.api.workflowservice.v1.UpdateActivityOptionsRequest update_request = 2; } message UpdateActivityOptionsResponse { - // Activity options after an update - temporal.api.activity.v1.ActivityOptions activity_options = 1; + // Activity options after an update + temporal.api.activity.v1.ActivityOptions activity_options = 1; } message PauseActivityRequest { - option (routing).workflow_id = "frontend_request.execution.workflow_id"; + option (routing).workflow_id = "frontend_request.execution.workflow_id"; - // Namespace ID of the workflow which scheduled this activity - string namespace_id = 1; + // Namespace ID of the workflow which scheduled this activity + string namespace_id = 1; - temporal.api.workflowservice.v1.PauseActivityRequest frontend_request = 2; + temporal.api.workflowservice.v1.PauseActivityRequest frontend_request = 2; } -message PauseActivityResponse { -} +message PauseActivityResponse {} message UnpauseActivityRequest { - option (routing).workflow_id = "frontend_request.execution.workflow_id"; + option (routing).workflow_id = "frontend_request.execution.workflow_id"; - // Namespace ID of the workflow which scheduled this activity - string namespace_id = 1; + // Namespace ID of the workflow which scheduled this activity + string namespace_id = 1; - temporal.api.workflowservice.v1.UnpauseActivityRequest frontend_request = 2; + temporal.api.workflowservice.v1.UnpauseActivityRequest frontend_request = 2; } -message UnpauseActivityResponse { -} +message UnpauseActivityResponse {} message ResetActivityRequest { - option (routing).workflow_id = "frontend_request.execution.workflow_id"; + option (routing).workflow_id = "frontend_request.execution.workflow_id"; - // Namespace ID of the workflow which scheduled this activity - string namespace_id = 1; + // Namespace ID of the workflow which scheduled this activity + string namespace_id = 1; - temporal.api.workflowservice.v1.ResetActivityRequest frontend_request = 2; + temporal.api.workflowservice.v1.ResetActivityRequest frontend_request = 2; } -message ResetActivityResponse { -} +message ResetActivityResponse {} // (-- api-linter: core::0134::request-mask-required=disabled // (-- api-linter: core::0134::request-resource-required=disabled message UpdateWorkflowExecutionOptionsRequest { - option (routing).workflow_id = "update_request.workflow_execution.workflow_id"; + option (routing).workflow_id = "update_request.workflow_execution.workflow_id"; - string namespace_id = 1; - temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest update_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.UpdateWorkflowExecutionOptionsRequest update_request = 2; } message UpdateWorkflowExecutionOptionsResponse { - // Workflow Execution options after update. - temporal.api.workflow.v1.WorkflowExecutionOptions workflow_execution_options = 1; + // Workflow Execution options after update. + temporal.api.workflow.v1.WorkflowExecutionOptions workflow_execution_options = 1; } message PauseWorkflowExecutionRequest { - option (routing).workflow_id = "pause_request.workflow_id"; + option (routing).workflow_id = "pause_request.workflow_id"; - // Namespace ID of the workflow which is being paused - string namespace_id = 1; + // Namespace ID of the workflow which is being paused + string namespace_id = 1; - temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest pause_request = 2; + temporal.api.workflowservice.v1.PauseWorkflowExecutionRequest pause_request = 2; } -message PauseWorkflowExecutionResponse { } +message PauseWorkflowExecutionResponse {} message UnpauseWorkflowExecutionRequest { - option (routing).workflow_id = "unpause_request.workflow_id"; + option (routing).workflow_id = "unpause_request.workflow_id"; - // Namespace ID of the workflow which is being unpaused - string namespace_id = 1; + // Namespace ID of the workflow which is being unpaused + string namespace_id = 1; - temporal.api.workflowservice.v1.UnpauseWorkflowExecutionRequest unpause_request = 2; + temporal.api.workflowservice.v1.UnpauseWorkflowExecutionRequest unpause_request = 2; } -message UnpauseWorkflowExecutionResponse { } +message UnpauseWorkflowExecutionResponse {} message StartNexusOperationRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - string namespace_id = 1; - int32 shard_id = 2; - temporal.api.nexus.v1.StartOperationRequest request = 3; + string namespace_id = 1; + int32 shard_id = 2; + temporal.api.nexus.v1.StartOperationRequest request = 3; } message StartNexusOperationResponse { - temporal.api.nexus.v1.StartOperationResponse response = 1; + temporal.api.nexus.v1.StartOperationResponse response = 1; } message CancelNexusOperationRequest { - option (routing).shard_id = "shard_id"; + option (routing).shard_id = "shard_id"; - string namespace_id = 1; - int32 shard_id = 2; - temporal.api.nexus.v1.CancelOperationRequest request = 3; + string namespace_id = 1; + int32 shard_id = 2; + temporal.api.nexus.v1.CancelOperationRequest request = 3; } message CancelNexusOperationResponse { - temporal.api.nexus.v1.CancelOperationResponse response = 1; + temporal.api.nexus.v1.CancelOperationResponse response = 1; } diff --git a/proto/internal/temporal/server/api/historyservice/v1/service.proto b/proto/internal/temporal/server/api/historyservice/v1/service.proto index 994b609ca8d..4dcd9ce0779 100644 --- a/proto/internal/temporal/server/api/historyservice/v1/service.proto +++ b/proto/internal/temporal/server/api/historyservice/v1/service.proto @@ -1,439 +1,517 @@ syntax = "proto3"; package temporal.server.api.historyservice.v1; -option go_package = "go.temporal.io/server/api/historyservice/v1;historyservice"; +import "temporal/server/api/common/v1/api_category.proto"; import "temporal/server/api/historyservice/v1/request_response.proto"; +option go_package = "go.temporal.io/server/api/historyservice/v1;historyservice"; + // HistoryService provides API to start a new long running workflow instance, as well as query and update the history // of workflow instances already created. service HistoryService { - // StartWorkflowExecution starts a new long running workflow instance. It will create the instance with - // 'WorkflowExecutionStarted' event in history and also schedule the first WorkflowTask for the worker to produce the - // initial list of commands for this instance. It will return 'WorkflowExecutionAlreadyStartedError', if an instance already - // exists with same workflowId. - rpc StartWorkflowExecution (StartWorkflowExecutionRequest) returns (StartWorkflowExecutionResponse) { - } - - // Returns the information from mutable state of workflow execution. - // It fails with 'EntityNotExistError' if specified workflow execution in unknown to the service. - // It returns CurrentBranchChangedError if the workflow version branch has changed. - rpc GetMutableState (GetMutableStateRequest) returns (GetMutableStateResponse) { - } - - // Returns the information from mutable state of workflow execution. - // It fails with 'EntityNotExistError' if specified workflow execution in unknown to the service. - // It returns CurrentBranchChangedError if the workflow version branch has changed. - rpc PollMutableState (PollMutableStateRequest) returns (PollMutableStateResponse) { - } - - // Reset the sticky task queue related information in mutable state of a given workflow. - // Things cleared are: - // 1. StickyTaskQueue - // 2. StickyScheduleToStartTimeout - rpc ResetStickyTaskQueue (ResetStickyTaskQueueRequest) returns (ResetStickyTaskQueueResponse) { - } - - // RecordWorkflowTaskStarted is called by the Matchingservice before it hands a workflow task to the application worker in response to - // a PollWorkflowTaskQueue call. It records in the history the event that the workflow task has started. It will return 'TaskAlreadyStartedError', - // if the workflow's execution history already includes a record of the event starting. - rpc RecordWorkflowTaskStarted (RecordWorkflowTaskStartedRequest) returns (RecordWorkflowTaskStartedResponse) { - } - - // RecordActivityTaskStarted is called by the Matchingservice before it hands a workflow task to the application worker in response to - // a PollActivityTaskQueue call. It records in the history the event that the workflow task has started. It will return 'TaskAlreadyStartedError', - // if the workflow's execution history already includes a record of the event starting. - rpc RecordActivityTaskStarted (RecordActivityTaskStartedRequest) returns (RecordActivityTaskStartedResponse) { - } - - // RespondWorkflowTaskCompleted is called by application worker to complete a WorkflowTask handed as a result of - // 'PollWorkflowTaskQueue' API call. Completing a WorkflowTask will result in new result in new commands for the - // workflow execution and potentially new ActivityTasks created for correspondent commands. It will also create a - // WorkflowTaskCompleted event in the history for that session. Use the 'taskToken' provided as response of - // PollWorkflowTaskQueue API call for completing the WorkflowTask. - rpc RespondWorkflowTaskCompleted (RespondWorkflowTaskCompletedRequest) returns (RespondWorkflowTaskCompletedResponse) { - } - - // RespondWorkflowTaskFailed is called by application worker to indicate failure. This results in - // WorkflowTaskFailedEvent written to the history and a new WorkflowTask created. This API can be used by client to - // either clear sticky task queue or report ny panics during WorkflowTask processing. - rpc RespondWorkflowTaskFailed (RespondWorkflowTaskFailedRequest) returns (RespondWorkflowTaskFailedResponse) { - } - - // IsWorkflowTaskValid is called by matching service checking whether the workflow task is valid. - rpc IsWorkflowTaskValid (IsWorkflowTaskValidRequest) returns (IsWorkflowTaskValidResponse) { - } - - // RecordActivityTaskHeartbeat is called by application worker while it is processing an ActivityTask. If worker fails - // to heartbeat within 'heartbeatTimeoutSeconds' interval for the ActivityTask, then it will be marked as timedout and - // 'ActivityTaskTimedOut' event will be written to the workflow history. Calling 'RecordActivityTaskHeartbeat' will - // fail with 'EntityNotExistsError' in such situations. Use the 'taskToken' provided as response of - // PollActivityTaskQueue API call for heartbeating. - rpc RecordActivityTaskHeartbeat (RecordActivityTaskHeartbeatRequest) returns (RecordActivityTaskHeartbeatResponse) { - } - - // RespondActivityTaskCompleted is called by application worker when it is done processing an ActivityTask. It will - // result in a new 'ActivityTaskCompleted' event being written to the workflow history and a new WorkflowTask - // created for the workflow so new commands could be made. Use the 'taskToken' provided as response of - // PollActivityTaskQueue API call for completion. It fails with 'EntityNotExistsError' if the taskToken is not valid - // anymore due to activity timeout. - rpc RespondActivityTaskCompleted (RespondActivityTaskCompletedRequest) returns (RespondActivityTaskCompletedResponse) { - } - - // RespondActivityTaskFailed is called by application worker when it is done processing an ActivityTask. It will - // result in a new 'ActivityTaskFailed' event being written to the workflow history and a new WorkflowTask - // created for the workflow instance so new commands could be made. Use the 'taskToken' provided as response of - // PollActivityTaskQueue API call for completion. It fails with 'EntityNotExistsError' if the taskToken is not valid - // anymore due to activity timeout. - rpc RespondActivityTaskFailed (RespondActivityTaskFailedRequest) returns (RespondActivityTaskFailedResponse) { - } - - // RespondActivityTaskCanceled is called by application worker when it is successfully canceled an ActivityTask. It will - // result in a new 'ActivityTaskCanceled' event being written to the workflow history and a new WorkflowTask - // created for the workflow instance so new commands could be made. Use the 'taskToken' provided as response of - // PollActivityTaskQueue API call for completion. It fails with 'EntityNotExistsError' if the taskToken is not valid - // anymore due to activity timeout. - rpc RespondActivityTaskCanceled (RespondActivityTaskCanceledRequest) returns (RespondActivityTaskCanceledResponse) { - } - - // IsActivityTaskValid is called by matching service checking whether the workflow task is valid. - rpc IsActivityTaskValid (IsActivityTaskValidRequest) returns (IsActivityTaskValidResponse) { - } - - // SignalWorkflowExecution is used to send a signal event to running workflow execution. This results in - // WorkflowExecutionSignaled event recorded in the history and a workflow task being created for the execution. - rpc SignalWorkflowExecution (SignalWorkflowExecutionRequest) returns (SignalWorkflowExecutionResponse) { - } - - // (-- api-linter: core::0136::prepositions=disabled - // aip.dev/not-precedent: "With" is needed here. --) - // SignalWithStartWorkflowExecution is used to ensure sending a signal event to a workflow execution. - // If workflow is running, this results in WorkflowExecutionSignaled event recorded in the history - // and a workflow task being created for the execution. - // If workflow is not running or not found, it will first try start workflow with given WorkflowIdResuePolicy, - // and record WorkflowExecutionStarted and WorkflowExecutionSignaled event in case of success. - // It will return `WorkflowExecutionAlreadyStartedError` if start workflow failed with given policy. - rpc SignalWithStartWorkflowExecution (SignalWithStartWorkflowExecutionRequest) returns (SignalWithStartWorkflowExecutionResponse) { - } - - // ExecuteMultiOperation executes multiple operations within a single workflow. - rpc ExecuteMultiOperation (ExecuteMultiOperationRequest) returns (ExecuteMultiOperationResponse) { - } - - // RemoveSignalMutableState is used to remove a signal request Id that was previously recorded. This is currently - // used to clean execution info when signal workflow task finished. - rpc RemoveSignalMutableState (RemoveSignalMutableStateRequest) returns (RemoveSignalMutableStateResponse) { - } - - // TerminateWorkflowExecution terminates an existing workflow execution by recording WorkflowExecutionTerminated event - // in the history and immediately terminating the execution instance. - rpc TerminateWorkflowExecution (TerminateWorkflowExecutionRequest) returns (TerminateWorkflowExecutionResponse) { - } - - // DeleteWorkflowExecution asynchronously deletes a specific Workflow Execution (when WorkflowExecution.run_id is - // provided) or the latest Workflow Execution (when WorkflowExecution.run_id is not provided). If the Workflow - // Execution is Running, it will be terminated before deletion. - rpc DeleteWorkflowExecution (DeleteWorkflowExecutionRequest) returns (DeleteWorkflowExecutionResponse) { - } - - // ResetWorkflowExecution reset an existing workflow execution by a firstEventId of a existing event batch - // in the history and immediately terminating the current execution instance. - // After reset, the history will grow from nextFirstEventId. - rpc ResetWorkflowExecution (ResetWorkflowExecutionRequest) returns (ResetWorkflowExecutionResponse) { - } - - // UpdateWorkflowExecutionOptions modifies the options of an existing workflow execution. - // Currently the option that can be updated is setting and unsetting a versioning behavior override. - // (-- api-linter: core::0134::method-signature=disabled - // (-- api-linter: core::0134::response-message-name=disabled - rpc UpdateWorkflowExecutionOptions (UpdateWorkflowExecutionOptionsRequest) returns (UpdateWorkflowExecutionOptionsResponse) { - } - - // RequestCancelWorkflowExecution is called by application worker when it wants to request cancellation of a workflow instance. - // It will result in a new 'WorkflowExecutionCancelRequested' event being written to the workflow history and a new WorkflowTask - // created for the workflow instance so new commands could be made. It fails with 'EntityNotExistsError' if the workflow is not valid - // anymore due to completion or doesn't exist. - rpc RequestCancelWorkflowExecution (RequestCancelWorkflowExecutionRequest) returns (RequestCancelWorkflowExecutionResponse) { - } - - // ScheduleWorkflowTask is used for creating a workflow task for already started workflow execution. This is mainly - // used by transfer queue processor during the processing of StartChildWorkflowExecution task, where it first starts - // child execution without creating the workflow task and then calls this API after updating the mutable state of - // parent execution. - rpc ScheduleWorkflowTask (ScheduleWorkflowTaskRequest) returns (ScheduleWorkflowTaskResponse) { - } - - // VerifyFirstWorkflowTaskScheduled checks if workflow has its first workflow task scheduled. - // This is only used by standby transfer start child workflow task logic to make sure parent workflow has - // scheduled first workflow task in child after recording child started in its mutable state; otherwise, - // during namespace failover, it's possible that none of the clusters will schedule the first workflow task. - // NOTE: This is an experimental API. If later we found there are more verification API and there's a clear pattern - // of how verification is done, we may unify them into one generic verfication API. - rpc VerifyFirstWorkflowTaskScheduled (VerifyFirstWorkflowTaskScheduledRequest) returns (VerifyFirstWorkflowTaskScheduledResponse) { - } - - // RecordChildExecutionCompleted is used for reporting the completion of child workflow execution to parent. - // This is mainly called by transfer queue processor during the processing of DeleteExecution task. - rpc RecordChildExecutionCompleted (RecordChildExecutionCompletedRequest) returns (RecordChildExecutionCompletedResponse) { - } - - // VerifyChildExecutionCompletionRecorded checks if child completion result is recorded in parent workflow. - // This is only used by standby transfer close execution logic to make sure parent workflow has the result - // recorded before completing the task, otherwise during namespace failover, it's possible that none of the - // clusters will record the child result in parent workflow. - // NOTE: This is an experimental API. If later we found there are more verification API and there's a clear pattern - // of how verification is done, we may unify them into one generic verfication API. - rpc VerifyChildExecutionCompletionRecorded (VerifyChildExecutionCompletionRecordedRequest) returns (VerifyChildExecutionCompletionRecordedResponse) { - } - - // DescribeWorkflowExecution returns information about the specified workflow execution. - rpc DescribeWorkflowExecution (DescribeWorkflowExecutionRequest) returns (DescribeWorkflowExecutionResponse) { - } - - // ReplicateEventsV2 replicates workflow history events - rpc ReplicateEventsV2 (ReplicateEventsV2Request) returns (ReplicateEventsV2Response) { - } - - // ReplicateWorkflowState replicates workflow state - rpc ReplicateWorkflowState(ReplicateWorkflowStateRequest) returns (ReplicateWorkflowStateResponse) { - } - - // SyncShardStatus sync the status between shards. - rpc SyncShardStatus (SyncShardStatusRequest) returns (SyncShardStatusResponse) { - } - - // SyncActivity sync the activity status. - rpc SyncActivity (SyncActivityRequest) returns (SyncActivityResponse) { - } - - // DescribeMutableState returns information about the internal states of workflow mutable state. - rpc DescribeMutableState (DescribeMutableStateRequest) returns (DescribeMutableStateResponse) { - } - - // DescribeHistoryHost returns information about the internal states of a history host. - rpc DescribeHistoryHost (DescribeHistoryHostRequest) returns (DescribeHistoryHostResponse) { - } - - // CloseShard close the shard. - rpc CloseShard (CloseShardRequest) returns (CloseShardResponse) { - } - - // GetShard gets the ShardInfo - rpc GetShard (GetShardRequest) returns (GetShardResponse) { - } - - // RemoveTask remove task based on type, taskid, shardid. - rpc RemoveTask (RemoveTaskRequest) returns (RemoveTaskResponse) { - } - - // GetReplicationMessages return replication messages based on the read level - rpc GetReplicationMessages (GetReplicationMessagesRequest) returns (GetReplicationMessagesResponse) { - } - - // GetDLQReplicationMessages return replication messages based on dlq info - rpc GetDLQReplicationMessages(GetDLQReplicationMessagesRequest) returns(GetDLQReplicationMessagesResponse){ - } - - // QueryWorkflow returns query result for a specified workflow execution. - rpc QueryWorkflow (QueryWorkflowRequest) returns (QueryWorkflowResponse) { - } - - // ReapplyEvents applies stale events to the current workflow and current run. - rpc ReapplyEvents (ReapplyEventsRequest) returns (ReapplyEventsResponse) { - } - - // GetDLQMessages returns messages from DLQ. - rpc GetDLQMessages(GetDLQMessagesRequest) returns (GetDLQMessagesResponse) { - } - - // (-- api-linter: core::0165::response-message-name=disabled - // aip.dev/not-precedent: --) - // PurgeDLQMessages purges messages from DLQ. - rpc PurgeDLQMessages(PurgeDLQMessagesRequest) returns (PurgeDLQMessagesResponse) { - } - - // MergeDLQMessages merges messages from DLQ. - rpc MergeDLQMessages(MergeDLQMessagesRequest) returns (MergeDLQMessagesResponse) { - } - - // RefreshWorkflowTasks refreshes all tasks of a workflow. - rpc RefreshWorkflowTasks(RefreshWorkflowTasksRequest) returns (RefreshWorkflowTasksResponse) { - } - - // GenerateLastHistoryReplicationTasks generate a replication task for last history event for requested workflow execution - rpc GenerateLastHistoryReplicationTasks(GenerateLastHistoryReplicationTasksRequest) returns (GenerateLastHistoryReplicationTasksResponse) { - } - - rpc GetReplicationStatus(GetReplicationStatusRequest) returns (GetReplicationStatusResponse) { - } - - // RebuildMutableState attempts to rebuild mutable state according to persisted history events. - // NOTE: this is experimental API - rpc RebuildMutableState (RebuildMutableStateRequest) returns (RebuildMutableStateResponse) { - } - - // ImportWorkflowExecution attempts to import workflow according to persisted history events. - // NOTE: this is experimental API - rpc ImportWorkflowExecution (ImportWorkflowExecutionRequest) returns (ImportWorkflowExecutionResponse) { - } - - // DeleteWorkflowVisibilityRecord force delete a workflow's visibility record. - // This is used by admin delete workflow execution API to delete visibility record as frontend - // visibility manager doesn't support write operations - rpc DeleteWorkflowVisibilityRecord (DeleteWorkflowVisibilityRecordRequest) returns (DeleteWorkflowVisibilityRecordResponse) { - } - - // (-- api-linter: core::0134=disabled - // aip.dev/not-precedent: This service does not follow the update method API --) - rpc UpdateWorkflowExecution(UpdateWorkflowExecutionRequest) returns (UpdateWorkflowExecutionResponse) { - } - - // (-- api-linter: core::0134=disabled - // aip.dev/not-precedent: This service does not follow the update method API --) - rpc PollWorkflowExecutionUpdate(PollWorkflowExecutionUpdateRequest) returns (PollWorkflowExecutionUpdateResponse){ - } - - rpc StreamWorkflowReplicationMessages(stream StreamWorkflowReplicationMessagesRequest) returns (stream StreamWorkflowReplicationMessagesResponse) { - } - - rpc GetWorkflowExecutionHistory(GetWorkflowExecutionHistoryRequest) returns (GetWorkflowExecutionHistoryResponse) { - } - - rpc GetWorkflowExecutionHistoryReverse(GetWorkflowExecutionHistoryReverseRequest) returns (GetWorkflowExecutionHistoryReverseResponse) { - } - - rpc GetWorkflowExecutionRawHistoryV2(GetWorkflowExecutionRawHistoryV2Request) returns (GetWorkflowExecutionRawHistoryV2Response) { - } - - rpc GetWorkflowExecutionRawHistory(GetWorkflowExecutionRawHistoryRequest) returns (GetWorkflowExecutionRawHistoryResponse) { - } - - rpc ForceDeleteWorkflowExecution(ForceDeleteWorkflowExecutionRequest) returns (ForceDeleteWorkflowExecutionResponse) { - } - - rpc GetDLQTasks (GetDLQTasksRequest) returns (GetDLQTasksResponse) { - } - - rpc DeleteDLQTasks (DeleteDLQTasksRequest) returns (DeleteDLQTasksResponse) { - } - - rpc ListQueues (ListQueuesRequest) returns (ListQueuesResponse) { - } - - // The AddTasks API is used to add history tasks to a shard. The first use-case for this API is the DLQ. When we are - // unable to process history tasks, we add them to a DLQ. When they need to be retried, we take them out of the DLQ - // and add them back using this API. We expose this via an API instead of doing this in the history engine because - // replication tasks, which are DLQ'd on the target cluster need to be added back to the queue on the source - // cluster, so there is already a network boundary. There is a maximum of 1000 tasks per request. There must be at - // least one task per request. If any task in the list does not have the same shard ID as the request, the request - // will fail with an InvalidArgument error. It is ok to have tasks for different workflow runs as long as they are - // in the same shard. Calls to the persistence API will be batched by workflow run. - rpc AddTasks (AddTasksRequest) returns (AddTasksResponse) { - } - - rpc ListTasks (ListTasksRequest) returns (ListTasksResponse) { - } - - // Complete an async Nexus Operation using a completion token. The completion state could be successful, failed, or - // canceled. - // - // Deprecated. Will be renamed to CompleteNexusOperationHsm in a future release. - rpc CompleteNexusOperation (CompleteNexusOperationRequest) returns (CompleteNexusOperationResponse) { - } - - // Complete an async Nexus Operation using a CHASM reference. The completion - // state could be successful, failed, or canceled. - rpc CompleteNexusOperationChasm (CompleteNexusOperationChasmRequest) returns (CompleteNexusOperationChasmResponse) { - } - - rpc InvokeStateMachineMethod (InvokeStateMachineMethodRequest) returns (InvokeStateMachineMethodResponse) { - } - - // Deep health check history service dependencies health status - rpc DeepHealthCheck (DeepHealthCheckRequest) returns (DeepHealthCheckResponse) { - } - - rpc SyncWorkflowState(SyncWorkflowStateRequest) returns (SyncWorkflowStateResponse) { - } - // UpdateActivityOptions is called by the client to update the options of an activity - // (-- api-linter: core::0134::method-signature=disabled - // (-- api-linter: core::0134::response-message-name=disabled - rpc UpdateActivityOptions (UpdateActivityOptionsRequest) returns (UpdateActivityOptionsResponse) { - } - - // PauseActivity pauses the execution of an activity specified by its ID. - // Returns a `NotFound` error if there is no pending activity with the provided ID. - // - // Pausing an activity means: - // - If the activity is currently waiting for a retry or is running and subsequently fails, - // it will not be rescheduled until it is unpause. - // - If the activity is already paused, calling this method will have no effect. - // - If the activity is running and finishes successfully, the activity will be completed. - // - If the activity is running and finishes with failure: - // * if there is no retry left - the activity will be completed. - // * if there are more retries left - the activity will be paused. - // For long-running activities: - // - activities in paused state will send a cancellation with "activity_paused" set to 'true' in response to 'RecordActivityTaskHeartbeat'. - // - The activity should respond to the cancellation accordingly. - // For long-running activities: - // - activity in paused state will send a cancellation with "activity_paused" set to 'true' in response to 'RecordActivityTaskHeartbeat'. - // - The activity should respond to the cancellation accordingly. - // (-- api-linter: core::0134::method-signature=disabled - // (-- api-linter: core::0134::response-message-name=disabled - rpc PauseActivity (PauseActivityRequest) returns (PauseActivityResponse) { - } - - // UnpauseActivity unpauses the execution of an activity specified by its ID. - // - // If activity is not paused, this call will have no effect. - // If the activity is waiting for retry, it will be scheduled immediately (* see 'jitter' flag). - // Once the activity is unpause, all timeout timers will be regenerated. - // - // Flags: - // 'jitter': the activity will be scheduled at a random time within the jitter duration. - // 'reset_attempts': the number of attempts will be reset. - // 'reset_heartbeat': the activity heartbeat timer and heartbeats will be reset. - // - // Returns a `NotFound` error if there is no pending activity with the provided ID. - // (-- api-linter: core::0134::method-signature=disabled - // (-- api-linter: core::0134::response-message-name=disabled - rpc UnpauseActivity (UnpauseActivityRequest) returns (UnpauseActivityResponse) { - } - - // ResetActivity resets the execution of an activity specified by its ID. - // - // Resetting an activity means: - // * number of attempts will be reset to 0. - // * activity timeouts will be reset. - // * if the activity is waiting for retry, and it is not paused or 'keep_paused' is not provided: - // it will be scheduled immediately (* see 'jitter' flag), - // - // Flags: - // - // 'jitter': the activity will be scheduled at a random time within the jitter duration. - // If the activity currently paused it will be unpause, unless 'keep_paused' flag is provided. - // 'reset_heartbeats': the activity heartbeat timer and heartbeats will be reset. - // 'keep_paused': if the activity is paused, it will remain paused. - // - // Returns a `NotFound` error if there is no pending activity with the provided ID. - // (-- api-linter: core::0134::method-signature=disabled - // (-- api-linter: core::0134::response-message-name=disabled - rpc ResetActivity (ResetActivityRequest) returns (ResetActivityResponse) { - } - - // PauseWorkflowExecution pauses the workflow execution specified in the request. - rpc PauseWorkflowExecution (PauseWorkflowExecutionRequest) returns (PauseWorkflowExecutionResponse) { - } - - // UnpauseWorkflowExecution unpauses the workflow execution specified in the request. - rpc UnpauseWorkflowExecution (UnpauseWorkflowExecutionRequest) returns (UnpauseWorkflowExecutionResponse) { - } - - // StartNexusOperation starts a Nexus operation on the __temporal_system endpoint. - rpc StartNexusOperation (StartNexusOperationRequest) returns (StartNexusOperationResponse) { - } - - // CancelNexusOperation cancels a Nexus operation on the __temporal_system endpoint. - rpc CancelNexusOperation (CancelNexusOperationRequest) returns (CancelNexusOperationResponse) { - } + // StartWorkflowExecution starts a new long running workflow instance. It will create the instance with + // 'WorkflowExecutionStarted' event in history and also schedule the first WorkflowTask for the worker to produce the + // initial list of commands for this instance. It will return 'WorkflowExecutionAlreadyStartedError', if an instance already + // exists with same workflowId. + rpc StartWorkflowExecution(StartWorkflowExecutionRequest) returns (StartWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Returns the information from mutable state of workflow execution. + // It fails with 'EntityNotExistError' if specified workflow execution in unknown to the service. + // It returns CurrentBranchChangedError if the workflow version branch has changed. + rpc GetMutableState(GetMutableStateRequest) returns (GetMutableStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Returns the information from mutable state of workflow execution. + // It fails with 'EntityNotExistError' if specified workflow execution in unknown to the service. + // It returns CurrentBranchChangedError if the workflow version branch has changed. + rpc PollMutableState(PollMutableStateRequest) returns (PollMutableStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // Reset the sticky task queue related information in mutable state of a given workflow. + // Things cleared are: + // 1. StickyTaskQueue + // 2. StickyScheduleToStartTimeout + rpc ResetStickyTaskQueue(ResetStickyTaskQueueRequest) returns (ResetStickyTaskQueueResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RecordWorkflowTaskStarted is called by the Matchingservice before it hands a workflow task to the application worker in response to + // a PollWorkflowTaskQueue call. It records in the history the event that the workflow task has started. It will return 'TaskAlreadyStartedError', + // if the workflow's execution history already includes a record of the event starting. + rpc RecordWorkflowTaskStarted(RecordWorkflowTaskStartedRequest) returns (RecordWorkflowTaskStartedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RecordActivityTaskStarted is called by the Matchingservice before it hands a workflow task to the application worker in response to + // a PollActivityTaskQueue call. It records in the history the event that the workflow task has started. It will return 'TaskAlreadyStartedError', + // if the workflow's execution history already includes a record of the event starting. + rpc RecordActivityTaskStarted(RecordActivityTaskStartedRequest) returns (RecordActivityTaskStartedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RespondWorkflowTaskCompleted is called by application worker to complete a WorkflowTask handed as a result of + // 'PollWorkflowTaskQueue' API call. Completing a WorkflowTask will result in new result in new commands for the + // workflow execution and potentially new ActivityTasks created for correspondent commands. It will also create a + // WorkflowTaskCompleted event in the history for that session. Use the 'taskToken' provided as response of + // PollWorkflowTaskQueue API call for completing the WorkflowTask. + rpc RespondWorkflowTaskCompleted(RespondWorkflowTaskCompletedRequest) returns (RespondWorkflowTaskCompletedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RespondWorkflowTaskFailed is called by application worker to indicate failure. This results in + // WorkflowTaskFailedEvent written to the history and a new WorkflowTask created. This API can be used by client to + // either clear sticky task queue or report ny panics during WorkflowTask processing. + rpc RespondWorkflowTaskFailed(RespondWorkflowTaskFailedRequest) returns (RespondWorkflowTaskFailedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // IsWorkflowTaskValid is called by matching service checking whether the workflow task is valid. + rpc IsWorkflowTaskValid(IsWorkflowTaskValidRequest) returns (IsWorkflowTaskValidResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RecordActivityTaskHeartbeat is called by application worker while it is processing an ActivityTask. If worker fails + // to heartbeat within 'heartbeatTimeoutSeconds' interval for the ActivityTask, then it will be marked as timedout and + // 'ActivityTaskTimedOut' event will be written to the workflow history. Calling 'RecordActivityTaskHeartbeat' will + // fail with 'EntityNotExistsError' in such situations. Use the 'taskToken' provided as response of + // PollActivityTaskQueue API call for heartbeating. + rpc RecordActivityTaskHeartbeat(RecordActivityTaskHeartbeatRequest) returns (RecordActivityTaskHeartbeatResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RespondActivityTaskCompleted is called by application worker when it is done processing an ActivityTask. It will + // result in a new 'ActivityTaskCompleted' event being written to the workflow history and a new WorkflowTask + // created for the workflow so new commands could be made. Use the 'taskToken' provided as response of + // PollActivityTaskQueue API call for completion. It fails with 'EntityNotExistsError' if the taskToken is not valid + // anymore due to activity timeout. + rpc RespondActivityTaskCompleted(RespondActivityTaskCompletedRequest) returns (RespondActivityTaskCompletedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RespondActivityTaskFailed is called by application worker when it is done processing an ActivityTask. It will + // result in a new 'ActivityTaskFailed' event being written to the workflow history and a new WorkflowTask + // created for the workflow instance so new commands could be made. Use the 'taskToken' provided as response of + // PollActivityTaskQueue API call for completion. It fails with 'EntityNotExistsError' if the taskToken is not valid + // anymore due to activity timeout. + rpc RespondActivityTaskFailed(RespondActivityTaskFailedRequest) returns (RespondActivityTaskFailedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RespondActivityTaskCanceled is called by application worker when it is successfully canceled an ActivityTask. It will + // result in a new 'ActivityTaskCanceled' event being written to the workflow history and a new WorkflowTask + // created for the workflow instance so new commands could be made. Use the 'taskToken' provided as response of + // PollActivityTaskQueue API call for completion. It fails with 'EntityNotExistsError' if the taskToken is not valid + // anymore due to activity timeout. + rpc RespondActivityTaskCanceled(RespondActivityTaskCanceledRequest) returns (RespondActivityTaskCanceledResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // IsActivityTaskValid is called by matching service checking whether the workflow task is valid. + rpc IsActivityTaskValid(IsActivityTaskValidRequest) returns (IsActivityTaskValidResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // SignalWorkflowExecution is used to send a signal event to running workflow execution. This results in + // WorkflowExecutionSignaled event recorded in the history and a workflow task being created for the execution. + rpc SignalWorkflowExecution(SignalWorkflowExecutionRequest) returns (SignalWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // (-- api-linter: core::0136::prepositions=disabled + // aip.dev/not-precedent: "With" is needed here. --) + // SignalWithStartWorkflowExecution is used to ensure sending a signal event to a workflow execution. + // If workflow is running, this results in WorkflowExecutionSignaled event recorded in the history + // and a workflow task being created for the execution. + // If workflow is not running or not found, it will first try start workflow with given WorkflowIdResuePolicy, + // and record WorkflowExecutionStarted and WorkflowExecutionSignaled event in case of success. + // It will return `WorkflowExecutionAlreadyStartedError` if start workflow failed with given policy. + rpc SignalWithStartWorkflowExecution(SignalWithStartWorkflowExecutionRequest) returns (SignalWithStartWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ExecuteMultiOperation executes multiple operations within a single workflow. + rpc ExecuteMultiOperation(ExecuteMultiOperationRequest) returns (ExecuteMultiOperationResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // RemoveSignalMutableState is used to remove a signal request Id that was previously recorded. This is currently + // used to clean execution info when signal workflow task finished. + rpc RemoveSignalMutableState(RemoveSignalMutableStateRequest) returns (RemoveSignalMutableStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // TerminateWorkflowExecution terminates an existing workflow execution by recording WorkflowExecutionTerminated event + // in the history and immediately terminating the execution instance. + rpc TerminateWorkflowExecution(TerminateWorkflowExecutionRequest) returns (TerminateWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DeleteWorkflowExecution asynchronously deletes a specific Workflow Execution (when WorkflowExecution.run_id is + // provided) or the latest Workflow Execution (when WorkflowExecution.run_id is not provided). If the Workflow + // Execution is Running, it will be terminated before deletion. + rpc DeleteWorkflowExecution(DeleteWorkflowExecutionRequest) returns (DeleteWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ResetWorkflowExecution reset an existing workflow execution by a firstEventId of a existing event batch + // in the history and immediately terminating the current execution instance. + // After reset, the history will grow from nextFirstEventId. + rpc ResetWorkflowExecution(ResetWorkflowExecutionRequest) returns (ResetWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // UpdateWorkflowExecutionOptions modifies the options of an existing workflow execution. + // Currently the option that can be updated is setting and unsetting a versioning behavior override. + // (-- api-linter: core::0134::method-signature=disabled + // (-- api-linter: core::0134::response-message-name=disabled + rpc UpdateWorkflowExecutionOptions(UpdateWorkflowExecutionOptionsRequest) returns (UpdateWorkflowExecutionOptionsResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RequestCancelWorkflowExecution is called by application worker when it wants to request cancellation of a workflow instance. + // It will result in a new 'WorkflowExecutionCancelRequested' event being written to the workflow history and a new WorkflowTask + // created for the workflow instance so new commands could be made. It fails with 'EntityNotExistsError' if the workflow is not valid + // anymore due to completion or doesn't exist. + rpc RequestCancelWorkflowExecution(RequestCancelWorkflowExecutionRequest) returns (RequestCancelWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ScheduleWorkflowTask is used for creating a workflow task for already started workflow execution. This is mainly + // used by transfer queue processor during the processing of StartChildWorkflowExecution task, where it first starts + // child execution without creating the workflow task and then calls this API after updating the mutable state of + // parent execution. + rpc ScheduleWorkflowTask(ScheduleWorkflowTaskRequest) returns (ScheduleWorkflowTaskResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // VerifyFirstWorkflowTaskScheduled checks if workflow has its first workflow task scheduled. + // This is only used by standby transfer start child workflow task logic to make sure parent workflow has + // scheduled first workflow task in child after recording child started in its mutable state; otherwise, + // during namespace failover, it's possible that none of the clusters will schedule the first workflow task. + // NOTE: This is an experimental API. If later we found there are more verification API and there's a clear pattern + // of how verification is done, we may unify them into one generic verfication API. + rpc VerifyFirstWorkflowTaskScheduled(VerifyFirstWorkflowTaskScheduledRequest) returns (VerifyFirstWorkflowTaskScheduledResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RecordChildExecutionCompleted is used for reporting the completion of child workflow execution to parent. + // This is mainly called by transfer queue processor during the processing of DeleteExecution task. + rpc RecordChildExecutionCompleted(RecordChildExecutionCompletedRequest) returns (RecordChildExecutionCompletedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // VerifyChildExecutionCompletionRecorded checks if child completion result is recorded in parent workflow. + // This is only used by standby transfer close execution logic to make sure parent workflow has the result + // recorded before completing the task, otherwise during namespace failover, it's possible that none of the + // clusters will record the child result in parent workflow. + // NOTE: This is an experimental API. If later we found there are more verification API and there's a clear pattern + // of how verification is done, we may unify them into one generic verfication API. + rpc VerifyChildExecutionCompletionRecorded(VerifyChildExecutionCompletionRecordedRequest) returns (VerifyChildExecutionCompletionRecordedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DescribeWorkflowExecution returns information about the specified workflow execution. + rpc DescribeWorkflowExecution(DescribeWorkflowExecutionRequest) returns (DescribeWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ReplicateEventsV2 replicates workflow history events + rpc ReplicateEventsV2(ReplicateEventsV2Request) returns (ReplicateEventsV2Response) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ReplicateWorkflowState replicates workflow state + rpc ReplicateWorkflowState(ReplicateWorkflowStateRequest) returns (ReplicateWorkflowStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // SyncShardStatus sync the status between shards. + rpc SyncShardStatus(SyncShardStatusRequest) returns (SyncShardStatusResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // SyncActivity sync the activity status. + rpc SyncActivity(SyncActivityRequest) returns (SyncActivityResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DescribeMutableState returns information about the internal states of workflow mutable state. + rpc DescribeMutableState(DescribeMutableStateRequest) returns (DescribeMutableStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DescribeHistoryHost returns information about the internal states of a history host. + rpc DescribeHistoryHost(DescribeHistoryHostRequest) returns (DescribeHistoryHostResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // CloseShard close the shard. + rpc CloseShard(CloseShardRequest) returns (CloseShardResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // GetShard gets the ShardInfo + rpc GetShard(GetShardRequest) returns (GetShardResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RemoveTask remove task based on type, taskid, shardid. + rpc RemoveTask(RemoveTaskRequest) returns (RemoveTaskResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // GetReplicationMessages return replication messages based on the read level + rpc GetReplicationMessages(GetReplicationMessagesRequest) returns (GetReplicationMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // GetDLQReplicationMessages return replication messages based on dlq info + rpc GetDLQReplicationMessages(GetDLQReplicationMessagesRequest) returns (GetDLQReplicationMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // QueryWorkflow returns query result for a specified workflow execution. + rpc QueryWorkflow(QueryWorkflowRequest) returns (QueryWorkflowResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // ReapplyEvents applies stale events to the current workflow and current run. + rpc ReapplyEvents(ReapplyEventsRequest) returns (ReapplyEventsResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // GetDLQMessages returns messages from DLQ. + rpc GetDLQMessages(GetDLQMessagesRequest) returns (GetDLQMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // (-- api-linter: core::0165::response-message-name=disabled + // aip.dev/not-precedent: --) + // PurgeDLQMessages purges messages from DLQ. + rpc PurgeDLQMessages(PurgeDLQMessagesRequest) returns (PurgeDLQMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // MergeDLQMessages merges messages from DLQ. + rpc MergeDLQMessages(MergeDLQMessagesRequest) returns (MergeDLQMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // RefreshWorkflowTasks refreshes all tasks of a workflow. + rpc RefreshWorkflowTasks(RefreshWorkflowTasksRequest) returns (RefreshWorkflowTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // GenerateLastHistoryReplicationTasks generate a replication task for last history event for requested workflow execution + rpc GenerateLastHistoryReplicationTasks(GenerateLastHistoryReplicationTasksRequest) returns (GenerateLastHistoryReplicationTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc GetReplicationStatus(GetReplicationStatusRequest) returns (GetReplicationStatusResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RebuildMutableState attempts to rebuild mutable state according to persisted history events. + // NOTE: this is experimental API + rpc RebuildMutableState(RebuildMutableStateRequest) returns (RebuildMutableStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ImportWorkflowExecution attempts to import workflow according to persisted history events. + // NOTE: this is experimental API + rpc ImportWorkflowExecution(ImportWorkflowExecutionRequest) returns (ImportWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DeleteWorkflowVisibilityRecord force delete a workflow's visibility record. + // This is used by admin delete workflow execution API to delete visibility record as frontend + // visibility manager doesn't support write operations + rpc DeleteWorkflowVisibilityRecord(DeleteWorkflowVisibilityRecordRequest) returns (DeleteWorkflowVisibilityRecordResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + // (-- api-linter: core::0134=disabled + // aip.dev/not-precedent: This service does not follow the update method API --) + rpc UpdateWorkflowExecution(UpdateWorkflowExecutionRequest) returns (UpdateWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // (-- api-linter: core::0134=disabled + // aip.dev/not-precedent: This service does not follow the update method API --) + rpc PollWorkflowExecutionUpdate(PollWorkflowExecutionUpdateRequest) returns (PollWorkflowExecutionUpdateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + rpc StreamWorkflowReplicationMessages(stream StreamWorkflowReplicationMessagesRequest) returns (stream StreamWorkflowReplicationMessagesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc GetWorkflowExecutionHistory(GetWorkflowExecutionHistoryRequest) returns (GetWorkflowExecutionHistoryResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc GetWorkflowExecutionHistoryReverse(GetWorkflowExecutionHistoryReverseRequest) returns (GetWorkflowExecutionHistoryReverseResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc GetWorkflowExecutionRawHistoryV2(GetWorkflowExecutionRawHistoryV2Request) returns (GetWorkflowExecutionRawHistoryV2Response) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc GetWorkflowExecutionRawHistory(GetWorkflowExecutionRawHistoryRequest) returns (GetWorkflowExecutionRawHistoryResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc ForceDeleteWorkflowExecution(ForceDeleteWorkflowExecutionRequest) returns (ForceDeleteWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc GetDLQTasks(GetDLQTasksRequest) returns (GetDLQTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc DeleteDLQTasks(DeleteDLQTasksRequest) returns (DeleteDLQTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc ListQueues(ListQueuesRequest) returns (ListQueuesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // The AddTasks API is used to add history tasks to a shard. The first use-case for this API is the DLQ. When we are + // unable to process history tasks, we add them to a DLQ. When they need to be retried, we take them out of the DLQ + // and add them back using this API. We expose this via an API instead of doing this in the history engine because + // replication tasks, which are DLQ'd on the target cluster need to be added back to the queue on the source + // cluster, so there is already a network boundary. There is a maximum of 1000 tasks per request. There must be at + // least one task per request. If any task in the list does not have the same shard ID as the request, the request + // will fail with an InvalidArgument error. It is ok to have tasks for different workflow runs as long as they are + // in the same shard. Calls to the persistence API will be batched by workflow run. + rpc AddTasks(AddTasksRequest) returns (AddTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc ListTasks(ListTasksRequest) returns (ListTasksResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Complete an async Nexus Operation using a completion token. The completion state could be successful, failed, or + // canceled. + // + // Deprecated. Will be renamed to CompleteNexusOperationHsm in a future release. + rpc CompleteNexusOperation(CompleteNexusOperationRequest) returns (CompleteNexusOperationResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Complete an async Nexus Operation using a CHASM reference. The completion + // state could be successful, failed, or canceled. + rpc CompleteNexusOperationChasm(CompleteNexusOperationChasmRequest) returns (CompleteNexusOperationChasmResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc InvokeStateMachineMethod(InvokeStateMachineMethodRequest) returns (InvokeStateMachineMethodResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Deep health check history service dependencies health status + rpc DeepHealthCheck(DeepHealthCheckRequest) returns (DeepHealthCheckResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_SYSTEM; + } + + rpc SyncWorkflowState(SyncWorkflowStateRequest) returns (SyncWorkflowStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + // UpdateActivityOptions is called by the client to update the options of an activity + // (-- api-linter: core::0134::method-signature=disabled + // (-- api-linter: core::0134::response-message-name=disabled + rpc UpdateActivityOptions(UpdateActivityOptionsRequest) returns (UpdateActivityOptionsResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // PauseActivity pauses the execution of an activity specified by its ID. + // Returns a `NotFound` error if there is no pending activity with the provided ID. + // + // Pausing an activity means: + // - If the activity is currently waiting for a retry or is running and subsequently fails, + // it will not be rescheduled until it is unpause. + // - If the activity is already paused, calling this method will have no effect. + // - If the activity is running and finishes successfully, the activity will be completed. + // - If the activity is running and finishes with failure: + // * if there is no retry left - the activity will be completed. + // * if there are more retries left - the activity will be paused. + // For long-running activities: + // - activities in paused state will send a cancellation with "activity_paused" set to 'true' in response to 'RecordActivityTaskHeartbeat'. + // - The activity should respond to the cancellation accordingly. + // For long-running activities: + // - activity in paused state will send a cancellation with "activity_paused" set to 'true' in response to 'RecordActivityTaskHeartbeat'. + // - The activity should respond to the cancellation accordingly. + // (-- api-linter: core::0134::method-signature=disabled + // (-- api-linter: core::0134::response-message-name=disabled + rpc PauseActivity(PauseActivityRequest) returns (PauseActivityResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // UnpauseActivity unpauses the execution of an activity specified by its ID. + // + // If activity is not paused, this call will have no effect. + // If the activity is waiting for retry, it will be scheduled immediately (* see 'jitter' flag). + // Once the activity is unpause, all timeout timers will be regenerated. + // + // Flags: + // 'jitter': the activity will be scheduled at a random time within the jitter duration. + // 'reset_attempts': the number of attempts will be reset. + // 'reset_heartbeat': the activity heartbeat timer and heartbeats will be reset. + // + // Returns a `NotFound` error if there is no pending activity with the provided ID. + // (-- api-linter: core::0134::method-signature=disabled + // (-- api-linter: core::0134::response-message-name=disabled + rpc UnpauseActivity(UnpauseActivityRequest) returns (UnpauseActivityResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ResetActivity resets the execution of an activity specified by its ID. + // + // Resetting an activity means: + // * number of attempts will be reset to 0. + // * activity timeouts will be reset. + // * if the activity is waiting for retry, and it is not paused or 'keep_paused' is not provided: + // it will be scheduled immediately (* see 'jitter' flag), + // + // Flags: + // + // 'jitter': the activity will be scheduled at a random time within the jitter duration. + // If the activity currently paused it will be unpause, unless 'keep_paused' flag is provided. + // 'reset_heartbeats': the activity heartbeat timer and heartbeats will be reset. + // 'keep_paused': if the activity is paused, it will remain paused. + // + // Returns a `NotFound` error if there is no pending activity with the provided ID. + // (-- api-linter: core::0134::method-signature=disabled + // (-- api-linter: core::0134::response-message-name=disabled + rpc ResetActivity(ResetActivityRequest) returns (ResetActivityResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // PauseWorkflowExecution pauses the workflow execution specified in the request. + rpc PauseWorkflowExecution(PauseWorkflowExecutionRequest) returns (PauseWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // UnpauseWorkflowExecution unpauses the workflow execution specified in the request. + rpc UnpauseWorkflowExecution(UnpauseWorkflowExecutionRequest) returns (UnpauseWorkflowExecutionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // StartNexusOperation starts a Nexus operation on the __temporal_system endpoint. + rpc StartNexusOperation(StartNexusOperationRequest) returns (StartNexusOperationResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // CancelNexusOperation cancels a Nexus operation on the __temporal_system endpoint. + rpc CancelNexusOperation(CancelNexusOperationRequest) returns (CancelNexusOperationResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } } diff --git a/proto/internal/temporal/server/api/matchingservice/v1/request_response.proto b/proto/internal/temporal/server/api/matchingservice/v1/request_response.proto index 08c7ffc2a6c..28bf1593a73 100644 --- a/proto/internal/temporal/server/api/matchingservice/v1/request_response.proto +++ b/proto/internal/temporal/server/api/matchingservice/v1/request_response.proto @@ -1,69 +1,67 @@ syntax = "proto3"; package temporal.server.api.matchingservice.v1; -option go_package = "go.temporal.io/server/api/matchingservice/v1;matchingservice"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; - import "temporal/api/common/v1/message.proto"; import "temporal/api/deployment/v1/message.proto"; import "temporal/api/enums/v1/task_queue.proto"; import "temporal/api/failure/v1/message.proto"; import "temporal/api/history/v1/message.proto"; -import "temporal/api/taskqueue/v1/message.proto"; -import "temporal/api/query/v1/message.proto"; +import "temporal/api/nexus/v1/message.proto"; import "temporal/api/protocol/v1/message.proto"; - +import "temporal/api/query/v1/message.proto"; +import "temporal/api/taskqueue/v1/message.proto"; +import "temporal/api/worker/v1/message.proto"; +import "temporal/api/workflowservice/v1/request_response.proto"; import "temporal/server/api/clock/v1/message.proto"; import "temporal/server/api/deployment/v1/message.proto"; +import "temporal/server/api/enums/v1/fairness_state.proto"; import "temporal/server/api/history/v1/message.proto"; import "temporal/server/api/persistence/v1/nexus.proto"; import "temporal/server/api/persistence/v1/task_queues.proto"; import "temporal/server/api/taskqueue/v1/message.proto"; -import "temporal/server/api/enums/v1/fairness_state.proto"; -import "temporal/api/workflowservice/v1/request_response.proto"; -import "temporal/api/nexus/v1/message.proto"; -import "temporal/api/worker/v1/message.proto"; +option go_package = "go.temporal.io/server/api/matchingservice/v1;matchingservice"; message PollWorkflowTaskQueueRequest { - string namespace_id = 1; - string poller_id = 2; - temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest poll_request = 3; - string forwarded_source = 4; - // Extra conditions on this poll request. Only supported with new matcher. - PollConditions conditions = 5; + string namespace_id = 1; + string poller_id = 2; + temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest poll_request = 3; + string forwarded_source = 4; + // Extra conditions on this poll request. Only supported with new matcher. + PollConditions conditions = 5; } message PollWorkflowTaskQueueResponse { - bytes task_token = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - temporal.api.common.v1.WorkflowType workflow_type = 3; - int64 previous_started_event_id = 4; - int64 started_event_id = 5; - int32 attempt = 6; - int64 next_event_id = 7; - int64 backlog_count_hint = 8; - bool sticky_execution_enabled = 9; - temporal.api.query.v1.WorkflowQuery query = 10; - temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 11; - temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 12; - reserved 13; - bytes branch_token = 14; - google.protobuf.Timestamp scheduled_time = 15; - google.protobuf.Timestamp started_time = 16; - map queries = 17; - repeated temporal.api.protocol.v1.Message messages = 18; - // The history for this workflow, which will either be complete or partial. Partial histories - // are sent to workers who have signaled that they are using a sticky queue when completing - // a workflow task. Sticky query tasks will not include any history. - temporal.api.history.v1.History history = 19; - bytes next_page_token = 20; - temporal.api.taskqueue.v1.PollerScalingDecision poller_scaling_decision = 21; - // Raw history bytes sent from matching service when history.sendRawHistoryBetweenInternalServices is enabled. - // Matching client will deserialize this to History when it receives the response. - temporal.api.history.v1.History raw_history = 22; + bytes task_token = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + temporal.api.common.v1.WorkflowType workflow_type = 3; + int64 previous_started_event_id = 4; + int64 started_event_id = 5; + int32 attempt = 6; + int64 next_event_id = 7; + int64 backlog_count_hint = 8; + bool sticky_execution_enabled = 9; + temporal.api.query.v1.WorkflowQuery query = 10; + temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 11; + temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 12; + reserved 13; + bytes branch_token = 14; + google.protobuf.Timestamp scheduled_time = 15; + google.protobuf.Timestamp started_time = 16; + map queries = 17; + repeated temporal.api.protocol.v1.Message messages = 18; + // The history for this workflow, which will either be complete or partial. Partial histories + // are sent to workers who have signaled that they are using a sticky queue when completing + // a workflow task. Sticky query tasks will not include any history. + temporal.api.history.v1.History history = 19; + bytes next_page_token = 20; + temporal.api.taskqueue.v1.PollerScalingDecision poller_scaling_decision = 21; + // Raw history bytes sent from matching service when history.sendRawHistoryBetweenInternalServices is enabled. + // Matching client will deserialize this to History when it receives the response. + temporal.api.history.v1.History raw_history = 22; } // PollWorkflowTaskQueueResponseWithRawHistory is wire-compatible with PollWorkflowTaskQueueResponse. @@ -86,247 +84,245 @@ message PollWorkflowTaskQueueResponse { // IMPORTANT: Field numbers and all other fields must remain identical between these two messages. // Any change to PollWorkflowTaskQueueResponse must be mirrored here. message PollWorkflowTaskQueueResponseWithRawHistory { - bytes task_token = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - temporal.api.common.v1.WorkflowType workflow_type = 3; - int64 previous_started_event_id = 4; - int64 started_event_id = 5; - int32 attempt = 6; - int64 next_event_id = 7; - int64 backlog_count_hint = 8; - bool sticky_execution_enabled = 9; - temporal.api.query.v1.WorkflowQuery query = 10; - temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 11; - temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 12; - reserved 13; - bytes branch_token = 14; - google.protobuf.Timestamp scheduled_time = 15; - google.protobuf.Timestamp started_time = 16; - map queries = 17; - repeated temporal.api.protocol.v1.Message messages = 18; - // The history for this workflow, which will either be complete or partial. Partial histories - // are sent to workers who have signaled that they are using a sticky queue when completing - // a workflow task. Sticky query tasks will not include any history. - temporal.api.history.v1.History history = 19; - bytes next_page_token = 20; - temporal.api.taskqueue.v1.PollerScalingDecision poller_scaling_decision = 21; - // Raw history bytes. Each element is a proto-encoded batch of history events. - // When matching client deserializes this to PollWorkflowTaskQueueResponse, this field - // will be automatically deserialized to the raw_history field as History. - repeated bytes raw_history = 22; + bytes task_token = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + temporal.api.common.v1.WorkflowType workflow_type = 3; + int64 previous_started_event_id = 4; + int64 started_event_id = 5; + int32 attempt = 6; + int64 next_event_id = 7; + int64 backlog_count_hint = 8; + bool sticky_execution_enabled = 9; + temporal.api.query.v1.WorkflowQuery query = 10; + temporal.server.api.history.v1.TransientWorkflowTaskInfo transient_workflow_task = 11; + temporal.api.taskqueue.v1.TaskQueue workflow_execution_task_queue = 12; + reserved 13; + bytes branch_token = 14; + google.protobuf.Timestamp scheduled_time = 15; + google.protobuf.Timestamp started_time = 16; + map queries = 17; + repeated temporal.api.protocol.v1.Message messages = 18; + // The history for this workflow, which will either be complete or partial. Partial histories + // are sent to workers who have signaled that they are using a sticky queue when completing + // a workflow task. Sticky query tasks will not include any history. + temporal.api.history.v1.History history = 19; + bytes next_page_token = 20; + temporal.api.taskqueue.v1.PollerScalingDecision poller_scaling_decision = 21; + // Raw history bytes. Each element is a proto-encoded batch of history events. + // When matching client deserializes this to PollWorkflowTaskQueueResponse, this field + // will be automatically deserialized to the raw_history field as History. + repeated bytes raw_history = 22; } message PollActivityTaskQueueRequest { - string namespace_id = 1; - string poller_id = 2; - temporal.api.workflowservice.v1.PollActivityTaskQueueRequest poll_request = 3; - string forwarded_source = 4; - // Extra conditions on this poll request. Only supported with new matcher. - PollConditions conditions = 5; + string namespace_id = 1; + string poller_id = 2; + temporal.api.workflowservice.v1.PollActivityTaskQueueRequest poll_request = 3; + string forwarded_source = 4; + // Extra conditions on this poll request. Only supported with new matcher. + PollConditions conditions = 5; } message PollActivityTaskQueueResponse { - bytes task_token = 1; - temporal.api.common.v1.WorkflowExecution workflow_execution = 2; - string activity_id = 3; - temporal.api.common.v1.ActivityType activity_type = 4; - temporal.api.common.v1.Payloads input = 5; - google.protobuf.Timestamp scheduled_time = 6; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_close_timeout = 7; - google.protobuf.Timestamp started_time = 8; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration start_to_close_timeout = 9; - google.protobuf.Duration heartbeat_timeout = 10; - int32 attempt = 11; - google.protobuf.Timestamp current_attempt_scheduled_time = 12; - temporal.api.common.v1.Payloads heartbeat_details = 13; - temporal.api.common.v1.WorkflowType workflow_type = 14; - string workflow_namespace = 15; - temporal.api.common.v1.Header header = 16; - temporal.api.taskqueue.v1.PollerScalingDecision poller_scaling_decision = 17; - temporal.api.common.v1.Priority priority = 18; - temporal.api.common.v1.RetryPolicy retry_policy = 19; - // ID of the activity run (applicable for standalone activities only) - string activity_run_id = 20; + bytes task_token = 1; + temporal.api.common.v1.WorkflowExecution workflow_execution = 2; + string activity_id = 3; + temporal.api.common.v1.ActivityType activity_type = 4; + temporal.api.common.v1.Payloads input = 5; + google.protobuf.Timestamp scheduled_time = 6; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_close_timeout = 7; + google.protobuf.Timestamp started_time = 8; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration start_to_close_timeout = 9; + google.protobuf.Duration heartbeat_timeout = 10; + int32 attempt = 11; + google.protobuf.Timestamp current_attempt_scheduled_time = 12; + temporal.api.common.v1.Payloads heartbeat_details = 13; + temporal.api.common.v1.WorkflowType workflow_type = 14; + string workflow_namespace = 15; + temporal.api.common.v1.Header header = 16; + temporal.api.taskqueue.v1.PollerScalingDecision poller_scaling_decision = 17; + temporal.api.common.v1.Priority priority = 18; + temporal.api.common.v1.RetryPolicy retry_policy = 19; + // ID of the activity run (applicable for standalone activities only) + string activity_run_id = 20; } message AddWorkflowTaskRequest { - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - temporal.api.taskqueue.v1.TaskQueue task_queue = 3; - int64 scheduled_event_id = 4; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_start_timeout = 5; - temporal.server.api.clock.v1.VectorClock clock = 9; - // How this task should be directed by matching. (Missing means the default - // for TaskVersionDirective, which is unversioned.) - temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 10; - temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 11; - temporal.api.common.v1.Priority priority = 12; - // Stamp value from when the workflow task was scheduled. Used to validate the task is still relevant. - int32 stamp = 13; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + temporal.api.taskqueue.v1.TaskQueue task_queue = 3; + int64 scheduled_event_id = 4; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_start_timeout = 5; + temporal.server.api.clock.v1.VectorClock clock = 9; + // How this task should be directed by matching. (Missing means the default + // for TaskVersionDirective, which is unversioned.) + temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 10; + temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 11; + temporal.api.common.v1.Priority priority = 12; + // Stamp value from when the workflow task was scheduled. Used to validate the task is still relevant. + int32 stamp = 13; } message AddWorkflowTaskResponse { - // When present, it means that the task is spooled to a versioned queue of this build ID - // Deprecated. [cleanup-old-wv] - string assigned_build_id = 1; + // When present, it means that the task is spooled to a versioned queue of this build ID + // Deprecated. [cleanup-old-wv] + string assigned_build_id = 1; } message AddActivityTaskRequest { - string namespace_id = 1; - temporal.api.common.v1.WorkflowExecution execution = 2; - reserved 3; - temporal.api.taskqueue.v1.TaskQueue task_queue = 4; - int64 scheduled_event_id = 5; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_start_timeout = 6; - temporal.server.api.clock.v1.VectorClock clock = 9; - // How this task should be directed by matching. (Missing means the default - // for TaskVersionDirective, which is unversioned.) - temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 10; - temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 11; - int32 stamp = 12; - temporal.api.common.v1.Priority priority = 13; - // Reference to the Chasm component for activity execution (if applicable). For standalone activities, all - // necessary start information is carried within this component, obviating the need to use the fields that apply to - // embedded activities. - bytes component_ref = 14; + string namespace_id = 1; + temporal.api.common.v1.WorkflowExecution execution = 2; + reserved 3; + temporal.api.taskqueue.v1.TaskQueue task_queue = 4; + int64 scheduled_event_id = 5; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_start_timeout = 6; + temporal.server.api.clock.v1.VectorClock clock = 9; + // How this task should be directed by matching. (Missing means the default + // for TaskVersionDirective, which is unversioned.) + temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 10; + temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 11; + int32 stamp = 12; + temporal.api.common.v1.Priority priority = 13; + // Reference to the Chasm component for activity execution (if applicable). For standalone activities, all + // necessary start information is carried within this component, obviating the need to use the fields that apply to + // embedded activities. + bytes component_ref = 14; } message AddActivityTaskResponse { - // When present, it means that the task is spooled to a versioned queue of this build ID - // Deprecated. [cleanup-old-wv] - string assigned_build_id = 1; + // When present, it means that the task is spooled to a versioned queue of this build ID + // Deprecated. [cleanup-old-wv] + string assigned_build_id = 1; } message QueryWorkflowRequest { - string namespace_id = 1; - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; - temporal.api.workflowservice.v1.QueryWorkflowRequest query_request = 3; - // How this task should be directed by matching. (Missing means the default - // for TaskVersionDirective, which is unversioned.) - temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 5; - temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 6; - temporal.api.common.v1.Priority priority = 7; + string namespace_id = 1; + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + temporal.api.workflowservice.v1.QueryWorkflowRequest query_request = 3; + // How this task should be directed by matching. (Missing means the default + // for TaskVersionDirective, which is unversioned.) + temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 5; + temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 6; + temporal.api.common.v1.Priority priority = 7; } message QueryWorkflowResponse { - temporal.api.common.v1.Payloads query_result = 1; - temporal.api.query.v1.QueryRejected query_rejected = 2; + temporal.api.common.v1.Payloads query_result = 1; + temporal.api.query.v1.QueryRejected query_rejected = 2; } message RespondQueryTaskCompletedRequest { - string namespace_id = 1; - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; - string task_id = 3; - temporal.api.workflowservice.v1.RespondQueryTaskCompletedRequest completed_request = 4; + string namespace_id = 1; + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + string task_id = 3; + temporal.api.workflowservice.v1.RespondQueryTaskCompletedRequest completed_request = 4; } -message RespondQueryTaskCompletedResponse { -} +message RespondQueryTaskCompletedResponse {} message CancelOutstandingPollRequest { - string namespace_id = 1; - temporal.api.enums.v1.TaskQueueType task_queue_type = 2; - temporal.api.taskqueue.v1.TaskQueue task_queue = 3; - string poller_id = 4; + string namespace_id = 1; + temporal.api.enums.v1.TaskQueueType task_queue_type = 2; + temporal.api.taskqueue.v1.TaskQueue task_queue = 3; + string poller_id = 4; } -message CancelOutstandingPollResponse { -} +message CancelOutstandingPollResponse {} // CancelOutstandingWorkerPollsRequest cancels all outstanding polls for a given worker instance key. message CancelOutstandingWorkerPollsRequest { - string namespace_id = 1; - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; - temporal.api.enums.v1.TaskQueueType task_queue_type = 3; - string worker_instance_key = 4; - // Worker identity string (e.g., "pid@hostname"). Used to eagerly remove the worker - // from pollerHistory so DescribeTaskQueue doesn't show stale pollers. - // Note: pollerHistory predates worker_instance_key and uses identity as its key, - // so we pass both for backward compatibility. - string worker_identity = 5; + string namespace_id = 1; + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + temporal.api.enums.v1.TaskQueueType task_queue_type = 3; + string worker_instance_key = 4; + // Worker identity string (e.g., "pid@hostname"). Used to eagerly remove the worker + // from pollerHistory so DescribeTaskQueue doesn't show stale pollers. + // Note: pollerHistory predates worker_instance_key and uses identity as its key, + // so we pass both for backward compatibility. + string worker_identity = 5; } message CancelOutstandingWorkerPollsResponse { - // Used for debugging. - int32 cancelled_count = 1; + // Used for debugging. + int32 cancelled_count = 1; } message DescribeTaskQueueRequest { - string namespace_id = 1; - temporal.api.workflowservice.v1.DescribeTaskQueueRequest desc_request = 2; - temporal.server.api.deployment.v1.WorkerDeploymentVersion version = 3; + string namespace_id = 1; + temporal.api.workflowservice.v1.DescribeTaskQueueRequest desc_request = 2; + temporal.server.api.deployment.v1.WorkerDeploymentVersion version = 3; } message DescribeTaskQueueResponse { - reserved 1 to 2; - temporal.api.workflowservice.v1.DescribeTaskQueueResponse desc_response = 3; + reserved 1 to 2; + temporal.api.workflowservice.v1.DescribeTaskQueueResponse desc_response = 3; } message DescribeVersionedTaskQueuesRequest { - string namespace_id = 1; + string namespace_id = 1; - // This task queue is for routing purposes. - temporal.api.enums.v1.TaskQueueType task_queue_type = 2; - temporal.api.taskqueue.v1.TaskQueue task_queue = 3; + // This task queue is for routing purposes. + temporal.api.enums.v1.TaskQueueType task_queue_type = 2; + temporal.api.taskqueue.v1.TaskQueue task_queue = 3; - temporal.server.api.deployment.v1.WorkerDeploymentVersion version = 4; + temporal.server.api.deployment.v1.WorkerDeploymentVersion version = 4; - // List of task queues to describe. - repeated VersionTaskQueue version_task_queues = 5; - // (-- api-linter: core::0123::resource-annotation=disabled --) - message VersionTaskQueue { - string name = 1; - temporal.api.enums.v1.TaskQueueType type = 2; - } + // List of task queues to describe. + repeated VersionTaskQueue version_task_queues = 5; + // (-- api-linter: core::0123::resource-annotation=disabled --) + message VersionTaskQueue { + string name = 1; + temporal.api.enums.v1.TaskQueueType type = 2; + } } message DescribeVersionedTaskQueuesResponse { - repeated VersionTaskQueue version_task_queues = 1; - // (-- api-linter: core::0123::resource-annotation=disabled --) - message VersionTaskQueue { - string name = 1; - temporal.api.enums.v1.TaskQueueType type = 2; - temporal.api.taskqueue.v1.TaskQueueStats stats = 3; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "by" is used to clarify the key. --) - map stats_by_priority_key = 4; - } + repeated VersionTaskQueue version_task_queues = 1; + // (-- api-linter: core::0123::resource-annotation=disabled --) + message VersionTaskQueue { + string name = 1; + temporal.api.enums.v1.TaskQueueType type = 2; + temporal.api.taskqueue.v1.TaskQueueStats stats = 3; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "by" is used to clarify the key. --) + map stats_by_priority_key = 4; + } } message DescribeTaskQueuePartitionRequest { - string namespace_id = 1; - temporal.server.api.taskqueue.v1.TaskQueuePartition task_queue_partition = 2; + string namespace_id = 1; + temporal.server.api.taskqueue.v1.TaskQueuePartition task_queue_partition = 2; - temporal.api.taskqueue.v1.TaskQueueVersionSelection versions = 3; + temporal.api.taskqueue.v1.TaskQueueVersionSelection versions = 3; - // Report task queue stats for the requested task queue types and versions - bool report_stats = 4; - // Report list of pollers for requested task queue types and versions - bool report_pollers = 5; - bool report_internal_task_queue_status = 6; + // Report task queue stats for the requested task queue types and versions + bool report_stats = 4; + // Report list of pollers for requested task queue types and versions + bool report_pollers = 5; + bool report_internal_task_queue_status = 6; } message DescribeTaskQueuePartitionResponse { - map versions_info_internal = 1; + map versions_info_internal = 1; } message ListTaskQueuePartitionsRequest { - string namespace = 1; - string namespace_id = 3; - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + string namespace = 1; + string namespace_id = 3; + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; } message ListTaskQueuePartitionsResponse { - repeated temporal.api.taskqueue.v1.TaskQueuePartitionMetadata activity_task_queue_partitions = 1; - repeated temporal.api.taskqueue.v1.TaskQueuePartitionMetadata workflow_task_queue_partitions = 2; + repeated temporal.api.taskqueue.v1.TaskQueuePartitionMetadata activity_task_queue_partitions = 1; + repeated temporal.api.taskqueue.v1.TaskQueuePartitionMetadata workflow_task_queue_partitions = 2; } // (-- api-linter: core::0134::request-mask-required=disabled @@ -334,42 +330,42 @@ message ListTaskQueuePartitionsResponse { // (-- api-linter: core::0134::request-resource-required=disabled // aip.dev/not-precedent: UpdateWorkerBuildIdCompatibilityRequest RPC doesn't follow Google API format. --) message UpdateWorkerBuildIdCompatibilityRequest { - // Apply request from public API. - message ApplyPublicRequest { - temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest request = 1; - } + // Apply request from public API. + message ApplyPublicRequest { + temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest request = 1; + } - // Remove build ids (internal only) - message RemoveBuildIds { - // The last known user data version, used to prevent concurrent updates. - int64 known_user_data_version = 1; - // List of build ids to remove. - repeated string build_ids = 2; - } + // Remove build ids (internal only) + message RemoveBuildIds { + // The last known user data version, used to prevent concurrent updates. + int64 known_user_data_version = 1; + // List of build ids to remove. + repeated string build_ids = 2; + } - string namespace_id = 1; - string task_queue = 2; + string namespace_id = 1; + string task_queue = 2; - oneof operation { - ApplyPublicRequest apply_public_request = 3; - RemoveBuildIds remove_build_ids = 4; - string persist_unknown_build_id = 5; - } + oneof operation { + ApplyPublicRequest apply_public_request = 3; + RemoveBuildIds remove_build_ids = 4; + string persist_unknown_build_id = 5; + } } message UpdateWorkerBuildIdCompatibilityResponse {} message GetWorkerVersioningRulesRequest { - string namespace_id = 1; - string task_queue = 2; + string namespace_id = 1; + string task_queue = 2; - oneof command { - temporal.api.workflowservice.v1.GetWorkerVersioningRulesRequest request = 3; - } + oneof command { + temporal.api.workflowservice.v1.GetWorkerVersioningRulesRequest request = 3; + } } message GetWorkerVersioningRulesResponse { - temporal.api.workflowservice.v1.GetWorkerVersioningRulesResponse response = 1; + temporal.api.workflowservice.v1.GetWorkerVersioningRulesResponse response = 1; } // (-- api-linter: core::0134::request-mask-required=disabled @@ -377,138 +373,139 @@ message GetWorkerVersioningRulesResponse { // (-- api-linter: core::0134::request-resource-required=disabled // aip.dev/not-precedent: UpdateWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) message UpdateWorkerVersioningRulesRequest { - string namespace_id = 1; - string task_queue = 2; + string namespace_id = 1; + string task_queue = 2; - oneof command { - temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest request = 3; - } + oneof command { + temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesRequest request = 3; + } } message UpdateWorkerVersioningRulesResponse { - temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesResponse response = 1; + temporal.api.workflowservice.v1.UpdateWorkerVersioningRulesResponse response = 1; } message GetWorkerBuildIdCompatibilityRequest { - string namespace_id = 1; - temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest request = 2; } message GetWorkerBuildIdCompatibilityResponse { - temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse response = 1; + temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse response = 1; } message GetTaskQueueUserDataRequest { - string namespace_id = 1; - // The task queue to fetch data from. The task queue is always considered as a normal - // queue, since sticky queues have no user data. - string task_queue = 2; - temporal.api.enums.v1.TaskQueueType task_queue_type = 5; - // The value of the last known user data version. - // If the requester has no data, it should set this to 0. - // This value must not be set to a negative number (note that our linter suggests avoiding uint64). - int64 last_known_user_data_version = 3; - // Same for ephemeral data. - int64 last_known_ephemeral_data_version = 7; - // If set and last_known_{user_data,ephemeral_data}_version is the current version, - // block until new data is available (or timeout). - bool wait_new_data = 4; - // If set, do not load task queue if unloaded. (Returns FailedPrecondition error in that case.) - bool only_if_loaded = 6; + string namespace_id = 1; + // The task queue to fetch data from. The task queue is always considered as a normal + // queue, since sticky queues have no user data. + string task_queue = 2; + temporal.api.enums.v1.TaskQueueType task_queue_type = 5; + // The value of the last known user data version. + // If the requester has no data, it should set this to 0. + // This value must not be set to a negative number (note that our linter suggests avoiding uint64). + int64 last_known_user_data_version = 3; + // The value of the last known ephemeral data version. + // If the requester has no data yet, it should use 0. + // If the requester doesn't want ephemeral data (i.e. it's root of an activity/nexus + // queue which have separate ephemeral data), it should use -1 (noEphemeralDataVersion). + int64 last_known_ephemeral_data_version = 7; + // If set and last_known_{user_data,ephemeral_data}_version is the current version, + // block until new data is available (or timeout). + bool wait_new_data = 4; + // If set, do not load task queue if unloaded. (Returns FailedPrecondition error in that case.) + bool only_if_loaded = 6; } message GetTaskQueueUserDataResponse { - reserved 1; - // Versioned user data, set if the task queue has user data and the request's last_known_user_data_version is less - // than the version cached in the root partition. - temporal.server.api.persistence.v1.VersionedTaskQueueUserData user_data = 2; - temporal.server.api.taskqueue.v1.VersionedEphemeralData ephemeral_data = 3; + reserved 1; + // Versioned user data, set if the task queue has user data and the request's last_known_user_data_version is less + // than the version cached in the root partition. + temporal.server.api.persistence.v1.VersionedTaskQueueUserData user_data = 2; + temporal.server.api.taskqueue.v1.VersionedEphemeralData ephemeral_data = 3; } message SyncDeploymentUserDataRequest { - string namespace_id = 1; - string task_queue = 2; - // Required, unless deprecated fields are used. - // (-- api-linter: core::0203::required=disabled - // aip.dev/not-precedent: Not following Google API format --) - string deployment_name = 9; - reserved 3, 4, 5; - repeated temporal.api.enums.v1.TaskQueueType task_queue_types = 8; - - oneof operation { - // The deployment version and its data that is being updated. - temporal.server.api.deployment.v1.DeploymentVersionData update_version_data = 6 [deprecated = true]; - // The version whose data should be cleaned from the task queue. - temporal.server.api.deployment.v1.WorkerDeploymentVersion forget_version = 7 [deprecated = true]; - } - - // Absent means no change. - // Ignored by the task queue if new revision number is not greater that what it has. - temporal.api.deployment.v1.RoutingConfig update_routing_config = 10; - // Optional map of build id to upsert version data. - // (-- api-linter: core::0203::required=disabled - // aip.dev/not-precedent: Not following Google API format --) - map upsert_versions_data = 11; - // List of build ids to forget from task queue. - // Deprecated. Use upsert_versions_data with deleted=true. - repeated string forget_versions = 12; + string namespace_id = 1; + string task_queue = 2; + // Required, unless deprecated fields are used. + // (-- api-linter: core::0203::required=disabled + // aip.dev/not-precedent: Not following Google API format --) + string deployment_name = 9; + reserved 3, 4, 5; + repeated temporal.api.enums.v1.TaskQueueType task_queue_types = 8; + + oneof operation { + // The deployment version and its data that is being updated. + temporal.server.api.deployment.v1.DeploymentVersionData update_version_data = 6 [deprecated = true]; + // The version whose data should be cleaned from the task queue. + temporal.server.api.deployment.v1.WorkerDeploymentVersion forget_version = 7 [deprecated = true]; + } + + // Absent means no change. + // Ignored by the task queue if new revision number is not greater that what it has. + temporal.api.deployment.v1.RoutingConfig update_routing_config = 10; + // Optional map of build id to upsert version data. + // (-- api-linter: core::0203::required=disabled + // aip.dev/not-precedent: Not following Google API format --) + map upsert_versions_data = 11; + // List of build ids to forget from task queue. + repeated string forget_versions = 12; } message SyncDeploymentUserDataResponse { - // New task queue user data version. Can be used to wait for propagation. - int64 version = 1; - // If the routing config changed after applying this operation. Compared base on revision number. - // Deprecated. using this is not totaly safe in case of retries. - bool routing_config_changed = 2 [deprecated = true]; + // New task queue user data version. Can be used to wait for propagation. + int64 version = 1; + // If the routing config changed after applying this operation. Compared base on revision number. + // Deprecated. using this is not totaly safe in case of retries. + bool routing_config_changed = 2 [deprecated = true]; } message ApplyTaskQueueUserDataReplicationEventRequest { - string namespace_id = 1; - string task_queue = 2; - temporal.server.api.persistence.v1.TaskQueueUserData user_data = 3; + string namespace_id = 1; + string task_queue = 2; + temporal.server.api.persistence.v1.TaskQueueUserData user_data = 3; } -message ApplyTaskQueueUserDataReplicationEventResponse { -} +message ApplyTaskQueueUserDataReplicationEventResponse {} message GetBuildIdTaskQueueMappingRequest { - string namespace_id = 1; - string build_id = 2; + string namespace_id = 1; + string build_id = 2; } message GetBuildIdTaskQueueMappingResponse { - repeated string task_queues = 1; + repeated string task_queues = 1; } message ForceLoadTaskQueuePartitionRequest { - string namespace_id = 1; + string namespace_id = 1; - temporal.server.api.taskqueue.v1.TaskQueuePartition task_queue_partition = 2; + temporal.server.api.taskqueue.v1.TaskQueuePartition task_queue_partition = 2; } message ForceLoadTaskQueuePartitionResponse { - bool was_unloaded = 1; + bool was_unloaded = 1; } // TODO Shivam - Please remove this in 123 message ForceUnloadTaskQueueRequest { - string namespace_id = 1; - string task_queue = 2; - temporal.api.enums.v1.TaskQueueType task_queue_type = 3; + string namespace_id = 1; + string task_queue = 2; + temporal.api.enums.v1.TaskQueueType task_queue_type = 3; } // TODO Shivam - Please remove this in 123 message ForceUnloadTaskQueueResponse { - bool was_loaded = 1; + bool was_loaded = 1; } message ForceUnloadTaskQueuePartitionRequest { - string namespace_id = 1; - temporal.server.api.taskqueue.v1.TaskQueuePartition task_queue_partition = 2; + string namespace_id = 1; + temporal.server.api.taskqueue.v1.TaskQueuePartition task_queue_partition = 2; } message ForceUnloadTaskQueuePartitionResponse { - bool was_loaded = 1; + bool was_loaded = 1; } // (-- api-linter: core::0134::request-mask-required=disabled @@ -516,100 +513,95 @@ message ForceUnloadTaskQueuePartitionResponse { // (-- api-linter: core::0134::request-resource-required=disabled // aip.dev/not-precedent: UpdateTaskQueueUserDataRequest RPC doesn't follow Google API format. --) message UpdateTaskQueueUserDataRequest { - string namespace_id = 1; - string task_queue = 2; - // Versioned user data, set if the task queue has user data and the request's last_known_user_data_version is less - // than the version cached in the root partition. - temporal.server.api.persistence.v1.VersionedTaskQueueUserData user_data = 3; - // List of added build ids - repeated string build_ids_added = 4; - // List of removed build ids - repeated string build_ids_removed = 5; + string namespace_id = 1; + string task_queue = 2; + // Versioned user data, set if the task queue has user data and the request's last_known_user_data_version is less + // than the version cached in the root partition. + temporal.server.api.persistence.v1.VersionedTaskQueueUserData user_data = 3; + // List of added build ids + repeated string build_ids_added = 4; + // List of removed build ids + repeated string build_ids_removed = 5; } -message UpdateTaskQueueUserDataResponse { -} +message UpdateTaskQueueUserDataResponse {} message ReplicateTaskQueueUserDataRequest { - string namespace_id = 1; - string task_queue = 2; - temporal.server.api.persistence.v1.TaskQueueUserData user_data = 3; + string namespace_id = 1; + string task_queue = 2; + temporal.server.api.persistence.v1.TaskQueueUserData user_data = 3; } -message ReplicateTaskQueueUserDataResponse { -} +message ReplicateTaskQueueUserDataResponse {} message CheckTaskQueueUserDataPropagationRequest { - string namespace_id = 1; - string task_queue = 2; - int64 version = 3; + string namespace_id = 1; + string task_queue = 2; + int64 version = 3; } -message CheckTaskQueueUserDataPropagationResponse { -} +message CheckTaskQueueUserDataPropagationResponse {} message DispatchNexusTaskRequest { - string namespace_id = 1; - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; - // Nexus request extracted by the frontend and translated into Temporal API format. - temporal.api.nexus.v1.Request request = 3; - temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 4; + string namespace_id = 1; + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + // Nexus request extracted by the frontend and translated into Temporal API format. + temporal.api.nexus.v1.Request request = 3; + temporal.server.api.taskqueue.v1.TaskForwardInfo forward_info = 4; } message DispatchNexusTaskResponse { - message Timeout {} + message Timeout {} - oneof outcome { - // Deprecated. Use failure field instead. - temporal.api.nexus.v1.HandlerError handler_error = 1 [deprecated = true]; - // Set if the worker's handler responded successfully to the nexus task. - temporal.api.nexus.v1.Response response = 2; - Timeout request_timeout = 3; - // Set if the worker's handler failed the nexus task. Must contain a NexusHandlerFailureInfo object. - temporal.api.failure.v1.Failure failure = 4; - } + oneof outcome { + // Deprecated. Use failure field instead. + temporal.api.nexus.v1.HandlerError handler_error = 1 [deprecated = true]; + // Set if the worker's handler responded successfully to the nexus task. + temporal.api.nexus.v1.Response response = 2; + Timeout request_timeout = 3; + // Set if the worker's handler failed the nexus task. Must contain a NexusHandlerFailureInfo object. + temporal.api.failure.v1.Failure failure = 4; + } } message PollNexusTaskQueueRequest { - string namespace_id = 1; - // A unique ID generated by the frontend for this request. - string poller_id = 2; - // Original WorkflowService poll request as received by the frontend. - temporal.api.workflowservice.v1.PollNexusTaskQueueRequest request = 3; - // Non-empty if this poll was forwarded from a child partition. - string forwarded_source = 4; - // Extra conditions on this poll request. Only supported with new matcher. - PollConditions conditions = 5; + string namespace_id = 1; + // A unique ID generated by the frontend for this request. + string poller_id = 2; + // Original WorkflowService poll request as received by the frontend. + temporal.api.workflowservice.v1.PollNexusTaskQueueRequest request = 3; + // Non-empty if this poll was forwarded from a child partition. + string forwarded_source = 4; + // Extra conditions on this poll request. Only supported with new matcher. + PollConditions conditions = 5; } message PollNexusTaskQueueResponse { - // Response that should be delivered to the worker containing a request from DispatchNexusTaskRequest. - temporal.api.workflowservice.v1.PollNexusTaskQueueResponse response = 1; + // Response that should be delivered to the worker containing a request from DispatchNexusTaskRequest. + temporal.api.workflowservice.v1.PollNexusTaskQueueResponse response = 1; } message RespondNexusTaskCompletedRequest { - string namespace_id = 1; - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; - // A unique ID for this task generated by the matching engine. Decoded from the incoming request's task token. - string task_id = 3; - // Original completion as received by the frontend. - temporal.api.workflowservice.v1.RespondNexusTaskCompletedRequest request = 4; + string namespace_id = 1; + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + // A unique ID for this task generated by the matching engine. Decoded from the incoming request's task token. + string task_id = 3; + // Original completion as received by the frontend. + temporal.api.workflowservice.v1.RespondNexusTaskCompletedRequest request = 4; } -message RespondNexusTaskCompletedResponse { -} +message RespondNexusTaskCompletedResponse {} message RespondNexusTaskFailedRequest { - string namespace_id = 1; - temporal.api.taskqueue.v1.TaskQueue task_queue = 2; - // A unique ID for this task generated by the matching engine. Decoded from the incoming request's task token. - string task_id = 3; - // Original failure as received by the frontend. - temporal.api.workflowservice.v1.RespondNexusTaskFailedRequest request = 4; + string namespace_id = 1; + temporal.api.taskqueue.v1.TaskQueue task_queue = 2; + // A unique ID for this task generated by the matching engine. Decoded from the incoming request's task token. + string task_id = 3; + // Original failure as received by the frontend. + temporal.api.workflowservice.v1.RespondNexusTaskFailedRequest request = 4; } -message RespondNexusTaskFailedResponse { -} +message RespondNexusTaskFailedResponse {} // (-- api-linter: core::0133::request-unknown-fields=disabled // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) @@ -618,11 +610,11 @@ message RespondNexusTaskFailedResponse { // (-- api-linter: core::0133::request-parent-required=disabled // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) message CreateNexusEndpointRequest { - temporal.server.api.persistence.v1.NexusEndpointSpec spec = 1; + temporal.server.api.persistence.v1.NexusEndpointSpec spec = 1; } message CreateNexusEndpointResponse { - temporal.server.api.persistence.v1.NexusEndpointEntry entry = 1; + temporal.server.api.persistence.v1.NexusEndpointEntry entry = 1; } // (-- api-linter: core::0134::request-resource-required=disabled @@ -630,16 +622,16 @@ message CreateNexusEndpointResponse { // (-- api-linter: core::0134::request-mask-required=disabled // aip.dev/not-precedent: UpdateNexusEndpoint RPC doesn't follow Google API format. --) message UpdateNexusEndpointRequest { - // ID of the endpoint to update. - string id = 1; - // Version of the endpoint, used for optimistic concurrency. Must match current version in persistence or the - // request will fail a FAILED_PRECONDITION error. - int64 version = 2; - temporal.server.api.persistence.v1.NexusEndpointSpec spec = 3; + // ID of the endpoint to update. + string id = 1; + // Version of the endpoint, used for optimistic concurrency. Must match current version in persistence or the + // request will fail a FAILED_PRECONDITION error. + int64 version = 2; + temporal.server.api.persistence.v1.NexusEndpointSpec spec = 3; } message UpdateNexusEndpointResponse { - temporal.server.api.persistence.v1.NexusEndpointEntry entry = 1; + temporal.server.api.persistence.v1.NexusEndpointEntry entry = 1; } // (-- api-linter: core::0135::request-name-behavior=disabled @@ -647,55 +639,55 @@ message UpdateNexusEndpointResponse { // (-- api-linter: core::0135::request-name-reference=disabled // aip.dev/not-precedent: DeleteNexusEndpointRequest RPC doesn't follow Google API format. --) message DeleteNexusEndpointRequest { - // ID of the endpoint to delete. - string id = 1; + // ID of the endpoint to delete. + string id = 1; } -message DeleteNexusEndpointResponse { -} +message DeleteNexusEndpointResponse {} message ListNexusEndpointsRequest { - // To get the next page, pass in `ListNexusEndpointsResponse.next_page_token` from the previous page's response. The - // token will be empty if there's no other page. - // Note: the last page may be empty if the total number of services registered is a multiple of the page size. - // Mutually exclusive with wait. Specifying both will result in an invalid argument error. - bytes next_page_token = 1; - int32 page_size = 2; - // The nexus_endpoints table has a monotonically increasing version number that is incremented on every change to - // the table. This field can be used to provide the last known table version in conjuction with the `wait` field to - // long poll on changes to the table. - // If next_page_token is not empty and the current table version does not match this field, this request will fail - // with a failed precondition error. - int64 last_known_table_version = 3; - // If true, this request becomes a long poll and will be unblocked once the DB version is incremented. - // Mutually exclusive with next_page_token. Specifying both will result in an invalid argument error. - bool wait = 4; + // To get the next page, pass in `ListNexusEndpointsResponse.next_page_token` from the previous page's response. The + // token will be empty if there's no other page. + // Note: the last page may be empty if the total number of services registered is a multiple of the page size. + // Mutually exclusive with wait. Specifying both will result in an invalid argument error. + bytes next_page_token = 1; + int32 page_size = 2; + // The nexus_endpoints table has a monotonically increasing version number that is incremented on every change to + // the table. This field can be used to provide the last known table version in conjuction with the `wait` field to + // long poll on changes to the table. + // If next_page_token is not empty and the current table version does not match this field, this request will fail + // with a failed precondition error. + int64 last_known_table_version = 3; + // If true, this request becomes a long poll and will be unblocked once the DB version is incremented. + // Mutually exclusive with next_page_token. Specifying both will result in an invalid argument error. + bool wait = 4; } message ListNexusEndpointsResponse { - // Token for getting the next page. - bytes next_page_token = 1; - int64 table_version = 2; - repeated temporal.server.api.persistence.v1.NexusEndpointEntry entries = 3; + // Token for getting the next page. + bytes next_page_token = 1; + int64 table_version = 2; + repeated temporal.server.api.persistence.v1.NexusEndpointEntry entries = 3; } message RecordWorkerHeartbeatRequest { - string namespace_id = 1; - temporal.api.workflowservice.v1.RecordWorkerHeartbeatRequest heartbeart_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.RecordWorkerHeartbeatRequest heartbeart_request = 2; } -message RecordWorkerHeartbeatResponse { - -} +message RecordWorkerHeartbeatResponse {} message ListWorkersRequest { - string namespace_id = 1; - temporal.api.workflowservice.v1.ListWorkersRequest list_request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.ListWorkersRequest list_request = 2; } message ListWorkersResponse { - repeated temporal.api.worker.v1.WorkerInfo workers_info = 1; - bytes next_page_token = 2; + // Deprecated: Use workers instead. This field returns full WorkerInfo which + // includes expensive runtime metrics. We will stop populating this field in the future. + repeated temporal.api.worker.v1.WorkerInfo workers_info = 1 [deprecated = true]; + bytes next_page_token = 2; + repeated temporal.api.worker.v1.WorkerListInfo workers = 3; } // (-- api-linter: core::0134::request-resource-required=disabled @@ -705,21 +697,21 @@ message ListWorkersResponse { // (-- api-linter: core::0134::method-signature=disabled // aip.dev/not-precedent: UpdateTaskQueueConfigRequest RPC doesn't follow Google API format. --) message UpdateTaskQueueConfigRequest { - string namespace_id = 1; - temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest update_taskqueue_config = 3; + string namespace_id = 1; + temporal.api.workflowservice.v1.UpdateTaskQueueConfigRequest update_taskqueue_config = 3; } message UpdateTaskQueueConfigResponse { - temporal.api.taskqueue.v1.TaskQueueConfig updated_taskqueue_config = 1; + temporal.api.taskqueue.v1.TaskQueueConfig updated_taskqueue_config = 1; } message DescribeWorkerRequest { - string namespace_id = 1; - temporal.api.workflowservice.v1.DescribeWorkerRequest request = 2; + string namespace_id = 1; + temporal.api.workflowservice.v1.DescribeWorkerRequest request = 2; } message DescribeWorkerResponse { - temporal.api.worker.v1.WorkerInfo worker_info = 1; + temporal.api.worker.v1.WorkerInfo worker_info = 1; } // (-- api-linter: core::0134::request-resource-required=disabled @@ -729,34 +721,33 @@ message DescribeWorkerResponse { // (-- api-linter: core::0134::method-signature=disabled // aip.dev/not-precedent: UpdateFairnessStateRequest RPC doesn't follow Google API format. --) message UpdateFairnessStateRequest { - string namespace_id = 1; - string task_queue = 2; - temporal.api.enums.v1.TaskQueueType task_queue_type = 3; - temporal.server.api.enums.v1.FairnessState fairness_state = 4; + string namespace_id = 1; + string task_queue = 2; + temporal.api.enums.v1.TaskQueueType task_queue_type = 3; + temporal.server.api.enums.v1.FairnessState fairness_state = 4; } -message UpdateFairnessStateResponse { -} +message UpdateFairnessStateResponse {} message CheckTaskQueueVersionMembershipRequest { - string namespace_id = 1; - string task_queue = 2; - temporal.api.enums.v1.TaskQueueType task_queue_type = 3; - temporal.server.api.deployment.v1.WorkerDeploymentVersion version = 4; + string namespace_id = 1; + string task_queue = 2; + temporal.api.enums.v1.TaskQueueType task_queue_type = 3; + temporal.server.api.deployment.v1.WorkerDeploymentVersion version = 4; } message CheckTaskQueueVersionMembershipResponse { - bool is_member = 1; + bool is_member = 1; } // PollConditions are extra conditions to set on the poll. Only supported with new matcher. message PollConditions { - // If set (non-zero), this poll will not match a task with lower priority than this value. - // Note that "min" priority is "max" numeric value, e.g. "min_priority: 3" means to match - // tasks with priority 1, 2, or 3. - int32 min_priority = 1; - // If true, don't block waiting for a task, just return a task immediately or an empty - // response. This is most useful combined with min_priority, to poll for task at a specific - // priority level on a partition that you think is there. - bool no_wait = 2; + // If set (non-zero), this poll will not match a task with lower priority than this value. + // Note that "min" priority is "max" numeric value, e.g. "min_priority: 3" means to match + // tasks with priority 1, 2, or 3. + int32 min_priority = 1; + // If true, don't block waiting for a task, just return a task immediately or an empty + // response. This is most useful combined with min_priority, to poll for task at a specific + // priority level on a partition that you think is there. + bool no_wait = 2; } diff --git a/proto/internal/temporal/server/api/matchingservice/v1/service.proto b/proto/internal/temporal/server/api/matchingservice/v1/service.proto index 42cb090b1ee..908f0b3eee4 100644 --- a/proto/internal/temporal/server/api/matchingservice/v1/service.proto +++ b/proto/internal/temporal/server/api/matchingservice/v1/service.proto @@ -1,215 +1,279 @@ syntax = "proto3"; package temporal.server.api.matchingservice.v1; -option go_package = "go.temporal.io/server/api/matchingservice/v1;matchingservice"; +import "temporal/server/api/common/v1/api_category.proto"; import "temporal/server/api/matchingservice/v1/request_response.proto"; +option go_package = "go.temporal.io/server/api/matchingservice/v1;matchingservice"; + // MatchingService API is exposed to provide support for polling from long running applications. // Such applications are expected to have a worker which regularly polls for WorkflowTask and ActivityTask. For each // WorkflowTask, application is expected to process the history of events for that session and respond back with next // commands. For each ActivityTask, application is expected to execute the actual logic for that task and respond back // with completion or failure. service MatchingService { - // PollWorkflowTaskQueue is called by frontend to process WorkflowTask from a specific task queue. A - // WorkflowTask is dispatched to callers for active workflow executions, with pending workflow tasks. - rpc PollWorkflowTaskQueue (PollWorkflowTaskQueueRequest) returns (PollWorkflowTaskQueueResponse) { - } - - // PollActivityTaskQueue is called by frontend to process ActivityTask from a specific task queue. ActivityTask - // is dispatched to callers whenever a ScheduleTask command is made for a workflow execution. - rpc PollActivityTaskQueue (PollActivityTaskQueueRequest) returns (PollActivityTaskQueueResponse) { - } - - // AddWorkflowTask is called by the history service when a workflow task is scheduled, so that it can be dispatched - // by the MatchingEngine. - rpc AddWorkflowTask (AddWorkflowTaskRequest) returns (AddWorkflowTaskResponse) { - } - - // AddActivityTask is called by the history service when a workflow task is scheduled, so that it can be dispatched - // by the MatchingEngine. - rpc AddActivityTask (AddActivityTaskRequest) returns (AddActivityTaskResponse) { - } - - // QueryWorkflow is called by frontend to query a workflow. - rpc QueryWorkflow (QueryWorkflowRequest) returns (QueryWorkflowResponse) { - } - - // RespondQueryTaskCompleted is called by frontend to respond query completed. - rpc RespondQueryTaskCompleted (RespondQueryTaskCompletedRequest) returns (RespondQueryTaskCompletedResponse) { - } - - // Request from frontend to synchronously dispatch a nexus task to a worker. - rpc DispatchNexusTask (DispatchNexusTaskRequest) returns (DispatchNexusTaskResponse) { - } - - // Request from worker (via frontend) to long poll on nexus tasks. - rpc PollNexusTaskQueue (PollNexusTaskQueueRequest) returns (PollNexusTaskQueueResponse) { - } - - // Response from a worker (via frontend) to a Nexus task, unblocks the corresponding DispatchNexusTask request. - rpc RespondNexusTaskCompleted (RespondNexusTaskCompletedRequest) returns (RespondNexusTaskCompletedResponse) { - } - - // Response from a worker (via frontend) to a Nexus task, unblocks the corresponding DispatchNexusTask request. - rpc RespondNexusTaskFailed (RespondNexusTaskFailedRequest) returns (RespondNexusTaskFailedResponse) { - } - - // CancelOutstandingPoll is called by frontend to unblock long polls on matching for zombie pollers. - // Our rpc stack does not support context propagation, so when a client connection goes away frontend sees - // cancellation of context for that handler, but any corresponding calls (long-poll) to matching service does not - // see the cancellation propagated so it can unblock corresponding long-polls on its end. This results is tasks - // being dispatched to zombie pollers in this situation. This API is added so every time frontend makes a long-poll - // api call to matching it passes in a pollerId and then calls this API when it detects client connection is closed - // to unblock long polls for this poller and prevent tasks being sent to these zombie pollers. - rpc CancelOutstandingPoll (CancelOutstandingPollRequest) returns (CancelOutstandingPollResponse) { - } - - // CancelOutstandingWorkerPolls cancels any outstanding polls for a given worker instance key. - // These polls could be waiting on different partitions of the task queue. - // This is called during worker shutdown to eagerly cancel polls and avoid giving out tasks to workers that are shutting down. - // Note: This only cancels polls that are currently outstanding. The caller must ensure no new polls - // are issued after calling this RPC, otherwise those polls will not be cancelled. - rpc CancelOutstandingWorkerPolls (CancelOutstandingWorkerPollsRequest) returns (CancelOutstandingWorkerPollsResponse) { - } - - // DescribeTaskQueue returns information about the target task queue, right now this API returns the - // pollers which polled this task queue in last few minutes. - rpc DescribeTaskQueue (DescribeTaskQueueRequest) returns (DescribeTaskQueueResponse) { - } - - // DescribeTaskQueuePartition returns information about the target task queue partition. - rpc DescribeTaskQueuePartition (DescribeTaskQueuePartitionRequest) returns (DescribeTaskQueuePartitionResponse) { - } - - // DescribeVersionedTaskQueues returns details about the requested versioned task queues. - // It is an internal API; there is no direct user-facing equivalent. - rpc DescribeVersionedTaskQueues (DescribeVersionedTaskQueuesRequest) returns (DescribeVersionedTaskQueuesResponse) { - } - - // ListTaskQueuePartitions returns a map of partitionKey and hostAddress for a task queue. - rpc ListTaskQueuePartitions(ListTaskQueuePartitionsRequest) returns (ListTaskQueuePartitionsResponse){ - } - - // (-- api-linter: core::0134::response-message-name=disabled - // aip.dev/not-precedent: UpdateWorkerBuildIdCompatibility RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::method-signature=disabled - // aip.dev/not-precedent: UpdateWorkerBuildIdCompatibility RPC doesn't follow Google API format. --) - rpc UpdateWorkerBuildIdCompatibility (UpdateWorkerBuildIdCompatibilityRequest) returns (UpdateWorkerBuildIdCompatibilityResponse) {} - - rpc GetWorkerBuildIdCompatibility (GetWorkerBuildIdCompatibilityRequest) returns (GetWorkerBuildIdCompatibilityResponse) {} - - // Fetch user data for a task queue, this request should always be routed to the node holding the root partition of the workflow task queue. - rpc GetTaskQueueUserData (GetTaskQueueUserDataRequest) returns (GetTaskQueueUserDataResponse) {} - - // Allows updating the Build ID assignment and redirect rules for a given Task Queue. - // (-- api-linter: core::0134::method-signature=disabled - // aip.dev/not-precedent: UpdateWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::response-message-name=disabled - // aip.dev/not-precedent: UpdateWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) - rpc UpdateWorkerVersioningRules (UpdateWorkerVersioningRulesRequest) returns (UpdateWorkerVersioningRulesResponse) {} - - // Fetches the Build ID assignment and redirect rules for a Task Queue - // (-- api-linter: core::0127::resource-name-extraction=disabled - // aip.dev/not-precedent: GetWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) - // (-- api-linter: core::0131::http-uri-name=disabled - // aip.dev/not-precedent: GetWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) - rpc GetWorkerVersioningRules (GetWorkerVersioningRulesRequest) returns (GetWorkerVersioningRulesResponse) { } - - // This request should always be routed to the node holding the root partition of the workflow task queue. - rpc SyncDeploymentUserData (SyncDeploymentUserDataRequest) returns (SyncDeploymentUserDataResponse) {} - - // Apply a user data replication event. - rpc ApplyTaskQueueUserDataReplicationEvent (ApplyTaskQueueUserDataReplicationEventRequest) returns (ApplyTaskQueueUserDataReplicationEventResponse) {} - - // Gets all task queue names mapped to a given build ID - rpc GetBuildIdTaskQueueMapping (GetBuildIdTaskQueueMappingRequest) returns (GetBuildIdTaskQueueMappingResponse) {} - // Force loading a task queue partition. Used by matching node owning root partition. - // When root partition is loaded this is called for all child partitions. - // This addresses the posibility of unloaded child partitions having backlog, - // but not being forwarded/synced to the root partition to find the polling - // worker which triggered the root partition being loaded in the first place. - rpc ForceLoadTaskQueuePartition (ForceLoadTaskQueuePartitionRequest) returns (ForceLoadTaskQueuePartitionResponse) {} - - // TODO Shivam - remove this in 123. Present for backwards compatibility. - rpc ForceUnloadTaskQueue (ForceUnloadTaskQueueRequest) returns (ForceUnloadTaskQueueResponse) {} - - // Force unloading a task queue partition. - rpc ForceUnloadTaskQueuePartition (ForceUnloadTaskQueuePartitionRequest) returns (ForceUnloadTaskQueuePartitionResponse) {} - - // Update task queue user data in owning node for all updates in namespace. - // All user data updates must first go through the task queue owner using the `UpdateWorkerBuildIdCompatibility` - // API. - // (-- api-linter: core::0134::response-message-name=disabled - // aip.dev/not-precedent: UpdateTaskQueueUserData RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::method-signature=disabled - // aip.dev/not-precedent: UpdateTaskQueueUserData RPC doesn't follow Google API format. --) - rpc UpdateTaskQueueUserData(UpdateTaskQueueUserDataRequest) returns (UpdateTaskQueueUserDataResponse) {} - - // Replicate task queue user data across clusters, must be done via the owning node for updates in namespace. - rpc ReplicateTaskQueueUserData(ReplicateTaskQueueUserDataRequest) returns (ReplicateTaskQueueUserDataResponse) {} - - // Blocks on user data propagation to all loaded partitions. If successful, all loaded - // workflow + activity partitions have the requested version or higher. - // Routed to user data owner (root partition of workflow task queue). - rpc CheckTaskQueueUserDataPropagation(CheckTaskQueueUserDataPropagationRequest) returns (CheckTaskQueueUserDataPropagationResponse) {} - - // Create a Nexus endpoint. - // (-- api-linter: core::0133::method-signature=disabled - // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) - // (-- api-linter: core::0133::response-message-name=disabled - // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) - // (-- api-linter: core::0133::http-uri-parent=disabled - // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) - rpc CreateNexusEndpoint(CreateNexusEndpointRequest) returns (CreateNexusEndpointResponse) {} - // Optimistically update a Nexus endpoint based on provided version. - // If this request is accepted, the input is considered the "current" state of this service at the time it was - // persisted and the updated version is returned. - // (-- api-linter: core::0134::method-signature=disabled - // aip.dev/not-precedent: UpdateNexusEndpoint RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::response-message-name=disabled - // aip.dev/not-precedent: UpdateNexusEndpoint RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::request-resource-required=disabled - // aip.dev/not-precedent: UpdateNexusEndpoint RPC doesn't follow Google API format. --) - rpc UpdateNexusEndpoint(UpdateNexusEndpointRequest) returns (UpdateNexusEndpointResponse) {} - // Delete a service by its name. - rpc DeleteNexusEndpoint(DeleteNexusEndpointRequest) returns (DeleteNexusEndpointResponse) {} - // List all registered services. - rpc ListNexusEndpoints(ListNexusEndpointsRequest) returns (ListNexusEndpointsResponse) {} - - // RecordWorkerHeartbeat receive heartbeat request from the worker. - rpc RecordWorkerHeartbeat (RecordWorkerHeartbeatRequest) returns (RecordWorkerHeartbeatResponse) {} - - // ListWorkers retrieves a list of workers in the specified namespace that match the provided filters. - // Supports pagination for large result sets. Returns an empty list if no workers match the criteria. - // Returns an error if the namespace doesn't exist. - rpc ListWorkers (ListWorkersRequest) returns (ListWorkersResponse) {} - - // Set the persisted task queue configuration. - // (-- api-linter: core::0134::method-signature=disabled - // aip.dev/not-precedent: UpdateTaskQueueConfig RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::response-message-name=disabled - // aip.dev/not-precedent: UpdateTaskQueueConfig RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::request-resource-required=disabled - // aip.dev/not-precedent: UpdateTaskQueueConfig RPC doesn't follow Google API format. --) - rpc UpdateTaskQueueConfig (UpdateTaskQueueConfigRequest) returns (UpdateTaskQueueConfigResponse) {} - - // DescribeWorker retrieves a worker information in the specified namespace that match the provided instance key. - // Returns an error if the namespace or worker doesn't exist. - rpc DescribeWorker (DescribeWorkerRequest) returns (DescribeWorkerResponse) {} - - // UpdateFairnessState changes the fairness_state stored in UserData for automatically enabling - // priority and fairness. - // (-- api-linter: core::0134::method-signature=disabled - // aip.dev/not-precedent: UpdateFairnessState RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::response-message-name=disabled - // aip.dev/not-precedent: UpdateFairnessState RPC doesn't follow Google API format. --) - // (-- api-linter: core::0134::request-resource-required=disabled - // aip.dev/not-precedent: UpdateFairnessState RPC doesn't follow Google API format. --) - rpc UpdateFairnessState(UpdateFairnessStateRequest) returns (UpdateFairnessStateResponse) {} - - // CheckTaskQueueVersionMembership checks if a task queue is part of a specific deployment version. - rpc CheckTaskQueueVersionMembership(CheckTaskQueueVersionMembershipRequest) returns (CheckTaskQueueVersionMembershipResponse) {} - + // PollWorkflowTaskQueue is called by frontend to process WorkflowTask from a specific task queue. A + // WorkflowTask is dispatched to callers for active workflow executions, with pending workflow tasks. + rpc PollWorkflowTaskQueue(PollWorkflowTaskQueueRequest) returns (PollWorkflowTaskQueueResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // PollActivityTaskQueue is called by frontend to process ActivityTask from a specific task queue. ActivityTask + // is dispatched to callers whenever a ScheduleTask command is made for a workflow execution. + rpc PollActivityTaskQueue(PollActivityTaskQueueRequest) returns (PollActivityTaskQueueResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // AddWorkflowTask is called by the history service when a workflow task is scheduled, so that it can be dispatched + // by the MatchingEngine. + rpc AddWorkflowTask(AddWorkflowTaskRequest) returns (AddWorkflowTaskResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // AddActivityTask is called by the history service when a workflow task is scheduled, so that it can be dispatched + // by the MatchingEngine. + rpc AddActivityTask(AddActivityTaskRequest) returns (AddActivityTaskResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // QueryWorkflow is called by frontend to query a workflow. + rpc QueryWorkflow(QueryWorkflowRequest) returns (QueryWorkflowResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // RespondQueryTaskCompleted is called by frontend to respond query completed. + rpc RespondQueryTaskCompleted(RespondQueryTaskCompletedRequest) returns (RespondQueryTaskCompletedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Request from frontend to synchronously dispatch a nexus task to a worker. + rpc DispatchNexusTask(DispatchNexusTaskRequest) returns (DispatchNexusTaskResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Request from worker (via frontend) to long poll on nexus tasks. + rpc PollNexusTaskQueue(PollNexusTaskQueueRequest) returns (PollNexusTaskQueueResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + // Response from a worker (via frontend) to a Nexus task, unblocks the corresponding DispatchNexusTask request. + rpc RespondNexusTaskCompleted(RespondNexusTaskCompletedRequest) returns (RespondNexusTaskCompletedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Response from a worker (via frontend) to a Nexus task, unblocks the corresponding DispatchNexusTask request. + rpc RespondNexusTaskFailed(RespondNexusTaskFailedRequest) returns (RespondNexusTaskFailedResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // CancelOutstandingPoll is called by frontend to unblock long polls on matching for zombie pollers. + // Our rpc stack does not support context propagation, so when a client connection goes away frontend sees + // cancellation of context for that handler, but any corresponding calls (long-poll) to matching service does not + // see the cancellation propagated so it can unblock corresponding long-polls on its end. This results is tasks + // being dispatched to zombie pollers in this situation. This API is added so every time frontend makes a long-poll + // api call to matching it passes in a pollerId and then calls this API when it detects client connection is closed + // to unblock long polls for this poller and prevent tasks being sent to these zombie pollers. + rpc CancelOutstandingPoll(CancelOutstandingPollRequest) returns (CancelOutstandingPollResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // CancelOutstandingWorkerPolls cancels any outstanding polls for a given worker instance key. + // These polls could be waiting on different partitions of the task queue. + // This is called during worker shutdown to eagerly cancel polls and avoid giving out tasks to workers that are shutting down. + // Note: This only cancels polls that are currently outstanding. The caller must ensure no new polls + // are issued after calling this RPC, otherwise those polls will not be cancelled. + rpc CancelOutstandingWorkerPolls(CancelOutstandingWorkerPollsRequest) returns (CancelOutstandingWorkerPollsResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DescribeTaskQueue returns information about the target task queue, right now this API returns the + // pollers which polled this task queue in last few minutes. + rpc DescribeTaskQueue(DescribeTaskQueueRequest) returns (DescribeTaskQueueResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DescribeTaskQueuePartition returns information about the target task queue partition. + rpc DescribeTaskQueuePartition(DescribeTaskQueuePartitionRequest) returns (DescribeTaskQueuePartitionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DescribeVersionedTaskQueues returns details about the requested versioned task queues. + // It is an internal API; there is no direct user-facing equivalent. + rpc DescribeVersionedTaskQueues(DescribeVersionedTaskQueuesRequest) returns (DescribeVersionedTaskQueuesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ListTaskQueuePartitions returns a map of partitionKey and hostAddress for a task queue. + rpc ListTaskQueuePartitions(ListTaskQueuePartitionsRequest) returns (ListTaskQueuePartitionsResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // (-- api-linter: core::0134::response-message-name=disabled + // aip.dev/not-precedent: UpdateWorkerBuildIdCompatibility RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::method-signature=disabled + // aip.dev/not-precedent: UpdateWorkerBuildIdCompatibility RPC doesn't follow Google API format. --) + rpc UpdateWorkerBuildIdCompatibility(UpdateWorkerBuildIdCompatibilityRequest) returns (UpdateWorkerBuildIdCompatibilityResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc GetWorkerBuildIdCompatibility(GetWorkerBuildIdCompatibilityRequest) returns (GetWorkerBuildIdCompatibilityResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Fetch user data for a task queue, this request should always be routed to the node holding the root partition of the workflow task queue. + rpc GetTaskQueueUserData(GetTaskQueueUserDataRequest) returns (GetTaskQueueUserDataResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Allows updating the Build ID assignment and redirect rules for a given Task Queue. + // (-- api-linter: core::0134::method-signature=disabled + // aip.dev/not-precedent: UpdateWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::response-message-name=disabled + // aip.dev/not-precedent: UpdateWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) + rpc UpdateWorkerVersioningRules(UpdateWorkerVersioningRulesRequest) returns (UpdateWorkerVersioningRulesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Fetches the Build ID assignment and redirect rules for a Task Queue + // (-- api-linter: core::0127::resource-name-extraction=disabled + // aip.dev/not-precedent: GetWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) + // (-- api-linter: core::0131::http-uri-name=disabled + // aip.dev/not-precedent: GetWorkerVersioningRulesRequest RPC doesn't follow Google API format. --) + rpc GetWorkerVersioningRules(GetWorkerVersioningRulesRequest) returns (GetWorkerVersioningRulesResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // This request should always be routed to the node holding the root partition of the workflow task queue. + rpc SyncDeploymentUserData(SyncDeploymentUserDataRequest) returns (SyncDeploymentUserDataResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Apply a user data replication event. + rpc ApplyTaskQueueUserDataReplicationEvent(ApplyTaskQueueUserDataReplicationEventRequest) returns (ApplyTaskQueueUserDataReplicationEventResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Gets all task queue names mapped to a given build ID + rpc GetBuildIdTaskQueueMapping(GetBuildIdTaskQueueMappingRequest) returns (GetBuildIdTaskQueueMappingResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + // Force loading a task queue partition. Used by matching node owning root partition. + // When root partition is loaded this is called for all child partitions. + // This addresses the posibility of unloaded child partitions having backlog, + // but not being forwarded/synced to the root partition to find the polling + // worker which triggered the root partition being loaded in the first place. + rpc ForceLoadTaskQueuePartition(ForceLoadTaskQueuePartitionRequest) returns (ForceLoadTaskQueuePartitionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // TODO Shivam - remove this in 123. Present for backwards compatibility. + rpc ForceUnloadTaskQueue(ForceUnloadTaskQueueRequest) returns (ForceUnloadTaskQueueResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Force unloading a task queue partition. + rpc ForceUnloadTaskQueuePartition(ForceUnloadTaskQueuePartitionRequest) returns (ForceUnloadTaskQueuePartitionResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Update task queue user data in owning node for all updates in namespace. + // All user data updates must first go through the task queue owner using the `UpdateWorkerBuildIdCompatibility` + // API. + // (-- api-linter: core::0134::response-message-name=disabled + // aip.dev/not-precedent: UpdateTaskQueueUserData RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::method-signature=disabled + // aip.dev/not-precedent: UpdateTaskQueueUserData RPC doesn't follow Google API format. --) + rpc UpdateTaskQueueUserData(UpdateTaskQueueUserDataRequest) returns (UpdateTaskQueueUserDataResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Replicate task queue user data across clusters, must be done via the owning node for updates in namespace. + rpc ReplicateTaskQueueUserData(ReplicateTaskQueueUserDataRequest) returns (ReplicateTaskQueueUserDataResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Blocks on user data propagation to all loaded partitions. If successful, all loaded + // workflow + activity partitions have the requested version or higher. + // Routed to user data owner (root partition of workflow task queue). + rpc CheckTaskQueueUserDataPropagation(CheckTaskQueueUserDataPropagationRequest) returns (CheckTaskQueueUserDataPropagationResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Create a Nexus endpoint. + // (-- api-linter: core::0133::method-signature=disabled + // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) + // (-- api-linter: core::0133::response-message-name=disabled + // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) + // (-- api-linter: core::0133::http-uri-parent=disabled + // aip.dev/not-precedent: CreateNexusEndpoint RPC doesn't follow Google API format. --) + rpc CreateNexusEndpoint(CreateNexusEndpointRequest) returns (CreateNexusEndpointResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + // Optimistically update a Nexus endpoint based on provided version. + // If this request is accepted, the input is considered the "current" state of this service at the time it was + // persisted and the updated version is returned. + // (-- api-linter: core::0134::method-signature=disabled + // aip.dev/not-precedent: UpdateNexusEndpoint RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::response-message-name=disabled + // aip.dev/not-precedent: UpdateNexusEndpoint RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::request-resource-required=disabled + // aip.dev/not-precedent: UpdateNexusEndpoint RPC doesn't follow Google API format. --) + rpc UpdateNexusEndpoint(UpdateNexusEndpointRequest) returns (UpdateNexusEndpointResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + // Delete a service by its name. + rpc DeleteNexusEndpoint(DeleteNexusEndpointRequest) returns (DeleteNexusEndpointResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + // List all registered services. + rpc ListNexusEndpoints(ListNexusEndpointsRequest) returns (ListNexusEndpointsResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // RecordWorkerHeartbeat receive heartbeat request from the worker. + rpc RecordWorkerHeartbeat(RecordWorkerHeartbeatRequest) returns (RecordWorkerHeartbeatResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // ListWorkers retrieves a list of workers in the specified namespace that match the provided filters. + // Supports pagination for large result sets. Returns an empty list if no workers match the criteria. + // Returns an error if the namespace doesn't exist. + rpc ListWorkers(ListWorkersRequest) returns (ListWorkersResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Set the persisted task queue configuration. + // (-- api-linter: core::0134::method-signature=disabled + // aip.dev/not-precedent: UpdateTaskQueueConfig RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::response-message-name=disabled + // aip.dev/not-precedent: UpdateTaskQueueConfig RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::request-resource-required=disabled + // aip.dev/not-precedent: UpdateTaskQueueConfig RPC doesn't follow Google API format. --) + rpc UpdateTaskQueueConfig(UpdateTaskQueueConfigRequest) returns (UpdateTaskQueueConfigResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // DescribeWorker retrieves a worker information in the specified namespace that match the provided instance key. + // Returns an error if the namespace or worker doesn't exist. + rpc DescribeWorker(DescribeWorkerRequest) returns (DescribeWorkerResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // UpdateFairnessState changes the fairness_state stored in UserData for automatically enabling + // priority and fairness. + // (-- api-linter: core::0134::method-signature=disabled + // aip.dev/not-precedent: UpdateFairnessState RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::response-message-name=disabled + // aip.dev/not-precedent: UpdateFairnessState RPC doesn't follow Google API format. --) + // (-- api-linter: core::0134::request-resource-required=disabled + // aip.dev/not-precedent: UpdateFairnessState RPC doesn't follow Google API format. --) + rpc UpdateFairnessState(UpdateFairnessStateRequest) returns (UpdateFairnessStateResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // CheckTaskQueueVersionMembership checks if a task queue is part of a specific deployment version. + rpc CheckTaskQueueVersionMembership(CheckTaskQueueVersionMembershipRequest) returns (CheckTaskQueueVersionMembershipResponse) { + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } } - diff --git a/proto/internal/temporal/server/api/metrics/v1/message.proto b/proto/internal/temporal/server/api/metrics/v1/message.proto index 3ec31791e84..5d290db0e8e 100644 --- a/proto/internal/temporal/server/api/metrics/v1/message.proto +++ b/proto/internal/temporal/server/api/metrics/v1/message.proto @@ -5,5 +5,5 @@ package temporal.server.api.metrics.v1; option go_package = "go.temporal.io/server/api/metrics/v1;metrics"; message Baggage { - map counters_int = 1; + map counters_int = 1; } diff --git a/proto/internal/temporal/server/api/namespace/v1/message.proto b/proto/internal/temporal/server/api/namespace/v1/message.proto index 5add2b28f38..2b50bb28770 100644 --- a/proto/internal/temporal/server/api/namespace/v1/message.proto +++ b/proto/internal/temporal/server/api/namespace/v1/message.proto @@ -5,10 +5,10 @@ package temporal.server.api.namespace.v1; option go_package = "go.temporal.io/server/api/namespace/v1;namespace"; message NamespaceCacheInfo { - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "in" and "by" are needed here. --) - int64 items_in_cache_by_id_count = 1; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "in" and "by" are needed here. --) - int64 items_in_cache_by_name_count = 2; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "in" and "by" are needed here. --) + int64 items_in_cache_by_id_count = 1; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "in" and "by" are needed here. --) + int64 items_in_cache_by_name_count = 2; } diff --git a/proto/internal/temporal/server/api/persistence/v1/chasm.proto b/proto/internal/temporal/server/api/persistence/v1/chasm.proto index 85db47d9d6b..f557cdcb967 100644 --- a/proto/internal/temporal/server/api/persistence/v1/chasm.proto +++ b/proto/internal/temporal/server/api/persistence/v1/chasm.proto @@ -1,68 +1,69 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/failure/v1/message.proto"; import "temporal/server/api/persistence/v1/hsm.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + message ChasmNode { - // Metadata present for all nodes. - ChasmNodeMetadata metadata = 1; + // Metadata present for all nodes. + ChasmNodeMetadata metadata = 1; - // User data for any type of node that stores it. - temporal.api.common.v1.DataBlob data = 2; + // User data for any type of node that stores it. + temporal.api.common.v1.DataBlob data = 2; } message ChasmNodeMetadata { - // Versioned transition when the node was instantiated. - VersionedTransition initial_versioned_transition = 1; - // Versioned transition when the node was last updated. - VersionedTransition last_update_versioned_transition = 2; - - oneof attributes { - ChasmComponentAttributes component_attributes = 11; - ChasmDataAttributes data_attributes = 12; - ChasmCollectionAttributes collection_attributes = 13; - ChasmPointerAttributes pointer_attributes = 14; - } + // Versioned transition when the node was instantiated. + VersionedTransition initial_versioned_transition = 1; + // Versioned transition when the node was last updated. + VersionedTransition last_update_versioned_transition = 2; + + oneof attributes { + ChasmComponentAttributes component_attributes = 11; + ChasmDataAttributes data_attributes = 12; + ChasmCollectionAttributes collection_attributes = 13; + ChasmPointerAttributes pointer_attributes = 14; + } } message ChasmComponentAttributes { - message Task { - // Registered task's type ID. - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 type_id = 1; - string destination = 2; - google.protobuf.Timestamp scheduled_time = 3; - temporal.api.common.v1.DataBlob data = 4; - // Versioned transition of the execution when the task was created. - VersionedTransition versioned_transition = 5; - // The xth task generated in this versioned transition. - // Together with the versioned transition, this is a unique identifier for - // this task. - int64 versioned_transition_offset = 6; - // If a physical task is created for this task in this cluster. - // NOTE: this is a cluster-specific field and can not be replicated. - // Changes to this field also doesn't require an increase in versioned transition. - int32 physical_task_status = 7; - } - - // Registered component's type ID. + message Task { + // Registered task's type ID. // (-- api-linter: core::0141::forbidden-types=disabled --) uint32 type_id = 1; - // Tasks are in their insertion order, - // i.e. by versioned transtion and versioned_transition_offset. - repeated Task side_effect_tasks = 2; - // Tasks are ordered by their scheduled time, breaking ties by - // versioned transition and versioned_transition_offset. - repeated Task pure_tasks = 3; - // When true, this component ignores parent lifecycle validation. - // Detached components can continue operating, accepting writes and executing - // tasks, even when their parent is closed/terminated. - bool detached = 4; + string destination = 2; + google.protobuf.Timestamp scheduled_time = 3; + temporal.api.common.v1.DataBlob data = 4; + // Versioned transition of the execution when the task was created. + VersionedTransition versioned_transition = 5; + // The xth task generated in this versioned transition. + // Together with the versioned transition, this is a unique identifier for + // this task. + int64 versioned_transition_offset = 6; + // If a physical task is created for this task in this cluster. + // NOTE: this is a cluster-specific field and can not be replicated. + // Changes to this field also doesn't require an increase in versioned transition. + int32 physical_task_status = 7; + } + + // Registered component's type ID. + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 type_id = 1; + // Tasks are in their insertion order, + // i.e. by versioned transtion and versioned_transition_offset. + repeated Task side_effect_tasks = 2; + // Tasks are ordered by their scheduled time, breaking ties by + // versioned transition and versioned_transition_offset. + repeated Task pure_tasks = 3; + // When true, this component ignores parent lifecycle validation. + // Detached components can continue operating, accepting writes and executing + // tasks, even when their parent is closed/terminated. + bool detached = 4; } message ChasmDataAttributes {} @@ -70,62 +71,61 @@ message ChasmDataAttributes {} message ChasmCollectionAttributes {} message ChasmPointerAttributes { - repeated string node_path = 1; + repeated string node_path = 1; } - // ChasmTaskInfo includes component-facing task metadata message ChasmTaskInfo { - // Initial versioned transition of the component being referenced. - VersionedTransition component_initial_versioned_transition = 1; + // Initial versioned transition of the component being referenced. + VersionedTransition component_initial_versioned_transition = 1; - // Last updated transition of the component being referenced at the time the - // reference was created. Can be used to invalidate this reference. - VersionedTransition component_last_update_versioned_transition = 2; + // Last updated transition of the component being referenced at the time the + // reference was created. Can be used to invalidate this reference. + VersionedTransition component_last_update_versioned_transition = 2; - // Path to the component. - repeated string path = 3; + // Path to the component. + repeated string path = 3; - // Registered task's type ID. - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 type_id = 4; + // Registered task's type ID. + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 type_id = 4; - // Opaque attached task data. May be nil. Usable by components, not the CHASM - // framework itself. - temporal.api.common.v1.DataBlob data = 5; + // Opaque attached task data. May be nil. Usable by components, not the CHASM + // framework itself. + temporal.api.common.v1.DataBlob data = 5; - // ArchetypeID of the execution that generated this task. - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 6; + // ArchetypeID of the execution that generated this task. + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 6; } // ChasmComponentRef references a specific chasm component. message ChasmComponentRef { - string namespace_id = 1; - string business_id = 2; - string run_id = 3; + string namespace_id = 1; + string business_id = 2; + string run_id = 3; - // Executions's root component's type ID. - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 4; - - VersionedTransition execution_versioned_transition = 5; + // Executions's root component's type ID. + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 4; + + VersionedTransition execution_versioned_transition = 5; - repeated string component_path = 6; - VersionedTransition component_initial_versioned_transition = 7; + repeated string component_path = 6; + VersionedTransition component_initial_versioned_transition = 7; } // ChasmNexusCompletion includes details about a completed Nexus operation. message ChasmNexusCompletion { - oneof outcome { - // Result of a successful operation, only set if state == successful. - temporal.api.common.v1.Payload success = 1; - // Operation failure, only set if state != successful. - temporal.api.failure.v1.Failure failure = 2; - } - // Time when the operation was closed. - google.protobuf.Timestamp close_time = 3; - // Request ID embedded in the NexusOperationScheduledEvent. - // Allows completing a started operation after a workflow has been reset. - string request_id = 4; + oneof outcome { + // Result of a successful operation, only set if state == successful. + temporal.api.common.v1.Payload success = 1; + // Operation failure, only set if state != successful. + temporal.api.failure.v1.Failure failure = 2; + } + // Time when the operation was closed. + google.protobuf.Timestamp close_time = 3; + // Request ID embedded in the NexusOperationScheduledEvent. + // Allows completing a started operation after a workflow has been reset. + string request_id = 4; } diff --git a/proto/internal/temporal/server/api/persistence/v1/chasm_visibility.proto b/proto/internal/temporal/server/api/persistence/v1/chasm_visibility.proto index 6254a50426e..cb475fbe195 100644 --- a/proto/internal/temporal/server/api/persistence/v1/chasm_visibility.proto +++ b/proto/internal/temporal/server/api/persistence/v1/chasm_visibility.proto @@ -1,12 +1,13 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; + option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; message ChasmVisibilityData { - int64 transition_count = 1; + int64 transition_count = 1; } message ChasmVisibilityTaskData { - int64 transition_count = 1; + int64 transition_count = 1; } diff --git a/proto/internal/temporal/server/api/persistence/v1/cluster_metadata.proto b/proto/internal/temporal/server/api/persistence/v1/cluster_metadata.proto index 952aefaba22..82f69aa0eac 100644 --- a/proto/internal/temporal/server/api/persistence/v1/cluster_metadata.proto +++ b/proto/internal/temporal/server/api/persistence/v1/cluster_metadata.proto @@ -1,30 +1,31 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "temporal/api/enums/v1/common.proto"; import "temporal/api/version/v1/message.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // data column message ClusterMetadata { - string cluster_name = 1; - int32 history_shard_count = 2; - string cluster_id = 3; - temporal.api.version.v1.VersionInfo version_info = 4; - map index_search_attributes = 5; - string cluster_address = 6; - string http_address = 13; - int64 failover_version_increment = 7; - int64 initial_failover_version = 8; - bool is_global_namespace_enabled = 9; - bool is_connection_enabled = 10; - bool use_cluster_id_membership = 11; - map tags = 12; - // is_replication_enabled controls whether replication streams are active. - bool is_replication_enabled = 14; + string cluster_name = 1; + int32 history_shard_count = 2; + string cluster_id = 3; + temporal.api.version.v1.VersionInfo version_info = 4; + map index_search_attributes = 5; + string cluster_address = 6; + string http_address = 13; + int64 failover_version_increment = 7; + int64 initial_failover_version = 8; + bool is_global_namespace_enabled = 9; + bool is_connection_enabled = 10; + bool use_cluster_id_membership = 11; + map tags = 12; + // is_replication_enabled controls whether replication streams are active. + bool is_replication_enabled = 14; } -message IndexSearchAttributes{ - map custom_search_attributes = 1; +message IndexSearchAttributes { + map custom_search_attributes = 1; } diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index d60626da7fb..9098d551c45 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -1,493 +1,519 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; - import "temporal/api/common/v1/message.proto"; +import "temporal/api/deployment/v1/message.proto"; import "temporal/api/enums/v1/common.proto"; import "temporal/api/enums/v1/event_type.proto"; import "temporal/api/enums/v1/failed_cause.proto"; import "temporal/api/enums/v1/workflow.proto"; import "temporal/api/failure/v1/message.proto"; -import "temporal/api/workflow/v1/message.proto"; import "temporal/api/history/v1/message.proto"; -import "temporal/api/deployment/v1/message.proto"; import "temporal/api/worker/v1/message.proto"; +import "temporal/api/workflow/v1/message.proto"; import "temporal/server/api/clock/v1/message.proto"; import "temporal/server/api/enums/v1/common.proto"; import "temporal/server/api/enums/v1/nexus.proto"; -import "temporal/server/api/enums/v1/workflow.proto"; import "temporal/server/api/enums/v1/task.proto"; +import "temporal/server/api/enums/v1/workflow.proto"; import "temporal/server/api/enums/v1/workflow_task_type.proto"; import "temporal/server/api/history/v1/message.proto"; import "temporal/server/api/persistence/v1/chasm.proto"; -import "temporal/server/api/persistence/v1/queues.proto"; import "temporal/server/api/persistence/v1/hsm.proto"; +import "temporal/server/api/persistence/v1/queues.proto"; import "temporal/server/api/persistence/v1/update.proto"; import "temporal/server/api/workflow/v1/message.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // shard column message ShardInfo { - int32 shard_id = 1; - int64 range_id = 2; - string owner = 3; - reserved 4; - reserved 5; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "since" is needed here. --) - int32 stolen_since_renew = 6; - google.protobuf.Timestamp update_time = 7; - reserved 8; - reserved 9; - reserved 10; - reserved 11; - reserved 12; - map replication_dlq_ack_level = 13; - reserved 14; - reserved 15; - reserved 16; - map queue_states = 17; + int32 shard_id = 1; + int64 range_id = 2; + string owner = 3; + reserved 4; + reserved 5; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "since" is needed here. --) + int32 stolen_since_renew = 6; + google.protobuf.Timestamp update_time = 7; + reserved 8; + reserved 9; + reserved 10; + reserved 11; + reserved 12; + map replication_dlq_ack_level = 13; + reserved 14; + reserved 15; + reserved 16; + map queue_states = 17; } // execution column message WorkflowExecutionInfo { - string namespace_id = 1; - string workflow_id = 2; - string parent_namespace_id = 3; - string parent_workflow_id = 4; - string parent_run_id = 5; - int64 parent_initiated_id = 6; - int64 completion_event_batch_id = 7; - reserved 8; - string task_queue = 9; - string workflow_type_name = 10; - google.protobuf.Duration workflow_execution_timeout = 11; - google.protobuf.Duration workflow_run_timeout = 12; - google.protobuf.Duration default_workflow_task_timeout = 13; - reserved 14; - reserved 15; - reserved 16; - int64 last_running_clock = 17; - int64 last_first_event_id = 18; - int64 last_completed_workflow_task_started_event_id = 19; - // Deprecated. use `WorkflowExecutionState.start_time` - google.protobuf.Timestamp start_time = 20; - google.protobuf.Timestamp last_update_time = 21; - - // Workflow task fields. - int64 workflow_task_version = 22; - int64 workflow_task_scheduled_event_id = 23; - int64 workflow_task_started_event_id = 24; - google.protobuf.Duration workflow_task_timeout = 25; - int32 workflow_task_attempt = 26; - google.protobuf.Timestamp workflow_task_started_time = 27; - google.protobuf.Timestamp workflow_task_scheduled_time = 28; - google.protobuf.Timestamp workflow_task_original_scheduled_time = 30; - string workflow_task_request_id = 31; - temporal.server.api.enums.v1.WorkflowTaskType workflow_task_type = 68; - bool workflow_task_suggest_continue_as_new = 69; - repeated temporal.api.enums.v1.SuggestContinueAsNewReason workflow_task_suggest_continue_as_new_reasons = 110; - bool workflow_task_target_worker_deployment_version_changed = 112; - int64 workflow_task_history_size_bytes = 70; - // tracks the started build ID for transient/speculative WFT. This info is used for two purposes: - // - verify WFT completes by the same Build ID that started in the latest attempt - // - when persisting transient/speculative WFT, the right Build ID is used in the WFT started event - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - string workflow_task_build_id = 88; - // tracks the started build ID redirect counter for transient/speculative WFT. This info is to - // ensure the right redirect counter is used in the WFT started event created later - // for a transient/speculative WFT. - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - int64 workflow_task_build_id_redirect_counter = 89; - // Stamp represents the "version" of the workflow's internal state. - // It increases monotonically when the workflow's options are modified. - // It is used to check if a workflow task is still relevant to the corresponding workflow state machine. - int32 workflow_task_stamp = 109; - // AttemptsSinceLastSuccess tracks the number of workflow task attempts since the last successful workflow task. - // This is carried over when buffered events are applied after workflow task failures. - // Used by the TemporalReportedProblems search attribute to track continuous failure count. - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "since" is needed here. --) - int32 workflow_task_attempts_since_last_success = 111; - - bool cancel_requested = 29; - string cancel_request_id = 32; - string sticky_task_queue = 33; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration sticky_schedule_to_start_timeout = 34; - int32 attempt = 35; - google.protobuf.Duration retry_initial_interval = 36; - google.protobuf.Duration retry_maximum_interval = 37; - int32 retry_maximum_attempts = 38; - double retry_backoff_coefficient = 39; - google.protobuf.Timestamp workflow_execution_expiration_time = 40; - repeated string retry_non_retryable_error_types = 41; - bool has_retry_policy = 42; - string cron_schedule = 43; - reserved 44; - reserved 45; - int64 signal_count = 46; - int64 activity_count = 71; - int64 child_execution_count = 72; - int64 user_timer_count = 73; - int64 request_cancel_external_count = 74; - int64 signal_external_count = 75; - int64 update_count = 77; - reserved 47; - reserved 48; - reserved 49; - reserved 50; - temporal.api.workflow.v1.ResetPoints auto_reset_points = 51; - map search_attributes = 52; - map memo = 53; - temporal.server.api.history.v1.VersionHistories version_histories = 54; - string first_execution_run_id = 55; - ExecutionStats execution_stats = 56; - google.protobuf.Timestamp workflow_run_expiration_time = 57; - // Transaction Id of the first event in the last batch of events. - int64 last_first_event_txn_id = 58; - int64 state_transition_count = 59; - google.protobuf.Timestamp execution_time = 60; - // If continued-as-new, or retried, or cron, holds the new run id. - string new_execution_run_id = 61; - temporal.server.api.clock.v1.VectorClock parent_clock = 62; - // version of child execution initiated event in parent workflow - int64 parent_initiated_version = 63; - // Used to check if transfer close task is processed before deleting the workflow execution. - int64 close_transfer_task_id = 64; - // Used to check if visibility close task is processed before deleting the workflow execution. - int64 close_visibility_task_id = 65; - google.protobuf.Timestamp close_time = 66; - // Relocatable attributes are memo and search attributes. If they were removed, then they are not - // present in the mutable state, and they should be in visibility store. - bool relocatable_attributes_removed = 67; - temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 76; - // If using build-id based versioning: version stamp of the last worker to complete a - // workflow tasks for this workflow. - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - temporal.api.common.v1.WorkerVersionStamp most_recent_worker_version_stamp = 78; - // The currently assigned build ID for this execution. Presence of this value means worker versioning is used - // for this execution. Assigned build ID is selected by matching based on Worker Versioning Assignment Rules - // when the first workflow task of the execution is scheduled. If the first workflow task fails and is scheduled - // again, the assigned build ID may change according to the latest versioning rules. - // Assigned build ID can also change in the middle of a execution if Compatible Redirect Rules are applied to - // this execution. - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - string assigned_build_id = 85; - // Build ID inherited from a previous/parent execution. If present, assigned_build_id will be set to this, instead - // of using the assignment rules. - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - string inherited_build_id = 86; - // Tracks the number of times a redirect rule is applied to this workflow. Used to apply redirects in the right - // order when mutable state is rebuilt from history events. - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - int64 build_id_redirect_counter = 87; - - // index of update IDs and pointers to associated history events. - map update_infos = 79; - - // Transition history encodes all transitions a mutable state object has gone through in a compact way. - // Here the transition_count field of VersionedTransition represents the maximum transition count the mutable state object - // has gone through for the corresponding namespace failover version. - // For example, if the transition history is `[{v: 1, t: 3}, {v: 2, t: 5}]`, it means transition 1-3 have failover version 1, - // and transition 4-5 have failover version 2. - // - // Each task generated by the HSM framework is imprinted with the current VersionedTransition at the end of the transaction. - // When a task is being processed, the transition history is compared with the imprinted task information to - // verify that a task is not referencing a stale state or that the task itself is not stale. - // For example, with the same transition history above, task A `{v: 2, t: 4}` **is not** - // referencing stale state because for version `2` transitions `4-5` are valid, while task B `{v: 2, t: 6}` **is** - // referencing stale state because the transition count is out of range for version `2`. - // Furthermore, task C `{v: 1, t: 4}` itself is stale because it is referencing an impossible state, likely due to post - // split-brain reconciliation. - repeated VersionedTransition transition_history = 80; - // Map of state machine type to map of machine by ID. - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "by" is used to clarify the keys and values. --) - map sub_state_machines_by_type = 81; - - // This field is for tracking if the workflow execution timer task is created or not. - // We don't need this field if we always create the execution timer task when the first - // workflow in a workflow chain starts. However, this execution timer logic is later added. - // To maintain backward compatibility, we need to track if the execution timer task is created - // for a workflow chain since later workflows in the chain also need to create the execution - // timer task if it is not created yet. - // NOTE: Task status is clsuter specific information, so when replicating mutable state, this - // field need to be sanitized. - int32 workflow_execution_timer_task_status = 82; - - // The root workflow execution is defined as follows: - // 1. A workflow without parent workflow is its own root workflow. - // 2. A workflow that has a parent workflow has the same root workflow as its parent workflow. - string root_workflow_id = 83; - string root_run_id = 84; - - // Timer tasks emitted from state machines are stored in this array, grouped and sorted by their deadline. Only the - // next state machine timer task is generated at a time per mutable state. When that task is processed it iterates - // this array and triggers timers that are ready. - // NOTE: Task status is cluster specific information, so when replicating mutable state, this field needs to be - // sanitized. - repeated StateMachineTimerGroup state_machine_timers = 90; - - // The shard clock's timestamp at the time the first valid task was created for this mutable state (either for a new - // mutable state or when rebuilding from events). The field should be updated whenever we refresh tasks, marking - // older generation tasks obsolete. - // This field is used for task staleness checks when mutable state is rebuilt. - // NOTE: Task status is cluster specific information, so when replicating mutable state, this field needs to be - // sanitized. - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: Ignoring api-linter rules for clarity --) - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: This is a vector clock, not a timestamp --) - int64 task_generation_shard_clock_timestamp = 91; - - VersionedTransition workflow_task_last_update_versioned_transition = 92; - VersionedTransition visibility_last_update_versioned_transition = 93; - VersionedTransition signal_request_ids_last_update_versioned_transition = 94; - - repeated StateMachineTombstoneBatch sub_state_machine_tombstone_batches = 95; - - // The workflow has been reset. - bool workflow_was_reset = 96; - - // Reset Run ID points to the new nun when this execution is reset. If the execution is reset multiple times, it points to the latest run. - string reset_run_id = 97; - - // When present, it means the workflow execution is versioned, or is transitioning from - // unversioned workers to versioned ones. - // Note: Deployment objects inside versioning info are immutable, never change their fields. - // (-- api-linter: core::0203::immutable=disabled - // aip.dev/not-precedent: field_behavior annotation is not yet used in this repo --) - temporal.api.workflow.v1.WorkflowExecutionVersioningInfo versioning_info = 98; - - // This is the run id when the WorkflowExecutionStarted event was written. - // A workflow reset changes the execution run_id, but preserves this field so that we have a reference to the original workflow execution that was reset. - string original_execution_run_id = 99; - - // These two fields are to record the transition history when the transition history is cleaned up due to disabling transition history - // Should be deprecated once the transition history is fully launched - repeated VersionedTransition previous_transition_history = 100; - VersionedTransition last_transition_history_break_point = 101; - - // This is a set of child workflows that were initialized after the reset point in the parent workflow. - // The children are identified by the key "workflow_type:workflow_id". When the parent starts to make progress after reset, it uses this data to - // determine the right start policy to apply to the child. This list will include children initiated in continue-as-new runs. - map children_initialized_post_reset_point = 102; - // The worker deployment that completed the last WFT. - string worker_deployment_name = 103; - - // Priority contains metadata that controls relative ordering of task processing - // when tasks are backed up in a queue. - temporal.api.common.v1.Priority priority = 104; - - // Run ID of the execution that supersedes this one (via terminate or continue-as-new). - string successor_run_id = 105; - - // Pause info contains the details of the request to pause the workflow. - WorkflowPauseInfo pause_info = 106; - - // Last workflow task failure category and cause are used to track the last workflow task failure category and cause. - oneof last_workflow_task_failure { - temporal.api.enums.v1.WorkflowTaskFailedCause last_workflow_task_failure_cause = 107; - temporal.api.enums.v1.TimeoutType last_workflow_task_timed_out_type = 108; - } + string namespace_id = 1; + string workflow_id = 2; + string parent_namespace_id = 3; + string parent_workflow_id = 4; + string parent_run_id = 5; + int64 parent_initiated_id = 6; + int64 completion_event_batch_id = 7; + reserved 8; + string task_queue = 9; + string workflow_type_name = 10; + google.protobuf.Duration workflow_execution_timeout = 11; + google.protobuf.Duration workflow_run_timeout = 12; + google.protobuf.Duration default_workflow_task_timeout = 13; + reserved 14; + reserved 15; + reserved 16; + int64 last_running_clock = 17; + int64 last_first_event_id = 18; + int64 last_completed_workflow_task_started_event_id = 19; + // Deprecated. use `WorkflowExecutionState.start_time` + google.protobuf.Timestamp start_time = 20; + google.protobuf.Timestamp last_update_time = 21; + + // Workflow task fields. + int64 workflow_task_version = 22; + int64 workflow_task_scheduled_event_id = 23; + int64 workflow_task_started_event_id = 24; + google.protobuf.Duration workflow_task_timeout = 25; + int32 workflow_task_attempt = 26; + google.protobuf.Timestamp workflow_task_started_time = 27; + google.protobuf.Timestamp workflow_task_scheduled_time = 28; + google.protobuf.Timestamp workflow_task_original_scheduled_time = 30; + string workflow_task_request_id = 31; + temporal.server.api.enums.v1.WorkflowTaskType workflow_task_type = 68; + bool workflow_task_suggest_continue_as_new = 69; + repeated temporal.api.enums.v1.SuggestContinueAsNewReason workflow_task_suggest_continue_as_new_reasons = 110; + bool workflow_task_target_worker_deployment_version_changed = 112; + int64 workflow_task_history_size_bytes = 70; + // tracks the started build ID for transient/speculative WFT. This info is used for two purposes: + // - verify WFT completes by the same Build ID that started in the latest attempt + // - when persisting transient/speculative WFT, the right Build ID is used in the WFT started event + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + string workflow_task_build_id = 88; + // tracks the started build ID redirect counter for transient/speculative WFT. This info is to + // ensure the right redirect counter is used in the WFT started event created later + // for a transient/speculative WFT. + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + int64 workflow_task_build_id_redirect_counter = 89; + // Stamp represents the "version" of the workflow's internal state. + // It increases monotonically when the workflow's options are modified. + // It is used to check if a workflow task is still relevant to the corresponding workflow state machine. + int32 workflow_task_stamp = 109; + // AttemptsSinceLastSuccess tracks the number of workflow task attempts since the last successful workflow task. + // This is carried over when buffered events are applied after workflow task failures. + // Used by the TemporalReportedProblems search attribute to track continuous failure count. + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "since" is needed here. --) + int32 workflow_task_attempts_since_last_success = 111; + + bool cancel_requested = 29; + string cancel_request_id = 32; + string sticky_task_queue = 33; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration sticky_schedule_to_start_timeout = 34; + int32 attempt = 35; + google.protobuf.Duration retry_initial_interval = 36; + google.protobuf.Duration retry_maximum_interval = 37; + int32 retry_maximum_attempts = 38; + double retry_backoff_coefficient = 39; + google.protobuf.Timestamp workflow_execution_expiration_time = 40; + repeated string retry_non_retryable_error_types = 41; + bool has_retry_policy = 42; + string cron_schedule = 43; + reserved 44; + reserved 45; + int64 signal_count = 46; + int64 activity_count = 71; + int64 child_execution_count = 72; + int64 user_timer_count = 73; + int64 request_cancel_external_count = 74; + int64 signal_external_count = 75; + int64 update_count = 77; + reserved 47; + reserved 48; + reserved 49; + reserved 50; + temporal.api.workflow.v1.ResetPoints auto_reset_points = 51; + map search_attributes = 52; + map memo = 53; + temporal.server.api.history.v1.VersionHistories version_histories = 54; + string first_execution_run_id = 55; + ExecutionStats execution_stats = 56; + google.protobuf.Timestamp workflow_run_expiration_time = 57; + // Transaction Id of the first event in the last batch of events. + int64 last_first_event_txn_id = 58; + int64 state_transition_count = 59; + google.protobuf.Timestamp execution_time = 60; + // If continued-as-new, or retried, or cron, holds the new run id. + string new_execution_run_id = 61; + temporal.server.api.clock.v1.VectorClock parent_clock = 62; + // version of child execution initiated event in parent workflow + int64 parent_initiated_version = 63; + // Used to check if transfer close task is processed before deleting the workflow execution. + int64 close_transfer_task_id = 64; + // Used to check if visibility close task is processed before deleting the workflow execution. + int64 close_visibility_task_id = 65; + google.protobuf.Timestamp close_time = 66; + // Relocatable attributes are memo and search attributes. If they were removed, then they are not + // present in the mutable state, and they should be in visibility store. + bool relocatable_attributes_removed = 67; + temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 76; + // If using build-id based versioning: version stamp of the last worker to complete a + // workflow tasks for this workflow. + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + temporal.api.common.v1.WorkerVersionStamp most_recent_worker_version_stamp = 78; + // The currently assigned build ID for this execution. Presence of this value means worker versioning is used + // for this execution. Assigned build ID is selected by matching based on Worker Versioning Assignment Rules + // when the first workflow task of the execution is scheduled. If the first workflow task fails and is scheduled + // again, the assigned build ID may change according to the latest versioning rules. + // Assigned build ID can also change in the middle of a execution if Compatible Redirect Rules are applied to + // this execution. + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + string assigned_build_id = 85; + // Build ID inherited from a previous/parent execution. If present, assigned_build_id will be set to this, instead + // of using the assignment rules. + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + string inherited_build_id = 86; + // Tracks the number of times a redirect rule is applied to this workflow. Used to apply redirects in the right + // order when mutable state is rebuilt from history events. + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + int64 build_id_redirect_counter = 87; + + // index of update IDs and pointers to associated history events. + map update_infos = 79; + + // Transition history encodes all transitions a mutable state object has gone through in a compact way. + // Here the transition_count field of VersionedTransition represents the maximum transition count the mutable state object + // has gone through for the corresponding namespace failover version. + // For example, if the transition history is `[{v: 1, t: 3}, {v: 2, t: 5}]`, it means transition 1-3 have failover version 1, + // and transition 4-5 have failover version 2. + // + // Each task generated by the HSM framework is imprinted with the current VersionedTransition at the end of the transaction. + // When a task is being processed, the transition history is compared with the imprinted task information to + // verify that a task is not referencing a stale state or that the task itself is not stale. + // For example, with the same transition history above, task A `{v: 2, t: 4}` **is not** + // referencing stale state because for version `2` transitions `4-5` are valid, while task B `{v: 2, t: 6}` **is** + // referencing stale state because the transition count is out of range for version `2`. + // Furthermore, task C `{v: 1, t: 4}` itself is stale because it is referencing an impossible state, likely due to post + // split-brain reconciliation. + repeated VersionedTransition transition_history = 80; + // Map of state machine type to map of machine by ID. + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "by" is used to clarify the keys and values. --) + map sub_state_machines_by_type = 81; + + // This field is for tracking if the workflow execution timer task is created or not. + // We don't need this field if we always create the execution timer task when the first + // workflow in a workflow chain starts. However, this execution timer logic is later added. + // To maintain backward compatibility, we need to track if the execution timer task is created + // for a workflow chain since later workflows in the chain also need to create the execution + // timer task if it is not created yet. + // NOTE: Task status is clsuter specific information, so when replicating mutable state, this + // field need to be sanitized. + int32 workflow_execution_timer_task_status = 82; + + // The root workflow execution is defined as follows: + // 1. A workflow without parent workflow is its own root workflow. + // 2. A workflow that has a parent workflow has the same root workflow as its parent workflow. + string root_workflow_id = 83; + string root_run_id = 84; + + // Timer tasks emitted from state machines are stored in this array, grouped and sorted by their deadline. Only the + // next state machine timer task is generated at a time per mutable state. When that task is processed it iterates + // this array and triggers timers that are ready. + // NOTE: Task status is cluster specific information, so when replicating mutable state, this field needs to be + // sanitized. + repeated StateMachineTimerGroup state_machine_timers = 90; + + // The shard clock's timestamp at the time the first valid task was created for this mutable state (either for a new + // mutable state or when rebuilding from events). The field should be updated whenever we refresh tasks, marking + // older generation tasks obsolete. + // This field is used for task staleness checks when mutable state is rebuilt. + // NOTE: Task status is cluster specific information, so when replicating mutable state, this field needs to be + // sanitized. + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: Ignoring api-linter rules for clarity --) + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: This is a vector clock, not a timestamp --) + int64 task_generation_shard_clock_timestamp = 91; + + VersionedTransition workflow_task_last_update_versioned_transition = 92; + VersionedTransition visibility_last_update_versioned_transition = 93; + VersionedTransition signal_request_ids_last_update_versioned_transition = 94; + + repeated StateMachineTombstoneBatch sub_state_machine_tombstone_batches = 95; + + // The workflow has been reset. + bool workflow_was_reset = 96; + + // Reset Run ID points to the new nun when this execution is reset. If the execution is reset multiple times, it points to the latest run. + string reset_run_id = 97; + + // When present, it means the workflow execution is versioned, or is transitioning from + // unversioned workers to versioned ones. + // Note: Deployment objects inside versioning info are immutable, never change their fields. + // (-- api-linter: core::0203::immutable=disabled + // aip.dev/not-precedent: field_behavior annotation is not yet used in this repo --) + temporal.api.workflow.v1.WorkflowExecutionVersioningInfo versioning_info = 98; + + // This is the run id when the WorkflowExecutionStarted event was written. + // A workflow reset changes the execution run_id, but preserves this field so that we have a reference to the original workflow execution that was reset. + string original_execution_run_id = 99; + + // These two fields are to record the transition history when the transition history is cleaned up due to disabling transition history + // Should be deprecated once the transition history is fully launched + repeated VersionedTransition previous_transition_history = 100; + VersionedTransition last_transition_history_break_point = 101; + + // This is a set of child workflows that were initialized after the reset point in the parent workflow. + // The children are identified by the key "workflow_type:workflow_id". When the parent starts to make progress after reset, it uses this data to + // determine the right start policy to apply to the child. This list will include children initiated in continue-as-new runs. + map children_initialized_post_reset_point = 102; + // The worker deployment that completed the last WFT. + string worker_deployment_name = 103; + + // Priority contains metadata that controls relative ordering of task processing + // when tasks are backed up in a queue. + temporal.api.common.v1.Priority priority = 104; + + // Run ID of the execution that supersedes this one (via terminate or continue-as-new). + string successor_run_id = 105; + + // Pause info contains the details of the request to pause the workflow. + WorkflowPauseInfo pause_info = 106; + + // Last workflow task failure category and cause are used to track the last workflow task failure category and cause. + oneof last_workflow_task_failure { + temporal.api.enums.v1.WorkflowTaskFailedCause last_workflow_task_failure_cause = 107; + temporal.api.enums.v1.TimeoutType last_workflow_task_timed_out_type = 108; + } + + // The last target version for which the server set targetDeploymentVersionChanged + // to true on a workflow task started event. Updated on each workflow task start, + // set only when the server decides to set the targetDeploymentVersionChanged flag + // to true. + // + // This is a wrapper message to distinguish "never notified" (nil wrapper) from + // "notified about an unversioned target" (non-nil wrapper with nil deployment_version). + // + // Read at continue-as-new time: if set, it becomes the declined_target_version_upgrade + // for the next run. If nil, the existing declined value is preserved (CaN chain). + LastNotifiedTargetVersion last_notified_target_version = 113; + + // The target version that the SDK previously declined to upgrade to. Inherited + // from a previous run via continue-as-new or retry. At CaN time, computed as: + // if last_notified_target_version != nil → use that (latest signal was declined) + // else → preserve existing declined value (CaN chain, never re-signaled) + // + // Wrapper distinguishes "never declined" (nil) from "declined unversioned" (non-nil, nil version). + temporal.api.history.v1.DeclinedTargetVersionUpgrade declined_target_version_upgrade = 114; +} + +// Internal wrapper message to distinguish "never notified" (nil wrapper) from +// "notified about an unversioned target" (non-nil wrapper with nil deployment_version). +// Used only within server persistence; never flows to the public API. +message LastNotifiedTargetVersion { + temporal.api.deployment.v1.WorkerDeploymentVersion deployment_version = 1; } message ExecutionStats { - int64 history_size = 1; - // Total size in bytes of all external payloads referenced in the entire history tree of the execution, not just the current branch. - // This number doesn't include payloads in buffered events. - int64 external_payload_size = 2; - // Total count of external payloads referenced in the entire history tree of the execution, not just the current branch. - // This number doesn't include payloads in buffered events. - int64 external_payload_count = 3; + int64 history_size = 1; + // Total size in bytes of all external payloads referenced in the entire history tree of the execution, not just the current branch. + // This number doesn't include payloads in buffered events. + int64 external_payload_size = 2; + // Total count of external payloads referenced in the entire history tree of the execution, not just the current branch. + // This number doesn't include payloads in buffered events. + int64 external_payload_count = 3; } // execution_state column message WorkflowExecutionState { - string create_request_id = 1; - string run_id = 2; - temporal.server.api.enums.v1.WorkflowExecutionState state = 3; - temporal.api.enums.v1.WorkflowExecutionStatus status = 4; - VersionedTransition last_update_versioned_transition = 5; - google.protobuf.Timestamp start_time = 6; - // Request IDs that are attached to the workflow execution. It can the request ID that started - // the workflow execution or request IDs that were attached to an existing running workflow - // execution via StartWorkflowExecutionRequest.OnConflictOptions. - map request_ids = 7; + string create_request_id = 1; + string run_id = 2; + temporal.server.api.enums.v1.WorkflowExecutionState state = 3; + temporal.api.enums.v1.WorkflowExecutionStatus status = 4; + VersionedTransition last_update_versioned_transition = 5; + google.protobuf.Timestamp start_time = 6; + // Request IDs that are attached to the workflow execution. It can be the request ID that started + // the workflow execution or request IDs that were attached to an existing running workflow + // execution via StartWorkflowExecutionRequest.OnConflictOptions. + map request_ids = 7; } message RequestIDInfo { - temporal.api.enums.v1.EventType event_type = 1; - int64 event_id = 2; + temporal.api.enums.v1.EventType event_type = 1; + int64 event_id = 2; } // transfer column message TransferTaskInfo { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - temporal.server.api.enums.v1.TaskType task_type = 4; - string target_namespace_id = 5; - string target_workflow_id = 6; - string target_run_id = 7; - string task_queue = 8; - bool target_child_workflow_only = 9; - int64 scheduled_event_id = 10; - int64 version = 11; - int64 task_id = 12; - google.protobuf.Timestamp visibility_time = 13; - reserved 14; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "after" is used to indicate sequence of actions. --) - bool delete_after_close = 15; - message CloseExecutionTaskDetails { - // can_skip_visibility_archival is set to true when we can guarantee that visibility records will be archived - // by some other task, so this task doesn't need to worry about it. - bool can_skip_visibility_archival = 1; - } - - oneof task_details { - CloseExecutionTaskDetails close_execution_task_details = 16; - - // If the task addresses a CHASM component, this field will be set. - ChasmTaskInfo chasm_task_info = 18; - } - // Stamp represents the "version" of the entity's internal state for which the transfer task was created. - // It increases monotonically when the entity's options are modified. - // It is used to check if a task is still relevant to the entity's corresponding state machine. - int32 stamp = 17; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + temporal.server.api.enums.v1.TaskType task_type = 4; + string target_namespace_id = 5; + string target_workflow_id = 6; + string target_run_id = 7; + string task_queue = 8; + bool target_child_workflow_only = 9; + int64 scheduled_event_id = 10; + int64 version = 11; + int64 task_id = 12; + google.protobuf.Timestamp visibility_time = 13; + reserved 14; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "after" is used to indicate sequence of actions. --) + bool delete_after_close = 15; + message CloseExecutionTaskDetails { + // can_skip_visibility_archival is set to true when we can guarantee that visibility records will be archived + // by some other task, so this task doesn't need to worry about it. + bool can_skip_visibility_archival = 1; + } + oneof task_details { + CloseExecutionTaskDetails close_execution_task_details = 16; + + // If the task addresses a CHASM component, this field will be set. + ChasmTaskInfo chasm_task_info = 18; + } + // Stamp represents the "version" of the entity's internal state for which the transfer task was created. + // It increases monotonically when the entity's options are modified. + // It is used to check if a task is still relevant to the entity's corresponding state machine. + int32 stamp = 17; } // replication column message ReplicationTaskInfo { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - temporal.server.api.enums.v1.TaskType task_type = 4; - int64 version = 5; - int64 first_event_id = 6; - int64 next_event_id = 7; - int64 scheduled_event_id = 8; - reserved 9; - reserved 10; - bytes branch_token = 11; - reserved 12; - bytes new_run_branch_token = 13; - reserved 14; - int64 task_id = 15; - google.protobuf.Timestamp visibility_time = 16; - string new_run_id = 17; - temporal.server.api.enums.v1.TaskPriority priority = 18; - VersionedTransition versioned_transition = 19; - // A list of event-based replication tasks that, together, are equivalent - // to this state-based task. - // TODO: Remove this field when state-based replication is stable and - // doesn't need to be disabled. - repeated ReplicationTaskInfo task_equivalents = 20; - history.v1.VersionHistoryItem last_version_history_item = 21; - bool is_first_task = 22; - repeated string target_clusters = 23; - bool is_force_replication = 24; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 25; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + temporal.server.api.enums.v1.TaskType task_type = 4; + int64 version = 5; + int64 first_event_id = 6; + int64 next_event_id = 7; + int64 scheduled_event_id = 8; + reserved 9; + reserved 10; + bytes branch_token = 11; + reserved 12; + bytes new_run_branch_token = 13; + reserved 14; + int64 task_id = 15; + google.protobuf.Timestamp visibility_time = 16; + string new_run_id = 17; + temporal.server.api.enums.v1.TaskPriority priority = 18; + VersionedTransition versioned_transition = 19; + // A list of event-based replication tasks that, together, are equivalent + // to this state-based task. + // TODO: Remove this field when state-based replication is stable and + // doesn't need to be disabled. + repeated ReplicationTaskInfo task_equivalents = 20; + history.v1.VersionHistoryItem last_version_history_item = 21; + bool is_first_task = 22; + repeated string target_clusters = 23; + bool is_force_replication = 24; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 25; } // visibility_task_data column message VisibilityTaskInfo { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - temporal.server.api.enums.v1.TaskType task_type = 4; - int64 version = 5; - int64 task_id = 6; - google.protobuf.Timestamp visibility_time = 7; - reserved 8; - reserved 9; - int64 close_visibility_task_id = 10; - google.protobuf.Timestamp close_time = 11; - - oneof task_details { - // If the task addresses a CHASM component, this field will be set. - ChasmTaskInfo chasm_task_info = 12; - } + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + temporal.server.api.enums.v1.TaskType task_type = 4; + int64 version = 5; + int64 task_id = 6; + google.protobuf.Timestamp visibility_time = 7; + reserved 8; + reserved 9; + int64 close_visibility_task_id = 10; + google.protobuf.Timestamp close_time = 11; + + oneof task_details { + // If the task addresses a CHASM component, this field will be set. + ChasmTaskInfo chasm_task_info = 12; + } } // timer column message TimerTaskInfo { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - temporal.server.api.enums.v1.TaskType task_type = 4; - temporal.api.enums.v1.TimeoutType timeout_type = 5; - temporal.server.api.enums.v1.WorkflowBackoffType workflow_backoff_type = 6; - int64 version = 7; - int32 schedule_attempt = 8; - int64 event_id = 9; - int64 task_id = 10; - google.protobuf.Timestamp visibility_time = 11; - bytes branch_token = 12; - // If this is true, we can bypass archival before deleting. Only defined for DeleteHistoryEventTasks. - bool already_archived = 13; - - // Number of transitions on the corresponding mutable state object. Used to verify that a task is not referencing a - // stale state or, in some situations, that the task itself is not stale. - // If task addresses a sub-statemachine (e.g. callback), this field will be set. - int64 mutable_state_transition_count = 14; - - // If specified, the task is a for a workflow chain instead of a specific workflow run. - // A workflow chain is identified by the run_id of the first workflow in the chain. - string first_run_id = 15; - - // Stamp represents the "version" of the entity's internal state for which the timer task was created. - // It increases monotonically when the entity's options are modified. - // It is used to check if a task is still relevant to the entity's corresponding state machine. - int32 stamp = 16; - - oneof task_details { - // If the task addresses a CHASM component, this field will be set. - ChasmTaskInfo chasm_task_info = 17; - } + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + temporal.server.api.enums.v1.TaskType task_type = 4; + temporal.api.enums.v1.TimeoutType timeout_type = 5; + temporal.server.api.enums.v1.WorkflowBackoffType workflow_backoff_type = 6; + int64 version = 7; + int32 schedule_attempt = 8; + int64 event_id = 9; + int64 task_id = 10; + google.protobuf.Timestamp visibility_time = 11; + bytes branch_token = 12; + // If this is true, we can bypass archival before deleting. Only defined for DeleteHistoryEventTasks. + bool already_archived = 13; + + // Number of transitions on the corresponding mutable state object. Used to verify that a task is not referencing a + // stale state or, in some situations, that the task itself is not stale. + // If task addresses a sub-statemachine (e.g. callback), this field will be set. + int64 mutable_state_transition_count = 14; + + // If specified, the task is a for a workflow chain instead of a specific workflow run. + // A workflow chain is identified by the run_id of the first workflow in the chain. + string first_run_id = 15; + + // Stamp represents the "version" of the entity's internal state for which the timer task was created. + // It increases monotonically when the entity's options are modified. + // It is used to check if a task is still relevant to the entity's corresponding state machine. + int32 stamp = 16; + + oneof task_details { + // If the task addresses a CHASM component, this field will be set. + ChasmTaskInfo chasm_task_info = 17; + } } message ArchivalTaskInfo { - int64 task_id = 1; - string namespace_id = 2; - string workflow_id = 3; - string run_id = 4; - temporal.server.api.enums.v1.TaskType task_type = 5; - int64 version = 6; - google.protobuf.Timestamp visibility_time = 7; + int64 task_id = 1; + string namespace_id = 2; + string workflow_id = 3; + string run_id = 4; + temporal.server.api.enums.v1.TaskType task_type = 5; + int64 version = 6; + google.protobuf.Timestamp visibility_time = 7; } message OutboundTaskInfo { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; - temporal.server.api.enums.v1.TaskType task_type = 4; - int64 task_id = 5; - google.protobuf.Timestamp visibility_time = 6; + temporal.server.api.enums.v1.TaskType task_type = 4; + int64 task_id = 5; + google.protobuf.Timestamp visibility_time = 6; - // Destination of this task (e.g. protocol+host+port for callbacks). - // Outbound tasks are grouped by this field (and the namespace ID) when scheduling. - string destination = 7; + // Destination of this task (e.g. protocol+host+port for callbacks). + // Outbound tasks are grouped by this field (and the namespace ID) when scheduling. + string destination = 7; - oneof task_details { - // If task addresses a sub-statemachine (e.g. callback), this field will be set. - StateMachineTaskInfo state_machine_info = 8; + oneof task_details { + // If task addresses a sub-statemachine (e.g. callback), this field will be set. + StateMachineTaskInfo state_machine_info = 8; - // If the task addresses a CHASM component, this field will be set. - ChasmTaskInfo chasm_task_info = 9; + // If the task addresses a CHASM component, this field will be set. + ChasmTaskInfo chasm_task_info = 9; - // If the task is a worker commands task. - WorkerCommandsTask worker_commands_task = 10; - } + // If the task is a worker commands task. + WorkerCommandsTask worker_commands_task = 10; + } } // WorkerCommandsTask contains worker commands to dispatch via Nexus. @@ -496,395 +522,394 @@ message WorkerCommandsTask { } message NexusInvocationTaskInfo { - int32 attempt = 1; + int32 attempt = 1; } message NexusCancelationTaskInfo { - int32 attempt = 1; + int32 attempt = 1; } // activity_map column message ActivityInfo { - - int64 version = 1; - int64 scheduled_event_batch_id = 2; - reserved 3; - google.protobuf.Timestamp scheduled_time = 4; - int64 started_event_id = 5; - reserved 6; - google.protobuf.Timestamp started_time = 7; - string activity_id = 8; - string request_id = 9; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_start_timeout = 10; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_close_timeout = 11; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration start_to_close_timeout = 12; - google.protobuf.Duration heartbeat_timeout = 13; - bool cancel_requested = 14; - int64 cancel_request_id = 15; - int32 timer_task_status = 16; - int32 attempt = 17; - string task_queue = 18; - string started_identity = 19; - bool has_retry_policy = 20; - google.protobuf.Duration retry_initial_interval = 21; - google.protobuf.Duration retry_maximum_interval = 22; - int32 retry_maximum_attempts = 23; - google.protobuf.Timestamp retry_expiration_time = 24; - double retry_backoff_coefficient = 25; - repeated string retry_non_retryable_error_types = 26; - temporal.api.failure.v1.Failure retry_last_failure = 27; - string retry_last_worker_identity = 28; - reserved 29; - int64 scheduled_event_id = 30; - temporal.api.common.v1.Payloads last_heartbeat_details = 31; - google.protobuf.Timestamp last_heartbeat_update_time = 32; - // When true, it means the activity is assigned to the build ID of its workflow (only set for old versioning) - // Deprecated. use `use_workflow_build_id` - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - bool use_compatible_version = 33; - temporal.api.common.v1.ActivityType activity_type = 34; - // Absence of `assigned_build_id` generally means this task is on an "unversioned" task queue. - // In rare cases, it can also mean that the task queue is versioned but we failed to write activity's - // independently-assigned build ID to the database. This case heals automatically once the task is dispatched. + int64 version = 1; + int64 scheduled_event_batch_id = 2; + reserved 3; + google.protobuf.Timestamp scheduled_time = 4; + int64 started_event_id = 5; + reserved 6; + google.protobuf.Timestamp started_time = 7; + string activity_id = 8; + string request_id = 9; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_start_timeout = 10; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_close_timeout = 11; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration start_to_close_timeout = 12; + google.protobuf.Duration heartbeat_timeout = 13; + bool cancel_requested = 14; + int64 cancel_request_id = 15; + int32 timer_task_status = 16; + int32 attempt = 17; + string task_queue = 18; + string started_identity = 19; + bool has_retry_policy = 20; + google.protobuf.Duration retry_initial_interval = 21; + google.protobuf.Duration retry_maximum_interval = 22; + int32 retry_maximum_attempts = 23; + google.protobuf.Timestamp retry_expiration_time = 24; + double retry_backoff_coefficient = 25; + repeated string retry_non_retryable_error_types = 26; + temporal.api.failure.v1.Failure retry_last_failure = 27; + string retry_last_worker_identity = 28; + reserved 29; + int64 scheduled_event_id = 30; + temporal.api.common.v1.Payloads last_heartbeat_details = 31; + google.protobuf.Timestamp last_heartbeat_update_time = 32; + // When true, it means the activity is assigned to the build ID of its workflow (only set for old versioning) + // Deprecated. use `use_workflow_build_id` + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + bool use_compatible_version = 33; + temporal.api.common.v1.ActivityType activity_type = 34; + // Absence of `assigned_build_id` generally means this task is on an "unversioned" task queue. + // In rare cases, it can also mean that the task queue is versioned but we failed to write activity's + // independently-assigned build ID to the database. This case heals automatically once the task is dispatched. + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + oneof build_id_info { + // When present, it means this activity is assigned to the build ID of its workflow. // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - oneof build_id_info { - // When present, it means this activity is assigned to the build ID of its workflow. - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - UseWorkflowBuildIdInfo use_workflow_build_id_info = 35; - // This means the activity is independently versioned and not bound to the build ID of its workflow. - // If the task fails and is scheduled again, the assigned build ID may change according to the latest versioning - // rules. This value also updates if a redirect rule is applied to the activity task to reflect the build ID - // of the worker who received the task. - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - string last_independently_assigned_build_id = 36; - } - // The version stamp of the worker to whom this activity was most-recently dispatched + UseWorkflowBuildIdInfo use_workflow_build_id_info = 35; + // This means the activity is independently versioned and not bound to the build ID of its workflow. + // If the task fails and is scheduled again, the assigned build ID may change according to the latest versioning + // rules. This value also updates if a redirect rule is applied to the activity task to reflect the build ID + // of the worker who received the task. // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - temporal.api.common.v1.WorkerVersionStamp last_worker_version_stamp = 37; - VersionedTransition last_update_versioned_transition = 38; + string last_independently_assigned_build_id = 36; + } + // The version stamp of the worker to whom this activity was most-recently dispatched + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + temporal.api.common.v1.WorkerVersionStamp last_worker_version_stamp = 37; + VersionedTransition last_update_versioned_transition = 38; + + // Deprecated. Clean up with versioning-2. [cleanup-old-wv] + message UseWorkflowBuildIdInfo { + // build ID of the wf when this activity started last time (which is the build ID of + // the worker who received this activity) + string last_used_build_id = 1; + // workflows redirect_counter value when this activity started last time + int64 last_redirect_counter = 2; + } + + // The first time the activity was scheduled. + google.protobuf.Timestamp first_scheduled_time = 39; + // The last time an activity attempt completion was recorded by the server. + google.protobuf.Timestamp last_attempt_complete_time = 40; + + // Stamp represents the “version” of the activity's internal state and can/will be changed with Activity API. + // It increases monotonically when the activity's options are modified. + // It is used to check if an activity task is still relevant to the corresponding activity state machine. + int32 stamp = 41; + + // Paused state. When activity is paused it will not advance until unpaused. + // Iw will not be scheduled, timer tasks will not be processed, etc. + // Note: it still can be cancelled/completed. + bool paused = 42; + + // The deployment this activity was dispatched to most recently. Present only if the activity + // was dispatched to a versioned worker. + // Deprecated. Replaced by last_worker_deployment_version. + temporal.api.deployment.v1.Deployment last_started_deployment = 43; + + // The deployment this activity was dispatched to most recently. Present only if the activity + // was dispatched to a versioned worker. + // Deprecated. Clean up with versioning-3.1. [cleanup-old-wv] + string last_worker_deployment_version = 44; + + // The deployment version this activity was dispatched to most recently. Present only if the activity + // was dispatched to a versioned worker. + temporal.api.deployment.v1.WorkerDeploymentVersion last_deployment_version = 49; + + // Priority metadata. If this message is not present, or any fields are not + // present, they inherit the values from the workflow. + temporal.api.common.v1.Priority priority = 45; + + message PauseInfo { + // The time when the activity was paused. + google.protobuf.Timestamp pause_time = 1; - // Deprecated. Clean up with versioning-2. [cleanup-old-wv] - message UseWorkflowBuildIdInfo { - // build ID of the wf when this activity started last time (which is the build ID of - // the worker who received this activity) - string last_used_build_id = 1; - // workflows redirect_counter value when this activity started last time - int64 last_redirect_counter = 2; + message Manual { + // The identity of the actor that paused the activity. + string identity = 1; + // Reason for pausing the activity. + string reason = 2; } - // The first time the activity was scheduled. - google.protobuf.Timestamp first_scheduled_time = 39; - // The last time an activity attempt completion was recorded by the server. - google.protobuf.Timestamp last_attempt_complete_time = 40; - - // Stamp represents the “version” of the activity's internal state and can/will be changed with Activity API. - // It increases monotonically when the activity's options are modified. - // It is used to check if an activity task is still relevant to the corresponding activity state machine. - int32 stamp = 41; - - // Paused state. When activity is paused it will not advance until unpaused. - // Iw will not be scheduled, timer tasks will not be processed, etc. - // Note: it still can be cancelled/completed. - bool paused = 42; - - // The deployment this activity was dispatched to most recently. Present only if the activity - // was dispatched to a versioned worker. - // Deprecated. Replaced by last_worker_deployment_version. - temporal.api.deployment.v1.Deployment last_started_deployment = 43; - - // The deployment this activity was dispatched to most recently. Present only if the activity - // was dispatched to a versioned worker. - // Deprecated. Clean up with versioning-3.1. [cleanup-old-wv] - string last_worker_deployment_version = 44; - - // The deployment version this activity was dispatched to most recently. Present only if the activity - // was dispatched to a versioned worker. - temporal.api.deployment.v1.WorkerDeploymentVersion last_deployment_version = 49; - - - // Priority metadata. If this message is not present, or any fields are not - // present, they inherit the values from the workflow. - temporal.api.common.v1.Priority priority = 45; - - message PauseInfo { - // The time when the activity was paused. - google.protobuf.Timestamp pause_time = 1; - - message Manual { - // The identity of the actor that paused the activity. - string identity = 1; - // Reason for pausing the activity. - string reason = 2; - } - - oneof paused_by { - // activity was paused by the manual intervention - Manual manual = 2; - - // Id of the rule that paused the activity. - string rule_id = 3; - } + oneof paused_by { + // activity was paused by the manual intervention + Manual manual = 2; + + // Id of the rule that paused the activity. + string rule_id = 3; } + } - PauseInfo pause_info = 46; + PauseInfo pause_info = 46; - // set to true if there was an activity reset while activity is still running on the worker - bool activity_reset = 47; + // set to true if there was an activity reset while activity is still running on the worker + bool activity_reset = 47; - // set to true if reset heartbeat flag was set with an activity reset - bool reset_heartbeats = 48; + // set to true if reset heartbeat flag was set with an activity reset + bool reset_heartbeats = 48; - int64 start_version = 50; + int64 start_version = 50; - // The task queue on which the server will send control tasks to the worker running this activity. - string worker_control_task_queue = 51; + // A dedicated per-worker Nexus task queue on which the server sends control + // tasks (e.g. activity cancellation) to this specific worker instance. + string worker_control_task_queue = 51; } // timer_map column message TimerInfo { - int64 version = 1; - int64 started_event_id = 2; - google.protobuf.Timestamp expiry_time = 3; - int64 task_status = 4; - // timerId serves the purpose of indicating whether a timer task is generated for this timer info. - string timer_id = 5; - VersionedTransition last_update_versioned_transition = 6; + int64 version = 1; + int64 started_event_id = 2; + google.protobuf.Timestamp expiry_time = 3; + int64 task_status = 4; + // timerId serves the purpose of indicating whether a timer task is generated for this timer info. + string timer_id = 5; + VersionedTransition last_update_versioned_transition = 6; } // child_executions_map column message ChildExecutionInfo { - int64 version = 1; - int64 initiated_event_batch_id = 2; - int64 started_event_id = 3; - reserved 4; - string started_workflow_id = 5; - string started_run_id = 6; - reserved 7; - string create_request_id = 8; - string namespace = 9; - string workflow_type_name = 10; - temporal.api.enums.v1.ParentClosePolicy parent_close_policy = 11; - int64 initiated_event_id = 12; - temporal.server.api.clock.v1.VectorClock clock = 13; - string namespace_id = 14; - VersionedTransition last_update_versioned_transition = 15; - temporal.api.common.v1.Priority priority = 16; + int64 version = 1; + int64 initiated_event_batch_id = 2; + int64 started_event_id = 3; + reserved 4; + string started_workflow_id = 5; + string started_run_id = 6; + reserved 7; + string create_request_id = 8; + string namespace = 9; + string workflow_type_name = 10; + temporal.api.enums.v1.ParentClosePolicy parent_close_policy = 11; + int64 initiated_event_id = 12; + temporal.server.api.clock.v1.VectorClock clock = 13; + string namespace_id = 14; + VersionedTransition last_update_versioned_transition = 15; + temporal.api.common.v1.Priority priority = 16; } // request_cancel_map column message RequestCancelInfo { - int64 version = 1; - int64 initiated_event_batch_id = 2; - string cancel_request_id = 3; - int64 initiated_event_id = 4; - VersionedTransition last_update_versioned_transition = 5; + int64 version = 1; + int64 initiated_event_batch_id = 2; + string cancel_request_id = 3; + int64 initiated_event_id = 4; + VersionedTransition last_update_versioned_transition = 5; } // signal_map column message SignalInfo { - int64 version = 1; - int64 initiated_event_batch_id = 2; - string request_id = 3; - reserved 4; - reserved 5; - reserved 6; - int64 initiated_event_id = 7; - reserved 8; - VersionedTransition last_update_versioned_transition = 9; + int64 version = 1; + int64 initiated_event_batch_id = 2; + string request_id = 3; + reserved 4; + reserved 5; + reserved 6; + int64 initiated_event_id = 7; + reserved 8; + VersionedTransition last_update_versioned_transition = 9; } // checksum column message Checksum { - int32 version = 1; - temporal.server.api.enums.v1.ChecksumFlavor flavor = 2; - bytes value = 3; + int32 version = 1; + temporal.server.api.enums.v1.ChecksumFlavor flavor = 2; + bytes value = 3; } message Callback { - message Nexus { - // Callback URL. - // (-- api-linter: core::0140::uri=disabled - // aip.dev/not-precedent: Not respecting aip here. --) - string url = 1; - // Header to attach to callback request. - map header = 2; - } - - message HSM { - // namespace id of the target state machine. - string namespace_id = 1; - // ID of the workflow that the target state machine is attached to. - string workflow_id = 2; - // Run id of said workflow. - string run_id = 3; - // A reference to the state machine. - temporal.server.api.persistence.v1.StateMachineRef ref = 4; - // The method name to invoke. Methods must be explicitly registered for the target state machine in the state - // machine registry, and accept an argument type of HistoryEvent that is the completion event of the completed - // workflow. - string method = 5; - } - - reserved 1; // For a generic callback mechanism to be added later. - oneof variant { - Nexus nexus = 2; - HSM hsm = 3; - } - - repeated temporal.api.common.v1.Link links = 100; -} - -message HSMCompletionCallbackArg { - // namespace ID of the workflow that just completed. + message Nexus { + // Callback URL. + // (-- api-linter: core::0140::uri=disabled + // aip.dev/not-precedent: Not respecting aip here. --) + string url = 1; + // Header to attach to callback request. + map header = 2; + } + + message HSM { + // namespace id of the target state machine. string namespace_id = 1; - // ID of the workflow that just completed. + // ID of the workflow that the target state machine is attached to. string workflow_id = 2; - // run ID of the workflow that just completed. + // Run id of said workflow. string run_id = 3; - // Last event of the completed workflow. - temporal.api.history.v1.HistoryEvent last_event = 4; + // A reference to the state machine. + temporal.server.api.persistence.v1.StateMachineRef ref = 4; + // The method name to invoke. Methods must be explicitly registered for the target state machine in the state + // machine registry, and accept an argument type of HistoryEvent that is the completion event of the completed + // workflow. + string method = 5; + } + + reserved 1; // For a generic callback mechanism to be added later. + oneof variant { + Nexus nexus = 2; + HSM hsm = 3; + } + + repeated temporal.api.common.v1.Link links = 100; +} + +message HSMCompletionCallbackArg { + // namespace ID of the workflow that just completed. + string namespace_id = 1; + // ID of the workflow that just completed. + string workflow_id = 2; + // run ID of the workflow that just completed. + string run_id = 3; + // Last event of the completed workflow. + temporal.api.history.v1.HistoryEvent last_event = 4; } message CallbackInfo { - // Trigger for when the workflow is closed. - message WorkflowClosed {} + // Trigger for when the workflow is closed. + message WorkflowClosed {} - message Trigger { - oneof variant { - WorkflowClosed workflow_closed = 1; - } + message Trigger { + oneof variant { + WorkflowClosed workflow_closed = 1; } - - // Information on how this callback should be invoked (e.g. its URL and type). - Callback callback = 1; - // Trigger for this callback. - Trigger trigger = 2; - // The time when the callback was registered. - google.protobuf.Timestamp registration_time = 3; - - temporal.server.api.enums.v1.CallbackState state = 4; - // The number of attempts made to deliver the callback. - // This number represents a minimum bound since the attempt is incremented after the callback request completes. - int32 attempt = 5; - - // The time when the last attempt completed. - google.protobuf.Timestamp last_attempt_complete_time = 6; - // The last attempt's failure, if any. - temporal.api.failure.v1.Failure last_attempt_failure = 7; - // The time when the next attempt is scheduled. - google.protobuf.Timestamp next_attempt_schedule_time = 8; - - // Request ID that added the callback. - string request_id = 9; + } + + // Information on how this callback should be invoked (e.g. its URL and type). + Callback callback = 1; + // Trigger for this callback. + Trigger trigger = 2; + // The time when the callback was registered. + google.protobuf.Timestamp registration_time = 3; + + temporal.server.api.enums.v1.CallbackState state = 4; + // The number of attempts made to deliver the callback. + // This number represents a minimum bound since the attempt is incremented after the callback request completes. + int32 attempt = 5; + + // The time when the last attempt completed. + google.protobuf.Timestamp last_attempt_complete_time = 6; + // The last attempt's failure, if any. + temporal.api.failure.v1.Failure last_attempt_failure = 7; + // The time when the next attempt is scheduled. + google.protobuf.Timestamp next_attempt_schedule_time = 8; + + // Request ID that added the callback. + string request_id = 9; } // NexusOperationInfo contains the state of a nexus operation. message NexusOperationInfo { - // Endpoint name. - // Resolved the endpoint registry for this workflow's namespace. - string endpoint = 1; + // Endpoint name. + // Resolved the endpoint registry for this workflow's namespace. + string endpoint = 1; - // Service name. - string service = 2; - // Operation name. - string operation = 3; + // Service name. + string service = 2; + // Operation name. + string operation = 3; - // reserved due to removal of delete_on_completion - reserved 4; + // reserved due to removal of delete_on_completion + reserved 4; - // Token for fetching the scheduled event. - bytes scheduled_event_token = 5; + // Token for fetching the scheduled event. + bytes scheduled_event_token = 5; - // Operation token. Only set for asynchronous operations after a successful StartOperation call. - string operation_token = 6; + // Operation token. Only set for asynchronous operations after a successful StartOperation call. + string operation_token = 6; - // Schedule-to-close timeout for this operation. - // This is the only timeout settable by a workflow. - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "since" is needed here. --) - google.protobuf.Duration schedule_to_close_timeout = 7; + // Schedule-to-close timeout for this operation. + // This is the only timeout settable by a workflow. + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "since" is needed here. --) + google.protobuf.Duration schedule_to_close_timeout = 7; - // The time when the operation was scheduled. - google.protobuf.Timestamp scheduled_time = 8; + // The time when the operation was scheduled. + google.protobuf.Timestamp scheduled_time = 8; - // Unique request ID allocated for all retry attempts of the StartOperation request. - string request_id = 9; + // Unique request ID allocated for all retry attempts of the StartOperation request. + string request_id = 9; - temporal.server.api.enums.v1.NexusOperationState state = 10; + temporal.server.api.enums.v1.NexusOperationState state = 10; - // The number of attempts made to deliver the start operation request. - // This number represents a minimum bound since the attempt is incremented after the request completes. - int32 attempt = 11; + // The number of attempts made to deliver the start operation request. + // This number represents a minimum bound since the attempt is incremented after the request completes. + int32 attempt = 11; - // The time when the last attempt completed. - google.protobuf.Timestamp last_attempt_complete_time = 12; - // The last attempt's failure, if any. - temporal.api.failure.v1.Failure last_attempt_failure = 13; - // The time when the next attempt is scheduled. - google.protobuf.Timestamp next_attempt_schedule_time = 14; + // The time when the last attempt completed. + google.protobuf.Timestamp last_attempt_complete_time = 12; + // The last attempt's failure, if any. + temporal.api.failure.v1.Failure last_attempt_failure = 13; + // The time when the next attempt is scheduled. + google.protobuf.Timestamp next_attempt_schedule_time = 14; - // Endpoint ID, the name is also stored here (field 1) but we use the ID internally to avoid failing operation - // requests when an endpoint is renamed. - string endpoint_id = 15; + // Endpoint ID, the name is also stored here (field 1) but we use the ID internally to avoid failing operation + // requests when an endpoint is renamed. + string endpoint_id = 15; - // Schedule-to-start timeout for this operation. - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration schedule_to_start_timeout = 16; + // Schedule-to-start timeout for this operation. + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration schedule_to_start_timeout = 16; - // Start-to-close timeout for this operation. - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "to" is used to indicate interval. --) - google.protobuf.Duration start_to_close_timeout = 17; + // Start-to-close timeout for this operation. + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "to" is used to indicate interval. --) + google.protobuf.Duration start_to_close_timeout = 17; - // Time the operation was started (only available for async operations). - google.protobuf.Timestamp started_time = 18; + // Time the operation was started (only available for async operations). + google.protobuf.Timestamp started_time = 18; } // NexusOperationCancellationInfo contains the state of a nexus operation cancelation. message NexusOperationCancellationInfo { - // The time when cancelation was requested. - google.protobuf.Timestamp requested_time = 1; + // The time when cancelation was requested. + google.protobuf.Timestamp requested_time = 1; - temporal.api.enums.v1.NexusOperationCancellationState state = 2; + temporal.api.enums.v1.NexusOperationCancellationState state = 2; - // The number of attempts made to deliver the cancel operation request. - // This number represents a minimum bound since the attempt is incremented after the request completes. - int32 attempt = 3; + // The number of attempts made to deliver the cancel operation request. + // This number represents a minimum bound since the attempt is incremented after the request completes. + int32 attempt = 3; - // The time when the last attempt completed. - google.protobuf.Timestamp last_attempt_complete_time = 4; - // The last attempt's failure, if any. - temporal.api.failure.v1.Failure last_attempt_failure = 5; - // The time when the next attempt is scheduled. - google.protobuf.Timestamp next_attempt_schedule_time = 6; + // The time when the last attempt completed. + google.protobuf.Timestamp last_attempt_complete_time = 4; + // The last attempt's failure, if any. + temporal.api.failure.v1.Failure last_attempt_failure = 5; + // The time when the next attempt is scheduled. + google.protobuf.Timestamp next_attempt_schedule_time = 6; - // The event ID of the NEXUS_OPERATION_CANCEL_REQUESTED event for this cancelation. - int64 requested_event_id = 7; + // The event ID of the NEXUS_OPERATION_CANCEL_REQUESTED event for this cancelation. + int64 requested_event_id = 7; } // ResetChildInfo contains the state and actions to be performed on children when a parent workflow resumes after reset. message ResetChildInfo { - // If true, the parent workflow should terminate the child before starting it. - bool should_terminate_and_start = 1; + // If true, the parent workflow should terminate the child before starting it. + bool should_terminate_and_start = 1; } message WorkflowPauseInfo { - // The time when the workflow was paused. - google.protobuf.Timestamp pause_time = 1; + // The time when the workflow was paused. + google.protobuf.Timestamp pause_time = 1; - // The identity of the actor that paused the workflow. - string identity = 2; + // The identity of the actor that paused the workflow. + string identity = 2; - // The reason for pausing the workflow. - string reason = 3; + // The reason for pausing the workflow. + string reason = 3; - // A unique identifier for this pause request (for idempotency checks) - string request_id = 4; + // A unique identifier for this pause request (for idempotency checks) + string request_id = 4; } diff --git a/proto/internal/temporal/server/api/persistence/v1/history_tree.proto b/proto/internal/temporal/server/api/persistence/v1/history_tree.proto index b53883f10d5..f60a67dbbe8 100644 --- a/proto/internal/temporal/server/api/persistence/v1/history_tree.proto +++ b/proto/internal/temporal/server/api/persistence/v1/history_tree.proto @@ -1,34 +1,35 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "google/protobuf/timestamp.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // branch column message HistoryTreeInfo { - HistoryBranch branch_info = 1; - // For fork operation to prevent race condition of leaking event data when forking branches fail. Also can be used for clean up leaked data. - google.protobuf.Timestamp fork_time = 2; - // For lookup back to workflow during debugging, also background cleanup when fork operation cannot finish self cleanup due to crash. - string info = 3; - // Deprecating branch token in favor of branch info. - bytes branch_token = 4 [deprecated = true]; + HistoryBranch branch_info = 1; + // For fork operation to prevent race condition of leaking event data when forking branches fail. Also can be used for clean up leaked data. + google.protobuf.Timestamp fork_time = 2; + // For lookup back to workflow during debugging, also background cleanup when fork operation cannot finish self cleanup due to crash. + string info = 3; + // Deprecating branch token in favor of branch info. + bytes branch_token = 4 [deprecated = true]; } // For history persistence to serialize/deserialize branch details. message HistoryBranch { - string tree_id = 1; - string branch_id = 2; - repeated HistoryBranchRange ancestors = 3; + string tree_id = 1; + string branch_id = 2; + repeated HistoryBranchRange ancestors = 3; } // HistoryBranchRange represents a piece of range for a branch. message HistoryBranchRange { - // BranchId of original branch forked from. - string branch_id = 1; - // Beginning node for the range, inclusive. - int64 begin_node_id = 2; - // Ending node for the range, exclusive. - int64 end_node_id = 3; + // BranchId of original branch forked from. + string branch_id = 1; + // Beginning node for the range, inclusive. + int64 begin_node_id = 2; + // Ending node for the range, exclusive. + int64 end_node_id = 3; } diff --git a/proto/internal/temporal/server/api/persistence/v1/hsm.proto b/proto/internal/temporal/server/api/persistence/v1/hsm.proto index c980cd3a1b5..0cd062adee4 100644 --- a/proto/internal/temporal/server/api/persistence/v1/hsm.proto +++ b/proto/internal/temporal/server/api/persistence/v1/hsm.proto @@ -1,142 +1,142 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "google/protobuf/timestamp.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // A node in a hierarchical state machine tree. message StateMachineNode { - // Serialized data of the underlying state machine. - bytes data = 1; - // Map of state machine type to a map of machines by ID. - map children = 2; - - // Versioned transition when the node was instantiated. - // This field, plus node path uniquely identifies a state machine node in a mutable state instance. - // This field will always be set even when transition history is disabled. - // NOTE: If transition history is disabled, the transition_count field will be 0 and - // cannot be used to uniquely identify a node. - // NOTE: Node deletion is not yet implemented at the time of writing so we can still uniquely identify a node just - // with the initial namespace failover version. - VersionedTransition initial_versioned_transition = 3; - - // Versioned transition when the node was last updated. - // This field will always be set even when transition history is disabled. - // NOTE: If transition history is disabled, the transition_count field will be 0 and - // cannot be used for non-concurrent task staleness check or to determine whether this node should be synced - // during state replication. - VersionedTransition last_update_versioned_transition = 4; - - // Number of transitions on this state machine object. - // Used to verify that a task is not stale if the state machine does not allow concurrent task execution. - // The transition count monotonically increases with each state transition and only resets when the entire - // mutable state was rebuilt. This case is handled by the task_generation_shard_clock_timestamp field in - // WorkflowExecutionInfo. - // NOTE: This field is cluster specific and cannot be replicated. - // NOTE: This field will be made obsolete when transition history is enabled in favor of - // last_update_versioned_transition. - int64 transition_count = 100; + // Serialized data of the underlying state machine. + bytes data = 1; + // Map of state machine type to a map of machines by ID. + map children = 2; + + // Versioned transition when the node was instantiated. + // This field, plus node path uniquely identifies a state machine node in a mutable state instance. + // This field will always be set even when transition history is disabled. + // NOTE: If transition history is disabled, the transition_count field will be 0 and + // cannot be used to uniquely identify a node. + // NOTE: Node deletion is not yet implemented at the time of writing so we can still uniquely identify a node just + // with the initial namespace failover version. + VersionedTransition initial_versioned_transition = 3; + + // Versioned transition when the node was last updated. + // This field will always be set even when transition history is disabled. + // NOTE: If transition history is disabled, the transition_count field will be 0 and + // cannot be used for non-concurrent task staleness check or to determine whether this node should be synced + // during state replication. + VersionedTransition last_update_versioned_transition = 4; + + // Number of transitions on this state machine object. + // Used to verify that a task is not stale if the state machine does not allow concurrent task execution. + // The transition count monotonically increases with each state transition and only resets when the entire + // mutable state was rebuilt. This case is handled by the task_generation_shard_clock_timestamp field in + // WorkflowExecutionInfo. + // NOTE: This field is cluster specific and cannot be replicated. + // NOTE: This field will be made obsolete when transition history is enabled in favor of + // last_update_versioned_transition. + int64 transition_count = 100; } // Map of state machine ID to StateMachineNode. message StateMachineMap { - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "by" is used to clarify the keys and values. --) - map machines_by_id = 1; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "by" is used to clarify the keys and values. --) + map machines_by_id = 1; } message StateMachineKey { - // Addressable type of the corresponding state machine in a single tree level. - string type = 1; - // Addressable ID of the corresponding state machine in a single tree level. - string id = 2; + // Addressable type of the corresponding state machine in a single tree level. + string type = 1; + // Addressable ID of the corresponding state machine in a single tree level. + string id = 2; } // A reference to a state machine at a point in time. message StateMachineRef { - // Nested path to a state machine. - repeated StateMachineKey path = 1; - - // Versioned transition of the ref was instantiated. - // Used to verify that the ref is not referencing a stale state or, in some situations, - // that the ref itself is not stale. - // NOTE: If transition history is disabled, the field will not be specified and - // cannot be used for staleness check. - VersionedTransition mutable_state_versioned_transition = 2; - - // Versioned transition when the state machine node was instantiated. - // This field, plus node path uniquely identifies a state machine node in a mutable state instance. - // This field will always be set even when transition history is disabled. - // NOTE: If transition history is disabled, the transition_count field will be 0 and - // cannot be used to uniquely identify a node. - // NOTE: Node deletion is not yet implemented at the time of writing so we can still uniquely identify a node just - // with the initial namespace failover version. - VersionedTransition machine_initial_versioned_transition = 3; - - // Versioned transition when the state machine node was last updated. - // If not specified, this reference is considered non-concurrent, - // and should match the last_update_versioned_transition on the corresponding state machine node. - // NOTE: If transition history is disabled, the transition_count field will be 0 and - // cannot be used for non-concurrent task staleness check. - VersionedTransition machine_last_update_versioned_transition = 4; - - // Number of transitions executed on the referenced state machine node at the time this Ref is instantiated. - // If non-zero, this reference is considered non-concurrent and this number should match the number of state - // transitions on the corresponding state machine node. - // This field will be obsolete once mutable state transition history is productionized. - int64 machine_transition_count = 100; + // Nested path to a state machine. + repeated StateMachineKey path = 1; + + // Versioned transition of the ref was instantiated. + // Used to verify that the ref is not referencing a stale state or, in some situations, + // that the ref itself is not stale. + // NOTE: If transition history is disabled, the field will not be specified and + // cannot be used for staleness check. + VersionedTransition mutable_state_versioned_transition = 2; + + // Versioned transition when the state machine node was instantiated. + // This field, plus node path uniquely identifies a state machine node in a mutable state instance. + // This field will always be set even when transition history is disabled. + // NOTE: If transition history is disabled, the transition_count field will be 0 and + // cannot be used to uniquely identify a node. + // NOTE: Node deletion is not yet implemented at the time of writing so we can still uniquely identify a node just + // with the initial namespace failover version. + VersionedTransition machine_initial_versioned_transition = 3; + + // Versioned transition when the state machine node was last updated. + // If not specified, this reference is considered non-concurrent, + // and should match the last_update_versioned_transition on the corresponding state machine node. + // NOTE: If transition history is disabled, the transition_count field will be 0 and + // cannot be used for non-concurrent task staleness check. + VersionedTransition machine_last_update_versioned_transition = 4; + + // Number of transitions executed on the referenced state machine node at the time this Ref is instantiated. + // If non-zero, this reference is considered non-concurrent and this number should match the number of state + // transitions on the corresponding state machine node. + // This field will be obsolete once mutable state transition history is productionized. + int64 machine_transition_count = 100; } message StateMachineTaskInfo { - // Reference to a state machine. - StateMachineRef ref = 1; - // Task type. Not to be confused with the state machine's type in the `ref` field. - string type = 2; - // Opaque data attached to this task. May be nil. Deserialized by a registered TaskSerializer for this type. - bytes data = 3; + // Reference to a state machine. + StateMachineRef ref = 1; + // Task type. Not to be confused with the state machine's type in the `ref` field. + string type = 2; + // Opaque data attached to this task. May be nil. Deserialized by a registered TaskSerializer for this type. + bytes data = 3; } // A group of state machine timer tasks for a given deadline, used for collapsing state machine timer tasks. message StateMachineTimerGroup { - // Task information. - repeated StateMachineTaskInfo infos = 1; - // When this timer should be fired. - // (-- api-linter: core::0142::time-field-names=disabled - // aip.dev/not-precedent: Ignoring lint rules. --) - google.protobuf.Timestamp deadline = 2; - // Whether or not a task was put in the queue for this group's deadline. - bool scheduled = 3; + // Task information. + repeated StateMachineTaskInfo infos = 1; + // When this timer should be fired. + // (-- api-linter: core::0142::time-field-names=disabled + // aip.dev/not-precedent: Ignoring lint rules. --) + google.protobuf.Timestamp deadline = 2; + // Whether or not a task was put in the queue for this group's deadline. + bool scheduled = 3; } // VersionedTransition is a unique identifier for a specific mutable state transition. message VersionedTransition { - // The namespace failover version at transition time. - int64 namespace_failover_version = 1; - // State transition count perceived during the specified namespace_failover_version. - int64 transition_count = 2; + // The namespace failover version at transition time. + int64 namespace_failover_version = 1; + // State transition count perceived during the specified namespace_failover_version. + int64 transition_count = 2; } message StateMachineTombstoneBatch { - // The versioned transition in which the tombstones were created. - VersionedTransition versioned_transition = 1; - repeated StateMachineTombstone state_machine_tombstones = 2; + // The versioned transition in which the tombstones were created. + VersionedTransition versioned_transition = 1; + repeated StateMachineTombstone state_machine_tombstones = 2; } message StateMachineTombstone { - oneof state_machine_key { - int64 activity_scheduled_event_id = 1; - string timer_id = 2; - int64 child_execution_initiated_event_id = 3; - int64 request_cancel_initiated_event_id = 4; - int64 signal_external_initiated_event_id = 5; - string update_id = 6; - StateMachinePath state_machine_path = 7; - string chasm_node_path = 8; - } + oneof state_machine_key { + int64 activity_scheduled_event_id = 1; + string timer_id = 2; + int64 child_execution_initiated_event_id = 3; + int64 request_cancel_initiated_event_id = 4; + int64 signal_external_initiated_event_id = 5; + string update_id = 6; + StateMachinePath state_machine_path = 7; + string chasm_node_path = 8; + } } message StateMachinePath { - repeated StateMachineKey path = 1; + repeated StateMachineKey path = 1; } - diff --git a/proto/internal/temporal/server/api/persistence/v1/namespaces.proto b/proto/internal/temporal/server/api/persistence/v1/namespaces.proto index 64daf4910ef..86591a9e3b7 100644 --- a/proto/internal/temporal/server/api/persistence/v1/namespaces.proto +++ b/proto/internal/temporal/server/api/persistence/v1/namespaces.proto @@ -1,56 +1,56 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; - import "temporal/api/enums/v1/namespace.proto"; import "temporal/api/namespace/v1/message.proto"; import "temporal/api/rules/v1/message.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // detail column message NamespaceDetail { - NamespaceInfo info = 1; - NamespaceConfig config = 2; - NamespaceReplicationConfig replication_config = 3; - int64 config_version = 4; - int64 failover_notification_version = 5; - int64 failover_version = 6; - google.protobuf.Timestamp failover_end_time = 7; + NamespaceInfo info = 1; + NamespaceConfig config = 2; + NamespaceReplicationConfig replication_config = 3; + int64 config_version = 4; + int64 failover_notification_version = 5; + int64 failover_version = 6; + google.protobuf.Timestamp failover_end_time = 7; } message NamespaceInfo { - string id = 1; - temporal.api.enums.v1.NamespaceState state = 2; - string name = 3; - string description = 4; - string owner = 5; - map data = 6; + string id = 1; + temporal.api.enums.v1.NamespaceState state = 2; + string name = 3; + string description = 4; + string owner = 5; + map data = 6; } message NamespaceConfig { - google.protobuf.Duration retention = 1; - string archival_bucket = 2; - temporal.api.namespace.v1.BadBinaries bad_binaries = 3; - temporal.api.enums.v1.ArchivalState history_archival_state = 4; - string history_archival_uri = 5; - temporal.api.enums.v1.ArchivalState visibility_archival_state = 6; - string visibility_archival_uri = 7; - map custom_search_attribute_aliases = 8; - map workflow_rules = 9; + google.protobuf.Duration retention = 1; + string archival_bucket = 2; + temporal.api.namespace.v1.BadBinaries bad_binaries = 3; + temporal.api.enums.v1.ArchivalState history_archival_state = 4; + string history_archival_uri = 5; + temporal.api.enums.v1.ArchivalState visibility_archival_state = 6; + string visibility_archival_uri = 7; + map custom_search_attribute_aliases = 8; + map workflow_rules = 9; } message NamespaceReplicationConfig { - string active_cluster_name = 1; - repeated string clusters = 2; - temporal.api.enums.v1.ReplicationState state = 3; - repeated FailoverStatus failover_history = 8; + string active_cluster_name = 1; + repeated string clusters = 2; + temporal.api.enums.v1.ReplicationState state = 3; + repeated FailoverStatus failover_history = 8; } // Represents a historical replication status of a Namespace message FailoverStatus { - google.protobuf.Timestamp failover_time = 1; - int64 failover_version = 2; + google.protobuf.Timestamp failover_time = 1; + int64 failover_version = 2; } diff --git a/proto/internal/temporal/server/api/persistence/v1/nexus.proto b/proto/internal/temporal/server/api/persistence/v1/nexus.proto index ce8f39c0c04..38fa5f8cbd0 100644 --- a/proto/internal/temporal/server/api/persistence/v1/nexus.proto +++ b/proto/internal/temporal/server/api/persistence/v1/nexus.proto @@ -1,70 +1,71 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/server/api/clock/v1/message.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // Contains mutable fields for an Endpoint. Duplicated from the public API's temporal.api.nexus.v1.EndpointSpec where // the worker target has a namespace name. // We store an ID in persistence to prevent namespace renames from breaking references. message NexusEndpointSpec { - // Endpoint name, unique for this cluster. Must match `[a-zA-Z_][a-zA-Z0-9_]*`. - // Renaming an endpoint breaks all workflow callers that reference this endpoint, causing operations to fail. - string name = 1; - temporal.api.common.v1.Payload description = 2; + // Endpoint name, unique for this cluster. Must match `[a-zA-Z_][a-zA-Z0-9_]*`. + // Renaming an endpoint breaks all workflow callers that reference this endpoint, causing operations to fail. + string name = 1; + temporal.api.common.v1.Payload description = 2; - // Target to route requests to. - NexusEndpointTarget target = 3; + // Target to route requests to. + NexusEndpointTarget target = 3; } // Target to route requests to. // Duplicated from the public API's temporal.api.nexus.v1.EndpointTarget where the worker target has a namespace name. // We store an ID in persistence to prevent namespace renames from breaking references. message NexusEndpointTarget { - // Target a worker polling on a Nexus task queue in a specific namespace. - message Worker { - // Namespace ID to route requests to. - string namespace_id = 1; - // Nexus task queue to route requests to. - string task_queue = 2; - } - - // Target an external server by URL. - // At a later point, this will support providing credentials, in the meantime, an http.RoundTripper can be injected - // into the server to modify the request. - message External { - // URL to call. - // (-- api-linter: core::0140::uri=disabled - // aip.dev/not-precedent: Not following linter rules. --) - string url = 1; - } + // Target a worker polling on a Nexus task queue in a specific namespace. + message Worker { + // Namespace ID to route requests to. + string namespace_id = 1; + // Nexus task queue to route requests to. + string task_queue = 2; + } - oneof variant { - Worker worker = 1; - External external = 2; - } + // Target an external server by URL. + // At a later point, this will support providing credentials, in the meantime, an http.RoundTripper can be injected + // into the server to modify the request. + message External { + // URL to call. + // (-- api-linter: core::0140::uri=disabled + // aip.dev/not-precedent: Not following linter rules. --) + string url = 1; + } + + oneof variant { + Worker worker = 1; + External external = 2; + } } message NexusEndpoint { - // The last recorded cluster-local Hybrid Logical Clock timestamp for _this_ endpoint. - // Updated whenever the endpoint is directly updated due to a user action but not when applying replication events. - // The clock is referenced when new timestamps are generated to ensure it produces monotonically increasing - // timestamps. - temporal.server.api.clock.v1.HybridLogicalClock clock = 1; - // Endpoint specification. This is a mirror of the public API and is intended to be mutable. - NexusEndpointSpec spec = 2; - // The date and time when the endpoint was created. - // (-- api-linter: core::0142::time-field-names=disabled - // aip.dev/not-precedent: Not following linter rules. --) - google.protobuf.Timestamp created_time = 3; + // The last recorded cluster-local Hybrid Logical Clock timestamp for _this_ endpoint. + // Updated whenever the endpoint is directly updated due to a user action but not when applying replication events. + // The clock is referenced when new timestamps are generated to ensure it produces monotonically increasing + // timestamps. + temporal.server.api.clock.v1.HybridLogicalClock clock = 1; + // Endpoint specification. This is a mirror of the public API and is intended to be mutable. + NexusEndpointSpec spec = 2; + // The date and time when the endpoint was created. + // (-- api-linter: core::0142::time-field-names=disabled + // aip.dev/not-precedent: Not following linter rules. --) + google.protobuf.Timestamp created_time = 3; } // Container for a version, a UUID, and a NexusEndpoint. message NexusEndpointEntry { - int64 version = 1; - string id = 2; - NexusEndpoint endpoint = 3; + int64 version = 1; + string id = 2; + NexusEndpoint endpoint = 3; } diff --git a/proto/internal/temporal/server/api/persistence/v1/predicates.proto b/proto/internal/temporal/server/api/persistence/v1/predicates.proto index e3d47d526b8..b6d2c71f39d 100644 --- a/proto/internal/temporal/server/api/persistence/v1/predicates.proto +++ b/proto/internal/temporal/server/api/persistence/v1/predicates.proto @@ -1,66 +1,65 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "temporal/server/api/enums/v1/predicate.proto"; import "temporal/server/api/enums/v1/task.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + message Predicate { - temporal.server.api.enums.v1.PredicateType predicate_type = 1; - oneof attributes { - UniversalPredicateAttributes universal_predicate_attributes = 2; - EmptyPredicateAttributes empty_predicate_attributes = 3; - AndPredicateAttributes and_predicate_attributes = 4; - OrPredicateAttributes or_predicate_attributes = 5; - NotPredicateAttributes not_predicate_attributes = 6; - NamespaceIdPredicateAttributes namespace_id_predicate_attributes = 7; - TaskTypePredicateAttributes task_type_predicate_attributes = 8; - DestinationPredicateAttributes destination_predicate_attributes = 9; - OutboundTaskGroupPredicateAttributes outbound_task_group_predicate_attributes = 10; - OutboundTaskPredicateAttributes outbound_task_predicate_attributes = 11; - } + temporal.server.api.enums.v1.PredicateType predicate_type = 1; + oneof attributes { + UniversalPredicateAttributes universal_predicate_attributes = 2; + EmptyPredicateAttributes empty_predicate_attributes = 3; + AndPredicateAttributes and_predicate_attributes = 4; + OrPredicateAttributes or_predicate_attributes = 5; + NotPredicateAttributes not_predicate_attributes = 6; + NamespaceIdPredicateAttributes namespace_id_predicate_attributes = 7; + TaskTypePredicateAttributes task_type_predicate_attributes = 8; + DestinationPredicateAttributes destination_predicate_attributes = 9; + OutboundTaskGroupPredicateAttributes outbound_task_group_predicate_attributes = 10; + OutboundTaskPredicateAttributes outbound_task_predicate_attributes = 11; + } } -message UniversalPredicateAttributes { -} +message UniversalPredicateAttributes {} -message EmptyPredicateAttributes { -} +message EmptyPredicateAttributes {} message AndPredicateAttributes { - repeated Predicate predicates = 1; + repeated Predicate predicates = 1; } message OrPredicateAttributes { - repeated Predicate predicates = 1; + repeated Predicate predicates = 1; } message NotPredicateAttributes { - Predicate predicate = 1; + Predicate predicate = 1; } message NamespaceIdPredicateAttributes { - repeated string namespace_ids = 1; + repeated string namespace_ids = 1; } message TaskTypePredicateAttributes { - repeated temporal.server.api.enums.v1.TaskType task_types = 1; + repeated temporal.server.api.enums.v1.TaskType task_types = 1; } message DestinationPredicateAttributes { - repeated string destinations = 1; + repeated string destinations = 1; } message OutboundTaskGroupPredicateAttributes { - repeated string groups = 1; + repeated string groups = 1; } message OutboundTaskPredicateAttributes { - message Group { - string task_group = 1; - string namespace_id = 2; - string destination = 3; - } - repeated Group groups = 1; + message Group { + string task_group = 1; + string namespace_id = 2; + string destination = 3; + } + repeated Group groups = 1; } diff --git a/proto/internal/temporal/server/api/persistence/v1/queue_metadata.proto b/proto/internal/temporal/server/api/persistence/v1/queue_metadata.proto index 76a061fba4b..c7c3538f437 100644 --- a/proto/internal/temporal/server/api/persistence/v1/queue_metadata.proto +++ b/proto/internal/temporal/server/api/persistence/v1/queue_metadata.proto @@ -1,9 +1,10 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; + option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; // data column message QueueMetadata { - map cluster_ack_levels = 1; + map cluster_ack_levels = 1; } diff --git a/proto/internal/temporal/server/api/persistence/v1/queues.proto b/proto/internal/temporal/server/api/persistence/v1/queues.proto index c87b9c60842..dc62e054084 100644 --- a/proto/internal/temporal/server/api/persistence/v1/queues.proto +++ b/proto/internal/temporal/server/api/persistence/v1/queues.proto @@ -1,54 +1,54 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "temporal/api/common/v1/message.proto"; import "temporal/server/api/persistence/v1/predicates.proto"; import "temporal/server/api/persistence/v1/tasks.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + message QueueState { - map reader_states = 1; - TaskKey exclusive_reader_high_watermark = 2; + map reader_states = 1; + TaskKey exclusive_reader_high_watermark = 2; } message QueueReaderState { - repeated QueueSliceScope scopes = 1; + repeated QueueSliceScope scopes = 1; } message QueueSliceScope { - QueueSliceRange range = 1; - Predicate predicate = 2; + QueueSliceRange range = 1; + Predicate predicate = 2; } message QueueSliceRange { - TaskKey inclusive_min = 1; - TaskKey exclusive_max = 2; + TaskKey inclusive_min = 1; + TaskKey exclusive_max = 2; } message ReadQueueMessagesNextPageToken { - int64 last_read_message_id = 1; + int64 last_read_message_id = 1; } message ListQueuesNextPageToken { - int64 last_read_queue_number = 1; + int64 last_read_queue_number = 1; } // HistoryTask represents an internal history service task for a particular shard. We use a blob because there is no // common proto for all task proto types. message HistoryTask { - // shard_id that this task belonged to when it was created. Technically, you can derive this from the task data - // blob, but it's useful to have it here for quick access and to avoid deserializing the blob. Note that this may be - // different from the shard id of this task in the current cluster because it could have come from a cluster with a - // different shard id. This will always be the shard id of the task in its original cluster. - int32 shard_id = 1; - // blob that contains the history task proto. There is a GoLang-specific generic deserializer for this blob, but - // there is no common proto for all task proto types, so deserializing in other languages will require a custom - // switch on the task category, which should be available from the metadata for the queue that this task came from. - temporal.api.common.v1.DataBlob blob = 2; + // shard_id that this task belonged to when it was created. Technically, you can derive this from the task data + // blob, but it's useful to have it here for quick access and to avoid deserializing the blob. Note that this may be + // different from the shard id of this task in the current cluster because it could have come from a cluster with a + // different shard id. This will always be the shard id of the task in its original cluster. + int32 shard_id = 1; + // blob that contains the history task proto. There is a GoLang-specific generic deserializer for this blob, but + // there is no common proto for all task proto types, so deserializing in other languages will require a custom + // switch on the task category, which should be available from the metadata for the queue that this task came from. + temporal.api.common.v1.DataBlob blob = 2; } - message QueuePartition { // min_message_id is less than or equal to the id of every message in the queue. The min_message_id is mainly used to // skip over tombstones in Cassandra: let's say we deleted the first 1K messages from a queue with 1.1K messages. If diff --git a/proto/internal/temporal/server/api/persistence/v1/task_queues.proto b/proto/internal/temporal/server/api/persistence/v1/task_queues.proto index ea9d317eafa..536f0887ba3 100644 --- a/proto/internal/temporal/server/api/persistence/v1/task_queues.proto +++ b/proto/internal/temporal/server/api/persistence/v1/task_queues.proto @@ -1,7 +1,6 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "temporal/api/deployment/v1/message.proto"; import "temporal/api/taskqueue/v1/message.proto"; @@ -9,122 +8,124 @@ import "temporal/server/api/clock/v1/message.proto"; import "temporal/server/api/deployment/v1/message.proto"; import "temporal/server/api/enums/v1/fairness_state.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // BuildId is an identifier with a timestamped status used to identify workers for task queue versioning purposes. message BuildId { - enum State { - STATE_UNSPECIFIED = 0; - STATE_ACTIVE = 1; - STATE_DELETED = 2; - }; - - string id = 1; - State state = 2; - // HLC timestamp representing when the state was updated or the when build ID was originally inserted. - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: Using HLC instead of wall clock. --) - temporal.server.api.clock.v1.HybridLogicalClock state_update_timestamp = 3; - // HLC timestamp representing when this build ID was last made default in its version set. - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: Using HLC instead of wall clock. --) - temporal.server.api.clock.v1.HybridLogicalClock became_default_timestamp = 4; + enum State { + STATE_UNSPECIFIED = 0; + STATE_ACTIVE = 1; + STATE_DELETED = 2; + } + + string id = 1; + State state = 2; + // HLC timestamp representing when the state was updated or the when build ID was originally inserted. + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: Using HLC instead of wall clock. --) + temporal.server.api.clock.v1.HybridLogicalClock state_update_timestamp = 3; + // HLC timestamp representing when this build ID was last made default in its version set. + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: Using HLC instead of wall clock. --) + temporal.server.api.clock.v1.HybridLogicalClock became_default_timestamp = 4; } // An internal representation of temporal.api.taskqueue.v1.CompatibleVersionSet message CompatibleVersionSet { - // Set IDs are used internally by matching. - // A set typically has one set ID and extra care is taken to enforce this. - // In some situations, including: - // - Replication race between task queue user data and history events - // - Replication split-brain + later merge - // - Delayed user data propagation between partitions - // - Cross-task-queue activities/child workflows/CAN where the user has not set up parallel - // versioning data - // we have to guess the set id for a build ID. If that happens, and then the build ID is - // discovered to be in a different set, then the sets will be merged and both (or more) - // build ids will be preserved, so that we don't lose tasks. - // The first set id is considered the "primary", and the others are "demoted". Once a build - // id is demoted, it cannot be made the primary again. - repeated string set_ids = 1; - // All the compatible versions, unordered except for the last element, which is considered the set "default". - repeated BuildId build_ids = 2; - // HLC timestamp representing when this set was last made the default for the queue. - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: Using HLC instead of wall clock. --) - temporal.server.api.clock.v1.HybridLogicalClock became_default_timestamp = 4; + // Set IDs are used internally by matching. + // A set typically has one set ID and extra care is taken to enforce this. + // In some situations, including: + // - Replication race between task queue user data and history events + // - Replication split-brain + later merge + // - Delayed user data propagation between partitions + // - Cross-task-queue activities/child workflows/CAN where the user has not set up parallel + // versioning data + // we have to guess the set id for a build ID. If that happens, and then the build ID is + // discovered to be in a different set, then the sets will be merged and both (or more) + // build ids will be preserved, so that we don't lose tasks. + // The first set id is considered the "primary", and the others are "demoted". Once a build + // id is demoted, it cannot be made the primary again. + repeated string set_ids = 1; + // All the compatible versions, unordered except for the last element, which is considered the set "default". + repeated BuildId build_ids = 2; + // HLC timestamp representing when this set was last made the default for the queue. + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: Using HLC instead of wall clock. --) + temporal.server.api.clock.v1.HybridLogicalClock became_default_timestamp = 4; } message AssignmentRule { - temporal.api.taskqueue.v1.BuildIdAssignmentRule rule = 1; - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: Using HLC instead of wall clock. --) - temporal.server.api.clock.v1.HybridLogicalClock create_timestamp = 2; - // when delete_timestamp is present the rule should be treated as deleted - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: Using HLC instead of wall clock. --) - temporal.server.api.clock.v1.HybridLogicalClock delete_timestamp = 3; + temporal.api.taskqueue.v1.BuildIdAssignmentRule rule = 1; + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: Using HLC instead of wall clock. --) + temporal.server.api.clock.v1.HybridLogicalClock create_timestamp = 2; + // when delete_timestamp is present the rule should be treated as deleted + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: Using HLC instead of wall clock. --) + temporal.server.api.clock.v1.HybridLogicalClock delete_timestamp = 3; } message RedirectRule { - temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule rule = 1; - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: Using HLC instead of wall clock. --) - temporal.server.api.clock.v1.HybridLogicalClock create_timestamp = 2; - // when delete_timestamp is present the rule should be treated as deleted - // (-- api-linter: core::0142::time-field-type=disabled - // aip.dev/not-precedent: Using HLC instead of wall clock. --) - temporal.server.api.clock.v1.HybridLogicalClock delete_timestamp = 3; + temporal.api.taskqueue.v1.CompatibleBuildIdRedirectRule rule = 1; + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: Using HLC instead of wall clock. --) + temporal.server.api.clock.v1.HybridLogicalClock create_timestamp = 2; + // when delete_timestamp is present the rule should be treated as deleted + // (-- api-linter: core::0142::time-field-type=disabled + // aip.dev/not-precedent: Using HLC instead of wall clock. --) + temporal.server.api.clock.v1.HybridLogicalClock delete_timestamp = 3; } // Holds all the data related to worker versioning for a task queue. // Backwards-incompatible changes cannot be made, as this would make existing stored data unreadable. message VersioningData { - // All the incompatible version sets, unordered except for the last element, which is considered the set "default". - repeated CompatibleVersionSet version_sets = 1; - // Ordered list of assignment rules. Also contains recently-deleted rules. - repeated AssignmentRule assignment_rules = 2; - // Unordered list of redirect rules. Also contains recently-deleted rules. - repeated RedirectRule redirect_rules = 3; + // All the incompatible version sets, unordered except for the last element, which is considered the set "default". + repeated CompatibleVersionSet version_sets = 1; + // Ordered list of assignment rules. Also contains recently-deleted rules. + repeated AssignmentRule assignment_rules = 2; + // Unordered list of redirect rules. Also contains recently-deleted rules. + repeated RedirectRule redirect_rules = 3; } message DeploymentData { - reserved 1; - // Set of worker deployment versions that this task queue belongs to. - // Current Version is defined implicitly as the version with `current_since_time!=nil` and the most - // recent `routing_update_time`. - // Ramping Version is defined implicitly as the version with `ramping_since_time!=nil` and the most - // recent `routing_update_time`. - // The Ramping Version receives a share of unversioned/unpinned tasks according to its - // `ramp_percentage`. If there is no Ramping Version, all the unversioned/unpinned tasks are - // routed to the Current Version. If there is no Current Version, any poller with UNVERSIONED - // (or unspecified) WorkflowVersioningMode will receive the tasks. - // Remove after `AsyncSetCurrentAndRamping` workflow version is irreversibly enabled. - repeated temporal.server.api.deployment.v1.DeploymentVersionData versions = 2 [deprecated = true]; - - // Present if the task queue's ramping version is unversioned. - // Remove after `AsyncSetCurrentAndRamping` workflow version is irreversibly enabled. - temporal.server.api.deployment.v1.DeploymentVersionData unversioned_ramp_data = 3 [deprecated = true]; - - // Routing and version membership data for all worker deployments that this task queue belongs to. - // Key is the deployment name. - map deployments_data = 4; + reserved 1; + // Set of worker deployment versions that this task queue belongs to. + // Current Version is defined implicitly as the version with `current_since_time!=nil` and the most + // recent `routing_update_time`. + // Ramping Version is defined implicitly as the version with `ramping_since_time!=nil` and the most + // recent `routing_update_time`. + // The Ramping Version receives a share of unversioned/unpinned tasks according to its + // `ramp_percentage`. If there is no Ramping Version, all the unversioned/unpinned tasks are + // routed to the Current Version. If there is no Current Version, any poller with UNVERSIONED + // (or unspecified) WorkflowVersioningMode will receive the tasks. + // Remove after `AsyncSetCurrentAndRamping` workflow version is irreversibly enabled. + repeated temporal.server.api.deployment.v1.DeploymentVersionData versions = 2 [deprecated = true]; + + // Present if the task queue's ramping version is unversioned. + // Remove after `AsyncSetCurrentAndRamping` workflow version is irreversibly enabled. + temporal.server.api.deployment.v1.DeploymentVersionData unversioned_ramp_data = 3 [deprecated = true]; + + // Routing and version membership data for all worker deployments that this task queue belongs to. + // Key is the deployment name. + map deployments_data = 4; } // Routing config and version membership data for a given worker deployment that a TQ should know. message WorkerDeploymentData { - temporal.api.deployment.v1.RoutingConfig routing_config = 1; - // This map tracks the membership of the task queue in the deployment versions. A version is - // present here iff the task queue has ever been polled from the version. - // Key is the build id. - map versions = 2; + temporal.api.deployment.v1.RoutingConfig routing_config = 1; + // This map tracks the membership of the task queue in the deployment versions. A version is + // present here iff the task queue has ever been polled from the version. + // Key is the build id. + map versions = 2; } // Container for all persistent user data that varies per task queue type within a family. message TaskQueueTypeUserData { - DeploymentData deployment_data = 1; + DeploymentData deployment_data = 1; - temporal.api.taskqueue.v1.TaskQueueConfig config = 2; + temporal.api.taskqueue.v1.TaskQueueConfig config = 2; - temporal.server.api.enums.v1.FairnessState fairness_state = 3; + temporal.server.api.enums.v1.FairnessState fairness_state = 3; } // Container for all persistent user provided data for a task queue family. @@ -133,21 +134,21 @@ message TaskQueueTypeUserData { // This data must all fit in a single DB column and is kept cached in-memory, take extra care to ensure data added here // has reasonable size limits imposed on it. message TaskQueueUserData { - // The last recorded cluster-local Hybrid Logical Clock timestamp for _this_ task queue family. - // Updated whenever user data is directly updated due to a user action but not when applying replication events. - // The clock is referenced when new timestamps are generated to ensure it produces monotonically increasing - // timestamps. - temporal.server.api.clock.v1.HybridLogicalClock clock = 1; - VersioningData versioning_data = 2; + // The last recorded cluster-local Hybrid Logical Clock timestamp for _this_ task queue family. + // Updated whenever user data is directly updated due to a user action but not when applying replication events. + // The clock is referenced when new timestamps are generated to ensure it produces monotonically increasing + // timestamps. + temporal.server.api.clock.v1.HybridLogicalClock clock = 1; + VersioningData versioning_data = 2; - // Map from task queue type (workflow, activity, nexus) to per-type data. - map per_type = 3; + // Map from task queue type (workflow, activity, nexus) to per-type data. + map per_type = 3; - // For future use: description, rate limits, manual partition control, etc... +// For future use: description, rate limits, manual partition control, etc... } // Simple wrapper that includes a TaskQueueUserData and its storage version. message VersionedTaskQueueUserData { - TaskQueueUserData data = 1; - int64 version = 2; + TaskQueueUserData data = 1; + int64 version = 2; } diff --git a/proto/internal/temporal/server/api/persistence/v1/tasks.proto b/proto/internal/temporal/server/api/persistence/v1/tasks.proto index c2a6c0b5d37..fae67cc7d04 100644 --- a/proto/internal/temporal/server/api/persistence/v1/tasks.proto +++ b/proto/internal/temporal/server/api/persistence/v1/tasks.proto @@ -1,102 +1,110 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "google/protobuf/timestamp.proto"; - import "temporal/api/common/v1/message.proto"; import "temporal/api/enums/v1/task_queue.proto"; - import "temporal/server/api/clock/v1/message.proto"; import "temporal/server/api/taskqueue/v1/message.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // task column message AllocatedTaskInfo { - TaskInfo data = 1; - int64 task_pass = 3; - int64 task_id = 2; + TaskInfo data = 1; + int64 task_pass = 3; + int64 task_id = 2; } message TaskInfo { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - int64 scheduled_event_id = 4; - google.protobuf.Timestamp create_time = 5; - google.protobuf.Timestamp expiry_time = 6; - temporal.server.api.clock.v1.VectorClock clock = 7; - // How this task should be directed. (Missing means the default for - // TaskVersionDirective, which is unversioned.) - temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 8; - // Stamp field allows to differentiate between different instances of the same task - int32 stamp = 9; - temporal.api.common.v1.Priority priority = 10; - // Reference to any chasm component associated with this task - bytes component_ref = 11; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + int64 scheduled_event_id = 4; + google.protobuf.Timestamp create_time = 5; + google.protobuf.Timestamp expiry_time = 6; + temporal.server.api.clock.v1.VectorClock clock = 7; + // How this task should be directed. (Missing means the default for + // TaskVersionDirective, which is unversioned.) + temporal.server.api.taskqueue.v1.TaskVersionDirective version_directive = 8; + // Stamp field allows to differentiate between different instances of the same task + int32 stamp = 9; + temporal.api.common.v1.Priority priority = 10; + // Reference to any chasm component associated with this task + bytes component_ref = 11; } // task_queue column message TaskQueueInfo { - string namespace_id = 1; - string name = 2; - temporal.api.enums.v1.TaskQueueType task_type = 3; - temporal.api.enums.v1.TaskQueueKind kind = 4; - // After data is migrated into subqueues, this contains a copy of the ack level for subqueue 0. - int64 ack_level = 5; - google.protobuf.Timestamp expiry_time = 6; - google.protobuf.Timestamp last_update_time = 7; - // After data is migrated into subqueues, this contains a copy of the count for subqueue 0. - int64 approximate_backlog_count = 8; - - // Subqueues contains one entry for each subqueue in this physical task queue. - // Tasks are split into subqueues to implement priority and fairness. - // Subqueues are indexed starting from 0, the zero subqueue is always present - // and corresponds to the "main" queue before subqueues were introduced. - // - // The message at index n describes the subqueue at index n. - // - // Each subqueue has its own ack level and approx backlog count, but they share - // the range id. For compatibility, ack level and backlog count for subqueue 0 - // is copied into TaskQueueInfo. - repeated SubqueueInfo subqueues = 9; - - // For transitioning from tasks (v1) to tasks_v2 and back: - // - // If this TaskQueueInfo is in v1 and this is set, then v2 may have tasks. - // If this TaskQueueInfo is in v2 and this is set, then v1 may have tasks. - // - // New metadata starts with this flag set (we could skip this when useNewMatcher is off). - // Whenever locking any metadata as the inactive one (drain-only), this should be set. - // If the flag is true, no tasks should be written to the active table until the inactive - // table has also been locked (and the flag set there for a potential reverse transition). - // After determinining that the inactive table has no more tasks left, then this - // can be cleared on the active table. - bool other_has_tasks = 10; + string namespace_id = 1; + string name = 2; + temporal.api.enums.v1.TaskQueueType task_type = 3; + temporal.api.enums.v1.TaskQueueKind kind = 4; + // After data is migrated into subqueues, this contains a copy of the ack level for subqueue 0. + int64 ack_level = 5; + google.protobuf.Timestamp expiry_time = 6; + google.protobuf.Timestamp last_update_time = 7; + // After data is migrated into subqueues, this contains a copy of the count for subqueue 0. + int64 approximate_backlog_count = 8; + + // Subqueues contains one entry for each subqueue in this physical task queue. + // Tasks are split into subqueues to implement priority and fairness. + // Subqueues are indexed starting from 0, the zero subqueue is always present + // and corresponds to the "main" queue before subqueues were introduced. + // + // The message at index n describes the subqueue at index n. + // + // Each subqueue has its own ack level and approx backlog count, but they share + // the range id. For compatibility, ack level and backlog count for subqueue 0 + // is copied into TaskQueueInfo. + repeated SubqueueInfo subqueues = 9; + + // For transitioning from tasks (v1) to tasks_v2 and back: + // + // If this TaskQueueInfo is in v1 and this is set, then v2 may have tasks. + // If this TaskQueueInfo is in v2 and this is set, then v1 may have tasks. + // + // New metadata starts with this flag set (we could skip this when useNewMatcher is off). + // Whenever locking any metadata as the inactive one (drain-only), this should be set. + // If the flag is true, no tasks should be written to the active table until the inactive + // table has also been locked (and the flag set there for a potential reverse transition). + // After determinining that the inactive table has no more tasks left, then this + // can be cleared on the active table. + bool other_has_tasks = 10; } message SubqueueInfo { - // Key is the information used by a splitting algorithm to decide which tasks should go in - // this subqueue. It should not change after being registered in TaskQueueInfo. - SubqueueKey key = 1; + // Key is the information used by a splitting algorithm to decide which tasks should go in + // this subqueue. It should not change after being registered in TaskQueueInfo. + SubqueueKey key = 1; + + // The rest are mutable state for the subqueue: + int64 ack_level = 2; + temporal.server.api.taskqueue.v1.FairLevel fair_ack_level = 4; - // The rest are mutable state for the subqueue: - int64 ack_level = 2; - temporal.server.api.taskqueue.v1.FairLevel fair_ack_level = 4; + int64 approximate_backlog_count = 3; - int64 approximate_backlog_count = 3; + // Max read level keeps track of the highest task level ever written, but is only + // maintained best-effort. Do not trust these values. + temporal.server.api.taskqueue.v1.FairLevel fair_max_read_level = 5; + + // We can persist a limited number of fairness key counts in task queue + // metadata so they're not lost on migration. + repeated FairnessKeyCount top_k_fairness_counts = 6; +} - // Max read level keeps track of the highest task level ever written, but is only - // maintained best-effort. Do not trust these values. - temporal.server.api.taskqueue.v1.FairLevel fair_max_read_level = 5; +message FairnessKeyCount { + string key = 1; + int64 count = 2; } message SubqueueKey { - // Each subqueue contains tasks from only one priority level. - int32 priority = 1; + // Each subqueue contains tasks from only one priority level. + int32 priority = 1; } message TaskKey { - google.protobuf.Timestamp fire_time = 1; - int64 task_id = 2; + google.protobuf.Timestamp fire_time = 1; + int64 task_id = 2; } diff --git a/proto/internal/temporal/server/api/persistence/v1/update.proto b/proto/internal/temporal/server/api/persistence/v1/update.proto index 334111f09cb..05405cd50e9 100644 --- a/proto/internal/temporal/server/api/persistence/v1/update.proto +++ b/proto/internal/temporal/server/api/persistence/v1/update.proto @@ -1,53 +1,54 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "temporal/server/api/persistence/v1/hsm.proto"; +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; + // UpdateAdmissionInfo contains information about a durably admitted update. Note that updates in Admitted state are typically // non-durable (i.e. do not have a corresponding event in history). Durably admitted updates arise as a result of // workflow reset or history event replication conflict: in these cases a WorkflowExecutionUpdateAdmittedEvent event is // created when an accepted update (on one branch of workflow history) is converted into an admitted update (on another // branch). message UpdateAdmissionInfo { - message HistoryPointer { - // the event ID of the WorkflowExecutionUpdateAdmittedEvent - int64 event_id = 1; - // the ID of the event batch containing the event_id - int64 event_batch_id = 2; - } - - oneof location { - HistoryPointer history_pointer = 1; - } + message HistoryPointer { + // the event ID of the WorkflowExecutionUpdateAdmittedEvent + int64 event_id = 1; + // the ID of the event batch containing the event_id + int64 event_batch_id = 2; + } + + oneof location { + HistoryPointer history_pointer = 1; + } } // UpdateAcceptanceInfo contains information about an accepted update message UpdateAcceptanceInfo { - // the event ID of the WorkflowExecutionUpdateAcceptedEvent - int64 event_id = 1; + // the event ID of the WorkflowExecutionUpdateAcceptedEvent + int64 event_id = 1; } // UpdateCompletionInfo contains information about a completed update message UpdateCompletionInfo { - // the event ID of the WorkflowExecutionUpdateCompletedEvent - int64 event_id = 1; + // the event ID of the WorkflowExecutionUpdateCompletedEvent + int64 event_id = 1; - // the ID of the event batch containing the event_id above - int64 event_batch_id = 2; + // the ID of the event batch containing the event_id above + int64 event_batch_id = 2; } // UpdateInfo is the persistent state of a single update message UpdateInfo { - oneof value { - // update has been accepted and this is the acceptance metadata - UpdateAcceptanceInfo acceptance = 1; - // update has been completed and this is the completion metadata - UpdateCompletionInfo completion = 2; - // update has been admitted and this is the admission metadata - UpdateAdmissionInfo admission = 3; - } - - VersionedTransition last_update_versioned_transition = 4; + oneof value { + // update has been accepted and this is the acceptance metadata + UpdateAcceptanceInfo acceptance = 1; + // update has been completed and this is the completion metadata + UpdateCompletionInfo completion = 2; + // update has been admitted and this is the admission metadata + UpdateAdmissionInfo admission = 3; + } + + VersionedTransition last_update_versioned_transition = 4; } diff --git a/proto/internal/temporal/server/api/persistence/v1/workflow_mutable_state.proto b/proto/internal/temporal/server/api/persistence/v1/workflow_mutable_state.proto index d52ca747e6b..127a2b8ca0b 100644 --- a/proto/internal/temporal/server/api/persistence/v1/workflow_mutable_state.proto +++ b/proto/internal/temporal/server/api/persistence/v1/workflow_mutable_state.proto @@ -1,67 +1,67 @@ syntax = "proto3"; package temporal.server.api.persistence.v1; -option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; import "temporal/api/history/v1/message.proto"; +import "temporal/server/api/persistence/v1/chasm.proto"; import "temporal/server/api/persistence/v1/executions.proto"; import "temporal/server/api/persistence/v1/hsm.proto"; -import "temporal/server/api/persistence/v1/chasm.proto"; import "temporal/server/api/persistence/v1/update.proto"; -message WorkflowMutableState{ - map activity_infos = 1; - map timer_infos = 2; - map child_execution_infos = 3; - map request_cancel_infos = 4; - map signal_infos = 5; - map chasm_nodes = 12; - repeated string signal_requested_ids = 6; - WorkflowExecutionInfo execution_info = 7; - WorkflowExecutionState execution_state = 8; - int64 next_event_id = 9; - repeated temporal.api.history.v1.HistoryEvent buffered_events = 10; - Checksum checksum = 11; -} +option go_package = "go.temporal.io/server/api/persistence/v1;persistence"; -message WorkflowMutableStateMutation{ +message WorkflowMutableState { + map activity_infos = 1; + map timer_infos = 2; + map child_execution_infos = 3; + map request_cancel_infos = 4; + map signal_infos = 5; + map chasm_nodes = 12; + repeated string signal_requested_ids = 6; + WorkflowExecutionInfo execution_info = 7; + WorkflowExecutionState execution_state = 8; + int64 next_event_id = 9; + repeated temporal.api.history.v1.HistoryEvent buffered_events = 10; + Checksum checksum = 11; +} - message StateMachineNodeMutation{ - StateMachinePath path = 1; - bytes data = 2; - VersionedTransition initial_versioned_transition = 3; - VersionedTransition last_update_versioned_transition = 4; - } +message WorkflowMutableStateMutation { + message StateMachineNodeMutation { + StateMachinePath path = 1; + bytes data = 2; + VersionedTransition initial_versioned_transition = 3; + VersionedTransition last_update_versioned_transition = 4; + } - // The following updated_* fields are computed based on the - // lastUpdateVersionedTransition field of each sub state machine. - map updated_activity_infos = 1; - map updated_timer_infos = 2; - map updated_child_execution_infos = 3; - map updated_request_cancel_infos = 4; - map updated_signal_infos = 5; - map updated_update_infos = 6; - repeated StateMachineNodeMutation updated_sub_state_machines = 7; - map updated_chasm_nodes = 19; + // The following updated_* fields are computed based on the + // lastUpdateVersionedTransition field of each sub state machine. + map updated_activity_infos = 1; + map updated_timer_infos = 2; + map updated_child_execution_infos = 3; + map updated_request_cancel_infos = 4; + map updated_signal_infos = 5; + map updated_update_infos = 6; + repeated StateMachineNodeMutation updated_sub_state_machines = 7; + map updated_chasm_nodes = 19; - reserved 8; - reserved 9; - reserved 10; - reserved 11; - reserved 12; - reserved 13; - reserved 14; + reserved 8; + reserved 9; + reserved 10; + reserved 11; + reserved 12; + reserved 13; + reserved 14; - repeated string signal_requested_ids = 15; - // Partial WorkflowExecutionInfo. Some fields, such as - // update_infos and sub_state_machines_by_type, are not populated here. - // Instead, only diffs are synced in the deleted_* and updated_* fields above. - WorkflowExecutionInfo execution_info = 16; - WorkflowExecutionState execution_state = 17; + repeated string signal_requested_ids = 15; + // Partial WorkflowExecutionInfo. Some fields, such as + // update_infos and sub_state_machines_by_type, are not populated here. + // Instead, only diffs are synced in the deleted_* and updated_* fields above. + WorkflowExecutionInfo execution_info = 16; + WorkflowExecutionState execution_state = 17; - repeated StateMachineTombstoneBatch sub_state_machine_tombstone_batches = 18; + repeated StateMachineTombstoneBatch sub_state_machine_tombstone_batches = 18; - // TODO: uncomment buffered_events field when we are ready to replicate - // mutable state as well. - // repeated temporal.api.history.v1.HistoryEvent buffered_events = 20; +// TODO: uncomment buffered_events field when we are ready to replicate +// mutable state as well. +// repeated temporal.api.history.v1.HistoryEvent buffered_events = 20; } diff --git a/proto/internal/temporal/server/api/replication/v1/message.proto b/proto/internal/temporal/server/api/replication/v1/message.proto index de1b5e07fc2..83d4566c591 100644 --- a/proto/internal/temporal/server/api/replication/v1/message.proto +++ b/proto/internal/temporal/server/api/replication/v1/message.proto @@ -2,11 +2,12 @@ syntax = "proto3"; package temporal.server.api.replication.v1; -option go_package = "go.temporal.io/server/api/replication/v1;repication"; - -import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; - +import "google/protobuf/timestamp.proto"; +import "temporal/api/common/v1/message.proto"; +import "temporal/api/failure/v1/message.proto"; +import "temporal/api/namespace/v1/message.proto"; +import "temporal/api/replication/v1/message.proto"; import "temporal/server/api/enums/v1/replication.proto"; import "temporal/server/api/enums/v1/task.proto"; import "temporal/server/api/history/v1/message.proto"; @@ -14,258 +15,255 @@ import "temporal/server/api/persistence/v1/executions.proto"; import "temporal/server/api/persistence/v1/hsm.proto"; import "temporal/server/api/persistence/v1/task_queues.proto"; import "temporal/server/api/persistence/v1/workflow_mutable_state.proto"; - -import "temporal/api/common/v1/message.proto"; -import "temporal/api/namespace/v1/message.proto"; -import "temporal/api/replication/v1/message.proto"; -import "temporal/api/failure/v1/message.proto"; import "temporal/server/api/workflow/v1/message.proto"; +option go_package = "go.temporal.io/server/api/replication/v1;repication"; + message ReplicationTask { - temporal.server.api.enums.v1.ReplicationTaskType task_type = 1; - int64 source_task_id = 2; - reserved 4; - reserved 7; - oneof attributes { - NamespaceTaskAttributes namespace_task_attributes = 3; - SyncShardStatusTaskAttributes sync_shard_status_task_attributes = 5; - SyncActivityTaskAttributes sync_activity_task_attributes = 6; - HistoryTaskAttributes history_task_attributes = 8; - SyncWorkflowStateTaskAttributes sync_workflow_state_task_attributes = 10; - TaskQueueUserDataAttributes task_queue_user_data_attributes = 11; - SyncHSMAttributes sync_hsm_attributes = 14; - BackfillHistoryTaskAttributes backfill_history_task_attributes = 16; - VerifyVersionedTransitionTaskAttributes verify_versioned_transition_task_attributes = 18; - SyncVersionedTransitionTaskAttributes sync_versioned_transition_task_attributes = 19; - } - // All attributes should be deprecated and replaced by this field. - // The task_type + data provide more flexibility in future use cases. - temporal.api.common.v1.DataBlob data = 12; - google.protobuf.Timestamp visibility_time = 9; - temporal.server.api.enums.v1.TaskPriority priority = 13; - temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 15; - temporal.server.api.persistence.v1.ReplicationTaskInfo raw_task_info = 17; + temporal.server.api.enums.v1.ReplicationTaskType task_type = 1; + int64 source_task_id = 2; + reserved 4; + reserved 7; + oneof attributes { + NamespaceTaskAttributes namespace_task_attributes = 3; + SyncShardStatusTaskAttributes sync_shard_status_task_attributes = 5; + SyncActivityTaskAttributes sync_activity_task_attributes = 6; + HistoryTaskAttributes history_task_attributes = 8; + SyncWorkflowStateTaskAttributes sync_workflow_state_task_attributes = 10; + TaskQueueUserDataAttributes task_queue_user_data_attributes = 11; + SyncHSMAttributes sync_hsm_attributes = 14; + BackfillHistoryTaskAttributes backfill_history_task_attributes = 16; + VerifyVersionedTransitionTaskAttributes verify_versioned_transition_task_attributes = 18; + SyncVersionedTransitionTaskAttributes sync_versioned_transition_task_attributes = 19; + } + // All attributes should be deprecated and replaced by this field. + // The task_type + data provide more flexibility in future use cases. + temporal.api.common.v1.DataBlob data = 12; + google.protobuf.Timestamp visibility_time = 9; + temporal.server.api.enums.v1.TaskPriority priority = 13; + temporal.server.api.persistence.v1.VersionedTransition versioned_transition = 15; + temporal.server.api.persistence.v1.ReplicationTaskInfo raw_task_info = 17; } message ReplicationToken { - int32 shard_id = 1; - // lastRetrievedMessageId is where the next fetch should begin with. - int64 last_retrieved_message_id = 2; - // lastProcessedMessageId is the last messageId that is processed on the passive side. - // This can be different than lastRetrievedMessageId if passive side supports prefetching messages. - int64 last_processed_message_id = 3; - // The VisibilityTime of last processed ReplicationTask - google.protobuf.Timestamp last_processed_visibility_time = 4; + int32 shard_id = 1; + // lastRetrievedMessageId is where the next fetch should begin with. + int64 last_retrieved_message_id = 2; + // lastProcessedMessageId is the last messageId that is processed on the passive side. + // This can be different than lastRetrievedMessageId if passive side supports prefetching messages. + int64 last_processed_message_id = 3; + // The VisibilityTime of last processed ReplicationTask + google.protobuf.Timestamp last_processed_visibility_time = 4; } message SyncShardStatus { - google.protobuf.Timestamp status_time = 1; + google.protobuf.Timestamp status_time = 1; } message SyncReplicationState { - // deprecated in favor of using ReplicationState object - int64 inclusive_low_watermark = 1; - // deprecated in favor of using ReplicationState object - google.protobuf.Timestamp inclusive_low_watermark_time = 2; - ReplicationState high_priority_state = 3; - ReplicationState low_priority_state = 4; + // deprecated in favor of using ReplicationState object + int64 inclusive_low_watermark = 1; + // deprecated in favor of using ReplicationState object + google.protobuf.Timestamp inclusive_low_watermark_time = 2; + ReplicationState high_priority_state = 3; + ReplicationState low_priority_state = 4; } message ReplicationState { - int64 inclusive_low_watermark = 1; - google.protobuf.Timestamp inclusive_low_watermark_time = 2; - temporal.server.api.enums.v1.ReplicationFlowControlCommand flow_control_command = 3; + int64 inclusive_low_watermark = 1; + google.protobuf.Timestamp inclusive_low_watermark_time = 2; + temporal.server.api.enums.v1.ReplicationFlowControlCommand flow_control_command = 3; } message ReplicationMessages { - repeated ReplicationTask replication_tasks = 1; - // This can be different than the last taskId in the above list, because sender can decide to skip tasks (e.g. for completed workflows). - int64 last_retrieved_message_id = 2; - // Hint for flow control. - bool has_more = 3; - SyncShardStatus sync_shard_status = 4; + repeated ReplicationTask replication_tasks = 1; + // This can be different than the last taskId in the above list, because sender can decide to skip tasks (e.g. for completed workflows). + int64 last_retrieved_message_id = 2; + // Hint for flow control. + bool has_more = 3; + SyncShardStatus sync_shard_status = 4; } message WorkflowReplicationMessages { - repeated ReplicationTask replication_tasks = 1; - // This can be different than the last taskId in the above list, because sender can decide to skip tasks (e.g. for completed workflows). - int64 exclusive_high_watermark = 2; - google.protobuf.Timestamp exclusive_high_watermark_time = 3; - temporal.server.api.enums.v1.TaskPriority priority = 4; + repeated ReplicationTask replication_tasks = 1; + // This can be different than the last taskId in the above list, because sender can decide to skip tasks (e.g. for completed workflows). + int64 exclusive_high_watermark = 2; + google.protobuf.Timestamp exclusive_high_watermark_time = 3; + temporal.server.api.enums.v1.TaskPriority priority = 4; } // TODO: Deprecate this definition, it only used by the deprecated replication DLQ v1 logic message ReplicationTaskInfo { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - temporal.server.api.enums.v1.TaskType task_type = 4; - int64 task_id = 5; - int64 version = 6; - int64 first_event_id = 7; - int64 next_event_id = 8; - int64 scheduled_event_id = 9; - temporal.server.api.enums.v1.TaskPriority priority = 10; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + temporal.server.api.enums.v1.TaskType task_type = 4; + int64 task_id = 5; + int64 version = 6; + int64 first_event_id = 7; + int64 next_event_id = 8; + int64 scheduled_event_id = 9; + temporal.server.api.enums.v1.TaskPriority priority = 10; } message NamespaceTaskAttributes { - temporal.server.api.enums.v1.NamespaceOperation namespace_operation = 1; - string id = 2; - temporal.api.namespace.v1.NamespaceInfo info = 3; - temporal.api.namespace.v1.NamespaceConfig config = 4; - temporal.api.replication.v1.NamespaceReplicationConfig replication_config = 5; - int64 config_version = 6; - int64 failover_version = 7; - repeated temporal.api.replication.v1.FailoverStatus failover_history = 8; + temporal.server.api.enums.v1.NamespaceOperation namespace_operation = 1; + string id = 2; + temporal.api.namespace.v1.NamespaceInfo info = 3; + temporal.api.namespace.v1.NamespaceConfig config = 4; + temporal.api.replication.v1.NamespaceReplicationConfig replication_config = 5; + int64 config_version = 6; + int64 failover_version = 7; + repeated temporal.api.replication.v1.FailoverStatus failover_history = 8; } message SyncShardStatusTaskAttributes { - string source_cluster = 1; - int32 shard_id = 2; - google.protobuf.Timestamp status_time = 3; + string source_cluster = 1; + int32 shard_id = 2; + google.protobuf.Timestamp status_time = 3; } message SyncActivityTaskAttributes { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - int64 version = 4; - int64 scheduled_event_id = 5; - google.protobuf.Timestamp scheduled_time = 6; - int64 started_event_id = 7; - google.protobuf.Timestamp started_time = 8; - google.protobuf.Timestamp last_heartbeat_time = 9; - temporal.api.common.v1.Payloads details = 10; - int32 attempt = 11; - temporal.api.failure.v1.Failure last_failure = 12; - string last_worker_identity = 13; - temporal.server.api.history.v1.VersionHistory version_history = 14; - temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 15; - // build ID of the worker who received this activity last time - string last_started_build_id = 16; - // workflows redirect_counter value when this activity started last time - int64 last_started_redirect_counter = 17; - // The first time the activity was scheduled. - google.protobuf.Timestamp first_scheduled_time = 18; - // The last time an activity attempt completion was recorded by the server. - google.protobuf.Timestamp last_attempt_complete_time = 19; - // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. - // It monotonically increments when the activity options are changed. - int32 stamp = 20; - // Flag indicating whether the activity is currently paused. - bool paused = 21; - // Retry policy for the activity. It needs to be replicated now, since the activity properties can be updated. - google.protobuf.Duration retry_initial_interval = 22; - google.protobuf.Duration retry_maximum_interval = 23; - int32 retry_maximum_attempts = 24; - double retry_backoff_coefficient = 25; - int64 start_version = 26; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + int64 version = 4; + int64 scheduled_event_id = 5; + google.protobuf.Timestamp scheduled_time = 6; + int64 started_event_id = 7; + google.protobuf.Timestamp started_time = 8; + google.protobuf.Timestamp last_heartbeat_time = 9; + temporal.api.common.v1.Payloads details = 10; + int32 attempt = 11; + temporal.api.failure.v1.Failure last_failure = 12; + string last_worker_identity = 13; + temporal.server.api.history.v1.VersionHistory version_history = 14; + temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 15; + // build ID of the worker who received this activity last time + string last_started_build_id = 16; + // workflows redirect_counter value when this activity started last time + int64 last_started_redirect_counter = 17; + // The first time the activity was scheduled. + google.protobuf.Timestamp first_scheduled_time = 18; + // The last time an activity attempt completion was recorded by the server. + google.protobuf.Timestamp last_attempt_complete_time = 19; + // Stamp represents the internal “version” of the activity options and can/will be changed with Activity API. + // It monotonically increments when the activity options are changed. + int32 stamp = 20; + // Flag indicating whether the activity is currently paused. + bool paused = 21; + // Retry policy for the activity. It needs to be replicated now, since the activity properties can be updated. + google.protobuf.Duration retry_initial_interval = 22; + google.protobuf.Duration retry_maximum_interval = 23; + int32 retry_maximum_attempts = 24; + double retry_backoff_coefficient = 25; + int64 start_version = 26; } message HistoryTaskAttributes { - reserved 1; - string namespace_id = 2; - string workflow_id = 3; - string run_id = 4; - repeated temporal.server.api.history.v1.VersionHistoryItem version_history_items = 5; + reserved 1; + string namespace_id = 2; + string workflow_id = 3; + string run_id = 4; + repeated temporal.server.api.history.v1.VersionHistoryItem version_history_items = 5; - // to be deprecated in favor of using events_batches - temporal.api.common.v1.DataBlob events = 6; - // New run events does not need version history since there is no prior events. - temporal.api.common.v1.DataBlob new_run_events = 7; - temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 8; - string new_run_id = 9; - repeated temporal.api.common.v1.DataBlob events_batches = 10; + // to be deprecated in favor of using events_batches + temporal.api.common.v1.DataBlob events = 6; + // New run events does not need version history since there is no prior events. + temporal.api.common.v1.DataBlob new_run_events = 7; + temporal.server.api.workflow.v1.BaseExecutionInfo base_execution_info = 8; + string new_run_id = 9; + repeated temporal.api.common.v1.DataBlob events_batches = 10; } message SyncWorkflowStateTaskAttributes { - temporal.server.api.persistence.v1.WorkflowMutableState workflow_state = 1; - bool is_force_replication = 2; - bool is_close_transfer_task_acked = 3; + temporal.server.api.persistence.v1.WorkflowMutableState workflow_state = 1; + bool is_force_replication = 2; + bool is_close_transfer_task_acked = 3; } message TaskQueueUserDataAttributes { - string namespace_id = 1; - string task_queue_name = 2; - temporal.server.api.persistence.v1.TaskQueueUserData user_data = 3; + string namespace_id = 1; + string task_queue_name = 2; + temporal.server.api.persistence.v1.TaskQueueUserData user_data = 3; } message SyncHSMAttributes { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - temporal.server.api.history.v1.VersionHistory version_history = 4; - temporal.server.api.persistence.v1.StateMachineNode state_machine_node = 5; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + temporal.server.api.history.v1.VersionHistory version_history = 4; + temporal.server.api.persistence.v1.StateMachineNode state_machine_node = 5; } message BackfillHistoryTaskAttributes { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; - repeated temporal.server.api.history.v1.VersionHistoryItem event_version_history = 5; - repeated temporal.api.common.v1.DataBlob event_batches = 6; - NewRunInfo new_run_info = 7; + repeated temporal.server.api.history.v1.VersionHistoryItem event_version_history = 5; + repeated temporal.api.common.v1.DataBlob event_batches = 6; + NewRunInfo new_run_info = 7; } message NewRunInfo { - string run_id = 1; - temporal.api.common.v1.DataBlob event_batch = 2; + string run_id = 1; + temporal.api.common.v1.DataBlob event_batch = 2; } message SyncWorkflowStateMutationAttributes { - temporal.server.api.persistence.v1.VersionedTransition exclusive_start_versioned_transition = 1; - temporal.server.api.persistence.v1.WorkflowMutableStateMutation state_mutation = 2; + temporal.server.api.persistence.v1.VersionedTransition exclusive_start_versioned_transition = 1; + temporal.server.api.persistence.v1.WorkflowMutableStateMutation state_mutation = 2; } message SyncWorkflowStateSnapshotAttributes { - temporal.server.api.persistence.v1.WorkflowMutableState state = 1; + temporal.server.api.persistence.v1.WorkflowMutableState state = 1; } message VerifyVersionedTransitionTaskAttributes { - string namespace_id = 1; - string workflow_id = 2; - string run_id = 3; - int64 next_event_id = 4; - repeated temporal.server.api.history.v1.VersionHistoryItem event_version_history = 5; - string new_run_id = 6; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 7; + string namespace_id = 1; + string workflow_id = 2; + string run_id = 3; + int64 next_event_id = 4; + repeated temporal.server.api.history.v1.VersionHistoryItem event_version_history = 5; + string new_run_id = 6; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 7; } message SyncVersionedTransitionTaskAttributes { - reserved 1; - reserved 2; - reserved 3; - reserved 4; - VersionedTransitionArtifact versioned_transition_artifact = 5; - string namespace_id = 6; - string workflow_id = 7; - string run_id = 8; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 9; + reserved 1; + reserved 2; + reserved 3; + reserved 4; + VersionedTransitionArtifact versioned_transition_artifact = 5; + string namespace_id = 6; + string workflow_id = 7; + string run_id = 8; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 9; } message VersionedTransitionArtifact { - oneof state_attributes { - SyncWorkflowStateMutationAttributes sync_workflow_state_mutation_attributes = 1; - SyncWorkflowStateSnapshotAttributes sync_workflow_state_snapshot_attributes = 2; - } - repeated temporal.api.common.v1.DataBlob event_batches = 3; - NewRunInfo new_run_info = 4; - bool is_first_sync = 5; - bool is_close_transfer_task_acked = 6; - bool is_force_replication = 7; + oneof state_attributes { + SyncWorkflowStateMutationAttributes sync_workflow_state_mutation_attributes = 1; + SyncWorkflowStateSnapshotAttributes sync_workflow_state_snapshot_attributes = 2; + } + repeated temporal.api.common.v1.DataBlob event_batches = 3; + NewRunInfo new_run_info = 4; + bool is_first_sync = 5; + bool is_close_transfer_task_acked = 6; + bool is_force_replication = 7; } // MigrationExecutionInfo is only used in unit tests for validation compatibility. // Remove it after v1.30 is released. message MigrationExecutionInfo { - // The proto json name of this field needs to be "workflowId", - // to be backward compatibility with commonpb.WorkflowExecution, - // which is what used to be used in migration workflow's activity - // input/output. - string business_id = 1 [json_name = "workflowId"]; - string run_id = 2; - // (-- api-linter: core::0141::forbidden-types=disabled --) - uint32 archetype_id = 3; + // The proto json name of this field needs to be "workflowId", + // to be backward compatibility with commonpb.WorkflowExecution, + // which is what used to be used in migration workflow's activity + // input/output. + string business_id = 1 [json_name = "workflowId"]; + string run_id = 2; + // (-- api-linter: core::0141::forbidden-types=disabled --) + uint32 archetype_id = 3; } diff --git a/proto/internal/temporal/server/api/routing/v1/extension.proto b/proto/internal/temporal/server/api/routing/v1/extension.proto index c06e44e34f7..6464fb7fdef 100644 --- a/proto/internal/temporal/server/api/routing/v1/extension.proto +++ b/proto/internal/temporal/server/api/routing/v1/extension.proto @@ -2,17 +2,19 @@ syntax = "proto3"; package temporal.server.api.routing.v1; -option go_package = "go.temporal.io/server/api/routing/v1;routing"; - import "google/protobuf/descriptor.proto"; -extend google.protobuf.MethodOptions { optional RoutingOptions routing = 7234; } +option go_package = "go.temporal.io/server/api/routing/v1;routing"; + +extend google.protobuf.MethodOptions { + optional RoutingOptions routing = 7234; +} message RoutingOptions { - // Requests will be routed to a random shard. - bool random = 1; - // Requests may specify how to obtain the namespace ID. Defaults to the "namespace_id" field. - string namespace_id = 2; - // Request will be routed by resolving the namespace ID and business ID to a given shard. - string business_id = 3; + // Requests will be routed to a random shard. + bool random = 1; + // Requests may specify how to obtain the namespace ID. Defaults to the "namespace_id" field. + string namespace_id = 2; + // Request will be routed by resolving the namespace ID and business ID to a given shard. + string business_id = 3; } diff --git a/proto/internal/temporal/server/api/schedule/v1/message.proto b/proto/internal/temporal/server/api/schedule/v1/message.proto index 41ae65a9a00..71b97922939 100644 --- a/proto/internal/temporal/server/api/schedule/v1/message.proto +++ b/proto/internal/temporal/server/api/schedule/v1/message.proto @@ -2,8 +2,7 @@ syntax = "proto3"; package temporal.server.api.schedule.v1; -option go_package = "go.temporal.io/server/api/schedule/v1;schedule"; - +import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/enums/v1/schedule.proto"; import "temporal/api/enums/v1/workflow.proto"; @@ -11,150 +10,155 @@ import "temporal/api/failure/v1/message.proto"; import "temporal/api/schedule/v1/message.proto"; import "temporal/api/workflowservice/v1/request_response.proto"; -import "google/protobuf/timestamp.proto"; +option go_package = "go.temporal.io/server/api/schedule/v1;schedule"; message BufferedStart { - // Nominal (pre-jitter) and Actual (post-jitter) time of action - google.protobuf.Timestamp nominal_time = 1; - google.protobuf.Timestamp actual_time = 2; - // Desired time is usually nil, which should be interpreted as == actual time, but for starts - // that are blocked behind another action, it is set to the close time of the previous action - // for more meaningful metrics. - google.protobuf.Timestamp desired_time = 5; - // Overridden overlap policy - temporal.api.enums.v1.ScheduleOverlapPolicy overlap_policy = 3; - // Trigger-immediately or backfill - bool manual = 4; - // An ID generated when the action is buffered for deduplication during - // execution. Only used by the CHASM scheduler (otherwise left empty). - string request_id = 6; - // Initially 0. Once a BufferedStart is ready to execute (overlap policies - // are resolved), its attempt count is set to 1. If a BufferedStart fails - // execution, its attempt count here is incremented. Only used by the CHASM - // scheduler (otherwise left empty). - int64 attempt = 7; - // If a BufferedStart is rate limited or needs to backoff while retrying, - // this time will be set, and the start will be held in the buffer until - // the backoff time has passed. Only used by the CHASM scheduler (otherwise - // ignored). - google.protobuf.Timestamp backoff_time = 8; - // The precomputed workflow ID that should be used (as-is) when executing - // this start. Only used by the CHASM scheduler (otherwise ignored). - string workflow_id = 9; - // The run ID of the started workflow. Populated when the workflow is - // successfully started. Only used by the CHASM scheduler. - string run_id = 10; - // The actual time the workflow was started. Populated when the workflow is - // successfully started. Only used by the CHASM scheduler. - google.protobuf.Timestamp start_time = 11; - // Populated when the workflow execution completes. Presence indicates the - // action is complete and retained for history. Only used by the CHASM scheduler. - CompletedResult completed = 12; + // Nominal (pre-jitter) and Actual (post-jitter) time of action + google.protobuf.Timestamp nominal_time = 1; + google.protobuf.Timestamp actual_time = 2; + // Desired time is usually nil, which should be interpreted as == actual time, but for starts + // that are blocked behind another action, it is set to the close time of the previous action + // for more meaningful metrics. + google.protobuf.Timestamp desired_time = 5; + // Overridden overlap policy + temporal.api.enums.v1.ScheduleOverlapPolicy overlap_policy = 3; + // Trigger-immediately or backfill + bool manual = 4; + // An ID generated when the action is buffered for deduplication during + // execution. Only used by the CHASM scheduler (otherwise left empty). + string request_id = 6; + // Initially 0. Once a BufferedStart is ready to execute (overlap policies + // are resolved), its attempt count is set to 1. If a BufferedStart fails + // execution, its attempt count here is incremented. Only used by the CHASM + // scheduler (otherwise left empty). + int64 attempt = 7; + // If a BufferedStart is rate limited or needs to backoff while retrying, + // this time will be set, and the start will be held in the buffer until + // the backoff time has passed. Only used by the CHASM scheduler (otherwise + // ignored). + google.protobuf.Timestamp backoff_time = 8; + // The precomputed workflow ID that should be used (as-is) when executing + // this start. Only used by the CHASM scheduler (otherwise ignored). + string workflow_id = 9; + // The run ID of the started workflow. Populated when the workflow is + // successfully started. Only used by the CHASM scheduler. + string run_id = 10; + // The actual time the workflow was started. Populated when the workflow is + // successfully started. Only used by the CHASM scheduler. + google.protobuf.Timestamp start_time = 11; + // Populated when the workflow execution completes. Presence indicates the + // action is complete and retained for history. Only used by the CHASM scheduler. + CompletedResult completed = 12; + // True when a running BufferedStart is known to have a Nexus callback + // attached. False when a BufferedStart originated from a migrated V1 + // workflow. Only used by CHASM scheduler, for migration from V1. + bool has_callback = 13; } // Result when a workflow execution has completed. // Only used by the CHASM scheduler. message CompletedResult { - // The final status of the workflow execution. - temporal.api.enums.v1.WorkflowExecutionStatus status = 1; - // The time the workflow closed. - google.protobuf.Timestamp close_time = 2; + // The final status of the workflow execution. + temporal.api.enums.v1.WorkflowExecutionStatus status = 1; + // The time the workflow closed. + google.protobuf.Timestamp close_time = 2; } message InternalState { - string namespace = 1; - string namespace_id = 2; - string schedule_id = 8; + string namespace = 1; + string namespace_id = 2; + string schedule_id = 8; + + google.protobuf.Timestamp last_processed_time = 3; + repeated BufferedStart buffered_starts = 4; + repeated temporal.api.schedule.v1.BackfillRequest ongoing_backfills = 10; - google.protobuf.Timestamp last_processed_time = 3; - repeated BufferedStart buffered_starts = 4; - repeated temporal.api.schedule.v1.BackfillRequest ongoing_backfills = 10; + // last completion/failure + temporal.api.common.v1.Payloads last_completion_result = 5; + temporal.api.failure.v1.Failure continued_failure = 6; - // last completion/failure - temporal.api.common.v1.Payloads last_completion_result = 5; - temporal.api.failure.v1.Failure continued_failure = 6; + // conflict token is implemented as simple sequence number + int64 conflict_token = 7; - // conflict token is implemented as simple sequence number - int64 conflict_token = 7; + bool need_refresh = 9; - bool need_refresh = 9; + bool pending_migration = 11; } message StartScheduleArgs { - temporal.api.schedule.v1.Schedule schedule = 1; - temporal.api.schedule.v1.ScheduleInfo info = 2; - temporal.api.schedule.v1.SchedulePatch initial_patch = 3; - InternalState state = 4; + temporal.api.schedule.v1.Schedule schedule = 1; + temporal.api.schedule.v1.ScheduleInfo info = 2; + temporal.api.schedule.v1.SchedulePatch initial_patch = 3; + InternalState state = 4; } message FullUpdateRequest { - temporal.api.schedule.v1.Schedule schedule = 1; - int64 conflict_token = 2; - temporal.api.common.v1.SearchAttributes search_attributes = 3; + temporal.api.schedule.v1.Schedule schedule = 1; + int64 conflict_token = 2; + temporal.api.common.v1.SearchAttributes search_attributes = 3; } message DescribeResponse { - temporal.api.schedule.v1.Schedule schedule = 1; - temporal.api.schedule.v1.ScheduleInfo info = 2; - int64 conflict_token = 3; + temporal.api.schedule.v1.Schedule schedule = 1; + temporal.api.schedule.v1.ScheduleInfo info = 2; + int64 conflict_token = 3; } message WatchWorkflowRequest { - // Note: this will be sent to the activity with empty execution.run_id, and - // the run id that we started in first_execution_run_id. - temporal.api.common.v1.WorkflowExecution execution = 3; - string first_execution_run_id = 4; - bool long_poll = 5; + // Note: this will be sent to the activity with empty execution.run_id, and + // the run id that we started in first_execution_run_id. + temporal.api.common.v1.WorkflowExecution execution = 3; + string first_execution_run_id = 4; + bool long_poll = 5; } message WatchWorkflowResponse { - temporal.api.enums.v1.WorkflowExecutionStatus status = 1; - oneof result_failure { - temporal.api.common.v1.Payloads result = 2; - temporal.api.failure.v1.Failure failure = 3; - } - // Timestamp of close event - google.protobuf.Timestamp close_time = 4; + temporal.api.enums.v1.WorkflowExecutionStatus status = 1; + oneof result_failure { + temporal.api.common.v1.Payloads result = 2; + temporal.api.failure.v1.Failure failure = 3; + } + // Timestamp of close event + google.protobuf.Timestamp close_time = 4; } message StartWorkflowRequest { - temporal.api.workflowservice.v1.StartWorkflowExecutionRequest request = 2; - reserved 3, 4, 5; - bool completed_rate_limit_sleep = 6; + temporal.api.workflowservice.v1.StartWorkflowExecutionRequest request = 2; + reserved 3, 4, 5; + bool completed_rate_limit_sleep = 6; } message StartWorkflowResponse { - string run_id = 1; - google.protobuf.Timestamp real_start_time = 2; + string run_id = 1; + google.protobuf.Timestamp real_start_time = 2; } message CancelWorkflowRequest { - string request_id = 3; - string identity = 4; - // Note: run id in execution is first execution run id - temporal.api.common.v1.WorkflowExecution execution = 5; - string reason = 6; + string request_id = 3; + string identity = 4; + // Note: run id in execution is first execution run id + temporal.api.common.v1.WorkflowExecution execution = 5; + string reason = 6; } message TerminateWorkflowRequest { - string request_id = 3; - string identity = 4; - // Note: run id in execution is first execution run id - temporal.api.common.v1.WorkflowExecution execution = 5; - string reason = 6; + string request_id = 3; + string identity = 4; + // Note: run id in execution is first execution run id + temporal.api.common.v1.WorkflowExecution execution = 5; + string reason = 6; } message NextTimeCache { - // workflow logic version (invalidate when changed) - int64 version = 1; - // start time that the results were calculated from - google.protobuf.Timestamp start_time = 2; - // next_times and nominal_times are a series of timestamp pairs, encoded as a nanosecond - // offset from start_time. next_times has one value for each time in the cache. - // nominal_times may have up to the same number of values, but it may also be shorter (or - // empty), if the corresponding nominal time is equal to the next time. - repeated int64 next_times = 3; - repeated int64 nominal_times = 4; - bool completed = 5; + // workflow logic version (invalidate when changed) + int64 version = 1; + // start time that the results were calculated from + google.protobuf.Timestamp start_time = 2; + // next_times and nominal_times are a series of timestamp pairs, encoded as a nanosecond + // offset from start_time. next_times has one value for each time in the cache. + // nominal_times may have up to the same number of values, but it may also be shorter (or + // empty), if the corresponding nominal time is equal to the next time. + repeated int64 next_times = 3; + repeated int64 nominal_times = 4; + bool completed = 5; } - diff --git a/proto/internal/temporal/server/api/taskqueue/v1/message.proto b/proto/internal/temporal/server/api/taskqueue/v1/message.proto index fa54307f6f8..77dfe38c2f7 100644 --- a/proto/internal/temporal/server/api/taskqueue/v1/message.proto +++ b/proto/internal/temporal/server/api/taskqueue/v1/message.proto @@ -2,119 +2,132 @@ syntax = "proto3"; package temporal.server.api.taskqueue.v1; -option go_package = "go.temporal.io/server/api/taskqueue/v1;taskqueue"; - import "google/protobuf/empty.proto"; - +import "google/protobuf/timestamp.proto"; import "temporal/api/deployment/v1/message.proto"; import "temporal/api/enums/v1/task_queue.proto"; import "temporal/api/enums/v1/workflow.proto"; import "temporal/api/taskqueue/v1/message.proto"; -import "temporal/server/api/enums/v1/task.proto"; import "temporal/server/api/deployment/v1/message.proto"; +import "temporal/server/api/enums/v1/task.proto"; + +option go_package = "go.temporal.io/server/api/taskqueue/v1;taskqueue"; // TaskVersionDirective controls how matching should direct a task. message TaskVersionDirective { - // Default (if build_id is not present) is "unversioned": - // Use the unversioned task queue, even if the task queue has versioning data. - // Absent value means the task is the non-starting task of an unversioned execution so it should remain unversioned. - // Deprecated. Use deployment_version. - oneof build_id { - // If use_assignment_rules is present, matching should use the assignment rules - // to determine the build ID. - // WV1: the task should be assigned the default version for the task queue. [cleanup-old-wv] - google.protobuf.Empty use_assignment_rules = 1; - - // This means the task is already assigned to `build_id` - // WV1: If assigned_build_id is present, use the default version in the compatible set - // containing this build ID. [cleanup-old-wv] - string assigned_build_id = 2; - } - - // Workflow's effective behavior when the task is scheduled. - temporal.api.enums.v1.VersioningBehavior behavior = 3; - // Workflow's effective deployment when the task is scheduled. - // Deprecated. Use deployment_version. - temporal.api.deployment.v1.Deployment deployment = 4; - // Workflow's effective deployment version when the task is scheduled. - temporal.server.api.deployment.v1.WorkerDeploymentVersion deployment_version = 5; - // Counter copied from the workflow execution's WorkflowExecutionVersioningInfo - // during enqueue time. - int64 revision_number = 6; + // Default (if build_id is not present) is "unversioned": + // Use the unversioned task queue, even if the task queue has versioning data. + // Absent value means the task is the non-starting task of an unversioned execution so it should remain unversioned. + // Deprecated. Use deployment_version. + oneof build_id { + // If use_assignment_rules is present, matching should use the assignment rules + // to determine the build ID. + // WV1: the task should be assigned the default version for the task queue. [cleanup-old-wv] + google.protobuf.Empty use_assignment_rules = 1; + + // This means the task is already assigned to `build_id` + // WV1: If assigned_build_id is present, use the default version in the compatible set + // containing this build ID. [cleanup-old-wv] + string assigned_build_id = 2; + } + + // Workflow's effective behavior when the task is scheduled. + temporal.api.enums.v1.VersioningBehavior behavior = 3; + // Workflow's effective deployment when the task is scheduled. + // Deprecated. Use deployment_version. + temporal.api.deployment.v1.Deployment deployment = 4; + // Workflow's effective deployment version when the task is scheduled. + temporal.server.api.deployment.v1.WorkerDeploymentVersion deployment_version = 5; + // Counter copied from the workflow execution's WorkflowExecutionVersioningInfo + // during enqueue time. + int64 revision_number = 6; } message FairLevel { - int64 task_pass = 1; - int64 task_id = 2; + int64 task_pass = 1; + int64 task_id = 2; } message InternalTaskQueueStatus { - int64 read_level = 1; - FairLevel fair_read_level = 7; - int64 ack_level = 2; - FairLevel fair_ack_level = 8; - temporal.api.taskqueue.v1.TaskIdBlock task_id_block = 3; - int64 loaded_tasks = 4; - int64 approximate_backlog_count = 5; - int64 max_read_level = 6; - FairLevel fair_max_read_level = 9; - // Draining means that this status is from a draining queue. - bool draining = 10; + int64 read_level = 1; + FairLevel fair_read_level = 7; + int64 ack_level = 2; + FairLevel fair_ack_level = 8; + temporal.api.taskqueue.v1.TaskIdBlock task_id_block = 3; + int64 loaded_tasks = 4; + int64 approximate_backlog_count = 5; + int64 max_read_level = 6; + FairLevel fair_max_read_level = 9; + // Draining means that this status is from a queue that is being drained to + // migrate from v1 to v2 tasks persistence (or backwards). + bool draining = 10; + // BacklogDrained means this queue has an empty backlog at the time this status + // was generated. This is inherently racy — new tasks may arrive after this + // check. Consumers must use version-based validation (see scaleManager) to + // ensure correctness. + bool backlog_drained = 11; } message TaskQueueVersionInfoInternal { - PhysicalTaskQueueInfo physical_task_queue_info = 2; + PhysicalTaskQueueInfo physical_task_queue_info = 2; } message PhysicalTaskQueueInfo { - // Unversioned workers (with `useVersioning=false`) are reported in unversioned result even if they set a Build ID. - repeated temporal.api.taskqueue.v1.PollerInfo pollers = 1; - repeated InternalTaskQueueStatus internal_task_queue_status = 3; - temporal.api.taskqueue.v1.TaskQueueStats task_queue_stats = 2; - // (-- api-linter: core::0140::prepositions=disabled - // aip.dev/not-precedent: "by" is used to clarify the keys. --) - map task_queue_stats_by_priority_key = 4; + // Unversioned workers (with `useVersioning=false`) are reported in unversioned result even if they set a Build ID. + repeated temporal.api.taskqueue.v1.PollerInfo pollers = 1; + repeated InternalTaskQueueStatus internal_task_queue_status = 3; + temporal.api.taskqueue.v1.TaskQueueStats task_queue_stats = 2; + // (-- api-linter: core::0140::prepositions=disabled + // aip.dev/not-precedent: "by" is used to clarify the keys. --) + map task_queue_stats_by_priority_key = 4; } // Represents a normal or sticky partition of a task queue. message TaskQueuePartition { - // This is the user-facing name for this task queue - string task_queue = 1; - temporal.api.enums.v1.TaskQueueType task_queue_type = 2; - // Absent means normal root partition (normal_partition_id=0) - oneof partition_id { - int32 normal_partition_id = 3; - string sticky_name = 4; - } + // This is the user-facing name for this task queue + string task_queue = 1; + temporal.api.enums.v1.TaskQueueType task_queue_type = 2; + // Absent means normal root partition (normal_partition_id=0) + oneof partition_id { + int32 normal_partition_id = 3; + string sticky_name = 4; + } } // Information about redirect intention sent by Matching to History in Record*TaskStarted calls. // Deprecated. message BuildIdRedirectInfo { - // build ID asked by History in the directive or the one calculated based on the assignment rules. - // this is the source of the redirect rule chain applied. (the target of the redirect rule chain is - // the poller's build ID reported in WorkerVersionCapabilities) - string assigned_build_id = 1; + // build ID asked by History in the directive or the one calculated based on the assignment rules. + // this is the source of the redirect rule chain applied. (the target of the redirect rule chain is + // the poller's build ID reported in WorkerVersionCapabilities) + string assigned_build_id = 1; } // Information about task forwarding from one partition to its parent. message TaskForwardInfo { - // RPC name of the partition forwarded the task. - // In case of multiple hops, this is the source partition of the last hop. - string source_partition = 1; - temporal.server.api.enums.v1.TaskSource task_source = 2; - // Redirect info is not present for Query and Nexus tasks. Versioning decisions for activity/workflow - // tasks are made at the source partition and sent to the parent partition in this message so that parent partition - // does not have to make versioning decision again. For Query/Nexus tasks, this works differently as the child's - // versioning decision is ignored and the parent partition makes a fresh decision. - // Deprecated. [cleanup-old-wv] - BuildIdRedirectInfo redirect_info = 3; - // Build ID that should be used to dispatch the task to. Ignored in Query and Nexus tasks. - // Deprecated. [cleanup-old-wv] - string dispatch_build_id = 4; - // Only used for old versioning. [cleanup-old-wv] - // Deprecated. [cleanup-old-wv] - string dispatch_version_set = 5; + // RPC name of the partition forwarded the task. + // In case of multiple hops, this is the source partition of the last hop. + string source_partition = 1; + temporal.server.api.enums.v1.TaskSource task_source = 2; + // The partition where the task was initially forwarded from. + // Unlike source_partition which gets overwritten at each hop, origin_partition + // persists across all forwarding hops. + string origin_partition = 6; + // For tasks that are forwarded, we should keep the original creation time that comes from the + // source partition. Used for dispatch latency metrics. + google.protobuf.Timestamp create_time = 7; + // Redirect info is not present for Query and Nexus tasks. Versioning decisions for activity/workflow + // tasks are made at the source partition and sent to the parent partition in this message so that parent partition + // does not have to make versioning decision again. For Query/Nexus tasks, this works differently as the child's + // versioning decision is ignored and the parent partition makes a fresh decision. + // Deprecated. [cleanup-old-wv] + BuildIdRedirectInfo redirect_info = 3; + // Build ID that should be used to dispatch the task to. Ignored in Query and Nexus tasks. + // Deprecated. [cleanup-old-wv] + string dispatch_build_id = 4; + // Only used for old versioning. [cleanup-old-wv] + // Deprecated. [cleanup-old-wv] + string dispatch_version_set = 5; } // EphemeralData is data that we want to propagate among task queue partitions, but is not persisted. @@ -122,23 +135,23 @@ message TaskForwardInfo { // task queue family (all queues with the same name, across types), ephemeral data applies only to // one type at a time. message EphemeralData { - message ByVersion { - // Key for this data. Data for the unversioned queue has no version field present. - // All following fields are data associated with this versioned queue. - temporal.server.api.deployment.v1.WorkerDeploymentVersion version = 1; - - // This is a bit field of priority levels that have "significant" backlog (defined by - // the server configuration). Priority key k corresponds to 1<l#ojPX{a)HF47PV8T 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%0Aht@D0h#KIEbDwydoT?@1N%oo7Vf%Q<)0>M6)J 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 t1%_}8$ev9L+j(cczgs}pD^ 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

@8|+1mYL|Hl#=TKFxvW`UREHn( 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

*qg@o{FNfKCKU|a3C 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ΠV6x+e1rpXjlWAST~+bgzi3@X^y(V#^GFXVo^q6}J_^ z_(0D$gN`(K$W%11J~Yp)W12?5j@RZ2fSLt=&(C+h{8TY$QNuNdISK?sdmsaP(1+aT5#jm3@Z1ufw++wB-Lkk4 z&1(htNKzeXdmDGcbwm zbGB_vh87|J#8Y%|JvVve9_j8GBjf&Dy`5im>JwYMqHlQkX~pSPUMEayguDA`rRnR3 z@mV~Pk|dtrh%zM#mEwU$7l*7H?%{`EqWL#I0~6{FPR=!Ylnq~Eu70}a8?ONkGIkgh z(N1n^^d3$eP(a&Qt;H`$?fev5fm{I>rdbDvYIa`AdwV6p)VnnC7bQj?+Z$86Scx$2 zICr79^T0?|X0Qt+c`R_Y$VtxGmmD&8V`4W+T8Z7;Cek(h%h$+-bhV}4k7M1&+kO3Z zcfSM|+tGd!le_keXvVJ;&;jXi`^)XJMv{+yZ}0TkyZZD_ zAJ-&S^#y0!+c{yVKNA74!`3uXE(%3ocS(v z`rOq1l(;`9o_MpjdSFu*je*i{0hHbOEQP0A2zt%61jrO}5Fbuyz(m~awtjbOzsjCG z7mA-em!ych?t41@9A`fbr+>wEv%~EtIJF$r6JD#R+`tV@z9e}IsS8Je{{z=zk18~t z7bALvbO}}Ulf6>x{@z%rX=fKzteN0&$dU|t>9wWwI&>W5qLY)_X^AZX3kd%zrtM%T znkZLv_8aHkklvBTucfu)P|(ZN4Iz#W=_@&>oSuvOH@Uf--coi|d#PG{0lzHFWfvj} zQlcQ^cuTAzvfHF~ZPs{1k+C7@ifwjWwe(M{Gj30AGtO;EN%wJaCTB4JuB_NA6?>~< zuT}aTmTSgEdLS`B{q9Hol9}DnpMuhtY$u@fW3xAk{$WDO-6Omfv+xW^o*>K2Y)R&p zD&aL&s1l{|Q~w&fqoNMaui-2x+NJcd6iS@~aiAX}l@<91p0ic{k6qBJQ(yJ}dYQ$} zU!AyxiCbJUY+X!K@+x@&Czfo{&}Y9m0l7Y?yMcj?SWEaTn;Yg1U$zrD4C7# z_>+k&YhmD&_NcWLDBzvQH;NeyK~noONcv(=7|<#>51{m>yj`+kaoAMRScz1I=1hXm@$sh zE}XxsebK(a7Wxv+3d-3YZg<;DONu_fdwC!Cl@8et%OmkjX8YInvjh3XLGAP1S33`> zea#NFuU8IDzn&eQ9_GJU{6_W2^oR*ZH;zi_C*i+a`p)`Le{q0`R0W{)le7O<<{pXk z?esYF$6B_~`~rMb>c?!0%$-Q@KTfm7=1*b}#Ci*ejWZ-cuL;F-%$){h4c`_{!vgQ9 zLU+DZI-G>fQVkjNK)b*)2-`&#Ut&Jgyc0nS(KRv0F>IEz5w0iCWFDn>zv&aBMcw%P zg@%C47kob-oZk?ZkC6;Z8cE?uSo1~>%N8&=#K&TIu12fMnWWE({ZGhfxJO1=$U{in zxUTHW2x>Bzwo7&hBEwLAhE&=Cw*rY0Nma&_UQ1SUV?;u@v3R0*ejQ7!LWvtZC^Q08 z8azsr7PzyTK3#+~QM>QCA|`}2>F)~H3g@*u?`h*|Mx&MQZk zrM|O{%g>%W_%2@DJt8Q+vih1 z5i81`a^V132ou&(xiHt`NjSQ@_G3~*Dxkde{DkO>TnyFYX8=yEjA0j&ZLy$(ar zMK{##oIoV=*~ul3?}2LxL?H z&l27vV&IzPSdNbAjINX!0~JiDC|(?DyRt8kGN>OaJR+`2y3q!~{8WgJnHw#4B=smx z1>=n*FJUFW$oyD@oF-}l3azLFi0MPhN{M`9`8Zk$*c`I9IloyH%^pOPl@iq!)ThnE zvtNeBf6V;oHpOJh<{6X^Ss;=YKOQzuLwByu9l==wHi-5o2UWICk{vO{ASz1G;=6F? z4nF}qg60$3-qN3I#2?K&16YqA4qOZJ>3BLoyQ3#~RNqNi$(FyENRyP0$ zpo;&WEFu}FYAS4%<9S+Qzu~Hnbh(j!N}hpGU{l(x0n zx2}CIo`R`pD0$DVPU4G46{FXYm;wnv*$Qi;lm~6#@R@B+IJw22B<=mf+#`H%4+jAQ z``?8_nYOU-iw;+$Y2}bV+4r31Id>#Acf6Qz($5qE+D$H@>IIvF!-ml{4 z;-E12cu31p%O?@j(g!jOVZY!c(}1jrrM6-_6?2g66pgR3OElCj(bgmFhCmEKs}*Q9 zcsN|M$IJ?`4H)UbF@{V~7YM6WSH*>rpAgKKs)-Xu;e8*M-lt=ncAied!gKH8U zmzED*BiC_(k%#$z!(ah?#3n}Z#9$Hn)FhdJJ_{c{nG!rCw-01Ei0Ord8P<7iwHEJ>eD9C~pWp`AVjxQpm zKo1h-S2opQ49yaov_-0fC@oZgHRuk5p)edTtYDedON8$uq7~X#K=uR>4irHUVWFs+ zsuwwbDF4FSXLQzO=C0IPS6DL0v7zf=r{Y?k`j*-R%z^;gdr-V%@cI@d8W!M5yioTd zT+C}=U18rcfQiwE_`lI0g*<`)s3?OAny!MRGTeE*j2_G}PpQshvG*K9C?^Y&Z0~mf zBE0k4kJ_OeU94>5KPV`}zfB1ox2Vm1-kwI|i3RT8XMPpsh~1an!6JyXqbg!$`)3={ zS6`UfB+cxTRv1%Q`;E=xhhT$I#w648iSyJk7fb!wT=3 z{0_zFF8ru$pp!$zxzh^%y;etbFr>O7T6K-7;B|~25(ive_dk2 zF(iLsaC=-lm1ugij9xriA_}`v)RfSH6cBAF_dlt3li?Mw6o-o0zFJHlR#jffTo8YD zlr|~T#j+@xo)IltRGj|+@v#^$r(c3-j0dbp)v?GhbGSFE1pQ6^y#qZ+iQ!Kz6*8)GJGV zNy%<<%2s}a7yv*mr&unl{qX+?V~<}Kl_aVn|KTW%{-b@bdnJqCprQ^4@WafVi#nf` z2UTGwvC|8cOF&9D+Fr@*G-(6Pb|Z1T)P%1Vp$E+9+N}Ctc3c71+f`vSd)vBh0nPUc z7!_V1Bu{bMe^Kp4`w-bl!#_!R&$c492gK1qLqjtBkMbpg;v=y=%xNI)%GjSSkD>UQ zr;@ZpiP0omewkqAiW^_%E6VHn%@GLCV}aF$O!$p`4{2Q+9g~A@Sfyv^XlbobG0^|h7T%A_wxHsOc5!xcB{@s(BNy+%kg<(H0K(oA966%8 zellLSBDP^)wNW6eHjc6#nQC_j7-9BoY^s^||2w z;-EGGBag0LL$K*VwcjiGjb#2yDhlnm5SgE}I#264UE_I~p)VjV=|3tOlu!OkxXXt% zTuW7Wv$>Htql65x{SLh?ioP9h=voj*swy&7h!F(AV`*J)*qzOG{!h$Z^$W9svAFz# z4}}R0TiPLow!4r$q=~y~e<-^mS)d5@CUNdxivC)Nw2@nF(3%w}Grc!@sV;Lyr4e@_ z+KN+4rW&;FK#fcJVo#&gL%sCakSej9b%72lhe%DpTZzr$1!u5aBVJ__<`shW&Gkn_ zgN;$aPBjetLU}+ek)p=B(%zqCeu_q(;n?b}HrFCB(8C&cBn|yQ1l| z?+cmE;}|Db8UH>tKNUj2&8?aJp|-1=Z3tJ+kS3<@KzcApu{Rg_3WCfznVk(|S0h65 z-N(4Gz!C!%g;nCDn?|VH zIRmom!*AYdafF<)CbC#?DO=Le@86(Bket?Zro|8H?hXepY>gjo@%^S^cTbj*9Q3GLR%r;13ajgkdqx zA65F1wlIGCt9qH<(BiLZv1_Of5HRKn64(AC0ePzue{Avn&R-&TcFsdCncnhgu-WWdz-swEjywD!k^$5rJ=^F z`*q#r^#-r5xs#>loKkU%dBrSbCnDijimCm*#eKjgpxyWVP4!1qtMjUgyO?xMzb>UO zL7!zn3hh?$I)5%iZi{4w<$7ZTBtvc1%M~ z`_>nDlzC5OFZ%DrF!GET@$ULc1O+^2FIo?v!_#4^?)UeaQNEz~9ek+OSR;`>_coO*g!&HYa;zNc1( zC9>4>AlJ1L+?(H{JMddi)Xkq-?7cu$AREs~9?o|H#Tb#1$563hk{t|J1Xe9^%$cI#wxVj@uGIXfhoKVQ=#WhG=T8-fi#23`_jig&KGMxk{?aZYcUQL` z17FK=WGfa4j$B4MAg@;_;{7(_pv+8q6|93oU;mQrmWG|Jpm*0Mv-NViR|RXroMmvc z=}NPe8dOiK$tZT!etfQ-V(H0dr&!@++cr9(C&Y?-JGIN@@dFnV8ix2)a4)o0AGD#0 zo6k47du4UFukP;F>s@uK!d=_puA`D-eFA%G1AxFEObGm8J;I|#;Trcsi+iz!LHrN> zmSgugttw@P_h%PvNq2fKE!OEm%Wp33_U9GdZH0Jp{BYgfRL@Rtac8vjp^PUqS{Z`U z$>4ny9T*>CbC_NL(#fF3V>ZxI`Yk!TG|U86Y$YblR%ef(mJ!Cwql!;&??5I*0;*I^ zL^E~;!k-;|IB2ZHzL^2v1zM2%#7FlKuN=2CPfA@2THUqUpOti$;RkLPhz}!n3aG$4J_7))xmK#5gxz=$zt0m>#assw?ym z1UY?^=q|2+8HF`rTwxTH@S*rZ$Y-EcdLBF7>J(`)Xy08Tp9<3l1p z^*kNPj_8Q49u0;uG0<~_JH6d6Y_|nu2RdGHHg>q1h_Ul@X+&&3saFfcut5r%S{*CT z5b7_xyjx{RnZ~Xjj9x*6d%J{pNwvf)?_p2$2+cn%_>0@^A}-EVE@>|uK-mb{#?|`s z^GZ=ByDJn(6;k&!iDT_p%3YOy#bR0Nz!(g z`lT2hgcbUazu95Gg(|4Nsi+SD@5MiK__sUkj~oRB-s*r4pB%bpE+sR3wEcm=F|P9F zXuCZ$*Zi(T4|=?}Y-!7jVCm#yaS z=m*HZ4oyTa?-z;iap~SJzoN_TCsNd{=!$Re@^^OGkNE`5?&$J8LB-Ub+tNW11+{rCwKXrSf%c-M*G+pQQ# z9Ir3*xK%y=-ae7Em1FE)u+kVPco{ z4(;o=yX}u$Soqw{)?D1LzZMnS?+0T*nadJSC7*u}tX%594k6qYf*N5rdG#2dqRhZKyWBrZP(C@KZdj#E{ z_^KX%O^^MEPk`j=9yLc}Zzwjp9%?2CJ=Va&P$M1}<1iLt6Uelpwc_Y}Fj;ir1Xl8Q zV+GgH8|Sa&34x90j1f#<9IxIuY-`mbQQeX0VsgX#tUJ1AGLtRZ$rJVS(`-cf96QN^cO{ANl`-MRd6O(X6!b5=S zr;dJ#!iMwy^~k@u*1z0O2HMqVAFiXVvHA!UTpye2(SuV`SKQ71Flh)cADL_qPY!r_ zixVK8pJHD?E&t*0_a9BNe@2)HG1=~4^V`R#5HI)F z;cuUrY)`NG?dwyd$=G?5hvxtOWcyqG{d-(zqCjxQTF}Ikwcj8ZY_SDA5d$3mnWW!9 zKHXoD-Gebm8%ZQy#GXpLVPg@l7d~U)!D;r%X45&Nlm2kdI2#|%nUt&*K4bU@pV!IO z3!lkb3!gW2Xw%#S<4gFw4eEwI?`S)P&%35y44;YKDJUMu`7h@Q0XOIRbS*IYVt6Jm z012=fB*5tb8~!^y2fi09Nxo@h^yF07UBOQ%^jn`zc74RK_aaNmf-0MMtNnP2Ah&#~ z+mxuQCRp2gZe0k`EH{OdCZnVj+^%S;+qrEyI@itN=xp1yIN$BdrS@?$;_k+tM@B(& zxpcAs{^nFdL%|q!@q%#Rp{em`ZuZ+r`u2vY7`HczcQ2OMnE8tQI+)Pcp*l=1MNrVpdT@d;aT1-LSqp0t&uFj`DijldF61sdAy#5NQWK5IPi zQAt{;msQE2B}Gv49Tf=|+!%MQSfes(igS?V!}e@y4X|vA-cWGv?Q01}cdpGGoofwh z4Qw+^ZN=I|voM;Z4Z&Umq?(7&t3)6WM^LAs<*h-0303}WVoqgH)!V}@O5wQ?oizE{57_@pwx!IIge4Mm|(}8L-WD{Sjmv z%s({xGm;saV@gZhky#xHi*1T~F|DK}mIsqrfgFA6Ag$I;_meB553@tf_2l)0sGyik zipK+?RrdXtQ4=kD8H?2X+U#`G88nHQdXv$MWpwm>l1^mG1p-b;4wSZHX|>`mirs}l zBXUb(yF5-Vi|tAzhjPjP#k==l$qecMN8CGUxGeS*R&YIA-(RYewV0kCT#icsP{>pa zLM8NDLJXwNHa|IqOlwSvQ%T6!9jUPmbHGk7(%~O!uw)-UmalRJ;}Z&iHsl* z;zzK%km^}Xl)s6NU&eL-r0PxNaS>6&kuIh@Xj8)`YZ~5gNi?60w2dF%7M|1aJTh98 zQP0;@(GN6v6aTGb4z{)0yJ{)lrF4~@9^T@Q?@nn3b)D?EvgEE0#LI3B1&VnigWA?n zwDn3+t3VNaTUcAnE?7B0;;L*u1ODLPyxBWE@A|Q;Mf0zA)V<5-d&F}8Gojr!O}E_% zN`F;&;tV4=nB3<}$k$2jlGM?-_#T=uk^6*HhkM~?iLF#)$j4G6YPe2sp;r=n6+y_k z%kj#)^h#kGQA2(QoIHObp2ptg2@B-km?rD^k(2|HIOxQRb393oqyALbj}Vpo4d;Pv z@E-cs<2doj0VMG1>fGr*UwGCPHnL+=w=!|WJ3lH_Bf}kWw{ySOb$;sz05=*v>uy(2 zVAZ=fAt_Et6JWycT}sg0Gy3{>&YuQE*p1*g9i?TIn;5Tahth|(V&g!gSoI7Q@zX}F zoa519IkOoXee`pC_h|pS{oF^yxjeko++*`?`9|&UOmUCZ$R2xizFV-D`?%0^#t8RL zKZS3DUx}5*t_%eTBTKWSPkuR8isH0Q7$D&vX}uG2ROBIjPP*M}9S6?}WhGBw#Ksi} zDI#YLeXpIXQ7sX&;%P3ZtDzU6ZX~PRLBVyfyV@1cKO7Nc6n=KCi-~?RG;3YW~_6o|m0Me?QFZq|6@n?n%!C9`o6w`8Q+n z!~e~@*Fjn$?~>#_)*|C%T(Ylt%%v0$&LPh!Y|fvNl5O}$p^u9DRQsMkc=xXN@A&NP z{EAWm{^YYe)Db?}V8nW0a|;d{=z}b&g<^wUOcEZh>#RzvY59tY6Qf{aNAM&lm5F5aLbHM` z=zY*f6y#{%n~iStf}C_>46$0x!P}JQ|9F_f{M!1q!N}+tlItLBE@e&WI}xJKd?~3D zVv1>(99&+;y(y`J*eavTM7An^PBA&FUs}S4ZC;MkX#PqM{rr4%NvQy)L1A!dA@Pr* zgp)Q5W?(cs@&&l8ibKmNl4do^BF0x2NiBDG$i?1GJU~!4nEqtjl!!$3Gi$&W12Ebu z?a8l;?!zjFFN-~IUn;swVyEPZm}cP(eCWS9)5=__mY1JH&bLaX z%dnD>q6i`ogdmyxZtw2#?rv{SdO8od7rlGI+pC^Ik72*JP$ST_uz=JXEXCL0D%9%? z_fje8ouJT=Tk@-fv@1Gf*n{4FF8A%@p4~B2xiO9jo1RL!70_3xEI8Bb14*&Vh@u7G<$|p$xKoSh)vNM;+~*Wszx{8){k6a|BkW-1aK_XDf2dtI zDpobvAbq#H>4DWDM*;!4p(N8W$#nw*o*h_kYj}>s^ID2Sn!gi!XR~M(B_6C$ct;^O zu%Zj(&-W{xgB(3d14lACXanmVK`Z$A7gRurl==^OEU5slzyh8XnMzmQl0Qc^ANV3p z_Dnjdhv&L=F=T~LMiFRD7i6qq~7BYe?rXRCwUPl|B87aW!5Gf#Fxh_`#z6 zv^cc8S5nckNJ3WpOqGZLE`37Pepd7pTHzk_cKC(v@JKOzteBlPod4nN_5j57abzWa zY})BNd39+p6Dxe?M#%p6i3K>_)(Mm!ZlH(&<$8dxqW3)MEiAx6Z~EhZD$|`?&zL1ynsG*tGa7oft z4K7^sQy`vW;;}d?bzZl9)stm%4hzZZN+Lbmh{nmM4s_v>?A+OqSmsuwe=Rkq2?Qhbt^QAKC=A8kkZqy146BC=!Bny9QYiWI&0BnAIs z|7`3ZQ#4AY9urE_!o{ZoWt>Anym#A1Q9nbIXgVQW{9_AO?|K=sHgn8+Ws0X{v*V*O z?%|JiM>!t#_0n|<94#Hywq82hj!Jo72;a~#JG7X`V~7%rB=&|GfgZ0infPVog=x`4 zP>O6fT)NP6VlOJGm`o!f4acEzNjT?C!<_*OVLSa46!P1v=Wf+q@5xbek_D=i9-~x@ zTcL9)U%t(-5+i{|&PJRyS9fF2sC1po-Sxk8+HKDLfh;HlJNj;u zaq8Slg!p-Q^o!1(miW$e+)^Tgv~=b-rlU}NDena%^v(;Y1~;_u)Cr@k{m=^E0AWD{ zxb%~?y~*5PhrZ=j(kE_ZH;^H8>5cpoen{svP}`A>&#vBJ^`wxQ5r zwUO$YA)49ewsvi~FVi7LeFHf^I*%B@pqPXO4unU(WQ>SBX90e!7v={stk@{C$1=1K zawKVoHjzB{h@^!@>T_dlOo}Mx$HRam;8n5Q36~~~VZkWC0vQ?`y+wsz*qSmaR31b9 z%Y)TJL&2VPntUMt!GmMP7Ef2$8pn4T*_VXF@Q%t-gh+$-U$eszrJQYmgu68@Mqqi# z9uhEq5Zl>ta(`?q2xCG>A;hTQ*NJHtXD3Os!H&AKE)egavrmj|k%}ymiPg=_^j*SU zsNiVYlL;sQ()mAo7p~{8kD1zrbYoG0i$DM<-2#6&D2l#GH1zXlnEUzZAwr9xg8d+C zO)wDd3!~ZDB4zZ?5PQTRpq|&cNgzX@&tQ@b1tC(|UC9muZk@0fcMi`|UV&Bp|25gt;l= z8c!tld&Z&)%-8WlAW|u|I%>K;giU~=BC+k@Pk?X6SW(j>Qqq`TxA-0(!*@QZX?GI~nF%RrO3Qg?(Te{zadZx!1JFOia%o!%H{}K3!$mKH085I{hniHE zV#HwUz^R!)B2w)HF&g`-ZG%TaLJevi>k&x!h4bUO5gXZ`Si2R<*4sxcYk^0DhaX*x z4%s1^tyo3v2(~Yr-jM*|N2;F_MFcy+Si(%uRy!csD$T8grf4(93uVI&NrZs_WW8K` z-gLZLWgZHxjoH?R>_JU7W zLvDzgL%KY3rj#(6Wbcfk+!@Z+txFPg_)ztRle^xJ+u~>WZ`g|F-BSWwt=4}_* zYi7lxbgx;ron<{Y`EU<+V01_mnE*GUj!LRd9HlG5tB=l7Duz&s4andUB)PJth(V(U zxd4^f780HPb5&cVg6_H4_DYqsdqS>MoQBQQ3mxiX5DGdY?8x_XeO*@)p7ZZj{BJAv zw$?oV^*a?G*@01fpcpvmtV26t`jA-X1^fy?_dM#j6%Sb}Lep`y_E2FIIIemR@ILk9 z*wWhhRhkO&qTV&q#%s|blk*u1uxxo|#i zptR%;5LGAh-&dnU+HP|GMpp{YEzw)SU=ps(c7dM#f=ds>Qp@$S^REtnu6kgDYJ?eF zV`RoH(IF!u>J4km*aX>u0=I1AbEQE4ce0{aZS;z z@{#U{dbALEiFsh5d>&VA=teI25kM=HF2pQU!lfBfg;TMdel~Z|< zcqMs#x&2q*IopQkJfE~ByibaQ;s>C$+I!9uKEVb6d5`lnRdnmh9e_il zh5JsHdB6<-TfB@AUB{{;~4VW(Ts za`7_#2w5qdznSOE%eY$nIln&n^I^g&mh}|mCU$t3?_Ii|(MS9~OPrRBy3Oi!TJfJL~QL{W8V|2PZ z>L~&xID&_O#pJ}0w1T38KAYE>nm2{qi)dCO8q5OZ zFr-6_rVT?z8B`r>jJX_Lr*cxaV)%)#1F!7n*j*PF1-avunQ4o#1u;%+$c*#-l-(>F zYz6GBuoZbLm5XkjvjW(3%OE*na_3@MKAtk1?rhin1Lw}BBrL4^tU~-V~3fRa=o6pJN{%0)~_~NWZ#j zxT({p@rzbvOR`V!?Nhk-Z0at}>~gF>NcsqDzRxxg?EDlSO3B@Im{o7jZP+x5GMzb`HI~%PEFh=T-)fWLQ7&0QDs|y|Bq~Mf}1U zCI_~XL=VW5dTaf``L}5nAxw?&H{g6DuW>-=rMUx{XABP8v}z^fe+55d-y|KRLUI}Fmm(HLq~;ypD> zqEkAu3qr%lMXjZhKeN>?Y^5NV-P&Xqw_+OnYt^4tvr}cUJ*#Z*G#YQU*s@l;1>cB{ z!9ae{k| z{YAe?Tg1`=p@4p4$G{%QOk9I@NgSa+6Hubu0BUE8wU%1gYMC+>m)q8BV0w=4O)^!? zs*XTIgo-t6%joqEcK%;tM|;du+Tz>W8_QKn-CKVJWU0h{w4dg8uxVwN%wVMiL(gDA z+%ifrrIB;PLG1U5qFn~tI%O#0AEJ6A^u`@j+Gfrgd~{zYR>T!n3_ZnmgM?>H} zkfWD-w~XbVRU1*mWta7AfM8UK&hIX?%kXQzEY_YX`d=0^9J~}+X5NUH8GjOkD6ruC zZ;I|d^4r5Ei)|l4a}0S!qwu+Kv;ZcO!AOIjXrBjmCO)Eu=!9hldkePg-~=JV2)y1U zdZZiCK(;{XjUh7^p=>H}hOE*abS#?)AducaXZ77S7Y?_$KD4=^dlhNNHY;rF4PG`V z?r8W33jViRqgT6&+MY$!50}YE!9vT#ayO_EZS(3=ev{1S)NcFl~GbEO{0`O%GVj88ozB;1tco5A_f)nP@uy`8$@q& z9R|(?S>pSh%WsN1l@-;3Cray5F^htm2m#`aCV-cdx|m)!1GM%}-r%f%gjq%_X4`r%R9b{sd2Xe%wHqfm3A1 z;F?cx&0~F#UixFomxgz060;oXmU~Y`pK_hrq}x#3MTiLt=qZ&jK@Z>gLky27BvP0P zZUU<&Mo1H9&MuQOt&SZWVDtQy!;+GYHCSIqJbt)j^W(^`QS^kt-cC5cZr;${vV|>= z?WgVTA78hn2iX#;$+uIn#Vr}xCm?Y(fD&X7;6p+jjOX@p3nftX? z*d1)&wl}Gn=<>th~nBun@ z%aItz&cyKD)Ob4I&ybCAz_t)4_Ig@}!XtNCF@Sz(k=A&hFHa#HDKD@Z%K<;*&fj1; z^3yA;PiJX3pM{LWp`qo^7jc0V!|~NiUX5;0AupmT@%Wh$V1A$~s4m68r|_J-ScLlL zUg+#V_nL!O&_ITg4un0T7pesQYm(TXrRK1Gt40_16&MxF9x_Tw+hwuL!mM|*jf^|u zF9OMr(9N7yn|1lKvzNkReFu-9=bn4pKV(ijf9PGaEsUw)>akgz?26xTQ_0GDF=iE6 z>_^Rxb+!<1x+U~K8s4%_!-Q*V4Dp3QF< zHloea+K&Eyfwj$6FVI5EH%-2(dS1PoSWA{BU(tLK^CS7G`Hsuq=jVV7V8E#81B_{0 zWq1XV$<*w6r8#!SB^0^vw$l*IS9!*Fc)l4}=rhfcwU3c{skyCKd+It42*SsDJ8}rX z>)ZDXZ|a}T?lwA^z!Yz!kyKoc=W_mI_>hB@Z*3Uz2uaKkvm9LVZpb1phpsHNR1Zh2 z(*7Wv-sM3`A%p{OEJZm1t^g-VZbH~I;}nWiW|1tYrSmcfVwnuDyCA=pP&WW+Ag;*3 zm{Fop3_o=AYsaJ#fL&Q=ebFVd^&D0?67$HH-ER2xitO)h)|nUSJ+L-PHEa?`cO|5o zsP7()4x&w_M^}^WMt%!T0EtqFB2RPBew{Fx6B^MbX;Mp*ouH!B?QrSH=q$y-Kb+Mb zmouy3{!|pw|8Ln{T69Z`_>f4Dr74#3!0EK4%e6xA;UZM_XADf4pmwKe(pZo?)bl&U z;SSBwBf9$Wh1yBVB6=b;4kF$OA%Y-ovE3G3aZ-M@pjjl%(IzNX$R&EDs#zc2kK(l) zy-|ZMJze#Li~OqUpRd|;)$~Qy2;v-jBzU5JepmJH61Eclg4h+Oh2v{}QO&m^bw{>C z6z>p4;*yfZF&@djEb$uxISR*RIT}E?8_XMGbsO^vVn;g1^F?R$C@vH%OlO)uTh%mE zWh$pEQJ5e%2^4o@uCpRmo#p+}E(H{M^|?n35GcxgNVg$7Kl- zvz%$`xJ6bwkNrI-=`^t8v8=rdF*aBTY5Z9guRU!i7jxz&5sxtP*$lbef;Z*x*?x;7>U(5)Bo? zBe<2c?uFjBp-I#TzUBqO5;(t58r&534`TalLLR@!9HmuB@e20>ZrlZ`6`qUHQ*k=p zjfMg5???fF1|Vo0?!L~T&r?F}j5+>m_$edM7$><&pN&BkjQ>Ir_8EX^~GWjB$4g$kc$LC< z8y>!dlx!|K_CE%wEq=qVFXL!Fqb5(T0zCk_0TWtW}kZ5}rWfhgf!a zUnD>%XWx|uMI+_sj+;RZTneta(F8$4cq{-7W}8G;(sF{?U*qh|wDFdd7V0XYjDmU@ z?gN_0sjA(#vhN1H3AGthxdgL$s|7PP@L=gN7KVpnTeJ~WqD5dW6h=i4aSi>WqI!eP z@nhFO>V@TALfv4}B}n+d-z<2Kj%r5BUg9|F+Z6W zuSML^IE+nSpdL>uUvC*j*c^0t!iL`TAJ$D9_2E_jcZ;tP~|bdm~I zl!Z0+LrZuggb1PL)@Kq)pwyl7(_Loe&+l{{5*c*cD=svTC^r$q;%MKQRZvWBHY(A`(Ki?y(NRHm|C3rk zFOj7e&GI1kEo8&fpwbEcK_rTJp}8&;;b91|!u&_K0!*95(};<|8REwaScQ0`xVOK` zqUb&%MrhI06g-heJQ|>t6AAH%>RAxX7P-$^aLkRMr<_?eeFObgzu;9i@?~-Srk7dR z>*x#AOSDR{0KqFm@JW#CvQ`#<46ELSoYLT|9X@qN4U1xfGe%=M%lkSvK>TvF+t_F` z;K67_Opt}J4^fpvS{^xWqB+#@;wQ^(t7QQs;kl5%k9I3u>D(2rBRm&pmp2_3?Tx2j z>#sWU{b&jpD7ZETAF!%~2)w(S&D|hY?jZ%V{Y8rq-o0_-2v!9Ygz&}$RSmTcI%DqL-Q3{$=3!=$bUn)jKXCbnDc{tt)#f+))|1} zku#$AF-EYX_#I(r+z$y{{&>Pb!kJyeXJ*lE6QeVb#6CG0Ff{>yR z*7~`4>oN~&A#XiG@1qr-Fey53C*j9IJ43F3T#t_XxuYa|lxYx&85Bd9f^8FT2b6XN zN%Fn@WzD=nMh4<`&`kW^x-)iCwi!hjlM$kp3>n2Di56mCA);G!7K>T5+gj0CNeXWv z3$=vty$Mhha3#`zhhI=r0f=`-*Ws8Gzf(vYhJFEBb#!6N*&MD>iQbocM#3#NCJw87 zjEi;~5iLdbWSJ!0@eE3F34TswlA=QxuBSyfRLv@3z3NMV?W5RxgnFW6@IHxI0lCmZ z!q{?jl2LKqCYX+PEAHU7cRTpyDE6-Qh?85Y`mpPY?x8=ierxxqZaI!o`)TTzXZByU z%Q8pF%^wz$S3#ZZL6o16>EDQF$gU{j&-B^UHA0YL6bkJDSoc0unRslwjo9KF|p4++6nixjRHx6CcSk0wb)|gb>Mm$ZIb85bdl@ z3F~Z1o0w4nOTAQo-pZP}RTOP-%H>4H0KhsJq2k+UW?8`@iC#u!{9V~GWA=Z(9;1W`7 zkc!FEWmK({A>pM0mB<%6JQvaOy!6pf*(0OOZonn9YZ4!{`KN}T3Z3H6mCH^3*lfV9 zAD*|6b#U&;z5Jf^?%PwWgTe=UcJnCiRorvz7KqJht29Q1Ms8u77o!K10LLzXp^D|F zFs-^*>I4gB_b2uMXR~LDepa?US{M{oO*#m%+@-iPs1Uo)`)SB@Y*hUkb4+GsY`afn zh#USX6l4*qfXn%t`-`635?$)V)ZR?oG^VpvOxBGg!#pbR8hFlC_Q1xdJR?Pb_28Ze z7fCmdB0BF__J=RFDvnkJ8!KL(anmU=VeZ)I$`X=1$|0~TVU*Q#2Me2%jxKG+7Rq7f z*fl9ow2hWQH0ORZ`iPu!b=SpGBS}5fiVF6^y1X$lR`+r;f#FZ4rCHha zLZ0}L#a{z+LOFfy$$;8(qxrp0fOHz7L{FHOzW_A7YHkBKz zi5_ILOeWgg-sq9KjNoK#Z5Mxuv)f>SVj}~7)8Jx`KZX>}9lgKpN3oc?`tGpBa3;at zC=}}M4^4gHnF(qmrHfd5KLc9Y#$xB|zNJ`3Pdrsh$!Mm(79$w6k%0z}4$qr}XSgV+ zz!HJE6{`YtJ=B!4!~^He-u7jE=dGQtE@Pvs8|iZI?+q!r5HS8x5sI;qI|3nxq%4ck z0b^>85$;#EN6MTdh=JAM=3`}hysRE~iyL-IgR?0~{9qiP+OUU|2c=!J`eb`ObGNqG zZ7u$_%$-C{H<$b@P9H)zv5Ok+;s#MYk5N8fg{G3L=@&V~^F2bKaQ@3IMN{tj-oZ^3 zHyWNKJVWe?F$v29{~=!&pBJHwq^4<)sWpt!Uuf%G#yVc6NnlERqd-UzT9{d$%g1vZ z&0Q;ckX9wscT>A{G|w~E1Km0v}N^V1>zRk)PkBVWIfukZ1~@(F!0i$K$JJ?#%BehtfEkn z_yCn$JD^d2toz673?avec@9$35%<%M1B+7>9q=xSB~DG~9bgy%?gNpaN3|=lUt6UG z-Z6JXbVdtCKe2aA2|$)&-w-~@1_L(s`%K2u{ikUUI#2Eqw~6Q`5sP+evpWN07-f8G z?nmrMu6}j14ZV9^?i9bMMdE?lMdJ;3gz_Li_%Xb2F4gu}1&|%15u$KBuWjEfx)0%7 zqEi@(*rx0n7{mE~v)(y{=vmUuDbJv2MrejSioOpr)q_V}n%QzYW|<)&PYC2s9H!pX z)O>Wbv=zq`H$?@NylZwDaaoWRwTq!F1ewB(rlk#R=u+Ovq=t)$d8nFM6Ln6Jk`Z~T zr7tQ+)q1h`^m1$(hWFj$ofCy&2(QUt2SvmQzviaocxnDeEJ+v#g6GizplwK17q-g` z&K)4-#eU`?eMA!&v=IU0y2!pPMmMz+Fo)Q{(ic?5;3G+Baw<3QW8>vS#o!7utqdB) z)xgwdepiWJRT4S0tF$)!{8y zvmZA%?r9bqq9!+RfHY3xUilo?6_VBBao{V*R9aJl)x6oz+BP2LeYsd)Q;mp4A;;Q~r8C-4Ims9|vv3<`;bCFc(0mO|I&+t2?<6y-s5PlNP(9 z)Bf42{WB0Z9&AMpq;el{db87h8{YK$$2>m$SMudSK1#!VT#cK<;Wz+;uY~b8N^dVJ+U~0CPLlSd) z`tX|VS(V(iSGp%*0?o+yU8GYEedC5+eRNxw{gW1L4GJW)?28)yA9eRpons6-c64t` z%f~l3ZYM(4AP);0?q}`xNV^fx_J_JX+Mb-=aA!5_Oups^r#0LU8g8(m`po)#aYOjQ zJoONDp(qEj3nFu9TC&DT_fQ!XAu}dPR6hzYi1-Z|`W0882TXEe^uJv!Ucbkziz-Ob zP(<^oaTN+vp>G5)3K-@VTnuGQ^?3A47tL)%4=Ny-<%F`b#fYzh$muB-Q(0w)C^vKe zQn;6!QymJt+ci2+bbzrx4yP>GBO2_(?1mxv^{IlR{`un^WvWjg8gLJ+t6{P{`3C94 zFa+=VVvqDYv_%gS6`Df51l5G@QY?s+AOVQvxd5PDHT?-BUhQ8T8c}0tMCIrPHawVo zLct$fu;VnKV+(3c9R%v*3htkoeS~{}0UcZLT|w0xi-ZweBD#kZ9W8>iRS84zkTH64 zIF9$zK(7>#Bee;*rV7S?!L-ZUv{?2U2fr}qzWhdw*x&d44C28ir2PnP?(VN zFaxLAKAxuE=>gnh2ELXlC?V;WCvwI-VOut*GkOYfSKE_&l*siO>+VVY6R}Ett(4gH zGikdQvJPe9ynfF1pT;V$P)JyMrK`|BZT=78lb_1c_!;X{y_X+?gFQ5^X}z*thV{ z6c7t{$1%}e*%|*5z5q&y{xPw)bDf^@I>q>iR6*2bf;&oX4(h3DS7a*H#Jv1ZAV2La zB)FjeGN?)*BfcEvMSF;1A><(CNoTQ_Xrc#!_rgiX2l>yb6~X@1t>P`)x3VpZ`xL+8_bGg(9K9c>!-dw?aVu%6hpU9D z6&cPxb)#iTr^!CLqiyeY;=_ho7Zu)ph@04^L!TVIN_t#Zz_`STatOy6ob~`4K>v_b zLTEnuh6p84Fqxnw`xQcGyLq37+j_K{{nHsZjb=2@#0$NR?@hvUaYo9q_L&*q;aE!2 zlb{B6Y>yq+13m);Ftm{TpoiKf;w-13t%4q6IUn)00B3i0QB2pZz{ejOL|ouvQ#IX4 z@RYx<%l|^7@5R8kHo~Sx&tc4id%UlRTK0FKH!6pgGiimT_+k+YF|!OcbE!%|3=1o^ zXPAiLHqSH}*gKO+c!inqzB8ZLe0kY9-&?Vt{ z%O%k1ORV(O5XCXSvSZfe=68P8&F}cCpWhm+^SMPn+tGGPd6aq7c~pnL!+wu@I&<6j zE9KB?;X4qIh>#xg-@A6IkvfN){|2+S>f)u>TdCJkwtz6vldXD&+#_+hgy@V>5Acog z;|U|EQB`oUB5FLS0hJVmYOk5JfD}NGznhzt@P^u*5>_6_MDZ z*t=lE5_@bhDp=y1*kkYfKi@M80*U@v?%cU^=gypY&U2n#ynTT=k8A8PwB#@&NZD88ooy#LPBbGs002h!DG@_7$o{Y z5Pj&wkyM}n;zX(ny`+`V*R@7*H466d$OBxVzCI0{?zxsUKqd&HF4Oc0;QQNtwtu&c zz+Z|MGB^I8MLpALp|R+BFh7t+@jQsOU`;>&e!*TV*o)z$^&k9=0!5)xjvm`=3Gel= zGQ=f1^3@`h!P7N9faxh9j)8N%(I%wI8BwL4@s*-)kYjuw`EigdWZi`aYTb2b-Bm&@ zFS1e5Jt0g*>z-vCY`tihNx>!cs3ycwN*P2~7#g&zSz%ipwsF{|UWj!6hql?}NI(}^ zmritV{b{_T|7pdJ`Nx!Z>J}sSz~Lwu?u_FeqwLX&nu?Eh?iiN|`xds@qg_va+vhPO zqN)&`BHK^_H^wVgU@R%7udq1yvgG1TeO&8poor%t?hUj3 zy}{#K%D}mnn6BMRJ2mlTiF$a);MGkP_J^E5Bj^86nfoSVUuWX4GVaSv=DUo2o3Z&A z3^Q5wTasF=VZ?>fIsTeVd|f7)mek-K8Us?_X7Ou@zd%SUYU$zSn&(a9ZQT%SO%2UM zl|WpO|L0j;^te*Wkrsli1eavWH+T{Dj8V2C%?~0^5IYsMF^HXTIZ}W1xM1)sEb9~;DQjPK=_P; z>cl!Ykg)2U9~lx->tGI)Wlyc31?X#z~^SQ{IE9q3{nT~A}lUISJR-~X63Y5(_SGuj&%be z3srbuGB*ZGqjQK3ic)6e;F5&=3S^mJ_Qb3}=(323ZtSJJawP_oI^BC}ydyC4Ew;h| z{uh<9iOif3?IK?R@rnL&);3FXDwFE4T8{|tZw43*BIpD2k)E)O`A~2#>ZpcH*7YfAYU~Ex-_Vwm(GU8Txx~5KzHKz9&!zhr*OvJeqR!zqMU;@N9 zJ!34}!K?<7?RdS+m^80GdUM_0Vj{EN4PzCfR&hHKr=s$J7F+%-mpPM|a+Cn)56K+P z*s(A15D9J(;7ci{aC)WUy_vvpdih!mIU@6W{eUqMgD177zz<|7neR$#_>7Pkodz=D zBjYqCjHp6{1ASk;hWnovzEkKkH&bIPjcu`Yz1UzGL8j!Ei(GO#*07~6L-3w$o{X1% z=HzWHBH;1X{q=t;9t!$PCX0UG9DeUF=5mXzVv&nacfE*j%HW#h3RD=hK5;+nSfol= zdC>N}Yh(l^EL^UvUNUn4 z5oSQ5P|AfvD(R*mFh|C(RAixo_!qMZPlP_eU;?tNCSE}CaEoDn0l{+7qLA@}m5hbP z9m>%Lct?U#q886VmlWX2GX;qRes#7O+#&|RL_0+)Q4CO@P-Go}D{|wHhWfGEtOS(Zi@gVRsRn&T!$8abalV(jD#6 z9qq-R+L|5EB;#jxv}1qfRtS!A(95a+$^Yo~+4uej-2)Ue6?%|~4(}qb0?zY5Jvl0E{ zVPY!mEX7G3tCTD%w%lR4o4GXYBSdpNvhD*&%iq0RqY*4IHXQ9LtDR#`afHhu^`^upPkq z9fw`To(^e@1q zrOYzD#oTp-7jhCy_(tWA!2M7uvhs{{geR_2AwvVWC#V zbk2*_^75DNoC?>B%GIcgWA^8y>J$`T<9XjUU%5x`+h;IZywdQ=sToEDG0Ghe7UyQd3zU5vVAinO%m`JA6aW3;2SC`N|Sf8*!;laUPO-I#S(C&ILNu*dC_j<@V6%qrsk7>98W64;batXw z^pSo6&K**{KvPMWu5+6ZZa}I=Y*6Xv~AafPSlP}JP3xZ3S@g%|p} z3N7MB=VDMR*qP`U@gf;2-^mC~Z%`1q!qp`yjD*h7AyBL(@za%syQ9LMBu$A0fQ#8d zIv>m=77WZ(;gDHP$l4v$lxAhsgl#^$tqjYh+3^a?_a~}u4;E#+S)q;on}Q4`h`iXc zaX+#vs~S~Q!u9h}NB`)l^yedi_$XjEkdnKc0Qfzr3jOiMN>%Qbw$8+riySEf0+H<+ z&Sdn#%%0G$N)~;IHIjYC<(?)W7*^{VqMOPyB14wBzsxG?OU!YVYj!fWoT=Oo1sP=oA$1ZC^$V;jGZuXb*yn&6~|a@A0snB zkdjrL*pYqLJe(kGsd_mWb+n;Oy1Pp-(dT5Q4;2xm(Qg?*i1-Z)q^@Q|)vx`$Bpe$U z&9A_ne_^>_T#jePFQJ+ieBoahqx^?7IRm#I$;Qvh91Ejj&7UbmGEdpzFR?rN#SL~z zLkM;O6KKTJ;6DlSkr5Q!Gk!vpO}`Akjvq+kd#Ml2r{x8^FPw*~OrP-#DF)mvbe_q* zjB=A@-ShQj$2$8~y@LIGie*-~_t7gW{Gtl#<0z-F)?JF$WlIRv0*|O$3?iII*SR7@ zr!4kO$zKIf0Xe2}il9uoDNL%xro=7DcKQ-Q6}sTTLsE?pbMSQ&-v}ayhzTTo;)^TD zJI>;Ft|vFlHxTrKIi;*CWld-yVSOA$O?-1ztM7|-5oh2)p^LnI)tC=DBx?xZy`qJc znVY7^H}CAvDYvuB*)pmezBVv6X=L3t!l`*n1$x$WqJYX-MJ|+Zp_?j+d6aCVSPQwK zL-{4nSZt>f1qyBHTARhSEzX6;Oio}im7)C?j4YFu3*U@lVxW9*NTrgVw7 z|L2zGaOyIz%y}VCX%30_0iGFu4YDmmTSIW_fuJIGfRT5N^wB}zu7 z0qY56o_Ck;RbD7lf}O{DL<{R`1vm%51p{sG>Ry@XJ6ge3AOa;(N4Mwc6zF3 z1iI9wV6cuHHapqvtdj@FJ5as3*;8h()RLJt1{G;mzlzMb~agOkE@eP)?S0S z)4dH8%+@Cj_0R_DHK{4?g_Rg4=a=GkYojIy$;-6iV_hqVVoy)&R2Dw7M$MqkrIXlD zt(;o$JggU<6vH2#(W0rYzVqwUN4h@5 zPa}?No$=`9U_2U$Lqd?RNqRw@F%_nTZoFT`1J+^q0r)9!6U?IegkN+WShMx$WgJr( z^|hu?diVh9{(!o+4OFcb0EC1A2)&}ar~)rGg0EGN`Eh0CXO&`^7ZmLD8Wq=ec2ObD zp-ne~3xmlg3n<}SyTn??BSZ*ef*3&fSETgc1uoE4_j3&@=W4nyASS-LAwPXuzvs;I@Vmm!ey4=%6K>FuMBmiNYLV@250>0Y zlmduB*yD5M$`V^+>)iQNk-&-q|Mb*oeWhg2qqf_1RYsf5cWT`GHBjS!*SL?k8r-~K z4^kGN@QoT-wX*oQRMA6<5o)kjqw>+a;X)71WZmD?Qr6vDQ+z;9_&O2}@2<5KwT|)> zD{B1+Or47QpllYIOa*(d)_qt@I+(<=O8dOleN~Gnw4Wae;Eyi(1*PQrDtALwOmo^D z?bQ6V9aqs>BH$L9 z7hbhRvL2Ald6jibR4lZDMqhq#6mS{ADnI*}c8G=8kG+zK&9w zLRsv7@*!f|6JdgM)sVKx=v!c1J}@r`Lpc}h5nam#(FXorUGr+SU`ZGn3Ty}(PM-{y zg#@9Zth?nzr>c(8VjFUCr+J74RyfgRPjCaHuexS_=l|FKUYA{6>*t#tiE4?=mMI)> z?tfTAB@_|fMqYCpwiljROg<}6gL|siiXFEbvMivMjsr>%M zsG~MItD8cT=Ue5GKs^_ps3Lk)t>MkC?vlN>OQ`KyWTN1zqWZ*!DK|*L62uTygdZg* zAmG2m&=}GEb;-&)uXchd97D3JQUy37-Vc#VR=6iq%gCVR4nSsMp9yk{*l4mb8(k|b zrgQ>o;9(&HvE5$npcZ^ChBL6JaHL7*X%~ zIen5A22B21_alsTaf!Jq+48QERqm^|>+0=>dUt)jW9#hHuJEsqx)K=#ho&(6RGXsA z3VW#|exZXk|6IL)5oado@LIillV2n%Mu==_m#U$!f2$tx9Dm#WUHpj+?m-`;m&<}y z?KAl<$qDt?9B$}_t?%#Z1nggJuvfy)Ck?3OB3MNXE|pLR#3A%}#70I^jN1rkr_fk~ zThd^Ac2<(%5$#)0L&#hP>mla`^usV(DC9S%v(+e}C{74$0)hK0ESejLEF#8pocn1} ziQAW9%@hp_bVc4cu3Oi)TMR`(UxcC*gE;E@u&kP0pSY>9KiJoo9$?N7v>|MVjvC>H zr|~vdu2UnxrF=koKN~TlAz$UQ#fDUJtf{k zf_XSY^SFjj_If?^_O-5>ziS%-i~#vUS9^(Sp8-U~qu8ph_Ig*sFVRZug+}*sqn(}D zIf*+v@$`gbh5AqH!Ng_t{>pm0re2uHZ!homBbfL}wS8IbzpJ)yc>;#^O?5menS_-x zU1v8c@QYekDr#JyGgOyEJ+b+k$zu(ON{a+Ap;(u2AG=~_9%_YL-!fOx(1c$wfqbU4 zzV*uWv8J*XeCjmvaOf1dO~8hJo*a5wH*AO!gm-wN{<9;y$b8TU`#7Zs{3nAmOIU+U zheDInrw~anDiC9g6;q?vX;v(yhXj886Bv+(SR>N%#C`Q*fDCGjP%yMAZVASW6+Lm* z>3O6pC|L-XYhXk3ZK+8P96-cThq}X8)f);Zk_ncwcw9x0QCvf1z@?I;lkh4A<6nwz zS$$U~23Z27DL{4+{f%5WT1>aOQ1hoO`lhFbpD5{%l0x+iX0^IuR~Jg|^sZEiw%cQO zMmM{<7Yi%CRxqK|U3d3tcYCF~Tdh!Ez{B)&+>Q;sa-Zl`TGP`O^s=LRX?thS;(a~c z***Ny9`?DK3;VBnNvp9-)6Tl|(uB3zBfUy5(G}M&>EW;Hfo<%eZt)XcV;Ro>)m=Y& zw`cg#nQE}?&(ivYR&m!`d)hZWlL7F$V!Wjr{s;H1P+*`-Lw7z(2`aQ()t?vllK)Th+sS)_4m+&3H4((E9Wjg)Vy<>An%C69^c2E(FaORSe#whTTouCB+G|F zH(tW97S-G7Pz~1VDfPJZvhnYNHBMC3+v|WpCY~*DB*k$Z{9kk-pKP8hR04dF;x}2f zN}^+W^%G4x0G%QEy{x<9BPI7@B(EkUq6#AH>k6!3&Lmhm+sI0c+eaW!IcWn;0c2%i zTONJX2gJ+OV)nGB!@!^IBTC3B1L;7Jhx@RiWW(x(TZ*?P-I)VysqiHsW%sp8aSn;9 zQ&b5PQXGzvw$Qn%iuI%}HdA|JV%*R@+&drL)tsqD5kJ1cFKV#U!4x|~=t6KEPk3dM zz<6ylS$A+oJ+Ld3=W;SY7l6U>ji3#or+S;~|JK`H?QO60w$;4_B?w>uk^QT;`>A`l z$^P5~2JK>3G;x(iYjrtdzc%+1Z8DQ|c5glB>&^B{$GgA(@q~FKv48 zo@gO>e=&1`M_Xr7A%O8F!^ttm{f4cOyi>g^a#hwJv&!yaHuBLaEm+orHPGQzJPzZC zA=}E93d!i&$+!v-nS@tvBLAzIAnrOc=dYn?M$#z^A-GLsT+(?#II zB1kRUq)R1v&Q{KCCR)~?S`!%3n8!c1GLMh-^$+&7C;Ebd&R*+Rdb6Ls*^dW%Z)At3A{j*1~@l$OZE z-v|||NG_*OBKotI2-O0?-d?^B+vco<3phgw@xl#C1_l?|mTjz+Jjjv(Mp_>@#sCzZ zmZJ|h!5CidZtUx?=^MtHvjUqoj{k9===nLjX}mIeTbdQxT#nf*=JA&UVt^xN~V#1Vqbm(|p)7sB*%q z6g@jA)7zFeGZ!~C+l{>2UCpfH2m9N@{q3Rt_Go_=5n_Ja&Hfg8A-_BbSG2Rg^mkA2 z6^IwxA4C7$)r0Jf=0K2FH*-+v6F&X38FTmeb#!%CEvOPLGpk)|+UdBGVXiGe1zv!015&#*IXyDTW?3n=` zsuO6HXd+&Su+VjPb!(Fq$4FGnrn2EBHxUY zi?k43L3s=NJoI7yYH%tghTg*O4+a6zFNV0QhS)>l&?!UwrJ)`0aYKkPJGXO}lW36r z#Q^Z~)4~3W!FJ;&cFQI-`nLIKZ=i|Flo!w*i46~WB4PnP9VQM{r&FQuK_)~-QW7Mt z5v;n6LCB`|dhlNf*cE&7kf{isDs`zq-936;kD#a$Vkn^%kA#6eHiUtF9L61F*Ci#p zMCLuD*O!I>M-%{gV8cvTQ#MS6GpYS7oq$o03(XecDtM6SRn!qjrw!$bKzp(68caI1 z>um`hl`$mMMQ5ZC7ar7xl~f+KL6YheK7(trb3f@^f8-|z!E}@8wxO98cX=zLUD|4w zrtg6F>cJwy%VhoA+m7mI7x4`u1iaibK*xe}x{aFbF3j-pOMm8hX)Gf zL#n6oa(z5%TeiuU$}5$#KWiL#Biz}VR|BsyUMjDa;iXm+9bH;nvq#v>K(RB2x*L+s9hnJdcNw?*X7%dnQ%-*fgG|JDGW*ZHfa*rijF zl6^TW^Vu->+_2K~!#E?XH@?M}8RjxXoV?89?1CT@r1Kkx*@?e(PfoK-hx;3bCl3wt z59^129W3NHZ#e7V@nP{*MZ2&w10$it|c)U&NRRt3?EO{Px|_I zhPn5LVcF4a$97%#>a$_KFJH3k#C8KY^;V9Y+v7!&c`D6rp>4P*WOk=gi`8z15 zBLYYEE*AJ2!CpC1si-f6v;qychWGMKMY0S z{GoPSrMk|2TISaG^qkldHwH6BB|){7)*H>GN4A?}NVyugI&L+wE0-;2X;e}{MN%d3 zKp`W1y)48V$YYxqfh=dZ8D6!~MI#w1jLy9=#NL&yK_b^lL+zBI>|>0B_=&R>5z30% zDHnL*s-f`2gmAqef5$FF^%P^yvs z!jW$A$aRSF+EAh06(hNOJzc4kmDL;SRE5keE+-)mzE(;=7wqx`tCUp)v!Uep#dc(% zu|F5@mtmo|*TVe|>u+VWa1{0*Xg9uG+P~6j0(pc|T)mPx%`VQ~l2CbnaW)RyVPU&n*p3L>ZNhfv zupJe)K5VxQ+d|l;SD1V;{+EslOx=O$ucLXr1I_9ZwxVmkzPij>%P~E$`Y0{yE6X;i z8DBZUgHWkZn8ws3gm{I(fE@lPerGu1ioBPFeZtfDED3lkT^ZV$(nbXnWIJW!R5VGA z3qev}k9rn6rB61MoA8gl$T9}tiTJWThxgI?*#QUopU{B5x%MUTpjG~~6{mUT(! zQ{3qLlxD9zOD5xBRNnl*pRMnL>k>FfNsa4k_ldVojT%+s@gRkJY&?dF|DUVw9(_aq z7^I=toj;Q0wQ_`gI?TQU<0x74?Fe_wNdK=9for@vf~BB%Fd}3rgoa&QvMg41_8+mr zZByK>aALc4q?Y4thxl*DyOXE7V{^7pD@@oHCudLPjMs*Tl{wnoXKq=W{rxci*$98h zh-ylU-7*sY#0}x_r-yh-37(npXJtIw<8s*#EPh{>xy!=$PqxF=v572egV<;7{|>GE z@K9FWnnP{b2+}o`?1o5g$4qfwPUfbchGBg%nh{?-Lic}bv^$25A0x4}KRVJL8R;L+ zx&?#$?W6r2qwV9-#ZN|KI3s9lQ~$~D?1d?|dptUMa#R1*rWUrMhjuQixqVt${G!tC zq`D}y2Jn2#O?l7#!gi{l((LQhNmHiUPRSQo@xvZ*f1z(HK>_l(JOmb`5txIy%2XE$aIV-?3?!@lEI> zzb~P5MSEdWAhvXj{e7rClx4@gF}D28vHbSZB})cGGi2*A${R z5RM+S-ZQ>F(*J#=y$l2I1o7V$adY$!1QabEoEhgooNOOVW`JEYwedFqJU&H=z+Sut z#6ORhDKQKG4A!1E+A|j`B1c9b;RGQ^$agXfmAeiy0H=^KP>6^61(vDE4#RE3D9Rek z^JTLsr8J_H#n##kX3Ln9$GqOlwKTQ&rVWj&rO@AqY_CFJS3S5*ccA|sOk0GKWKm<(+mls&9%87}v z$+&{B31f7Ae6l&)Hk%4FpX9gnTi8~`X+;dvwoPGsznyjXpWv@V_u`5>ZM6&MYHYhD zv)J3q?c-)=_Vsh({Q-~8IM5E}_}sEX*&desx-`%ImObbGU-En2k1Q;JL4*^JwaSGO z6i*1{8&$@U%(C)REw{-0>6Sl3!S>YzHdB`I9P=b^Qljx(Q>*IpExAa)#3s2!@0a4q zG;DS86>G13wH23XcK(cwUE9hVf{@4|&7_2tTdjZ;pN@pU?)!B1a`P*+Ek3aE*^gKS zS%K&smON!t?v9^X``hO%`y$^kQ!ki*8MGi_B(SvA+7{p3_)7n>Ji()`^1V_XnO$C zrE9aMZrvEPugyI)X`RvWD8?B@hsT?{K7LplyK)@LE4i81*?`qe zwrZq1wI+n5Aj#m@h5u78% znB?9uad#^8KE0Wk^s^MZ1tR5k>^L#%$0iVq0!p#Fazeahw7Y(EXHX&G40_zr=$uqx zQpi#+?uepoSO0`Zq(=}BN5m2KN9xQ5TOHf7B&cJ8sH}3mhhTJZy~?7Fh7Bws4bfE- zDJr-?Lq2-6EzppU(uZ>=Y;0?I7hNn%I$-?KD0l2=ECIrU#71gpM7k>uDHo4r|HBUA z64K~aVvfN;>25rzu$@F+^2->|e5*PV0kgL@mFqTn?Zt8e7079!9q_wGeoHE(mja|y zH6%TfDifvXv5CZCuWGeFRQuCl6ZUc|XcqfT?1k}y^Gg%Cr&5R^5}@(vn+e1(PQ)s! zQI+h1XmYTHdQQ3t;q7eht%MZmdXvO;se}E zVF6Q18HK$dc2$$_!nUhS-0cJ8rp-WDWT)~$_b7{2Zl1YrqQ7AxOaIP^{+@~c%gt)< zm0WSlM9?DRo47}RN7{wx1wa&Uqtpy(AG%~C*qE0CLXN4b=v~?kYxOXmV>d4?+&mcM zh=Wd4dZRnW72Z@3n*P2xc=yf(WT9mf{FM{zDhxeBBw4zNP|4f% z<8A49nK3~wDNK5v8^?3P(LW%|d;>iph!Rzb*WcRw%WXD^q9pfE)ZDLZv(>}hd{Rp0 z*+=u34arBGpPZg^SP9-6FI>2(4d^~L&R&>jFU|A!kMrk@v$Mx>-}kj)AZ%m*`8d0Q z&+hSg?x}h9JbB%@RgZjcea4dqQn=Q9UrafQ4^+cn zQ7*-M4Tb_!6wyYl`|U_91_)Sr#mff^o=si%-4vV7ciW^S;3U&}uQ(DM9smaAVP2`Z zHZ?hu=1Vu*iy2ut+Y zF>IQH*LnWMd0>ruXC_a_-W6YAEB zu{{?6W6+Cxf|lD67FEDt!%7_h*lC-pX_o%qX(AXz)#kbN~PbO<4i#l#kt!(R{b0 zHj(Zd*QUhhEbQzFCd{>pZHx!s1l zKegK(?RI;+U4c((m@-+eYR9ZkZ7#cgLVVW*IQSzI;-@AAz&`k608OXrM|! z&Al=S7s_#yh}OazF-a@&hAFmkFbOh0vbz2;8p^KKwR8&b*F1eW%5E8D%fl$wx6t1- zDpCCgS~I!LNcifWsbU(Hf5AbFI#`EN*_#RDe(7`u_mx~&tTfAK5#5!XoZUFuMUyO7 z=l7G0CX4%{Dc{O~D2s*E0TxNlS#LECRB0?fIybhn)zeyEz?kyrn<+s!QdUxS`|{3$ zEb(@x>Ot$h9Q&2gaZ|y;d)vyvnRUfg?rY-+YwSO_j^husA#XuCSm^ykF@k$K?uE*M zs7Cg32&$6U%M)$z(nH8ovD&5wRb$sxaGZ6?QESb$Qa#p^fsJvKw8%`ibvU~)EL=F*A2rz)Oy<_kucC_5M0+%#;`$5yib(bOx9dMDQ}!)w?H=SZ=3s9HxE!@UHvdr()GAx zlAS(j1Jd~o5CvYn5aBBz$^+mY?1E@Qfk5gq$Y2Vc$!_fT&}yhdd;cCB^&xAZRoZl> zlPzogR@& zP*xV5vt>ra7!h2L1D8`sO~@TX-slqM0sZ1>liZql#>(EYVn}W!M+sSau;dW1*DGgf zJuDv-E!#44(il4hLBLL*Y-efEXO2-E8m7N*^TqiEf&@R#7f)m^>@QoCp4vhhg61m> zMq53sbO=$gQ1CnbE*$3@++b^h=)qE8XMQ_h|8a}rSzF)WO3Fer_31Y7o{B6ha_ zoYz4Lby5e=LiF%7o-C$By`F{WL{Hzp&Epd{2h3d@w~!kYSYxYr6hjapUfqCSR=Cb< zKqpqzf&WT9m!^@BuOne2*-+y+Fh--fVk=xfO@J410gDc3?ysC`*G?to^7^TE%~W^w zRJ&m+qbH7&(1`-VX)l!7c6xrT+7cXFu%-Wc3p;H~k{36n)(r#B=HfmW;cwl-F4)3f zJq?NQfh`G2$PpY6jy}4D@8$lsMa@fFaMeEGH)nE8c7WwuaQmZwKnHm;%cZ{A5)pxa zL?{u}g;NK$i)f0?sZ5AM3@!F6IVAgKmPr6me!x2FQV08?Komv^1)Cd9-hYJ-Z;(nw6nGXU(Vc8ocHCeLoZrUp9D_=YDWRL7gHXF zqJah1KUS5X0L#j@*tS*C^Nv(!7Zz02msUEmdQx4Uh^FaT;b&}Lhr55bHKCxVZ)xXj zDXPjpO*sx^N1M+O;X2qYn9dwL_0YN?03- zTE3Aen2lC!M?wWtwGC+h%yj=3B1~n<27sbOTmJ}gT{IOlx@NJ0B5az~b%eG2KL{&Q zK(CE%osUl5E>E&AYfM^)ZW!CuKX$|Sr+7j@2^nk^oJf=12#hJ_Y<96y@i2C?Z3aViik{tGVM@5Tsrs|6F!`SC+5LQ5*5%@Dnb4EA1)bmDmchL@UFdw% z_Rxd7KpKVIp0YhLkOSF&xzYWt(O&L6{MiBS`0lo_yF0dfC_XIrMWyTK>J!zP3gV=T z$N2%Wb_v07(`~d0JmV?V6zGqKt5S$Z?7nN1^1GOuhsVki(Nxvxv(B+!Lf~JUYn~r$ zblxU)g{8Ar)Jj$3JQAWvsG?m|OGU8)Ha-q#rDm~i(K$b1S${s=uJctZB2|v>E!!UFjk8O)*%(R0N8HVcU9^oYOd(RdWs+FX+tclD(>Gv24{T#c zZzrgKHJ7V=3?{f+e`0t21du+o9aFGiJ8t@(rr@dV!h-H^_?8alTpR*~yq-RFl1tC(1*&@=DD9{#SSH z07IwBUJ|X|0c{~Bot*+;Z|y)J9{3KqgdK-ALUbi2SY3MMoo>0ISLT;vXzfq|k&r6~ zf>a>WNUIze-pR9O<*3Y6NS|+H&K9}S=ayYx9_mC)W$Qq@8!FIJH%w$88zkHzJELoK z%uf}-7(czWe{pMjVQVhP`+M8lhueoa1C}TuXLoe!FN-?>iEnq|`)Gz<^ z410ElecS=YJU@eP-`k;d+E1-t`SZ8&teOkqBrKr|w()lp3Y@U+1DjG8Oc~7yvIJ{4 zb61#~?irq{x^Zs6GIoFYRH9B^4m9Y*bzrk{F_2UA7xKE5cpy zgY}@Pc@L}+l~sL8O<~Gfzeji>Y<{2&Nmj~Mc2(=TTw!_6R^$!}Temz1+Ag%{qaBr) z1InVP0UQKYJSVDEkMx*l;GqS&z$|4~bDLC>l+N6dUlUd*hjyz1i;g!S73UKc(9+IPVlc5y%FF2^BDz=LC9enysc#bJnYCZ=d1B;C8ATyf7?Yl}39muX z!kxkUo_78Y8tnr+`6Vsxk`_z1M7r*zx?;!u*8bRHXYg>Hu)R_d@$TLhdmub;LTvns z7DwFtiuCinEjaM6()oh-pIhwy9qiE^a=8pGGBV|Hw`A*h0A{vwtk_+Zltp|243)JM zN)oXySxFFTs5GFcP?caKc>wZ3U6MR?NwUg333cPz%i5CY#hno=Rhv}u40YL}dpMwf z_<_`OjbZ?4l{mNi8cC@2Jo#Gu`XO8^M92Ir$QYozJ>1;+D1T{o=k&2N``B51{wwPI zsgGUI$4>8Ki+=EXiY@Ob)OmmB_%bdFg>jiHo8zTx_euCV`=^?WS9kVJVPyeAo2gZ` zX*@8kT4?~8Brn7fhpL3ja)7ly}yfA5Gy-Q zreUq$TJiNZHGq<~B}%GLDT+9n|8q@`H0$0;uWa6YuXvG3Fo4DmMt}CIvUBNmOUZu?V5mfJ*ZZn{*J%hK~2>8$=uO)PE0T^s6%QQq-ZS_9=q7FxEI+|mD*40?72Gse4R+KPAsSF!RvL!Rds%K zou0ssB4Zrf>Fp-Unj=DEgx{bW?Xj#s<=a~Lm5TU!BvrVGyhTiUnhd%)wNuxJfa`{^ zQXG_++@sF@nEk0aJV}VE1EMc>=i0Y*Ppp#t9Rp(`(q&B zs6Dw$@tIu&A^v99x@0I5B6xUu(Mv6#f|ZK~34UazHdsXBG5&Z-iI|A!M+=n+e*x5h z9OEwM6WsLuQQ<~X)>fUNR#&lRK(tHu=p#5Jw*WW!4u3DWpJRbZ7@IXX&{`C{vV+PG z>aLYdv7F(>i#}niy1CTRx$%PN#68epOnsry>;ONlV|I zpDGr~uiV_GP?Rs8E1nRy_-2oNCjVbN5DlH*ZEWRh3{N;OY+JYnxI%t@S-_T*ho@Gx zbl(TR9^OB`SK`<9ou3uV-#@x&W)8{?Hh#Ptg4+$d`Kx!cdv;@~bBKR51VRG7a#!Gf z^bgo(KHAYf-@|>i2mK#M*>y8*{!IJ=8M|DNt~1EP`BQ(lXUFFb;t7LA+6d~GxhK+`d72ux3^Pgy3=O53(!WX<^@3) zN`WPCJD0tDroUpQ{b?q6mA7kVa`Yd&+FQF4r1r1fdB`QZ`}23VXLiSA^^xiKBl&T` zuHGfSb{CNRlHGpfVORc4h~AtuBaXEOv!pQkggv>47t^RQge%+l71B;yti$JrK^KsjNNIs4*s=_O#D>vQ(H% zj~N(^U;VX-CvLQ7!?}+&CeM+;4tV0EpS_`bF5JV8-$O&@hErnVl|v-%345%)=arPZ z48I*?1pIwb?N6m$CRtLWL67;h{*qc75S&`ybDA>(hpodN(Uq=JoB03DRA9KRa0BpS zbjOI7>4&kQKS2Ss4X&?74}}d81fEf?_Tw{)i4HtzANw%tR!y*zYwXYc?VjX4%!%b}2`ia9wM=m3OVe zQ|24hKBkEhRMs=@5tH^6w{XcO-^cKpsbIuL*B;z3g71eCQZO_gwb6Iu8iS5qP60HV z>?YMtE}LXi-IUs?WmBp!z4~cgrTXMT6S zr!5e#oo%P=ELPWEnSE@w=Tp3MVJD83GL=2_&MVoea(~WOrP&(Wsdx})|I+b|p9+A? z+7k5@qrM6SZn8f>5DU>i_u^r0-;-td*-ZbzO#5giKhZLL54!Y&r0a%VLO0zE{^_{#)Br%`!8Y*y2rKyLqO&ZKj<$)6SU*Cc|lHEll`RO2im9mb=IH7T==Niik4-_;95n z<}Fy=TIWw@Bc79w-rhTN>nvdK;hz3qd)m8u3I<%`|7QLO46gZk2%DsICGi3qxqc{A zDS=m$4821^Rl`;cFf`%AY}k4+Ce{vxDNTyL+Z)Om``2dL+cWhTNf`z?@m+rDYkH79 z8d99P5YdbcgDFLBkf)ANQynQ}PMDZ&w&V^#Td`}U$_xBE6 z49d`WwD5EXpGJsjj#k@mRvG>g_aEEFaHK43hpiGM6?UZ@`KG{1n=rsg0hXeY=GLaF zaJoMvoRf=I%+8cDkIeEf&9Z7y&G?;J{@qy+0@aDVyAa<~i0>}gicn7d_a0x`dfRl*Gr+B z^R4kn+XsJk+2GUWwslo10v8PmRPp^d>$L}byh*O2=Lgt%V28iJm zA$+;Sb;@NW4r9<2yF*mv4stTu6NGycM-hrpO}r!_u@IlC0v)oL0!Hgo|OBk|g&&*6Di?gJqHFWlzseeK457|2<3xWG9oxRn}` z&PA%Rvjc!u?@LeeRO!xA3xAzi=YFtB6y$D#vQi^7{wEYKVKMcDh?eaJ1I1(q-^R{Y zsh&p#J|XrXBXXqP!!)aVqv)h&=)UFs`DTC(VmsJ|xuJOs(SBsSDcdpN$v6y&<1#qP z`P`P9T%1%f6{GeRrLFw5@@=`^)|khg{|Sa%=YJaS%mbL;3C4*2sm7t9Ga^)J>?)by zQjH)DaPC=wcUdI}0uCwLdSLX#ez@1OcIg~V#x?sX*fl3pWH~g0CC^e)!Axil%VMN0 zH8Mo1X}U%sf_*fPAMOW{MZ0t#me~ho+4sxr9duoHj$-%iEa(1Vc)N!%dr$Ar)!o%| z?D>7|>+sY2_K$BU3ll1V@r^ROasSLs`&TZ+Nb9I(qM!DloY_^*eQCI$-9yMz78B@{ z0PE|&pP!}NPYTok*Mez7wKqChi+NQ)Ns0D|u$r?->3UIP2fG{AOH8MlI35YI6Bo+z z@*nuFkRIp?3%WaO5UGcfB#k2Y^KFnQPpnP$zi!ae)|%!M*5~%oJqO@WlPdyxXRqzs zX)p&J3x1pcv1ASgo$4e9ZNv_unD$&%E}!ca`ph?CaM)B8b}~eS4~Sb82f#XzED!Q+ zA4XJrBsC~0yqeMuwzbicUr?AMac>-8Zyn(Np{;=CWtJ9g%#du$H5E9(k$g-Cfbu{z z)%Zsb*`Y^Ej->3Q0)%2z#*K3+<*ciZ9{5GxC4bx>Dd4I7D2s9be)iaY0=~ou2C1b= zd?`ru_`W|P6+FD9J=VYv9#I(~N*}#@fTkd!>SgC>f2Q^dbrg%h@1Nx;ZHjn??c^O6 zRnUey*BeR?6x5RzU#irKm~TCa6Zzs7$jq_}gqBVaDfDh6f+kEg+=m2aEL1Q?@X7bQdN&ncab*_B*uX3pacCC2eMEycJBdN ztBHT~KzDm}a!0j$E3tSsx9Nb}mZ30rF-i8*|YPE%Dhpd)+_t%nNc#t3B`vG+v zgZtZU`zxYDmjTbi`=s!mE#SF0^8o$e<%9g45}%}lc>5B;L`Grw-&i*m%Vh=S_A&L_ z@VJID0%S$VhGYI4h%Wvzr5|xY%G1%c9OE(Kg0I*bLd#(5Dr%9(Dyty4cczBVitg5U zJgytWg&;&1nAn7J*0&tY=y2OAoeU$0$OTo(nEn;E({jVZmMj;s>>3DT8jl^QCd-h+d|Y08J(v?qUYg>9Z7i!JO;R< zpyluraZEk+8pU7Il{;N4H%LcWWnQFZF;YZA$OxnNzjpIFkE|0X^~1<{qA7l}_bM5x<@bl-!kN*$+JTC)W%6nf>&CS1s-z}L-6YeVebNKMd!ComEB2|g)C~PUX4oUH8 z5QW@U3a%Cz$Cwt%>v5zi?gDQNr7`6y;Rs49H%KGio$st@D6Jy&WpJ4Ds`g~kQyGlB z@d59&P`y55CCUSAaVV-l!WAQv-{`zm2>T=gY3kE z?1Y1a9M8-p?DXH^DQ}xg9tOM;RDW%*e|>JIjyW%PKhe~uE|ZGXZi8e@kOEi_PcBnn zvM1D$ZC#N9j%Cq(zk>FGJRA}7>Z#c&*3c!o;IIsFCYl%q;4Z=IYl}jY$bTH_?sM@K zzXIC(A)#_R9IgAuzf1SYLH(`&!(2OUuAMs9Et+c&9a{RzyQdDeCl5yVv{tu`+riyU z7vY1k>mJFdNdL55(`r?2zqG;<9Ga;-EQ)^r)C72xN~`ETa3^4u&mN-!3d+Ymi}7_=BmS@@L&8 zJXHBL7GKi&$y`y;u&)m-eRe1cW1PL5@qj|+&J&R>k9UC&u(QvHdt*xi=Kv1|Xb>^Z zbqVo=3Yk?)Hl`rM6y#L!TS`cIbjsn-FAd?6-x-|>_cA!hD^Mlm;Y&g)yOp^n_o3NY?C!5ZaGP}3?yAf>NEwVCVF%p z!9N8f^u(U7Nz=0$X3rOS=4Kz3w~|KcsmNF2V78C zvCiFjsNHj@eFzCGg5veniF&+Ok_v)jTR|If^YR#L?S5gq z&+-^Tml_tR`-4EA#X?!&RZKqx1Hsg3ZmVQk{Z>`e8n>#R7H?II&iXxzRWcZ^T4W?x zBT)F@2>av+|M3y_)e+efes5>|o|zf!hp>(9{NLLJzX$3q0d@NF3*wW1Z|D3TB^dL_ z@7+beS4>g`9#Gnj|MdtCzI+7M$5)QvL~=RIxNPjs3 z4M6t(rANd~>zWQT$Nc|@Q7DF%KTKO$8hukbl-Cem{ez{bLiGS;?QY=XL!lM%+xGG@ zPntdcX+__1&k%vD8BsFVUg* zP2_j*>)&P=vyHU%2hFWibCIXb;nomq7A7-!$RU)Q^cCn2U70!=!+4Ws$zKR-z^t3^ zt(y#5cauCfX^+kUm$=NIT)4@#7|YCkWr*V^ICmmHR#+1^C?eA_0qEJfS-EdQo}tqu z)Q*%$5xtFXMj$u(MO#oWu0_2-%NQKCBUU;5@>YAPxus^88ag}m1=!}p)`e|)eEn@j zIQ~F-D8AH+OD(hZR4$aP-RDB-w}Ya85<3K?BG$@X7g|IL=gR%|#M&udzKIK+-Gj%MYUTacohJ)Eo;-(+??(Z!D`CLiUFVJI2Fh_cW^%1)s5Jpt z!XREkI^1yE*_NIfzZ6z+?rU$~2`j46>ha&&n(&zfbS}bS&eyLf1z{(}c1rA@^vR3f z&WuUzdEMJ<-rkfkDTYXkCJ?8;D7Mq%IBc_QDObo8<@^-kUZ77K;WsDU>(M?MzQ#m*uNGg$Rb=;6D**Fp5607azvCcbwVEC(W*FwWo&Lz1;4h zYCDFa564!!Wqx0AQ9+Nj3eTWz6N(iNfHuUjlprVNFy@#^qw*c40uVN9ra|d@@T%qI z=}3YL&LPR7d*oyu!69Tlvpwnp`Hpd>XT%w>e-ZPV35buAC~he>yJQn^GaGOdOa!qi zqZLdn?LFOH;$Y``cL5_o8AwbUC37DSUE3pOHzykz~O=cV!&?c><~sabg~F1`un zW0G^eiNz@d@kZ#pBKNThh84tBib0}WY&|ZPUxA=@w)wE#HEj0^TfR-W%O)7-a_eFK z`U_^=J`Zu_x)1A?$F9+7sywbzXg4NyOXAK{TLFJ`V#g)!+{DgH+_8x*Oso+yl|$j) z5q$a}OS3#%mDmM|J(+-#7bSLKVv7^|I{}MfjlD|2H@ER>8tZuW98mZ?sXRt?hfbnL z0?Vb)qcB41IY$tyVM2%=P@_V8wd5TN-DKR2^xY1X)>#vPi7CtD%Pra6nS>^RJu3#r z>MW^chNO<8*Souu|BAz(h-)CpL9P^|)2X}U;q}5c{tXN>qOVKpAFg#enOYJjSr5)}(LmT1=p-?L^_!VpmtQJ;~EjEmzx&RdNX(yo#Wahg}w} zVVXbARsP#DAKB2(PZd}%Z!mYgWp6Os$7bubbF@9^@_HzM-y0^LOUN=_X_*HZ17s^h zdNrIPp{Wnp%*22#Z#oH|c&G@15dkUqzQDG-qZVXL!~^|p>`>dU3^QhhC)g>y%n9a> z0Tsd}DO|&BfEjh@*jg}&La>>H0@fMEA6<%DzVBMSyPPS-?2&I_mHUbc^(!Lk6ha$f zwMM9T7x8;48rVLpEyu$bsM0emy3J^CWOjtZZd!|^i7}ZfU#dqbQVPW;XQUkMTz4HP zbp-L=kJKk;!?@V2{)n;PL$d68vP_|JR$$u+&n%=5C{2*S4g+^dk!m~sGy*=b67g{% zDcb>7cNuonwU63Q9;!mf&*-!)l!fBH6c-7;si5|B!+5mb%{ySjH+c|hXh71q%X?x_TGelBT~>-tx+d92)Q_IWWhQCIDA1)PadOdPu75=P3tz}) zgP6;22~Dj8*eb@9jV`cU6?l4w!;^`z(zfCqbIdfqYawbUdns!l2E?q6G3M*e_JG&J zBAIXvOmc{Y>_|od1GUKDR-sE9T9J$A+gORmj?P6FSay!zH=Yyk>*lz9yYL?Kxrz}2 z&lh9z?SEs>#Rj_oUmr^-#rm{Vo9AQq7s9owAz6p{3lpGy8{fGbfjfqyJd3;=!bmrV z04M}1VzsUUi%UmNz1_)5ZrYZFZ+dM0bI1gY<Vumy28X4pn*f(FnmJcg76mfMzt7qGSPK&o9f}WU7;G{goB%cAXNdl{Q%Up0c{%9 z1r|Ne;K2)if@R0iS(0}q4y~KSMeS2qzw&(6H#*OjfF`BO97X zv~-=iOEY#^hM>$Y37TY?%VMuZ6`|A^U>nFeJtUuGFL!60kuu1P0T_yma6`_?1ffy4 zt`U-K&1&C~%mc=k%pl5#cdp5BGe6~1xI>_Qq=DsZS$6Fi)MC<9PIS%CK5m41PN0GB z2VC`FvAqa%B>OhFR#;>`_2YWW2$^F&|D8#_+n(QEg%|Ivu>0fsJA2t5skOusU|8c> z%)QHd`KY!O-2mwU=Fxqf?=RMh_~#?^(c1E80X;O&r~}v4iqGhJ?xSbAYV6?;wLT(n zQ3%rugt^OQDF!F2>wy)=x$5W?mYdNZ`_o+7HSfV(g&@}A$1u#^sR#>d2_W~KeH$MJ&aYB59mA9NzAp57LDJJq9M z=;+~m|3@(a-;1fw_y zE(=2}s^RAgS$h%FGECv`S^Or7-RK8>tkp`wr9}_`ZcQ>FDF9jqGD()xs1wP2umJtB zau8rJ9wN0>2B~d3RY!P{^^v&Jbhovio@_liqV*HZxEArM(W?zq5Au_AJ>ucOZDLM2 z8{v!PU2Luk{R1>dvF#y*%etNuWc95lR93tPt^FK&}4YF%BA4Z4x}0;sAI z6}LAtHy+jL&|cg?#r`X!-80pltsQ7C?S`U^Yx$#oo$4u4gYJsL*$bHRDM)2*Al%}q zZq${*B!(?41hNO6tPinjho(g&LV9=;DGebg&&^DR5~ciW>xz+Qnj4mn<|7We|Fy&H zaNEt@1p+&}S^gPGtl9I@2TxiyNVzMPP1f%!g3IVZTb=dJoVm7wKB>pre( zv&2o`BR4a%NBPVOJpqxB=u3Q;zE_2SWJ?u0Jw6MO!52 z#wewBeo}$Lc|KePi&*Sv8^;5KdI2gi68=ZAI|?LsnVL)-Wx1bdyCf}TI*qf1 zskSn2SLWR{c@Ai`!8q6knh{RPXK~262MbjzsqX2X4yR#KE3zPJGLI3TOfty%-kQWl z7+D{Dx)ae9(&fwbH7XE2`6Oue_MpSAyBy9^=}F`*el|VI zx>+^MOX{q3@m;s|+P2w0j_S%5Bvqbq8PHgX*? z4uNrjJJy_l12i~pE~LZOj!} z?a%MNC2Y@?TPkzJ2E`txzznJwLb&R z5|suX^?mnUn^B+ZX4yU7c40x~MpH+)!R3`n8%b}!?t8$ZZ?i&+(8o%F)Q1x6HH#;^ zBhcfyT?l@>+KS#vM|uBRN@=~=ca%4$-WmugcYn)JYDLS_#VgF#&A-ytB(~;#{=WaV z_1|^tzB|yq@IAZiE4Kdg?y^Jeg)iv%BDj7jJCB_%)E(AxR^F}}Y{?`5MnQu^AzyvG z#-O2djS+xlQlHzIw^E$6tJ1j*>&7%v8tO)Lqn#@aZ<314`T#)Z`Zyk0nc&CAlZuHb z8C^Dh@69?v7d2jWUS;6CbBax^@v8GGP1zP%&o-Y@8!@gqHmP>cZhwa*_PbezY#>;zYi7P3b1pA1a5l0~2xc`iPR;f@cw`;$1|g1Rn@ z)AM70XYgXXy}d!x25ETf_JCFHH$!Cw^R*ga1>uppEK$6f@Pn5%0#Y{~nEMXcub`k= zU~n!VT0s-ofjX}WQ^7QXbDc6R6IiU&mGC#=Sf#S8qn5PFBsprSzJqnQx)MO9t;QY2 za`xM2!|b|ETGClmnCUU+Nx@}sj>~*1!u;~d?d8_kP|Ynqe?7-Le*&t8exF|C zD%d(MaVWSeBcUjTM}2v!gSsE>?EwdAM1@U9zhCQkA<^p)qg^J>9;`8XfiE8F8J#mug*SWDHE(mp?XLSa z@gpZ$6K`d7Ut32TKz+dN7eV&_0cbGufy^LT2_Xxc!0mk9!P_#rgJr(ZKPpf2Cl@SM zjezXIV?Hu^#s)p4qo7Fumb$p4w&7mF=b--7AFHwiLgav8du_ORc=pj!@o8?LR#l}} zVI~QRBAvn|ENJv6YQGF}iw~IC#U_GmH_tlB(mLDDlC8rAKU^YrpPCtmV=;5pFz$NT zY=?!J*fY9V?g;!31K!>C2&x}a@Ujrv!irVC2}dbMYIX1C*ltkxfuU>QgF_~US5Pl{ z*H+nRTzjV91Vl7>6pI5l6F~))B@U3-Ac`YUl+P=h5HutvU{L|0Ae2$clQdS+0L9|m zf3gCfka`SjJ3n>FVVH^bWN@>09S~I>b^AqYGaP8x_?lWC7g~kn-qcoGSjm{m^CWir#VE?gT=lJJDQ>3dO7P+=dXf>eLy?WF7@qXfj!eeT zj!wqW_9YW&$0n0#$0wW7PE4lKPEMxNPD!?)otDg?-8{hxSZtYWL%UV79qrb5ceaURvoswN?XC-qjD=Kk;ys|s^auXU3XeprOFW4nyQ;^VbB%)cN5nGX? zFHMRHZBbAXk-{~dw9R4&ZC5dZc4*N{JF*x@+gD7Y9bZhPom^}|JFS>WyJfK*?bgMP zwA&Z6X?H5-(C$*~MZ0^k5AFMleQ7^X>__|I;y~K{i%-xVRLrM6xcDsXr;7ii{ao=y z+Rqn<(SE5|K>O9=+qB;(zDN6=Vj)}0{--!fZ%0^hjKxO>DR2G^JZ-MP0w(<-LaV3( zFCvD7NqB+%cvYhBrxRdh{1*_js;Fj$*g<}_O}8)kozbGl?tl2SYs>|oaGTlDW+w+# z`X_A$q0jC>+re**L^uZKD+q?pkt!O6s2Fax5o|4fEwp7)=4j*Lmxl_x?NUw7D^LT% zbA^7YKZE^bhr9Kwd=}f21^)`#m0y z1f@itViIMmiO^YD9j0uh9ZDPRHy$BtvlCnz7#7%;A<-X%ZDQ2qAWa4ld}0%NPY&Mo zLd(vcisKpJ--jvUiOz(bl~#duV1D>kf%?G*rVP@GF-YtOS3`eM z3nrAL_#W2@dlp8~*(j?uX zcoO?XU0EGBI7fMKcUuAo)PfXOw!6V{X~}=L`h5q*KO3jbxbZ~n`@%Nk$+nMnnDX{a zZX4~IcrQ#LW(CD3h=NzY;yvZVKEF2Lk-<^>l@oBa1-@*eIw2 z%h7(a&uA|^f_ZCc>W@tQ2l@190zQx~SbdFIi-qnZZyD|I(2fjk9$F%SINufRH~mp- zJYu~}Vf?7oAFrp>^p#f(FqR4A$^rc1!BN>M=>FQ`@7InTTd|iamgH zI+s6PgZ?~Kv!}(s>EAOo{~g}|<^CdV(BasQP2GF^{2cK!>~ww+`yXXtR40?!!T0zV zOYYc`9aq9>U6UeJ{0v^RxG;9(1(;NpVGB(MjKUt4f=_HfR)}#dfmJIFEGAqZMCyTW zTeV?fzIrkRNsJ&5E{sCQ;eap}nNhW9RqlVB`!jMTo=p;fSd$F2;Ecl*Hp4`28`>R0 zTgI9e?e__mZQJPST&PNOfDMD0Z1XnI;$>Bx)P(W}O_%E{SKd@b-QEo&Lf8o)a@>1g zbyhVdl#*%i_CrS86Ms|MGd)Qu9mcwGm~#P#bR7CsHe?INuP54XYIK!ov=)=))I;9{%Fc2h_50@p!Nwyy*YN8dOydHbE|8WV2&S zZhzZQqN>vKr2w`|snM=;ZRPNkRW{ccXE|Hb;p!n*K`YQ^$JC5CTLB(B+ z4_V=UE4P}fEA~bix62`RAA#o;2&kV-wZ`j8?#7a>D7jT7JEa`PeJ9K>oju%sQes&L zaqpUfO}v)*#MNIM>O&R(+X|Dn%DWppnk0R5L&dMGq|X%YiNda}wEJUE6#0tWJ(pm{ z03MkHTx&p%ljHA^D0edi2(jE3TAA;uAndMD5Z9b2I>Y}=Dv*PC6dX}0Y;njrpUPF) zm+RJ9ZqVBox9-g83L=F7N7gF@+z_4Nu-P+ZP%zc;62+4)h1aA9Dv4pCHwW@{oQ~cd<`l z?ra4^vpZcjgN1JBTSz+lZGo#%8`<};FCMXRp|vLr5g$STobMF`T|50(Tl2|cFb0X;g3V_KRam+46 zP~MZ7#I4ajj40ZPfXUf1r=;w-288n~JMR}z@n{R7IMJS@2bdq%Kr4WbN0ALXfrhAX zjaenyZ(}Ag*0Qa)hr8mKit3ar25rf%P_xSlK+%3x_^S%Lvhddzj48Gigt6*$)A1XD!o)mwO+gr`#WL?IVH=-eNdtQLfz;UJ4o5 zZyMM^_-c5xrVa|Oq5Q0~1@UebR^dsbN!rS;&o8O_{p~1)Fr+9Y@PYSVk$ZX62PaT# zr?LswZ)T!TRiCGcndG)SbeBW2Xuo};o0~|&Nrl<$aUtz<;l`6@itj3W#d~%}&oz+; zbGxJ#U#rFI-slR|vqdN7>`1etcn|-OM0C?m2zc<6 zpv!!f>CybOCf)oQyT;F>O+q^3e#)jJ{M0;c7mJGV7>h^fb7Yd z*r3a}6m(G`NKRbhm=yoIuqhjaLe^l-l4{&&;LdV7uD?5i&j3)V^mZA9+Kg{{{$=*2 z^?k(-8%ACSmR2k(YO7p1uxzQba~*a{LfR6HZy0Ju^9h0jG{uHu$F|Gj_#hjJ%}{|C zRVlB)t{7kB0wiTijE;N=3m=k8^np#P2sdpfm9msbBM&drjqRdW`!MGwm5$O8W{a%cw6C~DRz(XJH)YkD*g;X0=$edm zsraAc+@OWQeK>4J#!Bx)e|#2T1fk-%>6617o;W|=tPN%xOA)2euSmnFO)OU ztk9)S%w}O7;GL5ZKr5QEDEjo86#eI(^tK}XpDYSIm=s;inm`lVEZx;E+f5UYt4VQ! z(g2b$0P@~_g>mpUFo4=u++m#^Nwbhv3a@8(QwALf11Ruzc+l*@q_{Y<^EH6;GWYY$ z&W7l)rIB=)L12&l&J2M8d}L)(Rp$X+4@>iT%mQx&j4N3MN|Io>yZJmO06f~xP>-NqZvz>Fw zKZM5;6C?Hj%_~NKbl+#{Gjq*SH{kc{aaD%vr(!DI*~xA&BC+?Kd$#iJo)!!87|`XYQUXy#+ZR zTd=!LDs1{I6x}O7C0LRIO;8p;koiXlLyIQjjT!dg2-2x-8Ab0uai8qmy~Mp}{BSGi z2>H)F6cNZ|F*R7IFh3MyKwxdZzv-G|r#WumY~d)Jfr2q9ZHKv_(G^`z_W9B&v7H(_ z%E6u{3i@+zX9|?1cHB*h{}Y&csY00b7Vb!z!i4Y;CT^=Y=F$>x4Mg#L-B&cVkmA1I zwhUMUb4DJ`>l%1AZb3;kJi8-$JWfNzU5xj^^4MMLaA=BeB?qx+Ahsx;ui#~Oxd0YM z*J=NEfv+)ZhfcJoXPY9-k5TQjc~9;pXgLs>ve(V_${hE#@H z03=a8XO&Pb=h8>0+)HV{pd07O1cPu**`D;a!INNOG$gZu2NPN3L^~NUc$OA#U6kAE0RgGNX)46sgrkd-#-RRxb-eVb0?Px+T%paGguXo#PgqOp_ zbM!KKf85pb=g@A_kRS?uQ8AG~J*PK+_} z0t~?0I)L#1%(Zj_UW###+mmU*T*Q_mvuJ$3*StP}-py7d?)KC^qiB;wfDWA0y4W^| z%@UlUd2`PG6x(xr@(a+-Y|9QTY!*(5WmpgK_jpS&{wNyfb_ZAV`OPSwjyxzQzOtW* zT>a%3^!g)P?6x~`ZyaW@)D7iH+%KS4?9NPx*9!}sSG{#8cwFy5V+kvaD;y}Kl_Mr= zqv;PgNVAcndO7ZkC&2UKho0l8QQFKV<88{Se=DrF3+;!N!063uva(unt!(*SE1UII zHsX~sS7iYvDxoa_4;5am_UhA;M}c)0&gqEy@dcyLX6(1IzbCd| z3y_B~W6UNb=`M6e|K~!TWwwjo9rfYeUKs2T%ux0NZ&#-S=<6D}a^}e!q_c7E{v>{Y zaEMfJwVHg?GLJo$*y9OE`i@v2b!v_`8CqNFuV!u+HwM1qIPdPz6aojDf4Ep2XX1Mj z!NS8rT*L>l`}pOa0tAc)&!qOcj8!kE_KG%^%5$~sBO8O)yxqfc$O)Me`IPoQ1Z7@a=dg^gJpU7CA}M#`7X! zbv`jV6DKdI8gf4TLPaH}AH*R||2T(&zhtKYj9D$7jFpynUKssb!nxdEnk5%y-7eW4 z^iLmw;?Fox@gD3%{H)@dXt9#X%Cs_`7m4wbiRzHsg?V1++Dg-%l|*UYx9C`B^ah<_ zZ7cB|l;A>*i!v7;29AGcoP+65j{S&TkZ0JPwBg(#WTrx;t`PuO^q80bfv5&YXBY4l zsHihUN<>}8xj42{geR`Us|z;dG%e3aLOm;PpHLQu?^?-3Sc*N=bGi;W+ku|Dbck>p zysi-Jyh+h%6|C6wb+t2S=AD3=n5{K}B+QcG?*387vI9Kon8a|@`UDX~^JYf(R_yos z^&eb}-Ft*+@1OL0WvBnEpnBk#_}=F^j$f3NraQ59+5$cM9< z9KGm9d`fPMa|pmQ;_Qr1Gi<$n9lJ}aw!CUT&&AfhRQQiW&PT=>sIY~@DIY}Y;Y|E) za(EXIu=HX%WSL~e&3DTUMjoArkXFYQ*%kC=l#6xQ!uVos?uGF-ZxKT_Z|E8;{b{f? z(1_u&hr zTvX*bpzf8(bmDKe_`V8evR8>N@?&yTL;i*Y{_&i|&gC{*^K)RA6pY16;Y@;G^iQkF z&(K+esva!c+OkdO=D&09G{YI!9_CU0AuZ@yW5f8fm?OJ5bFZcLMw+9D$cw+x6;(_= z0KCwSoeR5$`Pn^PEJ|46aW%!l{TG9tu8-Hcb@)8b zD>1jD_SskyZEqYp-tqb#bI0AXoqXjyf}KCiFF{L+1&%)7V|1+y2?+TKhq^Y#d7j&2eeQ!;v>Z1YbLYbcKAm+ zlDj)#u}9H=-oPmAd|Xv^pWV5S=j8@oty#cV9^l4CG}+UrqR5C7@09GkIlHi*tji~hEOUG%%MdtTp&pRnYA@l=Z|c-m-hrmll!Ksf*l zRYzT3jxRO*{^EMb`9GJ?8s;jE4<+$-zocoXUzjlH5B6JLnny`S;^0x0klQGk4*6Cv z`g+?}5rXrIz+;s|+aY{hXImjA4d)CTdvqgZ>F@^q4K)e8z1m`Wo%>;jxR=v9lY!@P zcg}(+kMFcM2_t9IUe@Wa=(JyT*cI4fBv}(-Q%Jt&?tj$1{N0jl^JG0ND?uj7~&S{fm@gs+P2YkFXi-q!FtX&zto1E zZ}C#Ao^NpurtZnz68@hKHN zh3KZeUUsjQ9qm8LT4^B!Hd1H$>w5Z3ogC~>*VE@cxiZJOWH(@NsN0f=5%D@DynxV) zBqQ1$MsgV}3Ouv{My_!F&LO6~DUd zR+JME7L0}L3k`IMqwR!d4BP$BntN7m68eYZJZO*VQ%&CuiWdcYwwXLd9w>o9ui(sI z!^QT)hMR2HG>{3dZn(?e{qwt&1`WvzcI?9N^A#04z5O%deHJ$SKkM!dYy|XflV^&c z05|b1-d6dnqjal+Jl*DQc`doNHUYl5`f7H1@)Z%|&!%X%@3>WhA zFkFbdsJi!ZBw^tXS<1s2Cf(%WXcaiVH?r~&sTk2HDR`#jR9r}Q_ihO+KqP6PNF{)$ z6RwhoxX?mZBTI6FbIp)8khyS`3htEF8r_|AO(G(GG?vf_Ff8O{i04lYO68Pg?*5%6 zINANq`~!k`cvJudKWX=x=>I|wA%EF<1RS7BtIxCz-JAVBgL-gY2VJhhl1*j@=~&^%aE zh)tMvXcx#cQP78wLTq1@v0!^hEh;l{V;MP8(L}ik(((T1n$3`Dr*hd|^sL`1@)g~H z8T=?lQYb(~Hz946)}qRoIg`h-im$ZV3huDM^+qR(hN_^Ru1$eSWQw{rlANyVu{xN!tEsvsj+R@W*xf_?FI5y^$qW$0C$kYL>*f>U>XVZ}EFw&u+**@6;Zgg`VrVmCYX)lGNJS;v0$FgCl9W}14q8%x@B{@9dzEh*8Wh%>|?k*mhU_^pT&(jX9CNK$b ztPG<~kEu!oDACn1Pwd5Q3%*eyOp%{{+i{7EeTnpP8(d1JH@AIqgc&OM9gOt+;ho&R zcq8vn=JSnK0s_+L^dx;gwijaeJdO(k>i7+cRz@5DL~IWUFA}4CUuOR>RK-=ouh zpu=UlU&5&iXi4o9(r6BJcVS{5Q+MBp?Pbm5U*GQNu?dMNuy5m3MrYj{h+Ryox+=3T zha`9OJE*t2T^_SJtMBSBHfTu>jJmD$ouOrmH#Q|51=~S5SG@CMO&>$^_9fC7Zy*Md z+#ne6q0ppb%BhKpwF;os3tPJ&nxj;4=0MqSG&E(c14-_h2^kRvj^^_%9!=phrL1ttfW0+n9& zabL6)IU7ocHIS>Wa`CUYm&^Lo&6cEDGnXNPpoFCDUqkI51c;!1|EKyI`E;z^ljHgm1cUa>cNlGl1H-)21aJI@~N z#Bs5(6F@8ef~t*+$Fc|6g8V7q8wU&KVM23nj*;0@;~Gg~kR+Ju^BUQ?aL0U?+WHF3 zV`94>t=Jc^XSW+2;a4e;^$7V{CKr6hAcwv%9qH740Yw6 zic%IIOry2X8!yg+1Y6vR4{!?b7o_UPPrbz$kF;dVusp0=_*Fk-N-#MTwqtUS zUSxJ^l7o;>DA%RpBQ^H+XudAI+JKB;{dE%@LRoth-(s&Xbc@8_2d$Zp z<{r?hb-IVm9C!)zfJ2-}%?`uSGX3|~W5nNNi!4w=} z&j6$6Qlq^SEfV|hGz2Ql=j{#|{N34-4(yq5a?zc#gR_dIyKTp~}k+ z%ns;-bKIJ9#^>ZsYy8Xnn}(&YYv6nG)pq_sU+Us|^yhlku-j7qu=wZ4wDB%Z2~~jS z4ztyl>}MxsZZRq_taMOyjvDcoe2*Q2lehb_C&ka>E`7{-Qv5(X(PceY^=qq^6S|tN zfy~$Lhu!5J4*8DEZ^-OPZe+X*tQ}Wx4EubTq-S^K)zL}e)H3hrnbsd&g_ zFf1K936E3?XAGZ!N8>noGW2Gn-pDMdwMok&_|&`NCc( zaDk~Z8bXb@MOWMbKILFy|Ms@9gK6nv!4!wJ&}~1s_vzwZ(FrZK*INo)PTz|CB7^~M zS}Ff1iBC*e7Kd%l)3+gUFUXsNoo|01x1~T2K<}|e$6j?z7q)#5ay&5R)<0n%Cy@b) z5e8AXe{@i#1~wftJwFtq#G4WWu)%{X`M_^8j0&sTKN>plBiSShh#;Kd0LvOkdk)_^ zx4~2DLH&aAX3D@1wn)T5s;)S1JTg2(+K}EVYc!SD*wM9U?qSi1RP+)s;4U@6jn0r= z0JCng3}|sYx>sB~)xTg0l=$6^y~*Q8z3KUhoS9&d0K^p`tJppz+%zj5tJLDNjycX6 zkgJoRTC00%*JHvTjzrTW$ z=mhjn-|w!tyDENUGAc>nc@^x4%z}!L15&XP5W&+X^eaE83`8&`As27IZ5!^4<+#nK z=FuKR!OUoF>1PZRQ;l!rFpq4zn;f&rmpvno4op!-+XJ1+?>Ym2kb#HFght7(aeb%1 zuG8Th4MNv6wa{{)m;^6<-M*QeU=A9<+@c>*8(zv0vD(Wb{$i5-CGjt{lD~HfwBoUl znxZWLZHr$ooZw-L5nTi^fH~0SsaQdd1vc<7Fc90nmQ@ionM#I{&%wsV0MwSF_1HCV zYuHz$?h052c_7^icE)tavqC{BO%?Q%p9eOEz(+xHC;*bcAnuXQvE8&vG4DvR19MW3 z{%U-)i;_dFG<-@a?SyGrL&98idy?If_-b$+M8mHE$pMjd6UVN;8ge-ZP}5tF_9(UM z1IWTb7-u6oQRuImSTnb*X1}c2#WlOAW|!9DpQF8pgy}DruNiC&eH*;3U`P3Lee_pX zi8d4ZXtULv#*b2`8?bu%bwHmP^2=}AACVfK-S`Cs5OfLWP7DNJH$lQalau^K%LY!q z3Ufxf7m;q)^A>u>zap8Rit;b7rkKbWF#L3Mg+zaz=c82{k4RE9aPB;T1HHV0cSK$F zALU(iGx(l+M{9V8qvHZbK;+8&INHD|On_n{nQ~EG_#mK)>43)wxl=FcxcljHRTFt9v(Dhr6XA$a>7Cg=O86nn${Fn=e!pzv z(Ty9wU9<7%>Gly>mtNm^-HGaKn2xJpvB;659@gmL3#`pJBze9IL&>9U&EiQwwV@Qo#>I&Xrhgp*6*bO+Gv!N!6SPZ!&)4$N4^o1sD#jz!_t5A4&?~Vko|$ z9j>NBuJS~|hO9aws;tZu?l!-d?hg$^kOCu#=5(Ot>H^XQ?Xae`u4pB9fk#7_q(4vj z>a*Yxo!(Z~?bsHq1k!pLto-L@{Ff#gQ{}L!!wciR zix}y^9TYjk>rK)CGnTOX!~o*|4F8RiXzvx;zUd0Y-#G;=`pyI0sz=Ab5dd08#Qr#K zGAdno2z3wrBk3$r=T)9>3vm-j>?modi=@dsR^4vWQAg3fEFJj8<8L0Gi-w{e&8aO{ z?6wPSc@I1uAQf^WL~n*8z%iLSI#UZpG8{>MqikqyY3BPFOiikhw(22=Zh*98KGO~e zNPyqu(Hs)oWMWexD4O|CGV3WSqd9q%I?p^0QX1MiD^OwP6J8ejQ3~gWL{CEunX2ou z_(qAN*9I4cd%b(XE2JJQx=QySg3(-{!^@qLRZoZ8qc02`J;2MEdqqwsKZ)&l=T4LY zNp$+ ztXxM)D)$7K8;zdtTu*73u82owFbk?I%h-2k>^XkIb9Q8mf->MBKwL9gEqxU|*MjO_ z2?t0F!X(NHbNxNphq_RMa}?Cvb2OsN7^TI+2PZ9*ZH zlr*VHjNA9z)syXwt&^M3`9<4?W#Tc~wQ?hbaN|csAN%b%3j}!k(2iRItz&b%bE(F& zR9YrColy(2C9_ZZgN;`utfDrj za~(&y#*x7=FbJ$es)R&`XOVxieKcRc%MNZui_2tiA|lkezlLRR1DZ{fl55sE?S3S8?U_HGpT-Ui+8VX;^nrqsu+GWPpS8y-fGb7BM%B0ze% zATR~+ottHoMacT?urTB9-&#DpS;bD~6j$9~P1qcOv2x&anY&t&Qi0N5@p) zDaCa0=NsVSYfElrWwpM)WY3mtT|O}Bmsk9Kx%+KykCg@yqMcpwTe)jX zc72Jd8@TQ$#b#<%?kd-QT_$JAB{>O%?1s00^=8?|VZ7az*L21%!nQjYXd*_dGO{l@ zjtV}0U+AZq+@ySoQr^IP1QCNRLZ1;Sm7GK5Lx{C0ty!2~5NVY_t0pon!cY6#z7Q5u zfK0a*>E5cna|0q3`EI{lEsfqJMhT{D48$rJJ3U4wp;%-Af-&-ED)t3sB7do{-&gFp zioI2~MHM>&3Qz%8*c*lYvv3PbwmF+-d_1a6>EsmP{SJ8HhS{dKfzk*L7>hDXfwn2L zz*?`gQ7c5t7g~6$uR!{P?zOuZHqXFu3Yl-PoLXQX7C2#nch&*}hJh#9!*-XC1#Z`Y zY7CEVtV$kMl-=@*V&AT>`0Fa~=FJbcZ|RDfyAaJaLGBC0z5pT*Rowat8(KqnP|(uESn_LXV%qFq)!IU|VFr& zkRJ%2!qS>vctTiA%?j)s?c0(DCLTURjT_7mD2g;e`i!?5m~tmd2Jw@WkPq-EE=j{( zFW>fAFC(O7WMlIQ@%Xl~WMPoC^%qROw?)pZPNuh;R(d8Y0j$1t29TXn}(lPBx;D8X#Fz^iD$3q_pf3IU@Yn-BxwCSN&~O zo3s%Z9@j{BM?;s~pRyNw@TlE}aa7yVZH;cFZjT@rYBKdt7>?Ovt|8UI`KKU<7dCZb z*>R2FuQ^$&-jHkn{~Gyi>iOEqBeULQbZ~KK7m#a-i%--9n@0(0c1~^MT#4Py0{nMu zJl7jN+rYXE#J*9nqw5=y)zStue=~a@kP`cU)%-DaxbAK{zB&NKk2c~ghAD;YoKoF{ z-rM61#Pm4c2`|#KllX=tl$7s4w8hiiwX(qHd4GVU1`T3hAR#0i3%($#-rUINSx*D= zcd*&H)naXeF_vw<87R60W=eP;q?~Pl&^8V@Dgeyr(2`ZU))kIALpv&3*<^b=R!u-1 z1Tdt+3Y8(ZU|wax#tlXPLVHoOdU+GfGZRDCr|i@5DY!N?<2Rehwaw~{O*^Y53W=-2 zWsQv#^5hP8U!BBm%j)*ax($-aQ(N|3#674Mg2tC&K0ge-*&R+`cbj0(G&I2HI{Ys? zs#lPFDJyC7+OH^<3V}rf4sosE0)(e$qYbdjTYfjvHf$ZwvfJb2WyLPAbHN^uz!SrU z#)mHsCQWi(4E zC>T1yGLr8L54|cUXR9zm4aAa8Ser zMqQg$VNu_M_GS;(E3l_i&u3_;*8h_1ILG49v$pEDiYJ($a=6JORfpq#{ zPurrldKoa2raMeC$?eq3$6EG8%P#8D{=d9O#=uYo?uedhzgL%D=(6X#Hg@ca9`cA? zV##^P6u6y?Yj-P9ZB3UtrS7TrKzjm_n-c<@q~)0^(l8p!^M%$J$(Eh#MijW*#b1rT z20y*0KeWM7M|4$%x{Iu%9a+-GMV?#^ATgUOa$CufzaX}6`ToRRmxG-AnfMlgJ`(E0lxRG4mwqYP8gkllb}0A z^b$Ltz;bm1DgCV^UX&UY<9O;-Wv}GHO3TJup?kw3vJ=5*z7T3yX&;rEhp~Ruf2|yD z*b~~W5BquL=+06B77Q!$SdP!;G0NVlsUzV#8WG0ELvJhvXG_Wb7>`(bNu7U@@amV7 z@`D-HFH-pDZq1PWF!5xY7eD zQf8s%kT?eIVdbQe>2T*J18%Qp3v=_YjL?U?v27S*9|Vr27e?)sUJ1TN=B)MhdF4R9 z&=y3$#-CA&HbR!eCYMr1Te(g3-~B0!Wn?_Y;V;H|Ur5TT^lMH4%gcYY^)*TzH0&Rx zlU5w^$d<}7t^M6|q# zWgCW^G3ih8l~npT7%>o%w-j7e0+8%b(hBYANWp)3M_R)YdSoEsCw8C{tJ!aA0Es)T z!%u+^M32GkVsU}M6LSKFWHx1anB9Z*dGLn1t#0_&j{J)svh%nLI{o z*yDg;$z7#dFi33N9D7dt<~@!%EyWF;(ap(nBZ{e+krcn)CMaLc&93VH<~0yrxJHyy$)Vv$i2!YT?YtBC%~=x4LKv{R=v?e%zf%n68Zx2 zY~O+!WaK>%6V^(?VOUL$9^Ww!{DBbZS4I$mQu6 z371SgScEUR5Z)VCJlbJzvCzwGeCS+! zNHV`1j&Rl?OGts58R0!lRF5rF$+Ujr|UC!U6zjUPjLhPT-DC1 zkd}&=kY7~db&NKEinlw1v1K~;${~JNsxY8ZYiCW3(Zw7>IA`p}qsyQlNRM)Lhg>)X z?gj;L50JMFlAXAK4lqB$aMJGxskDAKH?%&qqZMSi%xYmKXEJZ2m_KeGoJu{P_m5r@ z%A|5KOMa3kvpVlH^O`UVte(Wku$P43_~U~yW?ZCQ`*s)T?4 znvxV1%5V2{$?hxpwI%;xX*90q$n&Coy2sYMrYdb0SwE z1g}cMy=#3@RKOG&f`ASB31@{t|r0;tRGgJz2J-g=zK{ZbZxM zLo02-X>WzpTKNjwX{GrUfZyWYD5Iw)Hx*^%&E`TPl3N%+f<*AiX@_SO8L>hvUzd>{W9|;EH};DP3K}w-oM9xgEdl z-~U))E0u%DRu%5r!rlDNZ-1wc^aB2^g8OE7JQte9DHNb$*fwWKTcrO56#h596C;CK zbTa2QOFtLR4aM7ijBU-{s8a4UwJ+O$*rDW{soNLh1MCw<8KHkB?nSXxO1d~158?&( z0UOQMKwwU%xo`83y$Jx*d)rSBN1>e{O(Y zBK~uo@(}-q_jVGK771LuyO)W7-4?doeKo{YV4QJb$l^gPF^?(xCK`Y`E(daUO7Tq$ z??DV95~8^g8E0|A6mjYn3zT{7`K;NQwe-spFZs`h40d%ENvK_^nwHt@ z=w1ofnpmbaF!{l+kvh60<7PJ$Kuz!|+GhPwx%2j0?-Kcwk z$5;%vJk737>kn8=HtoV5x8y!BA3f~@x0I-Fl(C^|Q8{Y)yDIznIdXBp~G(i(O zfb?QqoR(Q{v>snIbd$W8GSnPV4PQt^nyf!RbV+XgdFPT`R}NRXi?6!{bzaFqsHYAB zuoldVUL>_x>yEE}IwJC1(ZaNVkL`Ewg>26Kjf$W>?Db1Ls>Fr~Zn^#7;}lTuj{+IbXKv%p7r9 zRifu7u!t^OS={PY2m{2S{b*>D6-9-656K!QiG`XTP-~2hYK0*dmRG==xcMUiUY0(b zbt_fls_hvtEgZ)IAd>l4a}q3EiJq?EC^*Kht7%pE*G~VJxYNCzgHj%|;B94lKsotN zsTjhlU{Y?#F_Hl~<~CB62ld6(n9ukz=o0>ZhTTU8gj%blo$L-NJkeJ1j!H|;U+kv( zu}J#301=J*)2e=PR@KfX#<`kaNUdw_I7J(s83|JZ)+V!eIqM0wECo}QdGW5CcBt~n zZXlY5ulF?GQBF6h3)uQJaa0wu9{3?;UUAQ4P*^2;4lhBdzIJMRlR-%(-L1Dn)?t%h zW)oTcapJwA%Nr#uG{l%-08RZlJVhB;L3IL9iUwWtC(8e;^8IA0Zr^RwbyHVj1cmb6&Zx=GN5QV=jKe`Ts@Uzz2e%A5r&z zl@o^i;)Y@Mo)nN3clcZsedcm$ltBRSXMi1hQX3jD*ZPYPXqVzw<10y7IRsHLCH;Q% z66EY<)KNXXshGbI8AR#ufxX-7pjh%Cg8FdzN;h%^`8}7})MzOs!AOte?A&^`v>t+x z+S|QPSVlh~(ll_9u1~(6Xd;-<`3?4Pa!5yXO;fZ-u3l*|5TKC*RTbDB5#6mjzeZLj?RkbN&GL&4M6ly5 zJ(d}XN5%KxCF%SxoA!7OSL+^e#@+sJ&C-j_^lwdju^A7KC&sF)s|ob>C3R~$br%3u zd26QeB$ja>afj5T2a3?mKxx>dNiTKRQJ`lE)9dXSZN0O3N&H`C`Kwk91%T=0hok-u zgxj~8`RxE{yqZYdhTRU!661RTW~bvhsFWZcgmqAUd|7I|zKqk1aVJ=SqB2IamDF3`!WK*_>Zc1g!Ort|a;MM^J3)ls)&h!%khspQv$m}oprEx~LdqJB?@ zt?h8nXSZsyvSFXzehN&4WoB~ zO%xM96KuGX&E~;`LjwJcq$IzGEZ)xl*hSGjZe@UBunFA|NhEj_A2#@6Iz~T97>`^G z>s5S#O=Xz4X5f^*4`HFw->$c9)}hY?Zvd_dg&oF6XLqLewB9k7ceens`&({}kT@|F zjNz#)eKo7^;Xe=E97zsAd};)AT{tpO7=;?)UDv^n-!KmldIHgmyw`oir)S ztPz-ug4G@pj>bi+I^{s^e`jJSRyR4!SgsqIb~md44q#&4h~!u-S9SLi94WII+qHRe zV-A5+pv`Cm#tds7hw9fZZCx>{Hf;oFi|J?>F4bX_mT$C|FUl~tEgVgFM#=5N%@*1ghv(56j5w?Zw~$;EsIvS@ zSdzg{?IS;wpVo2-1ZNV9ZCH#u!(uP;!~bV{WpcguZAU_eh%p=wW% z?g?d3Axya5Ca#k})mjIaw#@d5P92tRo9qOo^niS(aPO3P8Xlc6JUw!VKW<2=8*mxUJP!qH zYo!?LaIVU@YD{$b@a&gE{G~%mq!n=(q?EaBKt_sfv_vFTYvfF?d@;F`jfp^B_Q@4S zTOlsE@q($?4((q+7W+r1TTJPVJhe~RXWeHgK(JX5hmP@ftfc9M-l1PP8I3W1Z$I{S zIxHG9CC>tJSdt@g&G7Msp4L3}Ak=yyl zTx{;|{ba1^5AW_5;&T;yt>-|1!43cl)6Lq0ZdN|PGa1I!XpFXykH<&RI6}<1e%Su;N8SGZqoe{z(o>vU z3S=d})GzrF`{PN&;DYavGs^27;ugk!Ja;G)&d-~$DnZG@6~TE3A3&kat>{ZK%SR{r zl%fx3kf+lnYw?p(JB1MY^mKC==DY3GjFPlJN{x87@5Vn2sxDBx!k$?zty*B;H&4{_ z8f7MV3y2)pMpE4DOQG%hKiXEP zD$op(lU$z@38JV?WplCFHcR;qU`8~nOpNP8cVOKox-rs~?ya%Ftk}8?VkHG&)78BwW$9G%3$K7R)Zj!{*oXTZrwRKFGE^s0j zsE=Vkzypr&#G+lqxu3XWKI^VL7=7Wtwdc2y-z~Ab#H%F8<-6Mlz*67fe$ud1p@GCj z2kxme;6YmaS7_OGw9Hzo+9bKg;Ggyq3nA3hKvQ)hC8^1B(dUD}PZEB%04-a9Hh$1qIHu*ijupH7FLQ5N^f5&|f0K7?ccD~Q*e4hQf#^BEV z%SNWZE6u2RbB?oLIQMb%ywEd89M3#D2crY_WlK5_7tUspY*2zIuKm5Lx(j(!r z9l1h@QH~u!`W3s#rC^tkwV;z>gdfvsho%2_f-RIC@wfTq|0Lh{KTyysePbdn1>Ho^ zhvQFhEAc(?Y{E4@ zlxTM7lBSod*s!DHF>zl!E*qPSBg-54mn9+$9TW(dzu@n}Okq}LCcc@MlUnu(difdu zS@$0UolI~qW}REo)(8!ZgN?mv0zK5G%3w*Ef&riT9vNG|Jr33kiI%gl>{#jH8Vlm0 zwT)L=SK#O7hBlUgymJ6gJ){@Iv0+sp&iDZVNwI9D&Y?@Sxx3K z%e&bdiM^IsuY1;4pYqAm-tMD@H7SWwt{Y~@rL|uO%ioFPKMp#`$+znYa61;4w3+M# z<5>ZW$1s^3%IgX~$$t#-Mlj6K;g>b5XtZh}EJQY&kYa}5!zzQAilI9lfo2n+M#BQh zWBjqFrd3#JGCymfYX}cG+YRQa9>Wid-?;c*$B5m%AeVk^n%GedagB55qv&;xlz(sp z{J0`V1j$)4SO5uZF|FuIn-pe~6gsdQ=tq<27>`lLtw*EnNK0w=>2Cf$S6%~^J}^Ba zaX-d|1E%y0VU*`#%2Ib7d-8Q2K$$W zUK;23(O#$jPN8L3W`{I6T!P4|YFUB{;akM>4Hnhm zfI$@$_v^&nlF0tH0z_>a63?gM_B}B4P~XFhx6q29I}Raxj2FU96S6`fJ;)NDDEncs z`cQv8S|HXhj#j|}I-}aW^~R%Jw4f1DEOSpc7X_27Fl-!{MFw1s7LZnwyv}Z#?c_;$ z=}8HqclSlkOwXp3B2?sOON?hHv7ekBpDJ?pK+I>|n9?>OL=MH$A$DWto=NOk;0sXy zBj}NzY|W$C27s(!mTmhEvH;3r=HY_DWu@p9;MzwE-jQ>9S{xn6eSrBRgHOx_kRmmWKb5EJErveE!%CN)3AJ`Kpj*LOs#FmqX-ycuqkb}PM+Js=&=074%rZ4$b> z_D;>n1p}U>A0Uu4c-zQa#I%oy9*7mR7}6ZIX#t)u!jy2FA_Q#@nC>NTCkuL`Kk(Fw z_-Dl{wJ-0{q!!4)Nf_M(>6aFeDud|$HzgZn$!os+I<5@w_|-)H3IjhQBzlI-!Od$8%(HvNN5x3(GI z-{mISBsY;*)rr}8(jq+2bdM=JRlCbGwbRPj*yXOmS*V2S1aAGaYZU?u9cKRtuZdyN zFHm2jVv}p4#1G9}7A+W}RO4ah2Br&ct?f3D%u1tQ^Bu5lJiSApnZ?t@OwgH23D{UEmM*sOU_Tq zOQBa@E7hLD(Sz%U{!C}JbdBgL9;D0KjO#41sS8Md<=ifA=LT*UBchw};Uxp&bv{RK zy3o0w`NpkDX?0Q~%gTVCKy14T0@1^u-;Uc19T7{C(6|%5ZQn_tvCSZ8upTY(SWIef zv^Evkv4k$U$LII@{02w7nMa?Cra@bzfma2lViB_}-YJUScjJeEf^Y+NY;W|!+r!`? z(*_^Hs!>iGJ|o8xb28en;`r2`kox1({DibAh>>Qh%*_Q?915#vhCvh)RRTOm2&Wr7!H)`4zGFfu7m7nShJmN zDUPP8y_mYcrSXeta%>8Jw~7Q?PfJcCqsr@KQEeW6FP~Cn^JDuX_ z6=7a3HB@gR>cgTJDOLk9{B7#*N!@R9e=nOBSX3^X_KkYg=ZXk}bjdse#6|9oHWa#@ zJEGGHFzb)fe3E-bDCuqVI80p-q`-pF*OhkQ3UQ(sKtY70+5>J;=Odv>{t^_E4mm)u zBBDqpK^Aj&Lka|6o~Dnb$WZSA#K*x<)9v`)Jb4OTUHU9w?|+}U2gDe)2k>!W#=UWF z3odI1Up0Xl;pxDyauvK2f+af%dP~eNoyCdxlNozXq2C7L9wOZ3(qXPvwFMHKg=kiSLI)R2Z_SrNOJ}E-i@-q%Fj^! z!7aezsL0Zh!5RMUk{=3sDdPzz13rgA@5Z3eB=HF03n7+Yi4R`rKK2%Xu*mmdxcqe} zG(VJg7e0;pm5Z++FIcoILQ;;nLzoJ1mif-)Qip_UX9Q#21q5c;_0BGlMhp6fda5g# zjgISNs%+X6HyJ)>k@x>}wzv_W(okA+aNrn`I7sZ3VAe0G0fDsdO4PC}FmHTtcEdHaz@gni&lg0qo^;Q=Vn3eQlzhv@CYfAFx3qauewDPU1O`? zXScj>+~VGxRs;&jj1YGS4p0G}Hd@o=#eZw89ElE{gI;Ighq|9hq0Blsi#1m86mXq**Nx%Dp zVz~QV?-G;;i~QDThF&py*-D|Uy<+ZVYlP!;lQUeChOTgBXE?uU+v3{*Sk0Gh#4GT= z4}+y!c%PzAYrw@*pXF+^YhdVEQ*`{7>Zm)p)!b@r$$!^%-Xc2yfpO?a|BP`0)@4&; z%vprSVs)o}0P$->zyYCnv1;dwtW+hW=jPTx<09-+mJ-e>#?Y0P~pGjubTD z9K*xKT%BV>dSVzuGTEW*kjRqQ574_u(HO|pRq`4dEpHcCa2ciWTzKA&xr4mE)Wmy$ z*Cy|t5;J79JHgzQsK>9u`|R&)&D|3Fqo%ndw#B%!H6YE}i`D|SR4*avp$tVzd0&$? zfJ|r1R;ATelA>3%MoV@anU6@a-ozp$ZzZYr+Yk*V#!L0{Z-LAg`HpK{_j0#ybPb9m zxJE~bEeT=J+S7slp@bQd<$*wc>Mzmo#^w%w?| z7LGHqv5WlIdO$NM*~I(gL*j=leb`391=XTErA!uYIkPeuliG}wS&?ppeh;U`Gk|<^ zquH6lSTfOfgK4n1@EI*l=_1Y4pa;&ylx<-05&F;Ov|n9KZ>{1{+rEp=IBHaEshfkZ zrn)WRftn;2>r4u+`loYmI5$4%dITmH2Cd#Cobe~R9nlf;MFHlkz`Pw}QFMVV{<6F5 zP+M6AeB`OG5Okn6OgOGNdI)`y*DU}bh%CW0aZ(Eci_HSIRX@QvA?k7)c6JUIurykwB>oli=2dG=iulaLF1qP_c2I?9Wb35`DAwwr+N|<}jNT!jxu@ zjSfWwe-T^Gdud#6a<;l+SK_^u;LOvmL;epd4@a6Bhd{ffpBkQjAjx~EUC;A5O4vc~ z(2^ExWCdVp;;LgA(|Q{k_{L`RO)5B}Od+v9Y9;*dx?mMFph?2tqr!k#^A_9M+3a4X zm}%(xRQl=cQ|{B12wQ4OVp-TALLN?Sr9W(;AH)6aX!4y2K}dl+cCmy5#2sy`0ZrYo z6$l5%`AN}sup}sLSV`cijK?RNf>3isnecssWC*Mj(n;80-wI@;x&)GSoNr<%*wb!z zdC;JtFDt!4O!Gk7_5bkp-hp;iW#0c@W$%6ZIrrS&a_>zi2@oIzkdjb@P((mQbQp=~ z>!5=LUvX?RTtX-UB28)_mrw6K%+zC~PXk#Zwz^GWNm>N>;4mgO z6G8}*^QZ(V0YO$+QcB2?a=|zZYhKiKGL)!o?r|*EVYz~A3usClwyN+$w!jk7H;hj~ zedX@5apRhgZbBSN1RNwtu-5|Rg)H|w!HN-2=G1{XBpVKP>P?ajJ-r)=NoLmAj@7`{ zNn$kv44T8_1=mP+Y>u6jz%zbDY7Loh7^NXAo~Inh#n8UM4LgUlBs<7!#H43Py#b{J zDLWjeVLHk|RCx{8nHWYp--&sNyF{34?O_4S-Q>*0Bt;JNJ^Nh*`8x?SDE82wK&R#< zYyV=+n?OH#9jRQ`TQ@NbG4EMaEH(%f|FGs=sr9T~F5kqEl(@BDe|`Qwbd!6@mcZ&T zJ%6Mrq!q}S2k9B{A2zlB4$|N;TLpBLBEmY{)oFj)Zmw@1cmAsV{TD63de`Y0%v>wP ztI4OuP7+8X-W{+LgX za$9@X*ngO|aIQ*yJ=}Tjc!mIz_neb(?s?bvf0+1P;T@{Sb4MjZwJZNHt?xopEhH)c zFh*wskUw+g27yx>OXr==BA!_vOj=|3_;+mR&B@kWXw7ZTu5|vQaKE9impejkY$#uI zi-J6Y$6j>irS@pxfFBUgPZNK5CKKYH_qH;8_4osR?k6QR6g{M{P7-rnKO?Vebeh8@$5 zs3LlER&36U&0IT-uCMT_q+|uyny95BJ30()KxBbg%CdAkl;9Nhwf(Y z4L@$%umZcKh*tXgKLj%XQvUn2`2lu~APtNmp^khlr9}6#GecMe)$C$#*-}uQ5bkRx z<39k?m$74)@8+Y=34#+o>y7$q(aF+rV)>u|_TuwO^XtUb|3AC-g3x?O<~vrJcFOd1e=oY2JU#9E!laV*VB1&f-u) zX`>=zZ9nVM0kml=?ycS3?#h%feoEX7CV?`Y@cSHf3ric8N5b%54)~Fb3FZcZ4lW1y zZZg|)x&2;Y3d_Y`%H*B1!~NXl@JsuKz5)WCQVizS1*dcF$40Mn!Iqrh$Gm`ox1G&r9@!&;bu`lvWR0v~$D{=3u^)zL1*m*rYK zAk+-$D>ch}TfzV0ev@YX=-Dkp&2nF1Q#I91`BZc&LR1(*VsD75E})&=V|(D!tEOc) zlfBA!8+FCkv!#2tHJ2FsymzlOs*88ethzylMum5z2Z3yAiH0{_JJC<(es2j|(9XrP zFTLHUIfgsJ7bs?Yin=#dhd;Db{AVA|y-NiT18dcK=jd6gB5i#HOrh5JSdx>$UztW# zA68Xq^|pvS>@BfdNpsY-LHI!!Ky`1tF%_D~F3jP8Fx0IBfQl_U34ED_jvcGs&Qvd0 zs||se+7Du&Wod(NK?Ey8EwYsp&xO5Abhr2^i96jv1;uCS_3GF>8Jlg)b}VyeiU_qX zqncQYkRCq9`~;{M2?G*Bc3fg&w4dzUQjPnjzDF07gs7ufMI;4_NV}iO2|ZVo(?tnk zV>q;^mqDAd{~jrA8N$=OoJFa#OG_^5o3u%?@v$~~NfNB2Yl$d?#DZ8vn4q%3!i_}? z7F%F+ECAY>Z7Oe~8A6_l07<+|Sf9?6ro&W~DuO~DWTYYuQ9d??BfwSUB^+o>cC-^t z>YHI$J(s!s33z0JlI9lwQa;n3WgaB)ROB-iyQPI2jNWe>1UE^q0N1mhVOmpho38L* z4S^7P!?`=0OX0jxIQN9J3+HM$C*#iJ-)3n(`25LnxxHV?55G^EuJC-1ac}F(amLy= z5a0$p%C!vKWZLQUWU~ z6)JfGw8iGs7~dmu42P6G{K}OsaPBf^mtrlgRDQlj#=4fY*apoSl%2GL#g4rpk zxj6GTYu-j52~juCXN>?c(#ThsjGf6rFL<>q<5-fB# zO=hcRjlT(49vDL{I13E`?cvKXm6<*4>&iITXXMD4RVwx}ZbxIze%XM74TB>@N&eO! zW+wAvKes#BjOdJbXPjey?9JsqkyZ7E-|UhZFO25Fh8o?K?aW1B)%ZtZCydQ8oc+P2 zt`XNLF$>s*q;XfS({4)Gn8XYeI>U8)_8moxJQK5V+@atn)C0;AVKg{^-+rK8Xzkk5 z7P{-Qs1%s}fO&a`8$QCkx8=UWb1@=1}c=d;Vr(x7K&6!Wetx);nz z7iqgFhYq>u;nCMmkG`^ga0oS+2^^DyI*dUjMuu4V=wvr+qVE8qImM#_AFB$`H)0P$ zG&JtpU=M+p;pxX8CPs2>Wz_o(I-g7GzsoBbW{M{MPho{6T{d)r^8N1AZ+yAC&-lFV;M`B&o+lcQ1lE?#ADXby+;( z6sku$E+b~guyC`UhVR(7e;)%$IO1Tg6vU z>(by4${ZGd-QXh%>lB<$6`~Q$DTqWO`tEH!TqWipe@V*j(&0uO??;e&uR^srY2V|? z4F06Hzgkb(i_5Jv6qU$r20d0NC=X0|pqs%#0?xGM+Mr>BDeui11U~vZA~FxMY(Bga!vN-Y>fKV3F4`1$~G{ zRERapITx+mi-f1_p{AU^mpLumoUs>bEm^0=V%d+czM>>RDQyphv`=Y2x3Ad`4=ekj zK}&8S=Y@Kf+c)GN?$dqq(S7rnT4&C6=gc){&oxVQmSb}Yy=Wfefp0wXSRj#c6nxQK zbKzY2$-9&1n){ni&zft_3{UN{xo!YJ?esInWFY2Sgau-f;j2&>(FT6<`D3k{+ps5iCLH#s?LmLkFW((Ad-mbj>VLPsVt7q3Y#K zO)Td#H`i0e8uCZvqeF8iTlYiD+5XV_Eh}?dqSF=OEH=17cvuQgzl|;K5E65zuiVYj z!uCoV`02no7uZ>>JFL>kW)3whx9IN8wOb~0@q3iO8cF_`nwyc17<;=(?gC4z7a11K zABUWZJN#81>Z6+z6cLBQqy;8%x|oY*<-TbTH{VPTuSCOr^x187`*htr#nJ)td!`Px zv;|Y74J5}!TUiJWm=gE4%lF|zIntR4gKSnE5rW^GP=TAz*es1s?-GYXxHCC3F&8EN z;>2Ew77!Rh6~1{D|0Lpml+codLZ*EB_gWxjhZ%Tyze~ z)aH3P_H!=*Smv}gA&*r=YGepYtPUXxo?r=WjKIjtMbC6MVds(>dIYT?FEAU>>D>^; zrU;|ZP?G^*0PyK{s5sHVTeio6`vMBobdhB;stj@m;-cxL=x%%P49`EO(t+@Pwp$2dkXQrI=g$vD)ZZd`5j6ijmkV$ z*l=a;ZsbFp$dK6LK8b& zuWCHMx$#>8oz)|cHhw?b_>IDLc)mznDBhnOu5>12qX8jL(4UKz@my`VrjhQ}#`V6& zbyef~T;uv?<2sCW7T+MnVoDJ&$gSoiTP+{&O+{> z$h#7BM%`{RcBQVyEGl~G$us5X$WR!d7PGxW2N#Sq2u4a_lTMx!?Ju4?dm$O}_hLlD zmfjWMPn0DdfX`ReWyXzl5fv-&Ub~#%PnrrQH4ELB+!xK4(l0uWwUnOswcDC|iP&b? zQEcnD`xw}13BjohzKnQ;Kag!5hutnlZ$~dT&+@9QozHgV=vGKtS|pq*Ai@udfzJDk z`=t4f8Ol^J;AWiV+-c&`SXcz_PuM-VwS8iHeARpj3U$r)=I1+zv=^%jJh>7c{+G?^ zqOa~h+&*}?`PJd(;ls^Chr34)H;)`{R~;Vz_Hcyo;L#UUga2^fGU+ln+3!nE z-Y(O}-B}kH8Cq~^7?pM}#NCI=8tST2FGWqzodhcbnV*m;lmJ2(2!ymCgijBs{h}c$ z6MZd24LpSm=gO{+Ato+?c7uGY^wdk+B$(?%to7OMU}J~Uer5A@^4m&Q0V8H-@ynhx z2?Hu~DaMRp)$n3;hjLXK&I}*}=GGphwFVoLJa>;_$eF1&*&P2<=Nr7a;1XjGGsVqf z;TbZ=wJOYsWtE7=yU6F@E;j?tB~~2tv{Cg|{?` z6*EJKZublahTaer5|F9(ZIE*8w_jKRq4G#@vw2P0fY4y=rZ9baS7aiZG`A?%k z0U<9Ui<1}gQX-5BH1I0iUcn)hEe|lj$fH^l&Qfs6;l3FkIuPZOtaXWeLhEoi^Sxe0 z&HQ>(pv#w-?W1R4B>*9boo%M!^53^0#i`2kL7U%LqzYnRQdx9T>#GeK2Jw35!g^0i z;Yl*?d4IUCg>z*!aCE)Ng$jH^;+x5 zuj~ofLxpL@E-Ovu`#TYd!e4FS-7sRL z7!T4JYc@p#P5vpn>;&6_p9wn8m`^Z6DlDqDZyd*}HGk2v#k|7s33NHAh7 zmv>_6U1f5M#B!GzED}Zr56Zu%F<_KsC(Meh!m=lQh;Ob%=fFD^3 z6(+2KM{43J1l;P(bpH#)&@{Z}H)`g#F}Z!M7F`Kth-3#?8d30c`0%@ivkhm$ROh35 z@3+A8o94oKFr25usF@ypnol*v1f(%jX-NS#lyLHAAo*bpf^xqC+CX0dAQ38L|D}Is zRrXFGMMre>9%Rn<=3L*?B25&$OVpu4>-hD+;XF=B30MjHgBQ#yxF(-RK{k^`Y9$Ut z@L&NqEl4$r?^R7kML!aY@h$v=vgNsQIAjI%mQ|4xC2Sa7?UJwRRH=9!@Ic&YGH z8P|&t_?c%v?{=UQQB>&%h-tXnHo9*j_DARkP)}|0^U+Haf$=cN0aM9mrJ{a)epYnW zB)SWl6=qZs<8!TOt#WY@eZCg`j&}l$iODLHK1K%}kgQ|~+-WPbl^^T}?JE^D`Q@01Av*IgiEj4hX{lS9BIJx&$jGbLqEl@SDeBwUQe7nk zo4A9`XPM%axSq?oDHOpFW^fC>RUkS|SUI1euZw)$hQTs>A9x?5qr?_AKnz;3j{<1C zlYT4{XJ{1Vx+5&#&WbM2WScSAY)i8%p!yNK9T(guP$-6w^OO9RknLRv^nw^w+7vau z!@NyGC-P0A#oCUCI~s*YKqymOQ&21@K~p%mGZV`nAFfnkwyF?^S*%mX5iUB=&GfVE z%-k$DMe9G)X{MRAV{}ow`*qP=2{hYf1^dIGKm?sP%s$gx+bZ)595Ub6>V8%>HxOf& z&G5!%HjdQGNr#y3-R@aB3YD`h^EuW%g>u+j*ymF^v9bbi@Vhf=gmu;ACj>s!Ky;vK zRHq!_P5!)6R)seT_NitSep3KBu}C*E$%v6%0@BC}NT=_3osF*r5x$+n6J%`8%i)Yr z`m59@S3^K#&d!<3Z2SguKCh6kH9L`~WdSuf&uq%ni{p;yfg$WVvaV&(Y-g36GZ?)I zYg2p-(Ql9@B{ms~Ur+^a2$L<`7~q~$Jl*!5X6w;O#JeRKaoO-z7eZR?YIYZskCC>F z_wM)t4A}%BcZfK!Nq~FQVC4D=mO$A4)}tM2(Nzst$`IzBp}oG@>Ll6$#aT>G!7kWF z!JG&r3xt5=XW__mg(H%4xLvkxQuKmC3rnH;X=4$kU?+8}V?DP|826;H!B2zvt#Q9I z$U3pVxA%)oWUu`)6{A+xJZJ15!o4Af)p{%4q1%a>?r4<8p7lB26z8ilOp|4I54gj} z6X+>zKpj5b=?Pj1rr-kHQeE`-wXz`nmU2=a%~b_O6U*JUPNIiHwBa%gmK|~-wdT9N zGPwcF96>2~1VHQfba`NQ*r1+3Ffe1jPwq5T0+^INjeI>2CncNBR2#8_C}86Tq8+kI zm|Xih*~bxai~eZM7jWro{sChywuq7zx!jSCGn~U8fIpZmUof9DpT|}-wg+RkxzPMm zCVy1#gd!-7!4bZUL$zseL-sJb&l=!62S(c=ZPY<^!S85pQgl_E;OA#t9gvGXZgc;w zGj;f*`V)4=d)~mdCJc+e19@0%zF<)&xl=FiwfRh-R>I=~zfoNClR%f#{6vv=iSK4` zJr0jFMC(N=V1mz({#7m#=J>HSmuu5`HAdEELkTWLxl;*# zLTy7;X(_kTCaLO{`%9fb^%Y73mh}M3U--{Sq=$gWrli=WAT{OnRIc_c@XgY} zX3rd1wCBL7Lb9!vv8ZS@U{tp>6S6xDqV+ptAX^_|TC)&~eNVfXjaL9VB8N6%!fO3S z5t=#Zkn~|krQg9WpACQP&tnHlZe}XXexljOheW5=1C=8Mq^zaXNi9XKbo9*~+(~Po zks^o-r5uz-4~(W+&_?WUs7efCK^R58zf8*>z8zevrpo+{(Y=c2m7{%&GLBtfId+H0-3PUFY*rJ`K;E%JmTH5_er|Zp6Vt{ z)H=cn$1^J-NJB^w2Z2bID&4mdowIrJ@@DRh&CEla)gH#7iF6Oq7(Fi3IaMl{Xacc-(R>M{Qn-kiL-KW!s-)<)*c?B(ej z5vE%Xk+N2PNu$4!->SwJ{IvLG4c3!yUVaIc^WwP2WZh_gi?FnUhRl2C-*K+NAxY z9+74lSkAlCC)(2{0z*Go;7(n@{ykA7a<*;OouY=c`VQes(s<~fxFzrJH(goK#N!=M4ZZw6^ULh`ptJS z5Ye34gzjgfV@&kqchRL&TwVftfCy+9P{x1Fh@^+8+}}(7@ZIFu??TBwks3Ilk3jY4 zALobBGUBRIs94a2$MZ0BafdU1!9giNl8>UJkgUw4F-R<5m{p9;PC__ zYob9xlqJz3EUwvtCv3r&Ku8>~%BnX6>Pt-9|A`5Waz1WMSt6li%(4W}U>CA43^7{r ztSdn=8AUPRpvc~1G$mdq@F50i$_k`(vaqmz{y)J{T4oN3o|a8)L8DKug|s!@9KZ1R zEjRe^2v!1J;8KD{F$Q9Pf0$f$V@W5EbR@pVpkgx;qi7S8S?-hWfP~`?;(O)sbnJrz>ZeD-N4SEg zWBLh}x~ZMQUx%$yUy}TqeijMFc9DZ>(9U>Xk*c*tOK1ZP;s7DDx;po`9? zZwX8sB)v}-e1BsFbLQh(!Af=qNI9Nq(dCPH$3O;21h|TQ;90aHrdJYpSyYcZZRdwg z?uZiayP{ip3WQ3hfo`F}c)87XsW1jc-^r<3mg*Jda=CGr!VaXHkmk_vxAd@)wSaGa zJCyJ$J3YFRwO4e1P=*%uQUYsm5kBc7MKaM2<)WL5$zKchZ8qpMLmTvPVM7yrycT^Z zD;d@jT>D?cwRh3STZ=`^1d0f3>tOm~sWi#Y$hgE^lX5d0T66XAzAswF28U+l?ABTt zXhUM2B1}s9c;39I>$7>gEeknV(4NBY%ISGLW0lQmdbbR7FRXJInK;|-UFYzFg=;?8 z6D9WXX#pL1z67YzQS7l7D}yC@bE!VQDDUR5!1>~*@VVdYTR;o^ld_9i4%Eu&0>K8P zIGNN5JBCLhNH)+ADnB;gB{M!usp|N=DYnReabQ)n9h8sGV3t2Xm$T6Hq%uOa=0X!M z!*I?lHOX=CH{e->AUeJ3O!`{J%PUMPrLMqpWVp;)PP#K}cd#q4_2QApT1>IUeAO;8 zWmEz%uSA0}U(c`mB5r9}KXu$v+0etyzG}W!uXhuI?paV4#)CO~yvCv6zRzmHo*cg! z_DCixvohx%M-wzIzGcnRcy5c!Z&>p(<6^Oe`2+hp>ozsV!Sm#EB>dqUvdg|%?Bt}0 zZ-%9Y$G=4CdW;PHxyTTGVt3hR-vdaoh>N1WV(l`~*tX-2`9J9A<7A$fsE?15&> z-YI-^A9G=g_*d!zmd57c-X!b^Xzp5k@+7?sXf`>Y285&NL<>^L(?;B$*y$N35G#<_ z!X7qTM$s+ynzCJBck>+fYS+B&)`E!09L$M+E2yf7LW5kPXt%%%w2Z(x%qzwOqif8; zi+N1*leUny71pyWV}BF3R(isdZj91h8NxR1-VF7!dJj$Nw6mKK zra_SwRG8QHur=Ew#?fo=;Eh%Yq>POESGB`4U+06v;?MJu!fdeaNdE^P=D?G%)g>$q zmkYo%9GH&y3(Zn*M*Njr;%JY@ZS-_rywg;fiI773kd2WBl{H;|^xC_r`Ace$t^PT+ zucaoOxzE3*CsWbiHiYRGS3*M#xd04_;!+g=3pQD_w2W}z zo8e=uM^gW+d2nxY|K2kn*xQ`kYDW6wA8sNm_&?Qfw1sZjR#{=1Ndi1p5zwPM7xWbm zA`ti}qY_m0CZnp?GvJ~P#!2O7s7`}u# zjYgj(IDs3n*iV;6LX7tj+bc_(HFfty-9CvaG{ZM$5&YM>dAx2OqJh4O-xY0XCuG6Z z60gAEjV+!Ggf(137 zuM3y~;95k?Ea}KVJ*7|tex8qZ!6cfns7bVorV1G`0BqJo6~tN&OMIU35oa~~uMq7r zKRPi%V;O~fO4hh)NQsjTg$DmP?xZpm4C5J(jQ^jwIngeC(MpC2q7(n=Gzc39NLFqb zLbwB1-I#@WkW+FH${bKhhl`egBd@^vP@*5HC1H7RU#z79G8#E2nBN+x?^gI=ha zQ{G~{#(^RiVA9Zdmu}!4bDA_LJB9o+jmv?ccUh$TntW@1y<;=8dA>L8n;vRY2`P)A ze4%=iWOXZroO129FO9BBbEx;3AI9l9v3VYCD_qgTqGUkRg_gDG&))T!Taxr%{1tW; z|JGUhFJmtD@nzoKB``c!Lw{CcuEa=Ojd?iZL;HV92wfYHWlD<4B{h7XeC2@aD%yWDRM*RH; zeRl(kH~Lb>9EHC#e5?Q)#OwNf5+5&I%bwf4A-1>XltL8gj_H6v?Z+Wn>2M+c4f`UY zJ7c)Rx99MbLrhl;I@n0Hy4!MQWe&4RAHN6jmb@dkC*l^0SU}7H(nwOk*Ze-8zMv2> z^asl1M;GGL3;E4iqTSuj{FgKLI?wrThqHevRa)~yr9gy1ay%IP!r3gHCjE;kNDPP` z!`zS4ds>yMkSm+|I_JdCh^NB~oeC$STs|HH6(MfpRx{aFyWm_(q6N15Vc#xB87^f| zz%Ay>to||nQgN|h*lXbo$+7mkR*#<=cKw(`$FPkMMq_mdX!Mo`SvE5gNlR3cpCYDL zZU*nCx#?zFI%}Qxn`KNfF~`zwGbi4{puXz18u$?BZMD0r?EnUw*MUQ+{7&hP(|1XC zuIw6rxVQiT-~t0}V|FX=5$|5w%OY4rq`IRp_Zri-8k~D#3%ouMTZwobsqm7db`yMb zgLq;(Ht;9ewYEhrx>CG^MhfuP$u@cmq`)8Or}=!ntI(+abZs7^x$sQ)&F5i*#&7*K z(~Q50Ajnc@X~t4EQ#GHz-|y`ChH`0s*}SfsL-YCC*Z)_lb>FZ3Tfdt9vv$7k_v`;pXUoLtB{vPEw|C%YHD+%P?e9B@7P03V%as$j(xN5M1 zh79Ng)S91UcRDL$(Kcg$%?=4BhU1=(reNkXkwXy^wYue4*^bBYttf+>R{Rt7BgRcK z?xLcTdf_Pvb3g!rG%OTrx|G-kCpVvy5u~?xN4(3WOLIwdD)0DFOPAJg=bil+OqUKO z(HZ)YADN$DIzNfd>g`h2G=V15UbsuT3t1ILpthIFA;~#eD#(;|QbCfF%94t%T0$!Q z^!$X5KELrZ$Il$pNFxuZSZ%T>ht>|d`SE=GoDJH6wn0C@#&;JiQdoM`Q-sDD>$}FV zv0RNkV_9y7gUz1G@UL;lva6AqIxaO}J??9jF*&qWU%(OtA8!;bLOF(NB?Bfp#$#8& zJBNh5kxOJ-2jl2X@KfW#x-+GjFsnrP_(oQ_EF1Ej#0r~6cf_c;=wX(8{8`kJTHY)` z*XDi5M&C7|EmXBNdVu9r$r_Ezk~cm?`3rW=_>LUwB4ri+n%yk>qp`mn8mJf$FjCR^ z%@kvA*SSLz?SGKQUS^`>&?pmG{=fedPy(wo6RPU}pF=xONa88;(&**_j(b%4G!Eu( zFP!uwx|OFZI5bzMEQMHJnI&@}8TIWW8F?^eCu?(gNAtC&>=bgcnX+q`)w#(_nJ9ND zJ7(_WrL~x-?cvAZpNg?T1pUWFQh+8-kFnvVn2NOYRtj z$#7BFgcL`HYh(hd`(2ian8CpE{~!~_2zIg>L?(k|0*ZW;O!RX;Kk4dKji1AY-@}ym zc=LhP1G??W<^!(>pyoBr2WlIN*U{5@P#PHy^&DnopWy+mr7Z!chSt4rr)BMBz@}1F z222^LuET>~clqeq!h-*8ZtTRA8|#d6Q&WfMD>oDnpCh;MG;8s&(Q_7q=!MZ2q2Po#@dznZ7Is7&AYJSSebsSjUi zLw{>i+Un=^dtfa8=0L*bV2XZ|qpZn{knfV-F;ZSt)X=Q3)#dcaO}%LJjO8)zj>$({ zVUivi^@Oo*)_rW@Y(9}6+H5TpOl0-YyXJf4Dx#rT68E7(<-&z|Z1hMuom)>tjx4|e`nxq#J?rF*lCfuU^&#(8gG0w5|%t5k0&f?X0RR9bxrngL!W2QLh^v^FvpHo zt&`kEFO7Q=a!sk}+oKQJV2Ga+{#0GO&SS@&MUm-3>fDHDe@!O_nt7#=c6TYu0&|tsK&D&RI_}=0H!-;VJRjXV`j8%=AJ+RfhBwAj0F6ojd`&E!UC})fpf--IUv(yz z1ZCi{%3l-Mk4#u)At&TN$J)qHqH*otvuG?u3DE2jsupJCMN;2j;-A4dC!JJveC|FI|5A)}zp!wS zLCrlBhs82qOiiW~NbwSzRsS-P+2Oh4u`??|`U>pmbMR1rK4z>b909TKQQs!39yBXp z?~`6S5Yj6kaAu}PB!=MNR5~h}!eg+EM+JvWJ0^ zZ+lgx5#qML=FBbgjwF|G{(x^GZ5WA5WL`rTk$HEn)ro|)gpVd-ZCC&p|HvoyKb zn9H=G02Lq$>B83t1H=vv;Zs?Cp)58ES-@7FOr@e3H*_$1Sq9b*n+bx7$`PqD6|{&z z8$0@(zo;&{IWmfmQw{ln<7k zgx+fy7Hu`hm!bQ#FH(DyZHOv~uOYd8o{h892Oa>Xw$-YSdj^CisY|I6*J3o%k zlhjHLVeiUV02{KWjgUK#7vC~@!VHiQ&CX-^l@_CPmJ~W&0D(Sx3jpMkt8B|pl!a0R z^oW!hmV8JVm^wm(^U;M7v7@h2*@UGPZ?Ky#hSg%M{?1BP^up`J%WXtPU#W( z2qPxEmr+K4bRe6yoH-}&@alL$oIV)4`w@`miQ3Rmd_V?4NK8o}iL$VX94`PvxN~uF zKRJhafMSbVZq_+>j(gG}ufNHNiM}GyaEWgLNCXwN0udidZHO0vx=gvFiY?zDLFUji z!x=DCf$zsnsl3kcg_UiFdKh(n0#^#1d6xL1v@>yy}<9?9mEK>|a0H|(z zHWiAJ!RRfU(0GR42QMFLu=-rtBhMl=VBZen(tKIi20rf(IK3DpyAZ94&#WKB0r(tZ z_~5@8B7zL0woW%SZIV!S3PNm<(rDcXmH;@S1)8%{)SogOP5hjQpf*)Hei0Qii|7Nc z$D(Y(j8EwW4A10+t{il=O#BUgg=LFwU|(8Ki>4r%`B^4yZ8qpHQruEM(Ho;3t>qs*VZcI7MUO7%as^Ku z7E`Z|*=Xq@31=H`ExNT*f}JKEzmQQ`0!;ci^l1_Gft-+yU=&zcW_1jM`~Md81rZK0TsdJqvB^^ONXtESlBJ*IjP2y z_E}?oud6hlcWYEj0|Z9}CqGgwfX9gNGGNpJMDQV=iq`A{0b52u=w3p`2{aB)WQ+hK z3t!~}>d!?T(*?$|DnJ@Y;>(eD(-(1fSY1`a{w#N*JpxA5LAlpGTE}uG4QT+L{Z7EJ z#c<9ohkEId>o0tw7P^wdqZN*4W(6Hh$3Jh)#P*82S!%h=9i+0*mc6KZ6v5=IxRfa1 zt9{)ZZHjGZ{Y(MoB*7{2STh(1v2Bv^^BYYV{z*+KOjW+$N{=C4{6WmZ-YkNEai;;0 zCz_FM%z4|OIwi@l^gFuTr|6O1s3WDKRRZeV>r!aS#F^xam=O3v(1J!fZCO9sRGQj7|lD9Y@5_Y?uSaCDL+~XZBS#rVjL735=Br25!mILy{L>( zMW_Z<*pw=an1g~!jg={4*JL3-1_eAAu03IZ2!kHo1hxh3f#5vTTD2l0t~_Bw@jwWX z`7rhjm}eE;YBbA&j^@*7u{$iE*Gm6qYti6*-P$)%NdxT@^(i!=jlq;7DPt$lfFkHS z3}_ji{0umzfWDUf$)QliS7GU+gL^`c;tV$GLf9Wn)7ql9g7F~^87=M?al&{ES<^wK z1WTMyRu`RPMPZj9kl2W*hIn(QLlnmj

NUcSHb=EY|{||KdCg7DqigRYc=?&W<>f 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%0DczvuBpdj-q^ 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 z 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\VpgF(*`J2>{vt9oKOofF^*c8kd0iFHYhEj zB$jc^4^@=Fo{d-6LcT@K=PI>ObO0PRA8=x8H&1;p2Ys)lnlOF6h#jLLD1kV9j1zru zm&u?Miw++HuG(!(TPcVhALdhg++CK8$^}FQxEzi{q1aQy5NnjFsTOO10XXTbP~j*{ zdaIMq2=>{Olg^=Rt81C;Hhoa1mZJ({m;!Eao-=+ZHtmW~lX@bb7T^dQHnc(@!)T~$ zYtwqrV9WVGQNaIMq}gP;NF>kpV#8R~l5AdGFWTQq>t-h##EPQWFhsezfSDF%iBTiC zW5wVVuF~!~P~B(HS$b0x3pcyE3P-mu+9ZK6K zA_aU-T;{|gQ+>vS5*sW8>@SDh2D;kt4J$2z^)YcVNalEkH};DQ+LH~1Yb(sl+bCdj zfayYhDTg@+7XldLv)^|aLz^CMmwiV%s+fwQa#CX|BN7=gaDH$6zvqq!fD3MGebniV zt2F>~pAROTILme2nCrS#ZvH={B_k%^B&q)kfBHXS_&4}tTKYjV=Tt-bp&7G86YL=w z{e}M{qX&&l82f?%yrX8t+?b48#u<|#yKXFoB>uv;nEVg_iI`*Te{3w8xF?KdUoaLm z3sWWNhX3n%{tV2Vg|W!^FiYe9t3r+7KMXbaudt;2mbz#~P_R%oivNk}EL^xDDKGg) zf2`AIxgiPG1eqsbj$OA(j}O0OF!Em&()zzx!S5;=^C7Z;0YPUTTgb|%Ks*zclIv=u z{YmI%^Z&pa5Ts#0NKBxe=F@z{mF7KzZ3yyCoaK5o7>w&-4DuhwF6$+RU)9K~lE2;R z82)psH2wzzcUnlc=K*jH?2V}j!9uPE-4%P$GO;6MV z(zyrg0l_Ch{Tk84fi6;kxjr<6ngVnEFj1&+g-rDCo9jbINH1&|7@imfz^~Be`uUvT zyH+GZ3JvH~*g!r@s}&U&suc;Vpir%d!8;Hg8z5h11$O)O+CZK|wIU#HoLUh>S*Jj) zh!3S!1SPSiCV|^D4E(&Hy-vGBG>rm#_vV)RMj20;VKldBGk6C3qD>qLVWiVwK93|4 z(=o$q+L9~$gufibBfxaSXsj14y2A=f9bVv2@o?=j0+kOBT`?YR7|jjstLlRe;0gjI zrsqLIw{6K<(TVruZHkz@0eF-|}m>9*cXAe7wcmr^Ee6W;bNwwypB zIp@2&31#|E@EpUy67O%vMK~CXI)>m?>3EdqqXWQ6`mmj5s5zm|;z&>NUJpbW*&fFBC zxg$(9Haqme6n!pE$9hrvSDSTkd~h|?45r}{kNP?gy*viBD~IV+Xd5U~yit>V0!i~!2x0rQX?7AeJj<$t*8%s{39yoeh5HT$ zkhvv73r67f`0dS=^eG#hA#|X_&0xlMGhjZb1q3Un7EFvZb47#>gW-74cbyH%=-=+U zys%R3-&tu!Sc#6zQG*S`qq;av_tATT;76kjab$0=It2JfrkLsr!9OZ&l>2lz>%Rm1 zV+t#M;ctTvbRRbYyqXIsG6KOH3l;2#3<(6gb)C)I#jFyPkYa~EA7?t%W5v0xxv%jV@Vg(+x`q0T%7|yUcZ+~?KS23j47M+a} zyUBxY2Gn7!ELt&7#`1uUi5Yua5%gHX4dZlw!?-cxx#Bw<)uib0@LTTCiXq-^(Q#g| zQyBM#V}AvBQbdoXCvFv|rh=VfL%z7E%F(Bw&xZwOVQ>jLBh(`^c7!ee1ugfwI)tDSo9LVa z0rUg;5^wq?Y7lcY>S{Y#D=b;_Gu@Wh4VewtiLwIAQODxl`cBzYzgt%}Bq#mhIyFMJ zDR==){(}`4!^yso*EQBQrRp0eeVUpk{prx6eSA+g0w6$}^fvvgrMzrsx)r|5C%;v#e?Qq6$;(+M8P%afqgn1ve#DpdpUIW`@{lk|{21e~L)2uzk2edtW0moZNVyLax{X zx70}Bl^wjL$8leLhDDKK1jh-A<9+Twx53ruYL*u-6bF2rg%Tqlz`)ACnoGl1bN*LB zEtG_M0JiEA#z2CdA>jU(R23iIvILHe25EWyZM7sHi(k$ZTyB_C=ZR z5>C(nqq)2}lNd228@qz&4%0#@E;^o`H)%R-g2i0kd*0#TcF24LRoX+ZcjDMD;7pWj zBuOzakiatm&kEI$&a=0U7;omLlPIdq+>S7e_whzK18fc92K(k=2|0Su!whK?yGz1H zCUSMt6WLoj_h9xGIxgU8y`SM=Asg$6JVY1(`YZs_XJh4ag72Dfg%ldlsjz{3R=v1S zJfe_|b%b!O{Nba40_vS9-~3F?(VC_CjDDW)fD*XE?1Tn=V4M|lw7_7jo9KWN3^dK3 zni*12fmD8W0hbA$8Cz?UdQFuYMzXC*_*F+5p)0~84P#|nlXxXOavX&nrMXMZjrw_@ zOu7gDhWhAr(Ud`))r-v(n;zAV(ord<)wTru-Na5b5WFsE3M;?vze4bikuc`B$|J)l zF}LLly&WkC**uWXzuNbb$JSGfs1O`YvgnROsbje-vEerK^uw5j=|UP*M~izGF*Di( zC4GXeO=tyWTo8iwGd!`N86xrFm&4Kr>W`r}vkm#8my16yi6W*-n1+skCJyay6Tee; zb=eGc96Ug8PjNz(u|?Mv=afm$cYC}2%^pMza|=7_DCi8mz8-N#VSME&0wV?fR$ib%HjjA=fmo(#9fWOdVY_$0 z1IHFj1N5o!FW$6N486-;P>W88wlh zo;96e`%g>zciQ581%R3J_Ct}^RVUFU1)dIppXGvm#6vcjeyBWk64O`2Y~)0X`mHf_ z5+Y*YzW(VSsYqr;i&05Vv~6y(;GC1bWS@cd$w3o*LK?YHZN6RWZRdPm_z0oVR8w3 z*o0!b1;3+Ml;Rf9{NR2GbBPRd3H3lnQ1$>Y&fv?(Tufa-rg`dnAWw!8-&jSUr$1+7 z2=L_=6wI@9_4+u|A7v`(>+7LHh-LBzmD2qj1CVYBIA{Cv^{>(Oc||4k^Aypwov^F} zkv~_AMfrG8Y1^i-)9X+P*oLC`vT90)+v~o3p<59Wk_UJNXyHznDG_;b=xL%Sj0k2e z)HzC}o@}$j(B(^yQYRS6yaMKLARh;oL7hMdMnyBgTt#*Kv1V{JwPctOXwrhI0!#s- zzP*t}Vz_CZzQ2aEV%6xdW1jvtwT#lqIcl*hW0fKWkbR5I1>lv5FF}2xS-a5rDX^qI z#$zg$pg^#8(`F&_-hvsg@H+(-IQ_%qI@CQRIevqu!1Pn5@HA=}#UNYy(SNfYtE3hg zEPdV%cCvlB5Y3}Nv#n*%Y| zf=m6Dj-l~Ux-7b&Ux15vpe?479A-iMjai@@(C0xma2D*lmXEW5Gf&?S6Sog|QG7UA zjxLLqWteo3F1Q}!FhZAwW-G@<=(08!8o6O(p?}5{IH6I5dD?;4@~B%A)#uR%$sJ+( zXl6On6Y%mJ$xSqcn{y~rIA6VX$K@eLbj+`;ZnY9iTuk9iMEep8SaTkkBh+KNQ8H6l zP*pIS!tDaLBK+$X8n+<>e_VEMW10ryasw;3ag9bsD2+xVDN5U1g>3-!&?t5>pdV@c zL0bozu0bK!C^4I_3WuU2h!qYcMn!)w04pW{aF7jJ&DCK?VPLTdHfW}M0KO=@pur6X z1I0P&v^j5PHskLwGh+l5JJJhH&9GQy>4HnYf*tdCS~==2*QR6)30M?{UrpEpNO_eA zH&%vS2FA<2$fruGHI34d-ZK3?rs+LxhZbW#WiegV z3~nJ#^ejsCi(CQS%SMAu95+8mZV~#+w*^}Kt*}c8{e>=Jh0C~fIb3KGCKldsZ-^hb zD2l6R2kPj`W6^H65Ls=V?SF+puF$Bhg`_jBkhi&cT@rqb}_1 zpgkGRZ7}MphNnH4-C`%=C$v$Q%av#oN6SVQt1f>m&EE;zJWRG@hj^IM@ugLQXjFnp zSAVrtMcRC86U?{7kPC~=Jo#5m=I6*42XuB zc&bH1Q^X4fYiZJ%a#NytIN4CPPMtPmo@+P-N~6Ao*JPt353}a138!dlE`l&6Ap;c#G!0FS!k@!$Z6@NE!*(&@r#Zop zPR9Wvyjfw^Ut$9Z;B)w*x90XBb!p?jsCJ#ud;r3v}gq+!IQK9Lec^ry#QNWqbxF zG&RA+m-dP^Lvq_E^f{%7;8qLwYp}_((clSSd@?L>dI3AJJfH2s2d&mGbE18Ha__WU zSTje1XVs2;>HDEp1}_tz#}z>QWfib)PKG}t?u$`x1{xpWQ6)G*C!~Uwnm-kS^Qg3x zb(ubSh^9ohh-_#OePW1gkIi~q(djc##Y@n=~%t_4Q=bOnS=%%kZ(DQMl?*GxcY76w#wSHp&kOmnxx%X=jfe1>jNO$zl3m z8m3stTiLh7hsmTPW|d~uOu7!CT*0gv?vA6i?~uoRaJ*q!^h|fZnu(m{DDVt8XyAD` zG7~Oz=D=P6Ff*Bc40R!t`+2&bqsZAITp7vxY1;0S%aXX-+01eQ6?ZX|&TxXd?GS69 z4k0`R_pvP0#n9R%ged{lV~9OeytC!HS|PQ{7NlV#I){X>HV{^>Hf2^8gfyQ864!x$0ZH(srz5g&4X@+gkx> zR~TM+-+zQ-ho%`Wl{I)w;$E#Q(2GT27t$r`Y~01!slXa?HqjA-1Isb_R^M`Ym4g?8 ztF82~w+K+e;a5WL@ynR5gr(~M9?J=K=?!@XQf}R7Sf@D*fW7l`AyohAd#tE|(JMuU z#pV}S#Rc7eiE7+-W;lV(0dCd@xDpzd0BL}JSd8m`mLBS~QOVlPPShIM3SPfxly0tw za)KH|UDcgm2z&o6BpRSW@FYYh^bU5C)DZ{M9d8;3f)u!t1I+=fK6YqrhUxG@AAgRE z#mcn5fd2}35Rid1N7e>CyOsw1Nf$+27Q^p+rpCp zJia#%?m?OKD0x-b8}@hi%UU$gql;)n5xsG6aUUSI!U#b-0T|Qv@ezW2rtQN7DMNz9 z8CZh4;=!Y;d3lJz3n9ngEHrX2#UfCIRO6O^+y5gT;-vtQ5aHt+7ZC$*%CDV1+&qNvWCeI3qZQml-vaejI%LS!7Vn zqu-!NSg?9}06GOUJ=el8Oc~$76^z zL8G!&)w07C?A)mlam;=!JcqYbYihi<+tzx_`YaM+tb_=|q+OVDx>5Mz7ClQITE4bB zM9Y(uf|LowE>7ECUhfb3DkVU?us&q;1B;uKG%&JW_BZ5gP=2(Q*8GyI4X5(aBItW- z)+KZ^g$qy(RwTri4yIMy0AclM$BI>$Ve07zTVC%rn_Sr*{FS$9eLoEr9K1T7*4;${ z*CiTydI!MMW`w6PKS}SZ5$d*GiR!+8-9%I=GpKz4dzv!M6KHeMi1?Ed)VS1b18XDe zu!j546TPL2GF^47bO*dSf_al?(u65%u{tNWS z^!;^$alKZVekR@K1Kc;S3Q_(tc=~-JvR&@hDxw3B0nru&ak07PBE6)BWhGfh`1=}} zDMj?>j`2apgpmAQjm>lEatlHvF!OO^d9MRO04(~tggC(N?6R}G2J3KF=a61HHx^^9 z?CG>YDmN6TDrG7WN)8x(p=0zAb2((eRRg}i*dfG7S6G8FMf9f*G-G~*uClP@{qeto z)}m_xmc%Xk-G5P#uCcILKi?5AoQl0x4*h?tW875y!4jyh?ie=}do6Dn3SaAI(|v}g zAe*@Y0EpWktS-R~(~HJ1JDi{^g@iRtTePnuerQBUhkxo1$P8(&QG>At*M`MhS5~%(@v%CYg^sqbN2#xeVwNHQ=v~=Kj|z){TQEYHp^6U zw3;Rz+HmTcn&Q20IZYU zX1Ll`u4#k*>y))W)s)xX*qU5fx4JHwn!?irjL&4lszchihf35DZMF=S>UbmfiN?%< z@-!7_OpiBvU!y~27ljJZ1)mjkLbYeOf}d=iFcAq&x+%{bFD+1rMp%5P z+_pD{(dt*G!W1IHr}FmxI*FoR6b|&X_f?37S@!!VL?dQC?Cl%IDMah0Cn`ki+=CUO zr{yE89A+5n3Us5#>5GSnLVJH?qJQ7sf7-!{(gnc#iqiQU%sWy|VR(Ktpi_bw6v$`Q zH~Z;)c>Zak3|*iot>g8h9412$dw52%9=GuRuL6Kzxy5)UWhK zK}Cj9xSs%mqYo|nI0mb*Nz}1@1?(k76GOu9yuB2J4wdYSMXP$8DgPb*$76&M@3Wl2 zN(YvSTfGX<81^$1SZi23=?<#@N-;)N&V|04}Q8Lp||oC|+g#5QjE9*3;BuN1@$T z+qb;hzJ(nyV&Og3C^47(AVksw#;knYy_$Uf!-e_H`TU{6{0;g1U4{9V2KlookTn5? zVD?gvovIYH?PkY{x>d1CGQB9J}@{1S+}IYu5-nCCT!k{T;tSQ9%O7UL0Gi+2~!F5OK^Fht<6yOU}3_1p7ru zMWD`i9MjGK?C${zfDN_;I&Y`Qh&qlQHgmccHJdH^Vb2h1J!pFFa=`vW)EW;vVmzR* z*#mL|;{(1ZefXgaB_G>q`Uth>m%>j%=tc!P=mDcHjJ};1r9)Z5;(NoA8)X@SO^C6= z>m=62R+pVjm97Z6N5xhsiuGooskD_Hb(}?aMRIC3J&+dfo782~aUR@v_JJ959MccM zz{ThgJmon05t>EvfplCQ=Q|9a&2*n6e;j=MS-M{xr{g`~>yC6r9Y;TnAYA5brk{v* zvqrk*2lGaEvzhKp+Z;u|5|D(m$a^GhQxkjek9i@z`&k$Op%RW7<}l+HkuLcRX~D>20pPt67xm3q0((bfwaeimX{yM)(z ze*-S-l_YCR8g-}(=%sbGcFPSuFR@35dsOMszuVLpr_Kyi&Il3@y@vrrfeb%tptZIe zzhn&-rJy1U%+lv|y-N*%-T(dZNIYMtA zh8-@-?ttzNGER~RFAxI7(5GO^VTV2^J|vKZpz1m-5{3z8J91EnsTB^}4+pI|x=GaE zDib8&%cw98`H{olm9by~Hc^jVfd~PZhDz#C+w}C*EqO4Uy5IN$w_s2{kNR;)c>sPDHW#-V^!E_V zbx;r#`29#q9Y?>0jDr)%xFh0G+Q!x{`{*)uL|FP1G^srUNj-T^87I!Qfdvvtt zGUDhv;$Hvg5n5%tQLb@anuR$$-)4tqdV%bC$uGjj{-7JH(kgmccVkdyAYbOsx{Q!K z#QD8(?;>_EZv2Ts+L>Vw!TXyf$iRA=Ejk+0h_z8g)4iqe#!HR%3K8K0y-=if%oC`1 z1fOhbk5}9Y%5INaRZMOE#*F|Dh?ddb4-*!MWuQME$)OH)K(7`mlRfc1M9*H2h{LGj zm?ylA5@?l*Jezt)(R4ZvF)NYy%*V2TN@@kAN5itEDT~(H#NG-aVGD~8LjV{(nT?e; zhUU=nkgIF_Iz;4;V0lFsBc_EvCx3wz3@H3gwV3Yhu`$;>#ACl&%=Al~W^CbT`JTu* zvn=^P&gb7}0*Bd{j_xj6-2+S)Nf*#<{);gJ7bWz%;n5|%ww~`-(JNhs!-iflLT@2E ze-lFb3xQNjGb%Vy1p@t-IDk9T!|zW)Fff~`GQ~F1ORysfq1ql><+MJGQq#OMZ1YL9 z$y*?Mmh*inhPVKHK!d+mhO$e|rt9=b^a%CHk^Oz09fO?RkKeT5(Xh=wK&E^<))Ge{ zrBhW*yLxPOHlmF{xkAmR8%2aCbA%bKhJ+tqlj;gC5<2>fy^h#0!t z{m@xJmcMgKJD7Cw)>1=mjao(g3~m_UE#H*w7VEW!9dikFkJ2$1I-TqxX1e9u zn}}3SV{dH+1t%0+CJUHmdUB(p9tpz(TOV^CsS)b2t0^7d!+mlX$ds*i4rE#Ym6AHOPYw`YyyQq86Z)`&H$oLxY*8cCS+Y|$^_^1-lsx<9s)p|6_;4_ zJ0nH^i3NtM*z*PicTbpsV17?nXSdYJF|P0EK|eLjllP;0>KzJ{0CqebR(Zj%0#hH% zl{`*%>+=0U`R3uU{DBGO%Z#9d%?J?X%rJq z8_XPy^<-4BIgfraW28cv^s6SDMtkB+n@o>Jdi>V!o{_`iVfuAbTouz`Ip|-+>A!5U znfCYKj`3x4jQ01$*=&D@HY3E{J3Y4VI*hUift1k=viYyH*&GQE#q!^UgM3?nq`&lJ zn7#~6G^W=9DbpngGqtbBR>zqqP(R$j(c?Wh5-&9e)o~KG(G@A?(>+-5$I*3CUET;! zqZ-{H`=S8-6l#JT%z1RNIf4$c_enPxXc^OU#h$Y0v9-1;qq}DTw_jajb3&ET%QLWb ze05En>Cbpi(BNufL9qR=m~vrrRWW_L98WHj=@~)l z_2sgM?P-cLz0d=jiL1PjHM{{;lli)IS_V^t^U65ZJA zW{&C0Ac0Eyi;0~ynWVN%u+=SoC4}aAK!bysEDVjhZ2ZDtHnH5R{}%?cOPRFDKmPoS zxQ}ie>rnL})5Fkl{l-~)PEe~Ftuhs*=z_5OnpeZ_-^OKxvvholQgJ+pfKIuAN0CA= zmJi6cdOhxhD}{jWERAwfSK_YiM*7rKlZKJ%(}P-=TlHh6eG?oCg?d@!Ubg%yAHYlXA$z78Rp!(+E9-noK#$$vVlfBZL1M z?oO=VhV%l;(AQ`XpDg+srK)AfiE5r^6-Ur_d zr+k~51&;+QfRsC+SV+GjFRF?uivx`5opxK)A~_)sYU~*7P*Xp44(yDF$&(0&&S63y zbOEe*!4T|@Gd(PQn+xeDG{RL(KS3H@g$G#Q1yG?YW*RvxJi|6!U^Cr=yCeOahUuQ} zIGb&_=F)wb;k&zSGe-A=gzkbaAkMD`Xavj7VSL2{5u<;AM(9WAk?F^j^Yoj=nRlE@OFrt2Y?*RDjY!+Gd(jCFycv1e_3t2%AylJRZJ^wWzi`hqGDM2 zg0lW0pe%Z8HCSrj#U$eM@Ju;XzSCeMwY59$UmUJo>mThGTV6FTey?=cSa0-A8Gv~n z{f8xSJr%QK zl`L;viw+;AR=WbP`fgk2@c<4EI?bcc*!X)%2H|9*;JXEP?>T0uWAzO7Q(-C+~Zl^h#73FZ1 zDyAoja9z83oy`@X`R7F$9Ihy~K?xGiPHb;Z^$^Q*FN;V(N8|iTxU@2k&_N0OTp>{d z@e}1~oeiG?ev;a3gn1U)%Fqm{4CY-!9G34W1SaA%$wzgt>8*T9*de-DXI%e~KGHT$ zGpU}6>oZkX@9u}Py?khngZl)YrOWiiIBvdvVs)HOl*atsW*61iczz|^Wpo-CyTwdD zoFVJ>FRSDKo!#Wu5Dv&oZx;}J$Bf*6cR#u!Y$4&QGWt$YPL01^Kiq_qvW&i0j@$La zO>wpcNCF9CKhztiFwe)J|L2>}?yy2SdPitwyWB`DM-|f!p@MtY*{Ya+DaUeFne@H! zB;YcY<`5MUJSb4ZK(q#2SlK#<)&;MuJ>cJ}VtQ0GYHus@IFY2^OH*`BQHtsLbvB#J z=o>{N)!1hC$|v*RX<^l#A{`gm;mATG&L>DCqud8Hc=cUt`O;t?f$6sGUVSpcD7G1DUoYi?ZD6>*A`|?4rMvjVO~I>x$!&uZrnM@)A)d{i+Ks+`%52yT8k(UjhR2bh@W#gnquxrh5S4 z&yh*@6y-RB-}ED7J^=VWLU$F7(EaF;=_dgDr|WEsb`%Z5tpEG#;74EX}ivrvml|0>AtRjdSB?o$-dlW8WtZgbRFlIp6;^g+nl0j zx-#ZGx}Ha9PnS*CbB^|4fY&qbU49l@<*2fLh(4p6!c_fY8bpXD(~ny-s+jKAGX?xf z7G7L9F%ufwzY;STB;XYZZg6B#yZA~w*4yBkVyWZv-3s309}xQTYy3#%AW%`obmug0 zDZFC|d3+b1vY75edw^*GK%j}80@EEx`g8glkk6Z4AfHQljNa@5qk?ig0tlF1hMf^= zes!HqZ*&IXI32{as}rTZi-ze(EjC@vIr;-$VOR1XZpH7GVO@Q*6|6d( z4Km6XFf73O^;Xc`i%9yTyb7c22O-hpMZ?s;E^cLM16A zX{abix2&^isA!O*^f}`V*)Cj24gyeYbc} znY6pp#zf{P^eP^t7f_Jt8j#NOAgyz0GmqdVhxv0fz`dY|yFl19*kaRVfSqZuShnql z1)d)WfIeEn&zJ{7a~mu!tWa- zy+O8Fls_OR%!Oh4-4>f~!!pB8oK>bp`y2Y19u%iTSpsFrTiF6oLW1{z1kKawr$r<5 zlXV%k=)R&s9Qi`zKQ0=fpRKd$$3;2%8D23z3uuuox=Wg)2S8ju!WLXfzbFEy1iLMc z@Q1MrJ?JeX_cKh(sH8WA>31aEhq6p>t+&~Q5mzzO52mF;T6j?A_DA7d-!JKV zWI6v_e*ZZ9_ESmU8>a7-^v|U1Pg-#=pn>&6oM76m1~;>dE3HFc1P3l)M%J7XV3p~? z;#n~fUtgs4RhX_^j~QISj<_nr83e^3@EAeXc*(&d8eP2}Nzgl<0~X#y35HECy`VfO zk<{R?z`xXn4+dS*GkPM026wabB&;$e0UoC_9g9AzVdO1y^&_wX2z4=)~cERdEVG!7RI~*rOLZlD276#9KkHr){*UX4B26EsJ4Su?LkN>^Aa8-yt!#xM?8^ z%o+R}`~lZk;AG(iQrnQv!s@j-2qt_>72bo>J?rtUw^Y$Gd3pcQNYOWXCE9OMiq_l9 zsm5kR+;@b*DtdJ2x68&u5-bU%{?)FkWI7|K;o1&k%#GL>9z{kI$^+Yo~`bOsv{l3_f`*a2UQ63z0jU2UL`W7r5 z@G)iS{H_S%Y+wM_dMhSyL-1>D7F6L_-r`CwZPC!Z1?c)ZZQ56yqe}qP=jk;7MKe8m zx5H-Yvj!P1okqHBuAmJvK)Mic&?Zg^%-gxjqqk5vW1;61CPF0df9&jMZDM;>v8Rrs z7o}G@JHNqR6}{GJQ=jGa$u5A|q&M)|GZ)gu#UuLjmqETGhDU$tz|KM!TN2_KI|~@# zE@MW!Y+}#ah#5VQwqZ0+`_nc;pg>LH&9qHlig=;`$4O3r9Pq&RW#n#*p?Oi_iR%RV zOIqeN7fHbtDz+!u&?{L7j2xbg4%3A?<6R>M{)5APYBqffkPG2z_S`L2ifO3Brkkx4 zJu&Xz;jQ#oN58p{Znj3`)2)`r7H);YZbF|TxTi;@P1=s8c6Qhn-D>4(AtC!|L=K+5 zwKm;q4bo#BkjC9^4brbs1LcP4v5t87(L9;%utroF?TC029{+dZeyhspk!g7R-`N^x z`Xx|JgVwP8G6Mp8li`gpdVN{>apP# zQ^0gUQnjf`~;>)M~Vu{wkNGLs9y>MWA%-0W{VXr+r1xH^Ne( z$BH~~cvjoA*2WEVeRvz#=7u}njXL&t3|<9r`x6NOU&OfRJ6xm6=#KGd0@VB4V2Ll0 zc^R*vrLfDq|!YXq#sbo?UP(GjP+EsyCwlwrEp z%6SY~+gL>)4XmjOVf=x%sdV{HK!}rhiwL~5Y`~>KG(x|ya`b)a=oi*V?J^qdh|@37 z$M^A>>6ccnng;OOtfb#y{v^S))5=kIcqHws*8P@(!)Ur=!MVA?)A?!<#b%Xm7v%$* zHO3ZQAbJuI6X5Kol40;N#Y4jwP>8ifzlBzwOE;v4WMyd`VK8BjO%b$KP6ikKEVk&* z-chxP=@G+IXEEK`YpX>_V3S=o%5(~zG1$B;+#m9u8_~-I&y6rq{nQY{OR~eR7H_(u za23hXW04g1xln_Xopzh(u=O!L3(ZS9&|?Qq@OGRP4m%_I2>QMOuh=m;00HLZz5M|C z&l>2n91~D)ab4ua1!Eh*!sqXwg|S_|RS*{VtpTIqY(~ugcneo&W{<4Ci%c2in?0Dhi+tHTr9hFmaQ)4q3PoMe>)$L2b`OxOl_AIr*WE`$FkcR+ z4s+pFu>+fh^T5AZ#8d%KkRF`}NTXVh7@i>E1*S%M^bt&q>!PIRDUZ&!@pBQdh}Lj* zupn*^cDaF%h24Gt&qOH9J`Q;XxB|FOZH6PrnR2TE>krqJTeTxdhIay};`weMeaD@9 z&kI{DM~hjGX+{GidNOpm?O5B@S?s7qjyel}GsGHW>Z}U2h^y3DT%i_KxjD##mTCg> zeRS^om#}r#tz%dYCu;CRt~s);mZmW7b{iyF>ZwJdZqL=6;Y4Ks>A=TzTYBfeCNd^R z(Z5DuzJIG>7?H@krDaAW5{V#TOty(*m<|3ihnEsW})@hLfJ37E;l$dj# zDVCqgr1DwV=7J#q3;Yk9zzG$+ENk+)kN}!WFVSs&wpp&z1AZE=TXoLPgL3vAlvDn! zhQ+TnU;wF?0gqFP)X>6nRa2^NMfI6CoNQY?6)tAOPTJ~P+ZviCJxpWRzQ2{s zhe`Bg&qQlOeQn*OZ>jKcFn>jT+oU&q5Kihk>uT#7*G@KC!hVt~>Zh!2fALO{520`f zK{u^kQ#T#8tCO{>lWt40F*RlAYHF2}ddk|bZEip0rAm7BFIC!b-b6_Hx>`R^zKcOU z>HNIyJlrVw4hpb%Q9r5+XV~EQWL{Od%FqfrkX0M2p%(LecwrNKl3m0FaVs zrHN$A8hy_OAgO~w#_6UfdPvZ@P0e+Usro{v0!4Wb2|V6I0vATq3loJN5|D}hTeVAw zqCBL&JA^#!KHhu>2uPvy1a}DeEbR`V$S^uu>f7qp`sK$%?&BWc#BXyj!J(@QKUf^F zRNqiHeo717Aw&%0;9zl>_qL=!TJY0%Uyb(Aub)imzWr1~edGE5M{I5Fa-;X>4*3zo zSXI~7k@A1H)vaj`+KU)QT|-k-bE2tjwe~22oJJWxe!=;7qOPT-QM`FX45OuEO?z9N zP-Tf>6#lMlYLp%dvk}#)z_Zi6)XqT=?ZIiwT{m(te2<9(20kBVFP`|hVj1c z$fxDqU`&Ur4u9)9#LfW>qqV~fqoHXPejScDhkj9{%OxF7YdIXl)UVM##mfz&Rqh=q zc!XgzHZ`tjXsSKGzHwDJj&j3Tv$DQ1DT^OINc^y#M*y&F_PtdTR@FxITD(=LPHR;6AL zeI!;|vUat!nXfHNrVNu%dwxr@@%%*7%9VbDYg&?xt8|lo^TCG|^=-aOBE)E2Rl6qH za(-e}OI=-KTU~OMP>KG~+EG_0tF+88n(G@I^{#^MR<^b!Yh|PI+ip!nD^_S6&o;sZ zhI+}y`k=|yt~G7R6+RgG#4IE4vt^cHG&i)bs+Wnuv={W#TED6>nV*<4!)R(Pm`1>y zY-nf-dRx<+Tvb#+SVSBgmAVRnpPE1 zajI@*FvsJe(-%(Q?x;NZBMH!(TI7raOq#s?5A}_0E%lA9^|gZHBMk$KIM~OPVRR%L zq$oB4IRNDnq1PfVcP&A<<)aM)TVPoCFvD0=ySlFSe5@tEv)bl%{5l4o>KfZx^0U#} zoNR2Z3ukp@eW%bAN|#~VgiF!JrG+Ki;AX?SxJ8@?Aij_CgCaj3Wa>WUEmV4G(g3;uxlTIQ+*wF zEw(?(wNZwen>j-1c$D`*#e5e#uqr7O+L;J_cpHokYQp$x1+^n1)Qj4XDCpWyql6Gn zS3F1z7r@9NjN{^|35+q(a>@6%D1de$`b)=oe0O=VnlDCZw!+5WH-_3d2M|N8Gww!x z%ayz^Z7Wxp$GrXi@-&Fk;z0nlmN-rGT&lO3X8UrOsLST*Z?j{biWq8IRcblYQ67(r zp$2W8GQ>zA(<3pD(&q2xJYBa5fz;4`nHQ@RTRvEFOc1#l)54+t-wLF*b9&h2gg|PQ zATTXT?~X$V2|phQq?W=Dk!nMi0v5S&3?;%h3yMkWY!N{-+!Fx_q4>{wu~nPQuD?N0;%mN5Br@1 zc@*Sl|II*Zm2U?UG=D6MXM}D3SAo=qs;#Z0!>3IMq;{?ZQakm%T!H+|m(I;utlJh* z$M(QaM@^WkOvLZVB9^syL>u?lQ+`reY|*L16od7Xg8-3G*iLYwhFqQXQHYWuqfXS2 zqq9C1W~qOo^PtaUjd4OnnAVO4(Q)RSt)WwM=q&h*sH$OyA}O{g9pA`VeorKHMv1v2 zU+`&hgoTo|Y>h8fb57XY%f_rygtV|ka|Xjp@Kc-sY4rCFaYD^udcw#deRHp^=CDP( z4amgJCDb(<;AzLXVS}%uTQ!%i?d^wzb`I0ih8K*(_-DV|i8Omw>GnGyNQQ+u&Gmdz zJc;Q@sDPY^T>@Jp^bD)DA8~v<3KHe4IuY7D&5-G?fSeQVl&Y}E<@1u*c3*EMa5G^ei_jO3V_|RWz)kY#}THQ!yNPc6A~_H}2n2?DU|R zS@#xG19YYm?3l}F=7>cf?-#6^{{}RjO!i2Xg(%$ELb3=$4ly@1LE+y#fnbvXWrI6< zM*~9J`ze1W=hJhTao){80iOLVHwL!+vU!)t;Hr=mvO3Ja;=!SGgorIIpK@&%T zKC0A7u)0o2BeTZN?TKT{E&@;`NRl5BmOfvMNL1ooY zc~aYb!K#jk!*`-}U&WRCVyce5Xe+%?0g5&Q=tC%XM9DRAG7Iwha4D*MTUFBo zGMZhA=_hF$ZYrUy$V~U9eEa& z^~w@dOuI#2%G&Os-U=H*zk~fm?rfrwA98+^a zjmn|xK*Sb_$*QB~vd(6@D{ZT}Y$5CoCzNB+jx<~!DN~w31T2rU3P$3ia=bX~xS$P? z6$lNNUi?XL1=lJ?`wjTIOelw*OUJ{rBOZ?CK}{+eh|Tx5Sqjmtsio$ zR4K;QYpYU^ccY=SZM#)VRVlU=iG(kzX1lmbRAH$NXZ#ZZ8I;$CqsquxC8%nMD+>ne z0w`tyO9VLX4w1DjU-5EO9FMHM`LEmXRU_*unJf#)`fhI?SxZtO3_nF-HJ9#zC8M(F zej}yk(p`G4ACRRpm)^$4T~(!s?gY;0!L&{HL_CR(h2GAoAOm*h3ljOT9!S};Z4DQU6U{L z8!05wbIf@}U=>tG{pU6&+j2Qu&5v`!+A4RCEKXWnd1XB8Fz!s|DdJViw|Cl5AA;~D z_D_M8aUDv>nQlRd{l~Eq;TP)fm;>2x6HK>)Xan@quxOMR4g|QCyV%ZlCpe@1H=HN6~e12H{84jK> zbc1FF&>q6sY0-7vLrl-W#$Rl}%+FCz_-3hT2wV6aR3_D28t9TYi^cQ^hi{f``EoCy z9Ckt6y*IJLPXyo?Yr@9f5OAmrkIXnlJmC)Y;ZlKFb9gruc`hf^=b0|;hI>SdX&>c~ zzNFj6DWNQSoyOGX>9TG+rY!R#^a_o*lE&hF1wK>OWjMQZ#hzQLcso1l^A>%*JEJW6 zEo|y(O&C9OPE!b1XzsX~am$Y~086O3^fC-^0LA6S9%rMNv)4Lh=R!^eZr%=48hgtv z0iUS6*>86)O_v8LJA5l=c~gLp=Ab|+u9{iiavSgFgesvA4ysc6f{kCE{G!7=@m?Fw zF>VAkI4p&zD_}c-TPpQtQmTZG9#*B;#7dZE;OYd#M%^f!+D2XVFYv#^4!+LwkophV zSgqk|HJ2WVphuiGS-tqp6W+mSABm(?DcuR$m(6!B)2~3(s??&vblk-*n`IvoS=e9f z4^D%ft)-e{SQ-S1WxN$P^F^Rnw+hGH7nr_}t0cDghp^Gfb2I~IZvFwhp)G7>OC&I= zz)}X%!9zf=*e|Nsp>#h*Jki@vk!Ewa?BK$JHG^73gTf$!U7EvIpPEaL85sFTZA>{Z zi>6={>99kk9-dV$JDaI&5H`mCrQcyk+v7{-#ZFJ5U`RQqG7nv26eJSWx2|z*jIfnw8W|D2r=*)PSq`NWIBvdxyqy$ z(s;vdhwAbVaU3%3!||YB6QxkHSBvNheTcq>jjR_J(i)V%9J#bFg2M)Hk5ZPm$6O@- zj)SI@{njr5d0^ln?;Yb!i}E(l#u zZRiTIa0Lz^HJ4Vydtxcm337#40rhqaX)e>tLbfMh=-MYEo^f-TUJ=?|L|;i`E?yC$ z{}9k05Hem{Fuchjej-Rg!_?d#+#L3<3&FSrVBj%!64PZE4m+xpUXOULI+1>h(^{QG zAM(JjiHno=Ql|gJ$qbj$%WJrXvoP2M$vftvcVtcPLZ8~(akzml=?=Sm7G0{j^ewq4 zlp=VpnoCz>-sz8qpyBG?xc&g*WBNvKoMpBC(MaLlMU8fqnoE~Ul^3z%fS0fJ_N!9g z90$Xg%XCFh@pWU2$~-uKl2v2PSXw3PPPQN)U+p+osl7i~v~$uS$oEpgwRu54e+SJ{ zMB9NkAZBO8GktCH#rZ-%Ev&pGpZ{~*0dU#?U0AK=@!>UiZ@$>WL9qkD^eyNO0sA#R z3iA~%6EP{y?x?W2q*Zvc?zKU#dWLv_qi}wWaD?AZ3sRY$!Fy4PTf1@K_kt@#z1W&8 zn6UWwi-8S(ucu+h-_B&^L?nZg5Wb-h^H{v3h?`?}T*iJy>u?mzME`$#CqIp88bbau zmEcqW2go675HtC!2wp-K4%XAyq8~uSqfA6HI1QT!y#LL$=ob-Bokl-M!$vQX!lw+f ze;pY+INLm#ei0d&m~S4lAI(Z#_;o8ngYZJcWDcv-@DK(62$nBdb((n!tsmuGSZJCp zg0szW)tR)$RuvEpVRLuHUPqk88SGpP%8-j$v9G>b0o z?N=2{Zy<0qo(v@nkJ0KhGE-4t7br$%w~ZFaQzc^E104O01M@Vd(>0oT3{AVL1b4!O zI*rP31h>KKx;UI({$Cusr!mEbT~1JX)Jnl7j|r~+n2yHXbQeHdZ683fV`TQLYB&lb zOgPMNI*uZy;C6d0ayO zi0P|J;mTTq2)G!dJE%(Nqc-p6sP3yWrVe$lIF0F#hCH3X0#GK?icw6uaD_3cAdb0o zlN^#&nk@baj1ZKVr%N2^sWa)@y_i^WPG(jTT*~%j(E6|BO=X_EU!6%XfUMAy&(?r! zPAKweFg6^fwOEaGRd0WA|Ju9NUyUD@3mR|T%&zdiFmX({^6^&OYUW@iSx=ym;=!mo z3mz~&EWR)x<%F3~V@7?Lj_|7VJ1T9ff0I2yeVA^-#s*r$rv#X*wU{{US77bm%8pp7 zVyoXM#D_R2{&uk==L2|_2y@uC--`lq)j${4ETDo74m;qxgH`FuwN>KALn`1<*N|?x z(+-|dJ1_QIUQBSM&<9Q_u3c!&!D|VuoW@c30b9)U2|R%kuB%R>`(4RwJvn4sQx zF!;Se(X&Y#Gz->N9if7UPvN6qzJax86Ub3ty!94jLSDEBW{zvoEZvX>!#%tmoy`_Kl!ll9?6|XWV^Ai3&SknbGN{g`hthiRq`-WeYtgL{Pn}JV zrAKuFWgbh9S_4tsbo6_IEs6{RTM3t=eQ-~>ixXf6Sd}qSgUY1(xH_Ar!(mYiT>MY> zHi2!qogJ}9UJW*()rXDF<-{_10GkUTi@}nz9O~_t>j_RyHJ7#sDPdH`32ZZXVc-{_|odL^qt;*bvA8`z_COe+hcJ1srg1oDA(g9q2|(WjGPSk z_TD%~^l>~6C>sH#T$Q$wHEiUhO}(8u>I~k*33aaZlse-Hb*^TmRn;URvII)lEcKmJxeZtJzxIrJRnz&xI|^^WQkKTVg( z4J+ZouL@9cHJ%If2i)J8HbqkC_G`jprx#H)TBFXTE7L>jY`P-i$+g}=nHR;OPF9_* zdHlGf1(#Lhn?6=PN|(x=a2IET^|v_qb`E{T)FG*OJMJL*)o)Q`I8$wNm6}T_!PfC| z2~Q2Fb0E?TAI)_cP5>8khbt;@c+;~35! z8t%zp3T`w9bsUcKn0Cm`AJvvhf=WD&W91muSmdqr}{B zQ^7a0Gt6Idb76koTgTooUs-gwJd#AW>s*?&3NzzTySv%bg zR6=(`BVkhk#i+PofHcr3n-~Q_HU$DCfkq>OAd19=EfMnne9v?5tqu}@^UlcpKiZ`3 zvoGg4&w0)&XF~40hs#kzmzBjHFK$V&N&kKo23+=l6=KuqDcAVnxx5suNtF)=$y<2Z zpUONT&a7jhS&W`!T{p%#I+pZY-V;-s;?zGbzdO#^zkKFeKQb#{V*8Rwo*3uoSkjZh zmgCeq7WC2_uTERg7vO~?3%a#d=shx~?@z2x%mAErT`~$P{1+B=;Pb6MUw1P+Ud?u~IgPUurcxkY`wTgPGDewMz| z*6pc%-8*54z0{jtWd6){w_bty&)q6|-GA;jm#$ho@11D#xE9l{{&P2-0>e8o?jemY zMJ4|oF$Dhe8`IZ@9UFOPqLyXtt*6ajJby_``~1oW(Ox%uv{a4W+c+`DG$~cZ$5JfS zzn4tr^w!oh(o$IA#b+kA2q~qu47meZN9JnQKy#OxzCS7R7gsPaJNC3znjK<=na%K( z+R6D(tOvXm=8(Zx-ePNMllUWBOW(j-Z7qH3AKO}DvAAH74a@CsYq_8OQu;B}){;c} zk8Le$BW04z+1pa`MV|k%sqLTSEi1E`A(oZdOr~}5JR8<9?6ADyAcmo<+ALKwHPb>u zpf79pCWpiuuavxrrrC+bT`g!`K7ZbRea9QJU)rjrrFjcmF&&Jn@iQ%fF=frT5$n}? zLruE!nb^*AL?*HGB&ogCY_sZ8zcgyUO5R>pnRRLkGQ@TV!&84S<9%mDIe>daZrWe@ zYtK1I&{A@-olr||7+*_1F-aaJ--R+=k7rnWDKr(dlsimT?u7;D`DMfVv^WReUA>0$ z>}Q#u#$F@7O-W1Akt$!6kBSE}%Ijrr;QceGp>%A9fzfY0C6Yy=ZfKL1WW_egM|oLc zE;0!fy0|%#QK;-UAnvW_p|!-GTlqfg&|Ux~S(s`jZGCUr@8@{TC;jRTJbjQ$qH)mx z6XcY}{2y>VszrNGl~h)tZB~+{%?TB;>#x(+Om(MMKHH7|be?b}GrK4$k9FEci35?5 zjq90ACE>;OPfgp{#fW&2^|w?LYGmBeY4&g5Cc7=kKFD5k8|>|#N?6NUno|%N42oJ` zfa7VLGg*@AfuHy{aqDAEtKJiNqX&NVGWbAa6-JeP^F-!*6kO^YvKg8XbIlc|GHB^cxy&cNFC9-KZ}MKd^4LF_+3X`6~a4RM)d5-rO=f&@HG6wr$oP#x;sAWh$(@xZ{td9rwRK?s%kp zfI0R6$zD~wIK#Lk1Z-^7P+Kgl3)_i?QSugOJ2tIvdW-aTdKF)h?`ALN2dL=tIVqZ} zp&PBp9!eyHgJ^012hx!)`MBfK<{nGdL0q33>$S+|Qt_|RTu0V!F_>_|*2t+slI&Fe zEpQ{{{jdwSqqUI$_O4pPzoTK@?nasaMfpuvV2TXJ-tU(4%3CxX%3pBZo? zWuSYr|A4DEQf}_9>I~~e?nh$693Qr^U6%!rN2&~TZ-YU-1K?y`*n=CpoAGdp_{@Z2 zMJz$W2h$-NhbVknM95;@_8K zBR4^Q+12a|s$7!o;|S!AkyH03FXFZt>2jV2@)^3E{k6QOVe+wp=XGyp#J|tL>K&zW zo=L?<%4?avWJP_FQ^H5NQMiiL9$#lLUQyQanNH?E2UoXE=^V49IC=ea%tXneLakmqudY$m$incNVxYMuw-m4z}$M7^)_ z_10$Vd%2&s^z`zKp%FhezYd| ze>heBf=GyCB)9ri`+>pO1eNx$uwf55m=D$Rdf?W&BV> z&ybpNBW3(}LO?Wl`g69HjG_Tof?N5Xt)+xZJ!L~$?Pw55k2cy}CKD(pfb{qWHf? ze)>J_p^W^YGmxs?R1C5Om|zI=_z}<3aw82BxH{{?cpRe=zqHiIk0;=6GX(Wt4Y*PA zpUut@)N+(puq|q@bMhWo`J-^(4TW%I1ge%Yv<$+s{I~|@o;&Q#-u@yYULGZ8C9h$X z7>EHE&+J5{xb5P293uBae?r{=77V>WcvS>VWko+Ii@SeRk3yXdd5o)LEhtZ&0z$m} z+yW-$a%xq^S7c2UIbFK3n=VQFd7gf-Ob&SxF>P7Sw$Zhi`R9seEfxzts4-ByK|>+d z>A^_%>?}!FHS#dcNXi8*$Qe}w>@n=8ba>z_FmQIUpi&OxFCx}SrkH5(T!Ux1raPz& z|DjBzvbnn|_hl-wy7?59<;{S6J%+z1pq?2R!q1GzZTfUNp&rPOUPbAnp=^_(O9ORV zM;X>vXyOn69U&8*t24A0NcD(-q>x#JdaeOivg(*#FyEmC(E9NrQBpbVWkn0OHjC%b z#l57fDnHIvWJR+qYo0>{mRRV~|JiilH!*M)*pN{ct36g$c?CC^Qc#l*eY#%)tD7`d zzASBQkj8pBgzb=yCL9Hd@ zdXx0`Jjfl!;^;>DwJmE{U)J6RV-;{9*uP$FFeM7mtSLz4WyHQ>qk^GjOa-B{Zcx;` zc-#}mK+bFiBK@A7tFq9$U66lC;?zlf1y%gvQ9UaiV37!@67vK`jZJVhXS>OvI17>E zRDPM^?VyUYd(2K%UbIVKo`weD?}?Ov2GsL+OrwTGhzZM_viv~`ExJ#`4e}YTsOPOR zI^v({u3`}_D+3F&X2nelHVTlhv+gv?uwp!%{0kt4ng=2J>&y1Oq@i?HWmc8ws`Q{? zRsOxI-rAaDvX+NDP}2eqD9NhkT$C}pp#CqUoA|9P#wL;-;3hNZ;!zng41NH01;8xS zN<2>JLG07*-#R<+1r23s^Bgl0TV?oxE4nREuL#@2?vPur2C5*OI_yZ~%qq#oILpgI zE-cgE&bCj?r4yM`D}oa?W?Ifd&a+b7*Y0G#7PCHGMeF}to!Cw+G^X^>q! z>AF3#EO^qow}X>EBtWN%NulIZc&vzaF0g=26{9Sppq!smfXdp!eXT|YffnF+hc>jT z3U&tok0FhnMruXUy$wntsyDN8qSVAk@1g#5l+T&&R*zM+zHVdyazKs0xmGvZMG$hb zSOnIfx1izd;`Q~qz6Ar5*uT&2>(i#*^b?M61nDM#U2g#bBTj;5VV?HaUn~^uu0k$w zdQ}dqNO#ZxuJQ1Z-4hTVid$uDgl4{U1o?yV^Wsz)YcL_55HWei$?gHHBjxFiz0S#% zE|SqvAS;5|+RGJZOXo()lN~_pRW6d@Q6Qbc>|T`0+(>!IGOlrvhK<)W=EvzAkav!- zKg>X|jp`E*LBJVzC%wjoTqy{h!jk0ySCnwrqZ{}Sh(=BW#WxgziGqfY7a9uJr9=9v zi)2^?W$!&6c>20fZw1{*+10VlIoaeQ85zk^WFQ<1`HyPuA3XF;xjEj9F>;7k>(xSG zeL=daNsNw-wGxx#60#PZ6K>qSop7nfU~h&otWSb~7^Cu45C|!|J7Mo|w}diwK-vOw zzm}^&>&OO5sl3wJEbHwj%OfwtblZ@N8swGEpiz1tfL^m^${IEU$MPvFotNLzAIVme zZN;qPrEDMX7tDclhcoh$nTK>eIbzdQjbl&3kdboj#v>g}M;d83Lw0oHDCvbx`?cSI{W;@JP7XX4Ibm*aNIN{8w9N z77fs(k+RCyfv*Qr_>w#9t(}6{)0qUuK4)^LF_7-OR8+Jj`DGXIa;0TnnXAZ3K40uK z@R1KjiDYZjcT)iJz8KaU3Wg4b_|6}sVZG)HMJVg~^quy-w9+Hl@kJwjpzu9Z$h#wc zkYF#tu4hwOWS7eU*yn8{BfuRRGBt_afqFU z<=~KEDt{fxSZec}v#Yw`hRI#L=;sG&B1oBCRe29(u*5NEAH(~&*EDu!WBfP|IH58t z2s~^NLSKRcP!*V!KTY88Y7_V0<3Qw?-da2FqL;90xC%hl}fU_IUzhY z-5sz0Sc^}@(3xGVs19pKp6hCs%L%)tsJ_PnXQ0^LIP{%ornow~hR0mW0O`LcZF@p| zOIq8#hD5CGSG$^JgKzs~FwM30iZ=e3y&ic3t=Y!h406|Nh#)w;rd~r?k@6Ta_of|9 z_fMcAXsp~{oT0re30HS)-?Xt-_IO{GF) zhE&OY6q+PwPrR_eS(45)Ox20^rz3jA*bCmJ2fMzrAjh(`^6!BgC#M8%tQ-{;-8lIV zpmeNEjkpD|BnQSr3!QI-X{7l>3R=3+2*4W{NM26bOk=tCXxYDwB_Z=HwYVW@mAJ6>VRa8B#--JIr~1n>PDJ*Bblde zb|cMRf{)5(4Gq@3I`|D12kPAay*N;@%8}Ov%(N_Blqer9HQ7XFL~dNHO(chmPguoM zlBDrIX}utAI^lm++O%NtX-nVHa4bp8nzs`?Eye%uX^F%qE{VV=m&q(xT$#UehzIdB zsb$Hch8aT(QE&f(toQC1s$9;nnD*#g&H#Ury^TfDjkaqs8`)vg(Ca$+XO;X#-~Op5 zgG71Qc}@pNO}eWD?sd^R4J}M@#a|~Nr-lN!u-P zmDQUI;-W^h5OUL`W3P;?$kOJ5jEn*|O#TyNz4t`gXurKZ;?dwbMqolL22>NoghPih zLqjo$<~xmtP> z7E*wo5UACd#AUtCGb3b9^Nj&_zlLiYpD5Nx(%sw$`SxDV*OP1?mDqMJ$rhMX-POk8 zoAvckeNnoD#`QKbBoQmC^$J_ho{NPXhw=FTy~DU<+2ZyEi{>9Uf8~O9v}ym`GT>AJ zIO!Gjc?fWS>>bh8zL37~W2A zV?51Z=ZOD3*opnGFL@^e{(xp%msI92d0U8yM*v>VW|FUxX_xze1KErrcx!=DF>vgv zEu^VDO3s`rw#8^@JPe^ip-_O+ZCDNqIsXF=b7wVq2(=BSJNhFq4_gBMfU@`HK|ubG zR|1??kI1(gcrD37cA01Wlph9np6v z^&nBNNLY|#B6J0gBmi5n!U*4`EOZ<#jU5d zE?n5UV!!A+i{B>14Vp~ttKtP>O11Xmul9LM=eM_=wxliTFPq6MTRMMe2rg2%#nV+- zws>Bf=|D-w$kMhWW3@E#jznO(yg%{+kAf38Eynve&eUR047A*&VFckL*-BxCw!k~z zE;)SO@tz7*mcU5C$Bc$fQ1yv@uy6d!xT}#e(w@vt<+!w~ktv(y1CbkPu7S(ih=h)d zERXLAb5choWYQz$Us9LB8{ZSRJjy14Y{bx+*RSv1W;l7bU4l>j?_GlJ?@ZvwrSE|4 zTYAXfiNHj(-b*+zzq{~%YX#i_xzc!%M|3}u_qAvPIc3^r1jJV3+FGHKd*I3 zpY*UhnpvV1=brSRU-ckNyGLbe8{Ba9g=FWfTNOGRQEeqel z(9*GK_MiEbv&Zjj{AU)%UiN?Pp{GbN*=C3rxKr|{( zoz~j2WZvm-`-Gf#S}WvR7JHAm%M$v|?D^yLxvk3FEzVkSYRkN&SiEi1-*!1@gswl8)Xc#3V&PHgOTi zVp}P(NSigHgM63C42k?nO=3z~@vK?9?&K=rVoMg=EAth_P8)O>`6Er!`2NE5g$t)TY zNR)nu1QO+QXdqFJ%hHy`)8avR(MzeV)$X@AG2ZGSGa-dArfGpCt;^c>8@gq;wWm$A zE^6b^lT>J3)V9$3_h93o!{@awTios=2eax>rvG|+Uw(Qh}Eva-i z`P?5m+m;Q%g#4@l-n=FATiWNBz=tW9(7eZkW9*3*`>EQ(tm5?h($wqWrYi(3}WFD*TNK~lotjM5|0duc=_V_OxcT8WZf z_i;)L?IorW*d#_|_Dl4GeM{m}zt1B)ZlNiLWEf!nqGa0^wX_Xs7C4L(e3L^G;^s)1 z+p=WIf)pxR=9+0H(mmpW#~m%2pUimC{I@WNNm$&vcN{VLcGUT)o zZJAHHUa)vki*b3Z>8iy6;<;%fWjn$l3D9N%Idb?gVNtsY1i>cRz! zmnFT%6^3Hn^xk`G>f+YL_yUUg&Hjc=Sm1`yJW{VA*?7E5YW{B$oNQR?)FtziDJ^ZA zZ%j49_H)NrRf`7Qda6BZlBQbb%}?+Ma)2Z`g)h$a{PrcQk_?YoOLJRM_IOL;;#SC| zCG!^>7T|EqpSNUwd#Pn;4;S#n_7> z(=iHZ!Qx~!aa}ZY=lS-alk~MJp@IIW9O9KBr53j?Zfl#@Fe8Dh_LkFXnAv~SXc3@b z{?hjSkU6(>X>seREoUS!K?<{odud--3(!pifWpvYW(hGdo#Zc>f7*i9WHMG@zooPk z=%l0z+KjI_gc*yMww#*eEo(p3uKjE#v%F>DX-gI?t|WlCyt1^lv|ur7kd$72+WdCl zvvpBv+2W4}`j4_567YTb3^MPlmdTp+--7cscp_jK43~k3%yV|BB;- z_~=XLwX|6dP0edrT=EaO@#*D3!4WZ6epf8J#mPb%g10|Sb;D&upO)MRm2c$mEYX|a z!T;P}x$zn%-ZamurJrx04{*7Y|L} zRZIW$(Dcl6X}_0|Zsv)JKIXJq=I6*vIzO?N{)M6GZMF0-4Nc!zOaIEy^!{4<*Amm> zeQ7+vanGki`-bJ3D(d#N$tc=t`CHyD|Djs`faNz9+$f7(b28fcm_niUEGyX(Z|^Hz zHT|_;q`lu2*QR>4J8&na831Q{(- z&}5=g)Ylr(PD{VAd|FjaAYj}tzXZjA;rpFv$*T=q9WDv;*B1TqN{p$-Q6YDmQ>EEl22m zM8lf=H@^cjOc}n9VPyy9pD;EG(Q^-HCpuOeUreX4VYnB(wYZtGVRhh+l(pTA;T7db z;C}sOOZ}3K@MLl2X^zXB>TH#xO*GER z$Ig;av?*w~!4|GX{wc*6nU~_03-W!#7@!u6{pNWE9$B+(hRV3ejgn_Gkt@g-uuEwV zaBmbVnXwn%v=`eL?RB%{#_qrsR36Agd6cnDO9>UkQc-TwNbXzZ*O*oIVn@tRXlICO z6C!wY;!3m-s+3vY5`C1^ud;MEC+oaxcQTDSxjZ|b4J#*k<9?Im$u|HaGA)9Txi=3H zV9Jg6bQ;k| zP>?Tm1oF5q>3eA?xXE()>cABQs=k;>4<${g-q9sdH$pA0cvZZ%HLKkvUTWYsayoS5=%>)OmGf} zlz{<`Wj;#R^kEy9ECm$JR!Y8?0Rfij2)mgQ4iHNk%HIZVru;>%=B-(8#qw{>rdgXA zk=yl33YKPhq+5u$FsYJuwly0iGXH6>re$w7l+3y2Kh`nes|{LcVrp3G_&sC;$3%=~ zzs-{+L~V=9lxYa-Dj?zVtjdj@L*QDy)wr7~XJ;c8@GQ37O;xS|Es=6|_ckp0W8B~W zT0G9FG6?PmLzLGgWYX>=w;;Qih1XQYL*bDs*LK>@Kbo_6BAdH_i--LAu{%aOs~A?X zbDu*M)+O7f@)%y(VkLw!hR*@W(an?#x99ifwRC+>_SAro9JcO`UC3nyGSB#ekg2laKSl?#!A0`G8rCi}Ao}5&{eb8S zCe$MR5-D!#u)+o~nb93Kqwo6}-TKbW=*~3(v5)2Qbol6>vRcn#IYn?spnvQB1HaA> z{PPU_Y1Rb$Q2^B7Ge)#3w@5{c1veFA9~mg_a|QX)hXbZ}S1~gD#OW%i-2UOJe7Ndn z%1^u4ZV#Aei^mKdENp3ZO8mbW1KQgjrD^-(+=BcZnBrcEV$iok<3T4>>BI;kKO-R> zx26^AwfR>sxunYE{jv+9Ftv;uPqw>g_ z+6B7>OMOh!EAoU97)@@b+-zsbFYY(+R9frwA2uib$CvQk?7kM|YTU`V1f|FWvP#IlU-8J*49YtYfVHxt1>+lJhV*Si&(B{4vU)dGN6TV1tBYR&< zGSYSmqT{uWW>=7nF5=@QoByZykT0$YG`XyqkYThu?jrD6;Azvc&a0<$8(_)iTh?&B zc^$GuZf{p7PYs<+DW1q3sCR}IhK~F6(d_?}TKZ9Ez(MoZc7Jyone;@>X0n+~#ua2I zQ19>IZ*&4>9*Dl>(u;Ugt?2hj#t)i{qOBD14RANt^W1Z zLk^S5cWK^Dm3{usJ%@^1DR0mwh^m?!#HqwIZz!@UAFt5cqM}_%Zb5#|lGv>dxLj)B z@@oT^?{=i%@_WbCeEymm27Ju0gDg$$7)~4!cUX`O$!qR$Q>eu^SuOL*Bs1TT5c|7p zxz|{3qnj*0haPPpX(-*?(1FC9Y|Xm-)Y>ac)$zmWNmi5xjNDo_da{|1UH zywr*OiKCN_<%?My{%kOJeFl&AeqP7yd#yX&(5PX-jLmru)x(tFBtaRyT{>jYC#$+W zee!3<#+`|5W#ys7>bW3(kb&%mmJ&IR6=%b|&mYM7YqDf>vdMNcCF0u`Z|ZYL%D)oS zX*!OeXR3TPi#O)Mu3&?2p~V_oz$JdekcR%6SAcY+#O1hg>EM3n-<%~+t25W4QL&xX zYthKZv8FNJdFXJy15 zPYhtAt@$19D7g-*oc11D1z}FtpgH&v{F~n?pQt8;#R-QdBdxn@isZ!P=_w z+mAK#eg69*^ZQt*a@i-0q&@#{BEEk6ar=7iX#2YB69JW;>= z6({gin`YBJ(SARfv5&8H2Mn%|V2jLzWMJ~L!BCG5YRQm6ff=y@ECGKdYUbzJ{@Yuk zOea6}4R0bYv|1#-R^=s2-iEAo`bt;eX|g5~xA_X!b_aM4D`_b0DHmhP#B?}5n1G=s zC=JjP+>%fmY2;mzYn26ot0UTLtKv5B$Ud1>&BfuzHq{!!ZCTGwgNh>4F@twIk<6qg zFUwNK--(mb?t6Hj@p_~8LW^{#z`7|Zv_v30DbuSV93vLVu*P*N;|4TTX{+Lddho?<%1`%>9jPNLE_$1EC?2_a(ZOM;&oJN@k8{N=L8TRLtLNRyD zAIc4L3$l1K&f3VwIYfhZQ)*tEDV(d9(R&(X>E`}=b7|PpJdeebtGb&DD!(-f7ym;V z=8C>#^}1qxfg@|HYz56tBEk@ZGTG2wRUf>AE=u`%@>s@m!Q^Uq0Q5`J9NF9*@FO=v zy7$VMz6Qci#f zmbuB%)ljRD!JHaCq^k-=8Xs&Rb9EIbmO~QSt2{-9BrLDuOL0iTsZ}|`8ZF>$)%>@l z?Jum#1k1C@hKf5g-7jHfRofM%t#Sq|GTVeV!aokKcYQ<|ct30fSlxVBUk&(BYroS0AXE&j%1PRN=J zF#iAitB2#q?P)(`E-VMTl59!;l6#ATlsocO$k+IsZ+%}n(`!<+R%j}iS0Q*`dFk9- zZe)p(h4ibr8Nee-Y$(0yn6!aAVkeN?r~Mk?D)C3F0{3y1Yis`D_o)Spcx+8`8EOVB z&iJkkI7QA#!%I(_ z9{0I|P?;qy7BMhp;ga^Tgyz77vZxs^NVD+dwt{D>9F3syv0TMHo|SVuo81beOLB}) zjob>kxHFJ50yj%#dEib~IZqQ0ukgt~jz1afSo za%O-lDR@M)X1Eniu#5w@f|<+V5pEios`;u`8G)Z1|D48ccqcNm-}&bIIs^B3Rta~$ zg2$oa(wSQ_kV%oOX?7>eQ#R)FIs-RL<_B&CI0HTezg3Q?$b^bJdB}V>|W$LwityniNcoRMzihsL{;k4E9?haECZGGI`DvNt$ZdqkXwNXpnZ+brFjo+IY5TXU~E&z1hi7NB< za@q@Sm3$)Ldun7D7FG>0xjkoZ)q0hCGJSGWcc5Evjve!JPi#m#9u3{yi!+Ci27rF^ z7Sy0E?kIUGUtw+gy0&TPR;gUE3J*QG;;XW<_S6m2#(tPV#t$ACr%eUBMe*dtm~%(J zQaBF}pP`0jEyKMp<1!R{1&!21xEkJhICjg=K{Oh_c=81nMGc+B*XOum+T}}h39ZC( zaIBdDzo9b*kVJ`^xHylS(^BmDnFC58!(atjz{@1W$r#jZS9-9=HKzSs&JCkI8gUJW z(4ZU{+8~~VWCt{qrGbp97?tPA5IJVRsMhO>^`3$9FPk;Y)xTyQLe1HuOn}PdUV^2> z^kt#>#0Lg!Lo_c(=mg<=jH~!6b#jS<5Kd6@J_6v@HL|=%q;Ma#8+)_s<+H$*~pei9s1$LPiQxtLWEdGl!*D!-o7g z>1@V(?DFcIX6bI8eJ=9R=@t3sAa==4^|Tt%`KnHR{|sD94us`lU^B)&O!e<K1*)AT%(cQU4>q@Nd5nMv`CeQR&93UWeZrILlmA1)eS8ay4894 z;i}DALS`8cPkxL{%8n3{cv|8?I$7ll*@)(Lbp`OfN%?$I=V`0ctLN6&8|yw7Yq*^C z_vu8w#6%b!EN9N0-H8H5!SjC1jUof82y_FO%1S~EAr&s!6PEpwx#>ck^$&6TBjgvE z@tj9cTu7`dL(?)mD#(hU;ATPHyAiS@6InDeh@}~tYUu2)weu#3gvz|tq7jz`uc4YtqhZLVNSi>f$Xuz))qOm;zsc9ccbKjY~)7BZVMjvMuy+# zbd!FCrTc(qm)h?atjw!crq{|;E&UIc{u@jGl%;>s(mlu#=z)>n&B#@5%c83BbcO+3 z*xhVdKO{@OMTC>Nps;fbIFQ2Eo8n|T_1#)-=%EsfAIlWqTW26sZ z0i}P>`nfUQ?VzQg6%uN>%z6QdJ*)^q6vSGbhB9==dN&4G@>RahJCa6_Esu~lyP7r3 z9Vw&tu!YtVnXo=b7~^x4ykvap5h@oU>&XkeN;>Q%*ha(+RnI3}4jgPjzL>{#F*l{H zo1qN}A(=W~oc2#CXuQEeTkmGdXG~@cJMbuZL4eLeqZ;cB?F$5DDQBhTUH1}6~6M}TpzoyfCnW6V; zn0wdj297j_%MLCs$qz6TbMwyRL%#UiqM?i!Fj%I1xgbW58Fdf)09}vRDHEhpsOGK? zdR&&_`W)YvKuv$a2M>cf@#E=mw<59Xg=5l? zK+Qu*ZigHUdB4Gqk$j*+Y*CpO$%$1h7eF$(!(W%x=|MT`6LG5@as6t1L@{ zg1bG-6ESohDlHbRrzOD0+blNxr`bJo$+)zu8JrHmc8-4nmjsMq!*`FS>&9atkuIN`q{Pj7>xM zZTgS6{}0I%G|Oqhd0o;1!JB9yrc(YdTje{2)sOSSFpi%hvuo{7N!xGx&$RvDrR|$O zo^Qn*n-t`40~N`EM0#5SP@~}gBbCk5)qM_B_Y|%Jr;Bli=I6?f$%4V;e%PSgc5^^(V8n5e-IVcNV zvY|2YwagG=F66=E$zGQ2Tn;xmK2eQP^IA zXcd%LxtQ7TP*P{6>;P2UC0Y5qs+%bvthh_E3LA5#%7@^_M3!({ca@Z_Ht$>!-TBZm zz70|7X3FtVLgZ~K$K#nP7cv~}ML`XcU?xs^)+J-uRV#NX<$AR+$DTmNMnn%~?_#wM zsFZAl0J`1&nUpVr5h$}>>iWEPcLC3&+@(-8xx@F6TaMItUmu8Mo`HDo{f38c?k41< z{J_X{&VSjx4}y|w%FUE#AR(NyzY2ZHgX+-^ zO3u{0JdzniZu@SPANoqy$CU;d73ptNX-8&I4u#MQ3C820EGFMJ%}*iWO?KEr0j{|Ce?MZ1tM>we3PJrjW;r^-P$!JS~DQ-OlN4`fE9@^68U zAe@IDM>h%(frhuq+#v9TXUcSA?g{^JukAFo4|=s<_HL zl$oC!qVzda*5)D@O#a9NiJ|g70w7ypgT-qz<@9uIwlU{kXpT?qH$^v7ewAVBCJ(|F zq?;)XQB$EHUpMis?8sJRLZ5l;R~b{`Oymw1-6Jn_g4qbo!8o5mF$FOCY%X$jh=y3G z7c&)8M*FJ)C0@bDo8{roK(2?xQF+K$k?V*xGAU9yty++0Npy8`QPx0eZ8p*tS0}G` zHPhaC*@}$izBcfi8(1to3*ugEk<+#zc>NvluX29Y?iy3a2?teX?JX2=AIB2>BPV1R zp84~Pz0AY*1`Xx+otx$JxeCO9{9TZXDZp`O#H%EY8n=8 zN%85j&XcsIxfm)iF7|u=zhiMDrUZ#7kRlBVSrEAy&48q?$Eg45K zYS9wZK#x7wOB+=3`(#Aat6_y#TuJ3VJIzmM$<0#vrk$;C=xIR9r?{t+nU7u|1;%oMgmf_m}RUQ4WxjzAmTBn$a5T`uA7SgWQoAsw4}!#tUZU^72j~NgD|1&j+EP1g2A53Mr_No!|YDMPGh<}#fN-pH8r^e zr_0vik;xr+AA`Khjjk^%zqQd_Fg%hoQh9TEMLM91^6^tQe?2qYNl;^4v zonUmsJw$ZNNix`BtMEk^*`CRnRsV{W?#-6q^~&;)<0&}H@}Ou(0Ts76&(@*TkUW-7 zT$PS&*9iPCo+PiXu6pQRKdj6O_)xieI4I$Iqjk&fB)M&M+|c)jMU3E9 zMxdsjNb`3x5Is*G0YTXfPbPnGHB_$74Y1QYExQpRZ<%nh;GuKY*ebrkVfiw5r8`Nk zTWx7K4vUaDHtG!K_|xT<_=ORSEL3@aB>#yNw4wpG#r5!S-xJBl0+msH2sw<@mDSC1 z$Ur3Bfn7nMAl;#%99ogqUVzlKfvRI*9G^_z4)t677>-@J8` zc=v?zx7FFRwIqvEfyRm5={}u5%J0*CE8~6o=5X7mdsq4sb{FS>eR{FIwokL<*I6jt z=_;QyXx!l@$<-Vj+0H4F7ncWalHUg)?+kCXx#A6enq6c!n*ZQ&WsQWwdxDuJQ8gZd&xWC|y>wwW@F zV>p9r%Wvm72_kxPxShGPS@ha=zA^kQXU=cutg5s(v!Si2Ja6MU>4rWZZ|F;?m9k0~ z=50fNy~1zk7dSv{=tV;|bh^ASGC`T=T8OLZDtp*k4&S=vd;KZ<11ag@yT^{`EqPAf zd2G1)9VZDC{q`HaZkTQO8{pGFvElpT4L>*DaJ&4zlqW(Y`>pZAi-(4>ym(F@E03kqp(UqazSv9q+`|%`yo=eZ2*g^jEB}v=nQ~G2DdVqjJOB!+j8`> z8}h2{48KoOxifk9+&RJ^{&t#k)8$UuaidhOVMZ$3Mn%B?ua~6LcgM;(kj?DNx;!xO zCa|q?b{=x7;!cuVlCj?7$9gl3*Y@rE^ns%B=0q!{+?(#m<}pTJm9)swH=2QK6}1Z> z%u-6YH!u0*1u2ns!uqt4dy_`;2WiPQsywm;SnnTgVtNuDSkf$0AkHr_MS`wo5`VwM zAp7FcQGJW>{#g}hLs56QM%l6yEQYD>WR-Ken_VMjq;4`HgUy!cB$b_Z0XC{!(;4u+ zEeFcA;ky_`G%yD@OJ!5LQT@9{Mm}ss%p~8qA)Ujs%wfG2=I~AfE?Syeq`jJy%$%2& zTnjI_R~rkq>rGIg;P+gljl}$6%?Qtnn)+Hl!?VZq=_&BcmZqz>sn*`*zP)g$WlAmg zSLC{5Frfd+vi>siRs4#{zhT+wO^`;&2fz;sE@4Ca)%XJW?ADG3}r z)R#``f`buv_ZHwvJlr~z_as5&rpunO9DX-k<&wpLYZsz@6K2Sx5o$etW@Ed`Zx(S>FED|@JSK_AHdPCF==@{^b#l+a<9m4*C@EQi4X-#@H(Go`PUSz9Nv z{op=$Wr^zq%7xO^oQv5|Z5O6{atDEfm6%lpmg1&X;1BN3IHVJEz069k_sR0Zaeb_c zDMwP4k`IroXjp)vbbVQs3l;@#vb>&+=v_k=($agn_VSedCr+BN7d9Ufaf<LX8W!udw=8R_Nx5kkr{zp@MzVbcH2!1AYfG}%XY1tJ?08oqTt4OzK}M#V=a(x=*Fnr+}-Ssl^s zSCD*bE@FM)V=}yNTa5RDJ4%QvTtXw~SX89E$DZGGZg!L8OF6SqeyTH|#udicn56Ra zOvN3GI%?;kP-xh7Y+p}^-ue5zcZLk1_gXofohzD+$X-js{n3G!VU z*uN9wVN?5&PD0yQ{UMECo2$&7LH4#z1KaC!gYE=+vR2pSqWCi5RbO^T$?i;Fq0t?S zd`3gvGBkGs$^j*p>uzMZS!+SX2Jw7CZGAg=;wbCt0}+8RW{iCKf|ZW^SQyO z-kpF6zOBGZxkyf{(p_Sxd%_ozY2o*v&U6>pZ}?)3Di5z|Rxy7M>@dL4;*OKEnFpP} znT_0Wa(e=wcjfrJ!9I^wdC)$8jG}vsZO7d?l)yAZOICa+{~3wYc*>@9AYXLj70cX7 zGM9(pH1Gn9#N$-%P1^nj@++*T$AXyDR=Z5;zXoc-(eAjnlx7^G(=dG-YyI8n#=A)t zWSmid5G8zfoZRo1aDYju{{k)x0)1D|#zy&?MDoa*0It$z(BY0Xf%3w&bz-gZvU(eI zYpD66h6w^Je=(V9oxI@2yD{=BnA?ViE_4z22i2qN8BD)Jb%8*l-LP^l>$ao0Yj`9V zz=M+ATH%-M)(Do0J+*BuF7Do@AuDzkrpbf+>K#@cREuvIqOR}_T|17L>45TyFciDJVI_%%U z4%c4ob;rrCR|J@y9xJ~e9B$eqbD3>F6Da#R z+sO2PZo&q6&wvaE4Id{zT~XB%xG5hc1VYeHBuC<)gI_X(qaQa$4ngRdk?9OI6n56W-{Hykt~Rd@Ef5 zBS>4^3G#B%(s{nv@2$>;x(dy{!iUP*p^g5QG+AF4*+oV5^cw25R#|dL+G2vewtl^~ z7X2fkgDroamO%GV6w8ojLTT)(i`*FbJYA@~=({z-0{2{OpUSl(fNZ!yeeso4kh|9e za;ro|9G#pXcPCT2)lcdBG$;La6~ez2>Y3QRB&lqvi-2(H;8uBrG`Y4e60O+WXeXW9 zx6--I#Z;3$(cH&PTx<`cWU`ySkxW)z8_7%6N5Jd_&)Ln&(PN2jF!d`9i0B)$A=);l zbuWcQY6`jea-qIaUh4>Ct&xj13HdGP{X7?GV-bYTyT1pG+y*47D))5+GT`8Y^fqBt ziUcq69yMN-Nbby3jBG5*;A#L_u7b5>J15Vs-mG%@uqgHdH_tDD7V724l$9?Hi*!Az z3y`0lS;Ta9XhZKh2ItFTaEygJUj@0nBarX-(R=oPudSb{ooc90MrP73BO^2oxncP$ zfOc9KMnPU*9mqK*zYT#M$@Y0(M7f@35g?PTJ9UQ2P2GWCmd85+ z`MO^g8!8G<)ojE9-?uuD8-_)Z{LHAgGO<&;1G{WOl}r8Ljnts~azS>n17CEJoYt%| zsUpom0s`eN@irbKPoigL+ziR0S4Q2({mb>!2Sr+f$@imZM-@kcOBXftSho`eOw%rj@xWO(rNcG7eO}odw=%qI9vg5AKqLh%%`UJF zc5OGoVVD#vK+)OLP<7qGfaZ6c=ON*6JQp9Md8sg3Gos6LrNR0R<&4d8bR-`Sq;3GD z(L{qxxL`aquN_({$iJj;xcgh_KzE?eC9bc*O_p6o0w1GdrcR7_wwsLY329%;MuoDQ zEH5O5KAnwF#$Rg{t;kh&d~-({c9rERnX&8UxVuy20ls9?y?&ex&CB}`HcnGHA|iiMM9-hB z`k{a~`Oa6!TM-EF>F$8>lwG6TYnoC2gYn2t8xBwq>Jx<`J9<6vRnpK+lM=kNzviS( zQ~A#T??w+A;7-%L+|a$Zz^F>iozy)npA>Ba{j z?IWF8*z6B(k86bdpTY)zl4IKlDrP#T!PRZhV$n5vb*lf#`l{T&CSa#bhmGcM!cRsh zo4c!Enb-zz)3;ObyeGjjru4nRk9ICX0;4Bodl!5V*^))Nf@?pP`fL8H!)9tqL(PWd zQvZG%??^j%**b8OFqgRyPdF%2N)MZk!Z3s>>tK1%bl*C7x(SY<;8M*bY6x=wNB$H%U}1&m%5vAw@{M$ z)Y0JX`_nmHAJ54kjc4aXm1~T`Iau@ZsQJdxun9Rwj3U||NGo5(Rc=EHp~tjzZh_Gn z)d2<4tKp1d6ZY=*UcC;bk(fITbQM(22xKfioM1q81PyMI>G^W%jr`%DPx5xB2dmuG z6=+^^70fl{jH;HTwoSVqj7N2dJjRp&M9EeFI%B6GNc*|X>kyTTJDd6bm6!ke6c|IF zO$z)1EQ!{3f9i1i5P39Pag*e0W0(Rfb%@HnX9m*6vVVKb0Q_W6)*Yhq!!x&OC zPm){khe>n&ehxqKb5Jzf4pG^{z_k%C@TsZk9C-lWfMkCqYdW@1bn#FqY1thjU(TYi zxhxUp9U{LrgKF-EuqYi>W-u_ikJ?+7R0{y)4h`oN8e)#X$ z5gxZa=dt3|Y@`L6dacWLpoed@`MlN@xH^^ZcLn6%W`rGsfcn4AW+i{kQhg;Cf%~56 z4Db+o2<%aaWaVxuXc1lwG!Bh;rhR9?Z{fZOx=qXA_ZfCI z-J$jL@*Gb`RJs$Co!OSI*QcDJ>|uQ@SVO#C-|4pX+6uI&d=#F}Nh$?M$wOjN3lTD> zY6tKBrUIJsV1*w+LiTqBa(lKSf(Bl!T!NV~3+!L!JsL`fIkH0O9rPM17Bon^z_{{o zHj*#cvY202qfEPu04ZzOq$RnkD=?7@;9;_{(*v1UF=Y(1<#s1wK6QxvZJ;u2fX9-N ze$-+~vXP8WR@uycda_iR#GaoP=nUE0709|Q+ju#{aEEAKp36cvJmDteTp2q+V8S## z;*~kWwqX2zJFp-yVhQ{0Mo!0wTBJz92Tu4<|d!k&R&D`6OQ@?bzH6jNL|C zZmQ;`+BE16kso{{kYUW9h0JnZf2T!<4Sw5Ch9VhIFSFSHx zC+{221izH)GhsZ!i_lK;rX2JeV7gsHQ^dc$lLzLO0;=6dM9A!0QX1}|p-sM?u{Iyg z`ZjOmRcLMAkMNa#a#Q3cFZREd#Z(e;iJ076<(=sW#0n%HsPr39@5Ar8lmiG&6u{wCh=UxX7SnXfT};w^d*n=6Q8Jc-%m~5px4H< zR8L`TqFnz_QtbS!91jGp*EyJfBmJI-(mGUU?j(Ei8av~Zq=Q{g#vP28!uYx^*y|7h zg4vnHblC(OGPX}+A0ERai1~xGM{_3`b@eVY#}(phP%NBrXi$}X9nD(8cwO^S zHz>b3aPC2a^1^|0Ck@Ka4xIZJgEDyF-1iR3V+YQC|DgQvz_~L9<(>oQHVw+w1LuA( zGbpzlIQ!nrpxkia?1wUgCg3{2Vs6h2%2fxh{o~A_xr#qP?PoKC^63ND{#9mBE;w-Z z?=pkZd*JLnnZa1_zzx{I%`g-U&dv_X9w?``&+g3*%I^-GeL;3mAjA$(`_tJ$dG^4y zKc5|xA0IgT>g*uz-UBt>pB>!S#{~tOW()D?{0oiRd+1pO$;@qIw zp1u8KyBW|n$@g?=+Ph!>#)Wv`P#K}F^OwfkgHUdkM>_Qf(N&mTDZav8+k z@c|mI&7lU9fBKi)Dg*M}1NN6TnEQbIK=x>l(In6%IoS3Mit}2IU2fVNEyi{%tGvHY zUdI|7 z4`KFZhMV6QKJHLR^u3BEvUG5{=LwxAt^5pSO>;t*oV7Qe>Y?6`f}4z4VTt;SAXg)} zX@};f$lveP!g`!K$0o4j{*YF*5Lw#G%lb#=NZpUBFI!<$KiPj&U+ji5q@h^Da#O9L z8;pLkhUNvaEbCDDeEf7RnX|>ahV*c$cbL+>ITv#=ro533>Nd|K%MDnqS+(Y_wdMc{ zV66wiSv`Blpl9Y~x3v%Tf2iD$?JMKb*eHB0xFhA^u7IVvA=}4n-`nR7#hpb-x3gIh zK`i9>ikWB@+)UZmRqOWb93nG-qD+^klS7w>Dy4dMPZ)PI~vK6mi8pj|jxkKd#U0^)FRy@XKWF#LB;OP(ZJxfK_1hOEIaS@UD;zv$p=Dpw)hb;l~&9PUtg)80cg0pkqq z+*dlAS>W}#NG1>BHI?v)&v&vTS8=)HYIc-LA##TzqVcN2W6+7F+EVy}FsDYLIY^}& zsl+EcWT%~Ya+dN4Q&n?HI!fgpdsQE*wfy@#_j5XTC>N0%Wv=8772L{=k{vXGq|S|! zSM2wC^AU$jxd>*p_1~X2Z)>82>CEmC?fD4 zp%yRCkNg;BKOvH(K~uokR|hhh$ErIN^3;t&K?wy~?Az@1jT8GrBbAp3nXxfPb=-q| z0#1O)pvm2!(_~{;Dto3?!+W}f?UfL)8{73y8yolackI`*vKIEV1~B156Mq}4K2F~MD&nP z)6XcRyxV@g8@hwAGU3^IUfJLk$Ysi) zr-UkwITUo0N=hL z0mj{-{(8_T<_bz8B{U^qe1;WwZC2$EtGPe7xCyd$7`0XQ zt`2mcn;^YtGr2~UvpP1*l!}`mgXkEM^3@L8$!kzL!ThH|O>N;hvp@dts|iey-dv<| zb9aCk{nX7&=3bjj$xV=N(YtP!Nl+=Q-ei?8(4(6myIchNhF^-Zk0Hdotl4a?u1^RE zR#id)9)+Wl5@qPDHEBBsQl1@_skn0%U)2qJ7YxVZL3&lWC;`G;bB}Fijv8(Y zwz{u_6S!eGxP&>rywbLA!|=!*VIDsGnoxT*3yTsk>%P(8qU27Jjhrp=pY*L?N3x30eJ`l#O%*PkG-14p`Do<{bdF#{2AU%6Nl^`Qyrk-FBrdh{43DSwTIY5mVxeEACGB6A5dT|i1zrGSBqlV4bx9LbCZ$dMW;le{20IX! z**@9nc}XLC8?Z(qgG_7J$^a>&n;C=5AkctuP94HbhD3cfgyq1E+CpR$KbN9UH^ zRJl6O!)Ai!08t&RDQRz#(oGf_LQ&m~TNWlpfQ2Yyv&ZD?e@sp}Jtji$|H|Pv_-XLgb zqIq16G02^8cv|r$s|e9D6y!E01*Xca<6xkBIF{iR9N43m)}G$8nIqE1K0N~G5mUkT zljT#8bIfTwC)TT?wlry|Exd?$<>4TaI|FeF9yKP&4-95DXsEIXGF*@?DGHD_B?^9j z631hDV5fLVwMJd4mmoS;5>5Q(Ee zjuUWG&%)4biOYo(#Dq|wHw1YR+7>h@YpT@y!Ah3n=HantxiBE{0ZSZ66HT`cFB79Y ze`e!frZc*=4zwfptk^8K<_A>n!mWXw$;q0R_k&m_DDGRz9Ozxk@Y*rJElBe7!z!fy z7^Qi4lBE~i1bH58>rTqcXI3*fK6`DWLZceZa6F5#MBI=hEOcxnq9nbegwDU`A5Oqbd? zPyTfP?3B0@O2(;>VS^gd(E2r1W)vf|mxa5m1cJ&-b^vqyWd_MA57k9%?v`Z%9c-&J zzSK4T>#;hj(aw4Bua?09fzL8QE*VKqFKn&Hmy--*WP)61$>6{+a_^R#R|N8W7fh)| z^!9F*L+ql@Bk&suI)At3W!YXgO}=fS)<3E9I6=15Roparkip4~b-;WJWB>g!wgp=T zNB}${AZLHXBk|mneKIa;vN7>!0bom-U+<>M4X``xMtKqx;SDnH%Yc%hXX2uq<8o0; zxFR2r7hF`dtJwCy5Mu$acF4`wbu_E&K`}&SeFuCy?}^fXaR@^bA?E%O4w|I8v7n)>+9vr*Jw~8U1}x*N2}oRs`Zg&Tt*}65ebR&aSP4qgAi^+{ z=Dw1ZEN)Db*}xesyA$QCcGF4V2<{shxf50PEj234*lgFP3hfhQU=*NKF1SPFsg=OW zIh{=Q5S6M#yp$oeKQ|nC_|IZjHgg!2rA?M6mj&(+iUPi0v|Hj@bSYc3@v#=oH@TT| zQ#%mq0r&nhn(D*g`7 zN0OkjvprCrJ}GT@iFc3k%N2gkP}CDtF2nQ3=Uty*EOZ-39bLDQhO(s_Ix3WpHs_A< zc(H$4j2Hh4x95}(#APSQ<7UFrqt`Rs0){c%vN5hbt5)twZV&AR9t$?Lve&RMGN`PP zHpmrRcDC1fc_z=P=#`b}oBG7*X`er`wt5sNhSHJV=^JY$&KZpoD{Nfo^M_WTZ!tmT zR=-N$_p9VnICdcL`$4TF-Q)}1=L`9zl#m)!P4V(*AdR)vdlN zjeRE{X^%Sz=Fr`o4bt`>h3`P+?tDdNr?VY5Bc1JK%w_0X|HN9}L!-x|=6PhFqS_Qj zsL2iXYM7scN>rW7r|~?b1X}XM=z8zt;uRdfxCzobBBIpRc04%3 z-wgRqU(GwCnIPZu)o^PxL1oX#3Ir04z7#X87na%+=dw{o2fvE?8^zYctOUDp(tF!^ z>K#vXkP1%NLba&I@>eu5S-#TEIrt6ZnF|vL_0{)Sb9bLf&TS~;83}q<()_I?87*YE zVGC#!w11vCIGUij!)5R2J&2p)%Gd`5-Ua@h1YGweT|NF+MV0U$> z#TJgH?Cppjsn35ra5LqWrL<#+NUj~z=O)U%_TJ?+e>*e`^zuUg)Lzk?Fwb`t(uuy# zM4{w5RPO$-t9L*3aqg*l^y1u9`O@Eji{JVlc>%RP#ik$gv}zle zM+c5shRunbXH#QZ>7ut(avk#ZGePN=OzTHU1Md?Vc zOhz)-O_nQ{G=tMso*_cLqBA@f+P6pZ=$`ec95Em*FkpK)9&lK@n)wG;EI>Ce2zgL8a?pTf<#%lE3eMHI^Oib<MPkL8;PqysXze>8%~+>x)i z$#P?o_~2;D{3iLlV07eW%APZ6+yGVO7i0S{Liv6h+c!bw_OTq#xo)PMYsLH+_Kdar z)6JCMSgKW)-j2&<@<=1UA>PufVju97F5xn|5KBP+DxIPW#lOD(rLa`H|khvVL zVv~EfJiO$;NODe)d&U?sDkVErHAwxtbX9NT*~+S-!a7qP9x}=;5W7pKQo4{%g01zdzh7ojTg&6`~N=e@fY+6dm2R5q&tvC%BksD+st&9Up~kqWp=Gax2T-i z3^!5?dW^rtE48fQxt!$KLUcjUfCE^z+{oQtmWaPz4QF#uA#?ebu>%^4Ey4^U&)E}F z|3vv*MTx`O9n7#8Ly1hJ|0D5$kCz`K6Op^Jh#~%-)ju93$2Yq6`s?wHOohAi-0tRh z+!`UqPy8^YqWeuBOnjmf$lA9|=G)zSS;Wvy zmMEgX4+jN-#XdRdB`?Q7Sio?X&Z?Xn_XK;N9@nJT?^*wp_H`|NaqVBuRZX)$&wwJh z^CzpBw;r$Z0;<_=vRu*4^ibTKjOwmCUS7)Ba|fn6G)GIbJ03-D>Y~%``^cTHeqpsX_mWy=r1>YohcZ$MTCM~A|( z?v+`X7LALf4Vk$thU;*i*oOb!D!~|or_j{YOB-Im#)*QM=DjmnKF8=t;$on?XEL-6 zJNevXgv9eaNo8*=4&uphyvmzvxSvD$&*o$&GyjsVZZ$U^t#OdLH&g3{Sr_;D_kmn* z>$%ZDya@#wOHLXp40Q;Q`DKkZ`qzhG5~$QD=qVm3-16aBSu z+=7TpmLMrgPREh*qs$)q=w}Qnk7bN=0aa>e1Q}O%GQzv88zj4NH{VZ2cs{k|w|fwxUdM^@J+K)U zqES6=G8XGbN9Sh9FHG@cOVhc)rH#MjrmFln4?gq!-HGb)gQ;22@puCkGgpJMvL7g? zgtB@wNP1~%7S=y29mI1CLZr_fCFct>C^Wi>@>@Hji8G{vnL4?x%CDn74&F?&(hrR>*{4N@ zK7;Tlut0h|B<#&V4`0#6XiN{<9jS7q4er@opJL=AS2PX)f{ecGA@Av#n|aWrJx-Lt zT;C8w>$6Jf*sEx;Vb~GdX5Wa-B?K)dT7^ctsY@JJTVWhN8W_ze){HY%9HhaGZnEL% z{e0d{RQVR-Wm(?rUFGMc1IPTFm6^EJuE|bBC+Aj=Eo^m zDh!fCFlAoa%8P6Ok?MJjKdGS)HB2oQgT*e%ioJ*lP?{yIMzB_;*e7d@GP+?+vzsi3 zfme_qPn2KeB23>V%embF?^AZ~4;c{s>lVXiU6Raz_h#8wxF?fU)^@W>DEON2?9w&d z^%GT|m&nsz@B zFUV3z->LGNhlZ=Otg*_|nTi;bUcP0{q3>N|Lbi!QTo;?wZa=FpNhD_m+_Z$)Ma4$e8__(rE~^7Fa>kLB?9=l@_R@-KnzgU$6%sdCptr>6(&wOFsQ(3tY~$1O;6 z-{9~XS`T%IN=aL-#CL~{uj}DZ=1od9dH}iRJ*&aQw{ZWs$@0XSs!2{Vc&IfiARnv(i{&iYJ7B*t7juIt@R&~hc^wHxpDMqMplQq6Lo4V1V>8EqtMFwj z!&NvDwG#C7W21-@7N%`pT5IzxD-W-2qExj{OBIXGg^JA$4REPloqXPNt$)^7K^*Zp z|MhI6akMY@U(Yr6k@s2u^*nmtEbcG<9!Ar;D8;ia?#KySxCyKSz8v(yMUOi@({iy z?OHxsuFA92hVoeXYC4Rg<+G*@Rfj$n?OfWu%}tcYxGQ9KGfV-AY(S^*;*k-N`Jsjp zWp9K;bRHJ8%;-^X~w+?sRYPgU8JgFEQg;$B;e+&|ib zK|&Uw)ZjiYpEpeM^stCgn3Htl9-%)iBL>wguW!&$+@L)l%Nphn))_2iQA2k@R^|TH zNa`l4+&(O_ggZU%xno#g*!5wkTwP ztOy!a%;3FI!+fJcsnb_c{u0MOJaUT-#X=GFiZ#s&*&@rD8|xeF*A3l4BD#5KMLOUw zjTrAq#4FJjbQ9(I;r5JwjZeFkP^F!y(ldM?tVELedN{kbvH)3^XM?&rxd}}T&!w`# zT$3MAtR!rnNO)y*MGlMH2jxOo2lDITkrW_Pbqg$Jm0v9<@{AGLD$nI3&5Ih7z3${JSc$u(`n=!3G=~Rt#C^ds*jx6G|GFnmK|l)7h=5 zHW2OP;Y?JlSNU3ZKo?)P-LM@TR2c12vHK*IEu7Gm0@Vwsg0RMt?Q7DYscidwyvom( z!5#iVCX)Yve>zcRFIsh`_@=h$UtJNniSl?plEz48TF+O%?S}7U$nH3~*l+lYw&Az0 zcTN_pWZ?!1LnYE#Y?t`*>`DTW2cOI>pT158~&rTQeyQas3 zaRT1Aj#as14a;+Pqyld)HxcDIq~EwWLI`n{+C%BbZavxm9&KOz=dE-fOSD z_S$RS0p&5lpQ{%v19}ZJ+)j6-pXG+@U;A88B728U`5hP*y#3|eWhVkYiCGt&Wt;)w!%?M3e1%>@AihB>~?jIW8&EPG;ctrSwBT|AS2!w2v zy|+=_`3G|MaR$ktLZ~z*74iDI%grLhpTvF z6x9hYm-WYP=kX$YoO#xn%08Wp=gX|>yj)^07ib`EPY|M1EtJ|5CV- z;i}^z^4vf(x!^dsM|a=y^#5wR$=;TZ^Hp{=GI)8XB7mIddVRh>@yB0F)SnqqpB@wS zTQO0e;fVUp{#H)z%!sJph>6;gAT5v7Dk`5ewd9SEc{6izv0DJ=4aQ|{$skk6b*YlU<*Vp&%mxD;OBr@@)WHaO3-?!-eADj4$uwo7 z2n0F05>i@ed}69b1C;}Pq?CXnvz%5&B2>$ee?(?^Zwv#Q=87o}QrX zc84}A+7gxV5~|dJ_r|O&Y4_c$n+!SdiqL^}-wq((l7RdKAj3+sG1%$7L|2HI^IQ={`c1{3MAfoOMP5)`x)QCWgHb4k~*T5pnpl2^#IvU_4B;k%c<3B7?)@WPiA_DY3ZNOhg`u!r=57ck9J*u+%xs8iF~~GiTx( zj*Xq=eqn|hp>nntQWHRaX~k((CMG77&1WVPy5EeW^K~3^Xo)Pe3h2`f^nUz_?-6)+ zL8a_Mc?aPTDvo5cG%@2puxm!B>{iT5u_Sw-*aq^e#0k6ktYp~F7?wFx_xv8y&PS-+ zk+$qhJaQvc?o4wKbTx7!FnYTY)k|j7?@~Nvq}?k8sMoZ?+LvOMv4voTb_hm1I~n;L zY{dx2csvbhEKiKBg$s;ov zeg*LwDi6a4_bYlM?Qfb}i} zeXdpxR8BOvQk%-Hf- zCoAy(^6F|NM9s2{Ph;`mmj3gismtk})h~T(hO1vnl*tQ3(G9X)NPT$swO1!EwzF^- z8Y-K-R+(i^N{qcD^G zmb+~luIJ0_72Bs-12gp*TZ^h_@(Hw#$~QF${9JiKGU^}Qs9qTxu=|SL$(9V1@QW|o zeA&*PQkI${4Za{x;Ew%;$ve;6gU@Z(%rX1c2XGJ$0m%D(eyE)6h1o1Q6H!v-tP2wu z-g7VvDl=_ot98?Ui@ACKsnjzY-=R{i{bMUH&3W&n5J2%TH{m1ROH7CIJQlmK~_S^zlrh^oz=lW@sA z?4fUjp?7&o&i=QOS1+%GREKTMuJfwp_J5-W=Di_o?~xzDaSX3~b4M2T$Fd2vVX4P1|(9F->>*wgIQ%bi)+_uW78owSX?${ANF@>DNNvt_w^x^G7*tan6u zFX=pRdjn@CrVgYFuU_8%e!0;bA#)A{w0FNB$^-pEneLB=eaS;VO3tY@mn2iaUu&wD zP^q@^AF8=mE1gF1+ieHP^V2EJn~}?0-oWIXd?Vy^w2L>aK!{aoK+1zE32w3Z zcS02^Q%W)g@%HqjPuV5ZyeTxCj+ugSElClsZXJL;EVKEnOev}Su7s{Nj^&n)0F9#hUMGmww_g))D-FQ0Is4=q+1ymta`3pZf*#|$m$aAMY3`Opk( z=~;D1z)XA0+;|*j+)Ww?8+*D>=mFtL>^Np&{|o^Hkp_5ZusH=XvB!l$!NQ_U4aFZQg4{+<9Df}GWte##Ffy{k^>Ut5aKy$UptqT967kGl_30Ek{?hCV`>S-K zZ^E;%u0ii__qXfCsOTj{U%9)r1r@2(;;Re-P4vj*`YhPgwJfv?e!(u$r6k1K=H6(4 zU1@;vMP_HL(!a=gwQ>iqR91156eKoEgKXC(R#K+Ho*tN<1EQPzg&1EJOeNQ)Vx}3P z@?QUdI{lf{~56o*eLgmH&MMh>*;VG(BSu)eHg1aS*EfJ%ykCx(; zl)Qtkz;a>@s*{avzNKwml@4W%oDl18jVmhOS#p?JzAFzjZ9q@?~I1I1d+BxvC6wb~W;hb;soeb#{4y{aiS3lT6hPz2i}d_)^a<8vo3mpWK>W_CI| z#70AfbB2NR2`Nc$lVu&Hbk=c+suh*xc@@1!yMgHu4k(p#+<#t02DMIaND(W#7~_CO zx5zTqtwfePL0rx;nU|+mB8SVynGP3MN~ja%WqY=sgWuZR!vTrU!>f}U8O@$P4D7(M zqTPMW1BY}DJg!a)YJaq65tonhAp@5AWLS!G+g zEMLs@K|hul+i5Cq&Oj3dKhaHVJ1uwG%Y6wQT#Ig&GvBd+@=Wr{6;37?Qg$zHO`|?1_b;rW_LPj!>g$pqU_u;yA2$%6TS!M<4SNHv$%}S~aolP{;6`Lr~hLUuYkZpE%c*}d2 zr&Q+6M4)i8yLOpLklK^1?uW=7nr+ffFpTfp{0cI|ZxS+9=Bprw$$uvza^d#M6hE;k zYF-y)36r`k+A#9G9WopSfs@vm&E3&kl{=lVY)AbK5vMiw@LTNJvKK^h*>uCqAFZbr zxL-7RS>2D_z0BTcWOct#TO?&UE+}+C%&+H9#}v;TKepkxjvvv9yLJL4ot&Io4YWrC z8C*ut44kko%eh4gzg?7wx%AG;l-HvXK}1%hWEDXx(7}C5xIqWp?hV3mynj(y1+y~< z9_Kn&&fKsOkFhyqjfu-NA5IIV^3B9iae-~Kxn`+Na!N{;g8&{qHr=t;>-&XHNm9#p zW(J*HqIS{@P#7Invn}|tw+b3FgV`_af`*N(Hk~bQv)^l3Vse9{jc}UVf0Jz)jw_`j zX}7SlGXGxY-{r`yk;B1s8{%nf7R;nGEA6E@vC3O@cje=gk?k=#ESllVsqMbJlM3Vi zGTC(~)yi%&w5{i-+&GP2@{pB(y4rn6D_^yTjLBlh~r8TRTK7!dP3VY;slkt&-U zCgrl}Ic>K7H}(rBXrnex@S5ckqo+8=F94&jL9l-(MsH1rnDb)~;i?KQt^gO^3Qwt% zdm=j0jNGA+e@s~9(ch`tqGl^A%~q;K=6ZEN@t~_aLc(mNB(m&O^y*}xp;W0k=haySI#$|cy0N=n@RsW+BvY(3&BS~{ z-|n#Zo5)aJN}Jx*pm}X(g0RmW$9&J*)FhJ<`mDOIGR2pfqL$4H2ETM0a0M(thLu$c zM1WiFy+MB)j8)~{XyWVoh2#hl`&Dg!?H0Q1nO`MAvrGF7c4nr=P4ogtg}OXXm?4sz zLtOta^$VLCy*fEB8sL?Fp~?@qHH9FVDQsz3m;B5@FDB>*bDC06(e374;8t8k8*mR4QD2&s3@6R+ec3IX&)6?c zWo+uJ@asAIrE*38P-gn>LR;7bM%}4VLW0WAVVqM-vVVwO0MooVCrKm=?Ef*p{qIU% zZn1U0L570#C>cngdSBYeJO3Z%D7{@%Ed{!;z>{LU=v9qlz z3ft;+Rwz);H70ifHSu_nk=HZ5EQX!SUY(parR1HGqCN8l_20n}o+K-dc_bM|ZU;KX z`PeFrFwTMjxCq$44AXl&X$+^phvp6FHL>oU*PfffmANBc%k&S40yVa4xcfrE8V|N_ufc(VM@uXGXrg& z`UK>a1GJo3U7SyFo7XH?O}2E3Q&Ms>_8z#~!;@{h-B_K}_gVmyEERMAo0yf#S^YyI zV5O|Ma<%UmRND&&&{Jel25qTc5A zxW$PbIo1|CB9a|7DI~Mp+uIZt>to@}9!o972JY3Fqr#pq>+nZ;b<}}ReoFy4<^8zt zlRJ_^gbN=}z0j_m?&n8Wn$Rqsk{%lrAHRfs&1ph%+dV zCLiqZ1eIxrCoS^R5}YErQ*-na2?!TBRZkneRw2*C-K7k^B(ysdu^j&sGH|EYD8KTp zqeIeB*@ZQgj;`y|(QD?Qz|`J|$GDxWod2Ji(D)r2UP^pq$iWPJraR5-fNYID+NsSGCQPqv zzZT6;beHMw#|NA=G^P}YB{!=a3RWwgu7&Fo&`Rh^*bpLfCd@RSo!`Ylcv`!C#)7y_ z`c7)OcBDqa&#x>q&oB( zl(YwSkCIp5HORH;kXb^04Jv2N$hli|ZoETw}~l=yK^j%_P3(CuOfyX3r=k zIQo*2yrC*zOwT!#%#<0X|5d8S|`sQRlOyyNu z1UHp07{G4&WwyM1zR%k^>^iSO?wDEf8ZERFgIc^J20xc9_w{I{GPoSFnCvW*_o)4= zzLMO^O8493WAXOFQT#${Qt7|RJR>X-qL<2mO%R}1u`=1o{h&mcncU?VUu6hnlZoOZ zB%j>HIqUfZoKFD<#A0kD(}o-$DHAXEm#iCX2|Afl3^h= z<^YxXAb{c;+BKBJeN6m;^MYo&ywxe$HjPd?HRQ*q%#1~u2ln90-s+Ufr_(s%=4+@v ztv=%um6PK(Ha9oR0BfT|2YMVIzdLQa*C?k(jWJY8IwS?c`nSI=py42uTV`N)X{0oG znP8q4?;u%flUz-0O%n1rFr+<~lL4`g6=)zo?O{w%95CJZDh5QJ%7HQF#RSRcWbjR6 z8Jg;QLn%DNJ~*ethhAT+3Y*EbwF`lVVz#g9^%}Xb*sGIk|N0MJ)(}o*G|`bLoQ0PX zC@u#ILU8W1o!IESD7HHum>)QTB$)Mr&b<^XJy)GHVX;3{`n+YL8dPRk7Ys zcNrG5JjVuLHQ%l3g|otZ^F{r!oBqmI8SN`3Wv5eT zgQ|Ic0jEGUtqVDPgkB^GfW zRYEuY>3DZ5s65lq`bK}#bRU_*{1-a06k=#pxjjR(XRO)fhJA{xka__p^BUzkBe;jA z@b}G(bzEtb294=Rju`!VGKYl&BJ;(~Q*z$WuZ;7MjkDUux!%S(0R{U(N?n+=d7F)M zyNz>S#3=wn;XyCrdVcsX5n zGRC_4en?oYiXU3YDuaAYSHs>)x_Y*{#&Jonzsil1ed%Y`E#uG+R);)YHraOU>gq`0 zK-@%z-k^MxkxO^{YNPw=68F_})nN+^Ifx?FU{yGCs826CTEfMolcvGQ}tNkW~M?L8;9zn2+F4nd-~nqO|#=G{};-L!St-fvc-;S5!1IcdWHpyOp55$tVZSQP>GSRHLz8cDJ2b_)x5MP{Y=^t^)phQ zsj8{&ByxyIQ_VZSVbJ6q!sTQmu|QK9iz;?@FCF0g+jfvSQMvcL4j+Uw(wxu%ypfF~?~RlbI&!#rKkbqkr4lnk6gRg^ zy;D{Of0T^-0^>%iND&L}6g*=iRYux@D#m?mo8`g8U8dzqfAo}*DmSp0JYUIkp4^>U z&hLlYeSY1TD)Z|Oe#u=l1|u4FXXQu-*|CY$Zt%wmQ1e-JB}i9(#}>;h^I9?Gc+O5S zQsvWB8FIifUN77Sme=*(NZAA!M1|w8cg0mlHEB3X^V&kB<140TqyCPR>jbvB-6Atb zMkN&Gk1hlNwYXjun43Q8mmtzsIS?2Sut&<-=`u+fvC(nmCkgm#06!+$%&yq>zV)dF zl-E`qK*VxY%rX~mO+K^=K3IknRmZFQi)GzqO{ypqcUp^k-fasuoTSUhZkQBysDU$5 zu-mBv#v60$@{5Q-IUq4^^I_&cVxDP3!vkc%`vdt}Q$*nVOA9M;qP8W5s!T#9h zdn4U##WHn!3l&o9l2OO)m^rT*e!=wx9lq5H97|vo%6PMWozHS4r2}o>)>X#yRi3m< z3Al+5hUt7&BUK)?YwWEWsnL>rgZZAjGkomTBYgu4e_XAM^Su%BVw+Fm-bj@z(_H92 zLN>Jhl}Sz-c!VO7BgXg7PtUPQ2q7tYwespQ z@^WNrRZ-Tp6Nfb(0{tvTtX$mTJE`gWc!*x5Wa2OVtaie482_q%+^6siiI*x@^b6^8 z6W&i)y`UBoidL)gGuG{)DzLGAFxZ&uVA)oIW$sbFvN$rU14x#etNTwt0TS6@JE&Hf ziZ{~DLS-<_xK`ykm%X^^I3FR{8`o+){6(+-PA@Y!for zM?lneZbJSDB==VpO)q-ai0JJZ?1P8A(6_mr$0>%^KqT-PteeW4N9Mrx>uGvFtBo{Q z`&;=TZQD(}k!I^vmg~uBzITwkY@I%t;jn{% zR+I6H*a%0I_8!htmB$Dsu<~*kCW~3zAaB|*XL^=JY2Wh+=E1CSSeQqxywV0?xh%tx zD))H|gza!U772DMU-1xS@DP=E345mLDgDeuLRx5@QmS&t9k(jI+^d!4);*Wwjg;3s z7&ra`XF7(j!{f4DUadtWTesKQ5s?1QC-Y&yDo@U&>vHw@hbQC@thHwC#mQJJZ=&B@$7fSn-B$^K}yjr-r+R$X|+*Jk}DNADp|K!XZ?K?G;D~)1KwNE%g^09&Y4kiOpG|m5t=T@Su-pF(v>j#i-*&v9*5I8W&j>3 zuevDkO({5JayVT*31mmxY>DlRkt%Djv%?(}F1d)MQF%I&Mv3RyUo-L!PCjp>oZ8l- zrsP_Vz(z-*ubGTCQswM6^RV=N_hS6+0xGlV29}JQi{j-Y&|N#d{pFMxo-H$tIh-sN z5{8fSPhv8s>cjpjC-rF)k@sq#5&GJz+4dIFQ-T2?0XvWvnRDZM=9Nr@?%UnII_ z^xnUBR(2na{Ja$J*p<5^*k04WMn0PXH~5N%*-I!YVg%YJfzTY1a!tPw-RsHe5k_tx zdP=^Gf6neleo%{29t3p-y`1|80$n=MtFx#p>*khzVVog9Yu5;tA4nM;U+0_4qmaeC zq*+Ymwayx_xzh3+Ye^ytVNsT11nG4_u%y=eGN|kdnl9D`L^`W)YWv4jPt!!NH&+C4 zpm{kW@hMg9mcX;tM(H#ThH)72I$fr_RM?re{-!tIKl4t4}>kL zK}On2_(f=;GZIP(C+My0tOO#%^guY%{o;aICfv zQ%d47L1E>REdvJ@&-PKOkqdi0#E{>P~24`)4&UE2aIIfB^m;v7}tC z>y7xDQx?WL=fsyGRWa^KhTWa_!Bbd znlvDc##A+3%6Umd&6zv+#ebmkW@>p89o^PgZe*+7@>52^x1g5XjTTU+^8b?D-KYUQ z&vSzP(SABIE-&3PLDq9IvaqXSmq7CeabbhBmH2~yn5XhOfq0L(aVN8eB%N$a*MOjm z9IzY^_b7ioB31quh^!;E*ck| zC?3w5^#eIy%H|5|Vo!UmWM4#a@IR2V$$ql99*W4dc&+l31#58Z{y-MnTP_p4%PdO(dNC98QsQ=?D{uM z%z+l>#>`{%(4^5b)~L(jS>*>%WIiiDG#I3%7%$z@$|oRbe+hToHZvR8=wYnN&X9YU6RAnkjf7oPD1|`_g3v z4PX@Q38jwQCmFKS^=kaH%JQ~ZIgldXtsF-7?; zyA`rg{wpW@7h70z?DbfZJ4%?Mr0o5GVni@RD?T5|aifPNV4MjU^NHJ1`O;)8M+mdM zvgVOuS5#Xj*~61_MrLpH0pc?wmTuSX#_C?pLn|96`_SVvNyA!|>%URv)w7dJgjhw# z#ISu6C@ugB?IJWL%MLAQ3)9F0IC)WlE-i4c9zO)<+K_8juz8emA4HVxv+h$t9x$8T$3ideoJMn6A@%p_AE1i z#R`FwjLF3RfBe5gj6v>f@pGn=VMvU5-srcp$@`(4h{&Mpy;^c!B%g$Gt@)<}DYshw zwaFhJu=kngi57TLCm+n{!8CZW39hv&@1=_TUPwZ-i*CQ$X_vKfQY=?}G(893JuMY_ zV=ZI97F-edkt#NsxY7v9oFusdAsw3GlfOZetd)0CNRKjSCilruAi#zrg-bh4y0(bv zozUv)I$l+soYN~3vyAdmeC_(CJ0JAd>$bsBlErGNF`R*99UkVA? zJmP!l??d_=4fBpm|4b8RkXt6S~qMQO@SCPMJ6?_meZT(P7W={t}UyxhC6>KVBTDI&Q-DF~?%D2l|v&d34C$Be< z8s4ROyXT5X@TO)A4m13$3Cq>&dbB(QuniiF%7)kj^Y(kG26MYPU`2~KO51$I4Py@D zyjFR_Rv66J$|+c#4HvB{FHQ5KQ=nEZw-?f}Rs;EMPow;EQD*vC?mcSDjn5*Q_9wqk zjw&gha&*3Cd19JnBGp>tkSQnx64gqVvTp42YrR&xUeV>m)v8?Gzo<*SX72ju5Ox;4 zzhx6C_4$d9?K6p6n1oC$AyOYt0SB4v$0gkg`zLGqI%?zsFqsj-2q;sMpqtH=X7A8U zm!Pa|nt>~82Q`SvoTs5S=mO;=$ZG+GBs%XVjYvl8CKyD1Rg!-SVX1Rr_2bf!8*_7| z-Kn&jj{xw*%#vIyp=RxLch*bJCAZqhK6#Z#se^NagnEOPNR7=m#CUh%)Nxw_d2iAN)>3bEa z_mtXEnbLziU#)WIR5+ikA~?wNRUSB=@!zFl^OSKA=hPAMhZ=zS6u@Eyu= zIS|*G1;IfU`Q#%5#)g%52NX512u2OEMKXLRxXxa!G*aoRF2e0Eq+YJ~FG@Y2I#-&3 zf-bc(cYq}d2BZqYiso4L-&&<`K$a@_Dkq>g@~DJ?-`iW|NsP4w3oV@4bn=&{W5rW>YiOJxc-9x#BijQnbm1R?XmDT-2g;u5iCRs5B!Q*Z7 z=+&z9ri=1=f2&O{XOIV`@|G<=m2KqY{-Jz14Spjh7=M#qd}7kuFO*r+ed+C2)XaQu zgmkhl^!F+0PkC2vvrgqQiz}%mgz<&UwyesQX(f56Ul9#7FTeBCp)*D;3=^C$7>M*h z4Nf`ECpEv5Qi$@5mCdY`%eh`mkLvf64YBUbUUYLM>B3{U0dyvXV0U^~xYC`z~ zCvl!vS6GG8T9s!8l)b&>7Pcur&$eKhy;WX|pPa)Z{0p%h@d2NEKx*;9wZBF7kunkv|3Op{OhcaM9d#KIzivngKDZtgJt`CbS%7m!9X_G z+s8(m>%EHpY23}9Ho>^qaB$}Y&QqcQ$|Q2;cts4sFgLDu#wZ4vzSnT zR(a`IP7?IjTDfEpN32yNd-ymWJ5GQ0prRa;1P4$0VKTYfnVcDhq@zR@)*XbBgYHXR z5e=d&YE>Sx%t;m!{SU`06Ie&>X%qXOtJ>6;%wNxhas}l}Ri5dO4N|V0ilsm{^e<|9 z&rd1K1SaVEiJe09_w-&e2|oo(C3`T70xQUn%Q(Jx?lL#O^4HLD#4_n@N6%`-|WXLy#m zAcI4rFsqX0xDSjBJ&&zEQ))2w7t0Tbi=jY}Tuj zXOH19yK+z{@1r0oa0AlSgTmPIGWg%)KID#X5L2$MA_p^>faBJV3KziFlx72 z8m;n2fNt}7yu4pj7x_JVvgtZZ?2GEKNfz^7*6w8f8Kk7!k0yP@QSmG**2S#4Oo4#j zG&71DMgKLl$moIwk|`r{unG7lIWP~^6SW~ zIEntY=D-TGjdr78yl-~%5lU>U_)_m zE_86ZCv5Eb94#%T>4El)oCIEZ|AG7BME6BJv-j<*1zxM%nSeW99<7QjfA4>@3@bQk zQiT<~_syc$D%bRx#w|7tN)kqyT?>DfAmw?G!WK2Jhu130zHXQ=+%U8I471&axuDN5 z_YLMcr8zOtjrn;pNDntygPs+~BJVvia1;1{^Qa2`&)hl2|I;?UI|)wQ8Fmcg0O3UH zLPGrO^v22+G56mz&^E+n$3jMi%T0CyaAEK@aijIF(bcX|jNWwvt)RrnNDdj3fP5Pu z>jE90mHUtJf?XXv`9u&$=W(MlBS07BX zS3j_|WYzjGs$~w+)eh1a<<|qEHhZnodt^)ieJFbA&PLIXA6cR3>vo2s9l0<>t$eyO zq^7d0t)y~&y8M^1oJ+90@D~Y|uik|!^d(iIde}qBH zE*Kfdsw_T+9KKqWr`UyOw-iH?tfZ zy?+Ld2X-qXU2q^=^L3xkbDw|qXFgx;K0nlJl}o(|t{%hpUO=iqD#I)hbsfFxJYmyO*)$1R1;-Gjc5&CHz=6f-`Bz zZr0h6s0Y^B;1ET##S)`3Hz*PEf#OxQSVsQd$G#{gUknKq=`YZXjIdX~fHB*x7xGAl^{8em|?Olpi^b{{NmwztQX%qO_8&xd+{EyjV=zAH+~ndXsw*J2OpvgWu+e~2Vyt>Fx=hW2gvjH zh*HZre`qh$63w-cPlj2=Fx}W`%|^nZpOG0k1fM>gUA_k#DKl6b%`N_D1t67Mc!&G_ zxRMc1d*U1%U5JKhkYz6TAWs@6ShJifnVid55O>jH5(2hjyW_D48 zLXrYnl67`k2aGYYQ*W7&jQJ{KDi-@>3=>m{F9+?aHw z%`Q_=`KT?Ya-tPgkPq5Qa#A{kP_QtQ+&SN3#2QpSPK8+%`9S4AAu73!Ae$#rp~^jG zmCX~3h3N^(mfu>N58HCqcYr%3kPo7t%zgtPEr9m04N?TZLGDV2p$77LTaJ$9Zj8Ld zvFTDd!qD#OG06WUfj6Uz2_o(gNJ+eMBh*Bp&wY(QG{_QjQ&XOUg~x{qrI=}8MH1xD zAU9YuIi4$~Z}0Q`N-3jZxuudNNCi*e&D6Xt=%G4C^YYu|HjGJ!B_L7%a*4AkwB?wu z%x%lb_H?K+2ef~Qp%z;ZC<_Wow9foCKdM_w)3od8mF6LZE~@&&!U_ zxwwG;1s zu18+w!BUG4Ym_w*09!IMa~ffCf{x|1c$lokP0n^TBOh5Gw}oZYroUQLHtdE9KZ zvr}O@>*V#pzfacjHqY#a#K5$bOw7H0rZ106SfDI!&T1|PDi1mW!k>M=gmSi_`I%H` ztu<76(uf&8CTG}sX5@7y3TrtdvkLqz9Fk&1#BaLC$wiUV{w!e1)Hg zpSm6ca#wqgypt+=qm+{W;wqf6g{_KXo}CG+v0)yOP)?&$j8<|wXrv~FL(w~iUA#jD zZFQ@I_Gr~| zmE|2d`A%5K;kC`nE=9c#B=rmNGjYQX^-ap!tS(tKjSo>ee0A|1{d-~O)D+Fj<6e>S z2rBEamcUH7K=v@dDcjO4gF`vYC%+SIG7n#~Juu4Adb^K`<}BrB%;;erpJAFeGZTS5 z8ps(la`L8El=%d5Z#6!Rai=p~j-oSVds|LA*}^~DyvePnzcdYPKZYCPK9!( z>x$a*oGs}#M4Sw}C6Fgmq4dPJ;M~yeH_DkfT(2SLh}sq+A%*y4kFZJJp6+I{sNK){ zGP#7%7=_EFLSiIr+$(L|jbslnE_=)_S&=t@K;_JKpWFfkL$2PeKSZ!WS&q{q^xNunEdQ5}K@)>0<>}qj)PM+hB zi`xArm6se)=ePSP&~gqBZ4dv@r)+Va9FRCNs&IU=o$iTZ>?u?6)C>yb^dJCnu-{=3 z9I{IYa6E0MmC7yd`M}U?kQ?L15Am(qZ#IZbEy4edn>kc;BCG6cW?z|>h}8?HB#^v8 zEK`@s)aB{hEygI&`LdPZ?JjuZMV7*8Oa>E({1H}Eo*ozQS=jE=jyMj-5Cg2yc51=! z3z{`AGiR3OwOfN53_nXkwb_0)4Sl*nDYTNs$R_`7o3KBQbN~;C2r2QO6Kr4d%nf(UC!b4 z@U(bGJ>-Sf5=7i*<3sjxDJeVND9!BbHOR97;qFq9RnsE8sBH8Q;O%g@ily^L=|-IQo?7qFCJ(Y{^#;Udc9ubb?y^X z$G}kb{`8W{Yzsg#EYQ!38p`LEGa%Qs_sBB{sMrlD+gBXz@x7 z%K6?wDwn5=-ku}_^W&;C8quCAo9uM$%sN9zdf%yDhYQ zd*}$A4(6ZdyaxF`A6CY)88Wl(_IOAMvtXz>{3;$85oRH2yIX2c{d4o%FH||)SE&nG zXO6L>eR3B=qP4v}d2a0fgpN}DXc)3jhaY-FGjdo-<-E!(Vt46ddwbG>Ju0VE7IhHI zuw$?a&pX?D`h=+rRe2XBwNI!25Mtyx)aboGx7y>QYAwnBVT1g_mwiH&pF#W40dq)f zpItgHp|=}TkqJt6q8!p0$j~6}B+`-PeZ%To{H@%{0J`38CzW(jL{Z+J@<-GQ*5Ddi zt88rdwYdS~WjR&ki;voU4RMMF@@rfzMBw$Nv@2d&(+9LYx;|>wjSfgxa zWWM+X$V%Q&xuD&)%G#_ZOM8wnq`Fw}8psUtiRAPe-nR+2H8Y+2*)0> z9B)u$xJspcyGO|g)@76uNooAtb*J({Rnf_M1vqU#Gai zEf1Ah?ZWkZQQRJ6Ke$vPc^ql zqL_7RGS18KI4}jqV=rQTH`5hOQ!Z8;NlLn(eRh!BClv=RmJqx8q-qH|>%o`=A zxylReJ~8+NCfEojUBCh`e9KN@vmEPqlLsRL223?>#bjU+8HI=0rbbdg8dO#ws6b9@ zY`kU3c#kq(I=Z5H))~nRUxWvPI3Xx7KpKep@8%b|OD+zzPz`LPr=WgNxHx_r4+*|$ zIiXT$eoJN8%gt=QK*!NUmUQ^C#!kb0HaW}+XGcH|k>|j>mNf^4_=0~0Ks4ouUs-3N z3laxO)j7%M@AG+gbOAMmyoYc{mj*5jKk@rcs(i7XFIZ^id5}7zsy9S2oX$Z*9~Up_ z=E_HN@QLuzd^P5Fvf*ZkB0EJNMTg#)a}(H3yXtSX}wwfp*Jfo7WNhriRG& zRN1SO6>*D4+U;7H^;v|g+vBJA*@&%F8;jh}L*zsZdDAKEM(w;KEy6;(ve^do&1y>g zWV4pU+oVf20>wG0qBn$EL0uYT#%EQ&jcx3IaMp2CmnvHX63seQht>q z(B?VwU*I0ClNFeynM`Et??Bcl7otAvI0)+G?JCBi1#2jKL#Ogv_6?^M&&+yH3@K#_ zh}pyjO}AYpDsMGtx;y+Pua1+<8zQ~Pc2=K{W+!xz!+5P>5)b1LS&_C;68WJ{Uf%%< zk^_EJ!QXkV`CgH@A;0)DhyYmmeF@@_8M$-E;%xtag2AjQ5 z+JV*;vp=iPL|;tf zga#e^DsyK%yE$AoipW=lzr%tB!`7&n%kBQ0Sh`^+n9US{;bIAu~v_Sdy@unK&-1lMurt}#&v=ifnOz$05R-ymUSl5mK1SvP+V%PX5=5RkPI<1#8}I2 zrAMMbhbUz#CzMq7D*KJND)H~pVIVUTi|?>26NKH)bfmwLW6g50u=WW}8u(FVM4T{3 z`HH}wr5k)5a7Z)F0OWdaMM^H4>1Vxf$)g%J^Kd~LRJIs zV3j91@pyLajJ2*rbymZMQM}dy_BcX03Rq{GaSa6I=+%VMVPaua9OihePREoO<%XFI z`HFcOihg_x?%=DFiJpX!-Mc&`PtEL07&=O26m0OVbV%;Z!Wk3`7^1Sp3)$VJm;^4a zbae?`b+u^Vw?snKL^@u&aidP(E6Yg9P;~A4XqPL@JX3WophFi?rnp!F&!{cZL$S(1|~CM#&+?Yn3l7NtE+@h;*6RIJW^HPhTvKUx7wxnY z4zm?LE7Y#GN%dMTjMa$VKDh*h)gkrZkO~$}@xey7Wr)GL#;SBPd!EkjZ_%Y+Oyx4` zA#!iO(5sV2Estji_1NAYu#G-mp6q*L<#aI)TB3?MXGopgO4)yz1SdVeI+ST8Z>*d+ zjbL}{eO7KY zEoL=A|8+qE=J^Iqz(^Tncc0=?miv?$B`_+_cwtOGqn9VpT@;PoM42$0XkB{fa=(S^ z5abA-;EQFPZ{L$a+p^cWAy8wam=NQv6-j4{EbC@TL0b@3GBxmJY2S|OUC4R|%NFxx zEb)d=yyY`45OL>3#1Vk7e}la zgvTy}cjh(7#TJK^HzmM*ihdK3@xo`f>Z5eTK=^Xpq&Y0S;Z$0Y<=4o#<8^96`t zS!F>-NiOQ}<(aBbwgJonjGmvlYQ%Xfa$yDk9nVvFx4k5%q>Q)E*Pw(~**I1dK0NxC zWYndMs@*!?1V9F*?1sG5jrHG{_(d2I(JegB!?6?LAn)eKRkcZ5xC79MrUlGAZZ#jfdo1K3t&b?>VRW@dDTFx6MP4MTkwfU^g_^dViKCL*L#>oSenvySBEP9?D zkM%mCVLUm%%_q2n@h?b)Jh=c7-K#UDXq@6{(iT00HR5#hn>k*+I(a2s^u`gMC9%*FW zRxnE*1U@;r&6mfh-l7KZK`M{T^u2NNuc17j;*!k=txIp5+|%Z({KQ8>m(%Pn$pZjn zoXSmYzOKA(QiY9z~}sSwImTe!OouzQ_0 zeH%aA&Dfx+i=Xm6g3G|YI}tRCeZs4AoysX4zAQ>vyc6Y?4Q*K$Vn>Z_uTFl#*)dKt z@^Sli?;!jkz;bDac}+L7##Cl6dgBOH^6Ib;d*dJhGp~{#OiTz2lKmQ_Sg)iM|p1=nxU$%p~jQk&j zb}sR*0fKANEWJ05HJ2A{z!Er?FR(hxg%Ar6UtP&{t-2>!sZUrb6mTDK?&foK=v3CI zUkFxFuDm*V9)lUgiXlYhVMEky?UZL_Wp|kUTd6S12I@0=YT4IWo|^5L`xy%-xHryD z5mtFr1mWU)15tN7mn!Huc}Lcu!R^rNte- zRk4FF+tvoueRu;gop0EQZg@=Wa?u++#6DjkWra82C7-m)%T6Q`PNPM%iFiT!HOEiw$BnTU20^%6I}`2Gln zy{l!m)=+h`PUS)>b9#G|F#eKk!@erW4_Gf2nBWXc+gm;{sq_X5z1dskQL|ygny4da zY|2EpSkPGbKxM3plCe&xj5W*K2ALW2YE^mGhQb`Ow*@rKDD~+xGIWoAFz!6{8GhCp7 zQMlY!RYv3pnqx6wbJAGuV9#}DwXpy^^Rn8~q^ zK{zav`sM!|;xj4gMz2;LtO>pSW&P2VHGI`noTQHYXq9WG=2Uk6%0Pp|CU1Xvwm(M3 zTFuCH(|tGs^4Q1izBiiVlKSUT)GVExG!sd@R^>tj^d85fv$tQ~V=+)6m?;~fV zYMQf6P{-Xk`JQk0<>FN6?IU-$`;h4Fp|_7p!Ed%~Pl&pOr0yD;{2qo~r{pZ>y?rzz zqqjH5n{{(#h{dTITJ2;#9Sg4-$k)eX-qu1BtWag-@~DrWZ>Rr$w3z8yBz?|$ECKr| zh{~%&17m2Z3CRxRv~}qLb#g_DeAj(cYC}syx24?2E>QbDq&Um48MUXw@ji0N%2XoJJ`DbE-3d#epz~d#jkl9cCau7(1#P zaG?oNttu~@)Z)6pAC23t#uEsAC=2Ni#!{_A&bC=?QuCA6M$HTbs(5>tc!Hy^IrK>f~!TC zRm0~ z`6==Ph)y^VRf7SDCt)jPmhwC)BQa}T^8FCZG7HFd$rFs|)yYy8z&lXoag-JftX6WD zO)#&Sr7SrXJo#O+c?;{4?w+cusb0ch6SG25E#+|fDeOX*XhHMz3)lFmnuL0w%3U4E zauaao_&&DoOqY<4HEgim?(#$XvP>j}CxKmcV(7Qb)J;r?w zgc;fIYfZXckPf{Ai5AO3fGbiVEgr$nDpP$v<3f`I)vi+s%rFbhjm-hnPjcRY+={TO zDug_F)ZNj;pJ?Xb%uVcHxLq!%75)2n75Vd-2>)lMgaT?rdSRT>PTgc%G|&tj6oY|6 ztTLr!@`B}r0?IK7X)JxV(id@Hf4ML6KBkqNR2mVP9GCR9a8uORe$}!kGOcEHp3Mv* z0wM%C)Zs(T&}H&b%GNmhjf=jGkJg3HRe)Vx zWmCnc1#^H1`3zYw$kHPgo!FxEeEgl9#Ho+q3H42&VT_4Ml}2aN2)0a*{L5rDFsDLP z+GIQowoa=z-a4LqY_dfc3Vara;NDnc+HMh=u=FC-s6lY)~V%fhTkYd`|N z9yXNqe#SEhc|Q|2G@xu4547O@jIHM04dj!6-96v-zI%NH41y)!)n1!PZG9GIgvjQi zIJ}=>-*UYWKdV8LW;AwS1g5x(Ko{@<#W0IbUkJ0101HLG#&eJ(PvYXXBoh z_Gx^v)aU|N1T+t_vU6LKHHLaRvoU>*d?g-V)(b}EY}OkqFJX>mQ=BJZvo}^=u;16G zRGyWhw;!JSy!S+kf}}I4!FGL0=6j*{L`pv99D2KoBo@AZDcy^)7gToPR*L{`K#{*J zuVY?~MjbBSr>exaRPM}_z2PdWrsTYD$sL){8!oHxjqq+|hJcPIQYvRnL3tRi8TrRh zIEJu*2TtD)v~o}Hu-B3S?lr4nr#6GGWpkyXmCvd)S`AT0oGeE}=s#nq1AbGgZDVk_ z@QBP?c1ur-j>1nKHtHxy*zpLNk%ap#eLaCBDSE$DJ9WvZcnJo-k+ir0LctDDa4n)u zG^x_!6Bda$1F{J5RuH43GS&xqBRHJ}JBkM0aCDA370mcQ^}XSeF1BdEdG=$U#z8m= z4#Z^KKiDiA4>4+@-;(A0j@GO2W(;CAm8rWZS1D!CBPQcCWr==(ZAE0gGWkHBHBpcu zGVIQ!+NevL%?^(r%?N9_%FgCRa{)y5J49H&tj$3EBP)IAA)HuZbvLLDJC^InhI364 z{!5r=del}O<&Bj??4y4~SHb;%NDemPSQA|jC*U4yjXMq4RIoh@ckXJ|ATvRI3%r#6 zgt(k%(i(EzaCyZmYmgb-s0AI*Mrx#pz`k)!zuUvr&9cPvU|2@E$!xNI2^-hdU zDfg?XUeX2s06`$M0FrWrP^Ko*)8KcL_BSJNQrDa42ShLakI!LrczZFOL#-x~Scd_J zRC?&vQDh3X`%rLt->pm#m)5?Y@fVFI^~9^a=DlRR4U7kT1rP|&&1&!rD{ZGV9%MWG zt_P4WG@y6Ifi7*Ogt>0nlJs}FqLoA@p{nm#4{stRy?xB#wBI)3LhkcUqBV!E{+w3o~Rb7 zf;|I~)*VL`B9WcKf*xXMcLp@dWmvT0s%dD4!&NRd5rP##zgNj@(ox#f42i-vWNZ&x zoKcuqABME~!vv=1feGmVkjVR!Z}qZDzf7p2@KMtE70VSv5ius!%rc|?1a|ss7Z(j4 zEH6#VdBbJChHgqo5@atSK}HP?>jn7(nDlEa11f$s109BO-c>yxdkot!E-{x0RZ_gXDtgu%;K>buWgpsGuUaJV?&B z*Dv)8m77FShI*&iTAyIdq(z=exc#uJjI)PwYGzF5Q{XP$iq%N*Y>X zG!``bOHL-XUahc2BnWKTnqcoLu-Bc{;5wE4tT3X~v8-<5c7Y072I^-|EID5MY4Z6q znHsAU4qaphdk4$q=1T7LhRde@VF6j04D*9kX0s#Y3$t`WyO~ZV!CNJ83VTqVK%i5m z{z)j(aCx`Cg(96Y6%L`Byy5beX=+x0Lqc2N6buTw8LR;voj4WC>~MLhe;5NkdRqec z-KKPK8G=)8jAs-Q`dXT_td_L{YP>NjUmRazK}r-chOUUuPcQez$a}}z`JDBJ%gF;m zn!b0u^Jil|x;FOqdBf$d0p<9J9kV?FWkUptsi_J7Dsa1gblR?|G=7D~M0*9WKZpTF zJBQ6>=@lF0*iy_jXMLWGF`w(ZqpO6ne48O>p%GI1AU^O;A%gW#IX-8qh^uxI&@pHB zyHiljOusW>r<@5DamUC^ew&j$jMxk!pz3Twa}&~wsl1O2mI-0o=ra5_(nhQ?A_|wE zWB6bNS<9V5Io=nG^bWGv6_ZdmFWxLyQG8 zX+058FHYLZDHf2@sdNrG1eV{PBq<4 z6kW+!#Q^(o!%WtFkp*N1aGXSvUYdL<*GFSp?5}XagUaj2_ru%>-WJ3|fn1{gE4Os5A%7v5t$aG6T zI1@61C(w7V|J(r0IV521g%tCLvH1NKwGVeH&RfelwaR^lYfQ{_1RR%*?6}2wxXNYK zy)=2^xXN-Y8CdpOWercLf{kf+tP!@gA6S&JrI^uQtTE12ZW>s_4i6Q=ff!}*pkXGJY1 zpaZCUK})8(`8W`K$xfkHFHg<1z_ybd441N0eERtmY$3x{=47m^?Nc0QCGc;!%52VD z+HG?Op~?c+?xP*saXuyqE?fG?NRf|-0qZj4$+0>9%f7$>0dbo=as+3Jv?WY+VJ7~% zTFxI-;|j(;U9nrQiQgX}`w<+{rIvm(R^>7bsonu9&&{wX<4f$4kOSno84wpc$utpf ztXvVd+%&_eg)e!}tPfC}o9u)xZOmjF3sxfnB>*966y#_B0!{yJE88zB7`n7Ng8Kkj zgZ{ao!4;+a(!8?=sFY?JRG$l1mmDA^js-JJZ9qTr#@ZW$f9_0!Y8x$`%HqEFxl$q$ zF=p-LImUQ^yuom=4V1f4+MOk&lmy9$Pn0!a;#qApmm~bD3yhmQ8k6g!fk}T`>90HM z4U~Tky#wUmKn5u&wJLvhO8ra(fd=Dhh#PqOOXmRVVE$BGpzq*ZRVg296`()kbY$cwj?Q@p z$T`#ftaDx2i6JBCLdN<{)VGna*$z}^3bzw`ztC#`XvVpZrQn;!t1=F^wd@(PzXHw3 zPu5~GvLee|r1L-IF$z0bGjhmUuT}Pl-GOl>(gRdZY%}uwARYGI3Jc8MPqIy31AHub z%_nY=rGt|NeA28bPTXVt3UgwauxjU|(Y;Y48f3Fci_w)9E3k%;c@^O3wsO$YKg2L^ z(!PbLI5RvTA50b!wNf&2gfF{AJzVeQR^$rkjesb~jQ<6P1YnNndPXRbPWaXf;j`I$XdycUszkN`sO^%0U zc6Gb^)?+Bmc4y)YJwR5u+##>VKM9pG03 zc!3?A0RF@=lsqc1<>iV&M&-|10gNnMU)8kH3LXo`FRM$n<$+Mcfv5;SQp~-3WX< zxwAdT?|V|=cnu0N!}scCZU4~wx!gU~N3K#isswz=!{!E+U-q~XnWVqe%Q=~-J+BsZ z^jLx9PQ^jKAhX=uJ)@a)ygR^!>} zpUB6lZ7xj0g{;@Be99~D=bHYpte&==HJ3kdrXYOAI^VD%$#Q>DVKsccq(3ovw$xNG zfe3Ez@Bso(U*vTlw!8^wSw8-oZ<0xd>}CDRv|T$LAg_XMtgpWo;3ua$fT!o;Nd3)q zF{IUUQ@Uop=HcBf&`x5fAGTNAMmDfRu^_<->^jUY&U>2kUrB|8k7Z~P$dp`clHF42 zVHQT@F6fihv*-k;Yu2e;-ckCK?eHwCDsNVmX-XW!pZl*dPcu5nv>se)*k8;-X6lOf zEm@|J<8!=43qU1a?8?&FY+&^q?y>FY=PLJ2^TiH-U^Sz~;x^y=maOiRKJ{~1YbX7R zl=7D4TIPLAp0>fhK;mPdxgK2vxGCUMHQZV~fa+&|ME$jwsb{R1;?=iMc}=61Mm zrjf^OuI;_9t@4@_fdC&e|8LR=Rv4@VWPvg2DMEyF*nt8$F}{agEg zv--&TD!b^!W^x+w$==VUJDrpjoHQ&VgCAD%`e7nY(>hcGnZ}J+t5Tu&Lo2NAwMxkO zj{W+F(t>Kq(Q;K{C;6dDST?_^X69$b<(MWFTnhWf<5kPs&TWbAnYEqfRm<6KAdBw6 zN*QQ28--xTNB-ud3;76HQr(rtU2|h$bL2n7W60+;fQW(dn-#cUuOfwSyi$S{n{ZyU z-VbHBA|{{Ds%Xbi@I!f_s@z9;*c^8}M!r>3O<5%!=R%Y|AyU4^ZgSI2^K?&&q2O2>g-%l=S-={qlKXZanIw%GJo<|GHK8x?TM3M@VApuOy$5}@oY-vKR7j(cw=N#n4J^z_DPk-eMsin zl(ceg)4G|~k#@AX()t8W0tgIS#1htdPHev$Q=b4ZuWzeNAsKdMrO5)CuyO@&V~QHp zxTMC&uNgnnh0Y~yiFh^UJ4x4P@Ev9+J68(Q?uT@ME&x%w;@o0+CTlVLWR~i@Q5wkc zNoddD?F<2vYK;-iYT2&&Y#2AN04DBl=a2#4nZcZ+oKgH2Dqu&* z&0G(0Z;Cy)(IoZ2f(x67lgG)}wl;ox8!eHHvicOQrt&7AMbHm)3ALlxBw0C)K+dxG?lVUG8F1wHdW+-FLycEI&A>^2m2X#Z6kRa?XEQ9OQ}LHOJ})2ivdLzGJ`c zb??qIsa7ukZW-b8ZtAQfE}fSO3seIFzbx7^tko<2g|n2PWww#5{$E&%@+#V!wXMy3 zAShOvkNJlV%YUH~sK83%iq%4QO(|GumAdIyX8)TnUxZ99P=4?qN_^+F$;Oa@M9n&p zMe{)VUR9_~qg0OIEUzOlbh~@;X_ayKCC##TD8I>ZPEN{ci`-uo$_i>nP=%|*Q1X5x z(Tn|d^03OS82&i)Ov<$Xvoq8Fvi~Cuk`|oV8pycqF_m5HC#(06t-3q^vP}u&-dt(1 zF_!IuLIHP677Hh_`SvgQVp2oW=gaXI*}1W#x3u#1b@w*V%re&WhBh!$ud?1YX5h6_ zi#5`_>g0KD$+3$c>*fBG#lGF$?wdBjNAIOV%OuR}dUVrxXSO?ewmlag2}?)C&{a!s zx&{MZHa_e(_jz}U)N~P#@a0TwDD^5QnCREsH7B0Q$i_-fFG`I!R^F~EYnHa>b(n?X zs~$!!MnXSyQ%d3G^vNihrWAkoo_X#?$mxZaYg(cikUD`*TCZNP`tP zf9~_qyAB7ZrbEpWJn>ETU3%0f+USj?mQ_8h!RDQjec8=y?r5gte3hdSSMxlvU0lIc z^Pj{>yqd&F)XTYFA0zQD+N;WG79+7QM(FWArntu(l$TR~c^VjX+sSs<*Zp6NxLGYR z;^bC~FyZKFj5poN%G*2t^lep`e7h=s+nI!VjZWTH%Y$Bxq^K)HG5VM)CnXK!aXybq zMy3y>xHBurKf8R1O%Zt&$NWn?W;G!(+U5OBWlo#T*sGV%(_!5GtUfJ2@Mu{lcMw4S zAJ&-d=eXJlsa}Hnl`=F+{hc41Mr!Kz1n+083e?J)U_e9%^L}QrTo_5}v6m*!xiLS8 z!E-^)Qt}AFPBiW}ZELo8t@2tD=Ugx6^ebzprn9)pGaIy1mwE*&$(r}-rL$j%N%XVn zK1OFAsm$pI5mG|sF(VnbM^5iYG-}BlH+c=l#W-8@uL)336NnN;TCeN_!AgSwFSMD9 zJ4v6xbk?W$Tl$%l9IeBw(Mg{##C;<8Vl$k9JtLqh7d zu{C5~n!TCHYBSop%vjr?OJs7wKnnjW0pk|HP|1|N3sWi|n|@udvOtUW;3F(7DrGH8 zDs1xpRUW4WyWXpp8?;W57ETkP&o9(LNptvsa575xaqIoTwQz*(^6#VS&ewaXQC!zuYkGwDG@ zmvLh0^;vH}d3ieIeLfwq%23LLEb>H`Nd~C4S?R4pU_iIx;YsFu5N>hQj0`}v&Iryc z&BzHISYGEQMJeiKyPDub`2}Y9fy*(w1Fi(<_nP1W8>DW9^yeaUf~38P@&VpDcP^Ft zho;;>h0j^%j>fzC10Z}oJ(MdODZG*CmSbH_r*k`UF=H$Id$OQ+5CkM=d<%PRoyr@( z$bl1;P9T#{W58VbVkE7JTC(#BadHC)lH(Km&#)gQ{jR0o`CJKa?v==(pR`<9X}Ot} zxVviy3~fXv5Coj1Xv+0ii1JBQ8!X#jUE3)=GA?n;R{b=Y z_|3%MbkPa=Un0Lxs8V<1oT49E`v zGV(FPIEW=#-CnX(8Ch-PxEcT7OtV;RFH3DR3T(^8cpQCTvJl%1k6p}S#ng&~xt-OP zx}p?)v&1lc832h9*BEh7lt|drDegntn-`ly3x$YJm_v}2UzFlKHSA}}$hQ#EV^Qey zz$ENUD7t`iq)slL>Xt3Nue{iQo3}3)CwpUM88`zaAFz5VC+>)-Egq$oAR0Da#%jz- zx62dnHm-FNc5RjSr9-X1J2P?ItsI*m zXFbTlpwNwp%XB6%J->pLPx^23#@Z2Y_~`D(RdlxJrE? z2^nnu&v?9Q**c&G?wuLThTdrt^zZ^Wk_$6Mm6s3CWAdvHV zEQp_d#Ir)9hTeYi0yydG>8MxP#(PuEvznL8CPA^$Kna+I;h^b5y~Ga0#*wU7?M@_8 z?)ryh5#5=R4U=IkS6AbUyfdYCMS8vR0FW;`-W>!Pd2v9mlFCI@-N`;*zQUI(=SA1& zzdYFxcV2ZUb~mAUFr6dZhW;(PilKK8ZT_n#yA2>WFgDsbn~61&nK}A@Fm7ZRvLT;p(;ixb6jBF%2SLcPGtS>q^l3<%Bz-Nk~mPW^6gE{)aRG#;v7EV%=&Lr zKjojoIC|sa%I9D5d88d~oEFJ+*C;sL&>oIE%7 z59P?DlGV~&0@xge)g{^)BcbY$gCj(P&wwNhq;8CNtF+- zvWTf>l^c%hkzx3cuZf>*JdS)~ z8Z>HC!E2Ut*)WQu{BJ(87qDweP?OgxmlKMHfa}%Erh#Sl$tQnKzPQ4rjDt8^*!)ftt*vIKMG-(>^l6&&&WznFr*D6o*#Hj)%Bdi!? z^=P%xIYg-9Br~_m)SSrxZ4A$_NU6;lJk_j{-ULX+5>o0?Z*EGq9Rs4vgNoS1F!TK( zIn}BrB!u`31;7@VWko(aJ_mPDnKQsdtnDsX+1p3u6q1@{yfGh@3kHTvysO~tBM-tZ zi5bu+Y1@U1)~npT6XBvaFtdmR9vf8VfNgTw5qZ9n`;WCH&nvTv4SM$t46|OH+-vW= zdU<;wn58h^X?>UJ$w6hZ74Z01tDJqD2`sNe-4xwM2X5rW>F&kbwz;4w&N-;YGBD33 z62z;QHwJ}rbDOX7fY}@E??C0jbP-9u$!nJGV@qxWtz{+IQcI3y13fV?WCLZrX4wLr zu`GZ423j!yq5FY|zsjv{E$(w`frSg)-eRY>Of)Vl6O$^tRmFH^t7YXkOkQawtwd3t znm8ND>IihAcwr$iGbA6$o9-U|rnv|p>V$a^*%V#E3^_7p!N}ZZ3cUv|=Vn4yCh)dW zdwStvF+6h4H*2apxp0ylU1%yuC1Z0-rPWQe>T1&bT<*pyc>Bp2$KoSr(rk~>n81BQGN0o=(fx$Yg;rUZyX(o5sSWI z63f#^8y)q01MPgNB~MxIRI8kj+}K|)FMbm@Z=I%x%e|BA$oQsxea|HO+J4Ealh}@k z7M*Tipa0F}UY*>0w6Q&v)!%>}bRAt$Qa}AB;C0~)`oJE;k?Q1JOJS*3dHNgd43&!} zRieP_Ro?j~sTobEvhi%ze4|Lj_)RI0+<#nbhx0szakfV~&$T7c57JS`cGC-_|?&oX^GlhTYF2X;-ZOLBS zt4-cm>7|7k!*f_#UCfjN?OwaS9~%mGQ@84Q?L1TwsxXBanSQA)2;g~wM1%CmFHL|g zXnxM2=Enh{Wt%Sut(OgUZTTR094cM1CTV|;gr%pDX$ zEm{C6pnpl9@t@ooW2_8|{QI`3<29S5Z3Y%)Ug8ZJDoK(nkIBJeDl*p{gF;}Z;#eL| zyWa4>Xt3?Tr}?4MGZ+SD*;SdWvpqk4ln>Yb6-@j2!9}lGu06{4>hNJkCy&bGgAHss z-WO`;wrF0CSW9@2269+Vtw6c$Xj{QgZBDFUy}Z41Sq@Ah5iXvTU~Lw+(MJ+$St^L# zk~FE7M|Lr0E&Y4FpUH_w+3I@r@}W(FVtZNdyYdN#0wrN({s8kmibq5twq(b1Y!mL6%E*5f-{NOZ@P|*gp}<&hfPgNIF{LgS+qmq6K`du%X{e3vZ*&9k+4^oh}mWM13j31 z6!409)6o*%SQ;$XB2>${4xg{OOyF~lv?Jg(_v_vxEq`GI5)9w3S3~THJNJfSRb3uE z{3|%WZP(sHpXA9&ljHeU%PBQh&WmH15Q-d&x$W?1d0*HiTHZ^Ju;qP`-|EtBFpZq{ z)qy_PwO8KRmBmHcy6CG*`_Zm_mo`AY*@(OP?^@gS-(M9zq& zrY89k!=TEohwpHDKC)}C%*M=*J^ynHQttHXWzJ4vOfZ9|R@UJ9-D>ROY}*hji;i#x z4tc`R4XtEbpu`t;9OO#FChGyRA7P_8yU>lhY-{%Rlf{NwHvP(7Lir|jPrd!*j`*qa zSevaDNW3#fPOCum>~1yM#WJhhyW_xj@7gQJ8x20O*?h*Tpq3jm6C>{n0zt{~qFS^m5Ja2vtZB z1wMbyeQ;?Fr%krN0eEY2gMPjA>`rR@sWBIeMeetD4!bc#pY+ui@9iAMu908oV%2@v z%ou<26ca>rfoZ@+nyt9kc@Bz~0UH{pMOT;7m3$-YDsJe08s#m#a7t`1_A7O{DT(nDQr*pbNUOmEj>g3#AEKlkcnp95Gv)$9@j$pGXM9+7rl51R^U@x0VmelW81az!R}?3-cdkf z-SS(csD*nV6L0R*YW5yDm0DFk_+7H5DqrqiMh5d*WozZJJw>ssR_S&K-dYnDynUtn zcSbW?cQ4b^o4;qMGj}rrn(G>FuL-k`##E*zA#9^NlDREnZe7|0(?^dKD*X2cg6I1{ zaLFD>knpF6D|6^0NSCn|f9SH%`iRn3+wE=^;DKty$~PqcmjsHBo|Qmz_Z~GI`st`9 zs9NB$Mw`^eO1y6@iD@w?M^ z4dr{~CTIo7>s8L)-3a0}M=R&-4ic6h9w}>&xlXR%v6Dx44dtbMZOJ)gqJR7?CylqO ziTbRg9Arnm{648RJ-C!C=nAe_lRtiaUzd&dW&ZF`KF;As+hjZC>OG2@M*?{Hw|&n| zJ44RiqXw8sR3tjd~w z%PPw-{0MdKR37+E)b7UGsK<@&NqS$ovUZztxOM+7YO{6s5T0#1#sj~NH}8(YTi5In z6?sVi3#GU*5Fy)V0sqb47+abZhI0NPIj>HZxv^B9s)fVVscilYiU=&QHe~(JHM%hx zlm}~BmpmZ-@i*Ai|3PJ8|1zJwH`V9YDb6SLuKRVW*?{X+7VgZ4SvK)0efoRS`pY(v z$EURaU%JK0gZlB{4GFjNPEe2GDE) zoP6o9z1?1AHYob|jlVTf^3MLDO2tbEiRoYE!ke{=a}6GEgr8`uqq~!NJ!ta+2q#NF z32m!fzeiYLvSz`%(>+_YM_8biSi%<%iO9PzcxrJ z3e2)Sdrf8|T>UpOiPp>7J<-SUXRY{kw33e)CU;oL(~PJcwLQLPIZKW^wpJG_l}vZo zf@Hd103HF#B5&8zPza1?2Wcw@T+>ca8F~; z5B>CxtDekW8)&E3EcgAIbN!AzBgXjnum6NGF50u$+@LKMj4jm6lhZ9w76R!*?LNk!&;0j@Ro)13f!7azjXtrPrt9Ul4h)Oy_C$4rdE}Mi;qj;4 z*nqrUDSnUD^D`W{{G2BdYfPSy$(>gzcTiBFUS-+tl%jpCs!UfZoAPXlg37~HMSBvo z_+GWFaiFD#$_-81Q2(k`?j=e_<+MFRva5X!r6rU*_6_9$hobI2DB3y%6cIo7kv(-7 zc-JuSFIFA(^k0{}idSLto=oE+)*F|@^I|8N%IDAwn7QgOFjKFxVF)ef9|qX$m?e%M zeLMso2>LmyJez%MF~aW0VJK&h_sJ?&`5ms&I+abu9%q%ktd9OR#f>kk1L2sdv$EB0 zjNt_7wu2MfU1ZDM+8qM(zw6UF>q1?|-Fh_&25>b1(LhsA`r<5BwD5OxC#;e7USvF%d)U6rxtSA zf(+o*`F;9&!1`LB&1U7^Kd=p1aa$#)S8wgV&8w?Ge0E*fk|k0y&|cf-`-k#lA7OJJ zl?`^IBVrD7U0oM?`^cHUg?U_T+p6B4)u}u=F=rH(n4YdRD9m`ak{R*+%Dq^JW*ks3 z=jpz3zsH>^nMd7UuU;-463XZ4&}!7ccyNof5Wh^L&)_L^(>as~lB?rhH|!Mx!BrED z`)%B-%r)*RZ*ish1qB~R&$;-oUQLI1%3hmT!QweX*@`U2lZIO(3|Tmo#b}YI_X(w^ z;6rtRX60?Fg;$Vi>liK3SBT(!?t!1hm`uh3_Eo6MA;fEL5 zwp1)vC#JA@hZ)VeoirO$-2ZcU+j*Q|GJY){QQNjx_aX0Fjj$l1KwjA=lyeJyb3s=B z6*D>+_*EXS+9a>|=yVD&0j)@}0Yz9xA3{^TMRQVKTzDq(&ZFj5jX`j`2vyQ#uR%6AmP#(_v z{E#25MG5C}{vd>^l*7HPaH0cO_q1$ zVq@yCOA;w`-B4oW-`%?`r%uT6q1?1rDEGs>HQJYSMYsIpZ@Jg#aY2zYBR)qi*o$-I z{@?H{K1X;~r}Ab@re_$LW-O>Mg>cBNiEMxe|Lg`>S+`sRyVT77Kh}X3UYbncT_!*z zST(fI26@Fc2$IX?L>x`z;4|02l6ky-BMiB-^6Y;jV(67HEM&DYD_7*#$}^aEuFU%y z5QM^+9;j>{1_aDqTq^iwdFEK+sETCuC-w?kWb?4HT*S8~z80u8t5Orn7ekigl`+Jf zU}SqlPIkfMZPz#i7PbD2$|>%IQp*wg3-ahSdxa{+9OY84(?EXVQ?gKQ8djDYru&R! zpq_mg$?Hy#4&-2eRFkBNGCY)nODYYZBSTgrQa?dO^5&3Ul?zO}_$mp#ekdfg1YC$y z+%od+kWk($`m$*lRXoRQAQ$Hoci_q^lf}7tZw%fpU=VRqo*WWtRu&)T+h2y1Ymv-U zHrT51gbDBuJ@B$3^9bVF6jMq#cMq+c_E+p(E@&fapw}vQn;75@{7d$RiP0QtjUB!h z@>Cu*xxD8vKOKea4ZAvl_^}-zj%eU^gL>6rzWoJ^a?fEsNOoH%U?BMGY%TKs?k>RK z3%vJ%EH^OjKFm-5vx|Wnc1;4t^MD~FPk5)KRGvB2sOD9w|(sTQiCZ5e=G2VlpuS3=!Od7EIOj)V(qH8!(AC2BX}YI6^nr zxmk@QQDm6~ZD0my6_>yCHi=yJbE5=>a`8}WaR8==FN$$<_{Qel| zsyfSi&Uw#!-u=*q!>5P83G3aX^5oYmUcH>;8h9ZO5OTIj6LS04Bf07zKa{_HJu*rY zhD?ZbAJi%jeI0c6>g9A-^XYsEZsRof^lZK&fAHnHgN#BR<)-van^Ms8_?p4I`pt^0 z|6dK}oL{2U|Jd4agL%dVvqG-@dIb_f7Zv-Ao_>V9_05Vr{J-krBJ1K^Yr}Q%!8a?R zT-sUACXxR=De|2)B-VhAfwr-TEI`3jp?K=mmE^ikv|Xmxv@Bu^Oo@5#WLvc>zk$m- z7~bsMMlLM2q8GtCQ{|#>CZ^`U7Fj+wq??CSys>f}Z&LS6xzm#J7Zo-`cBVx+|0|Hz zK-S`!Y}2s@Dv$jNC5k(p2~GRlbvE=n#jX1D`3g9Y z;u3aDIiUn$KcPC6D#s0}s00zKc;YAC%fG?R$@hGfm4xQ1WJ^W4zSDH8ad2z=U&5J1kF{TWryiwD~QvT}kC2(lp~KgffxOUS9f&|FR5n5u&?SFMoC@ za}4s6yjL`p4TSmSZ`eY-z?-ci6`X8U{tEa;=&<6t12WKMlfAq=UWm!#N0ecRbKMX> z_$DiW-XG|dI13DlM(#M4@^aI^KAa~QjuPil%!f59*QuAMD|2tWGOrb|kQml**3`?& zq6svW&UgUF+k5DH!Pe-)A(8wRw(kv|<)k5|o_M~12KJaCkvs?AtC$>UbrH2E;xy6X z<>h%;1dcX2`5O_wwNE53RLc_%7;UoYABEm|zr>4G4lh!yn0;M3$xkuKUS8HsDoaAq&NuB#UggnAMi7<^O&LC~UM_M| zzj{d2;^k$H;fv{NIA~eXbbqtEjCz7g|2Ho06Z`ufTb^tG(ek_>FHb0krc!jzU(+$a z9gq2}NwMGm%4EPdF<`lVXr!@ato*O9>>1yPx=E88d;3hiK80iM4yYrf5{5Ff3TO5E zv3xfX==zeKt~Z(3XkJE}CfP2U2jn*6R_^K&4z=tB9@Y3UB zGdyoWFY_8@PSJ;B;&^a-&=6yeqX(NfHoZo@`us;kT zG5NXj7IKCLGW(mbia|EMQe^y1)mle=s{w}{^Zp82%pH}Z!+FlkcfY-Qd3~a7wBv?Z zLkbOgd7>@(vBMyn_43+8TkjKwML^A1@O@mXgvy=bQ17RMGo0ijeh_GkG z#j~3g-!Lo&gzbJJ-ay!Ec9$21*#(q=H9rFp(KT=7XO*EMODN5HQ1$;nDH>Hy!=4jE zdG!t^##z*2!DP@tyA!E*_N~eC$Q|F}gz3^SOwYuX)vwoo`fJ^7Y2C|`ujJkSxsmCK zNf$lUVwe1#V#V9j*5;5L(DB{J9+2~NwEEsoe?iuJXeeEN&u0k{!X!}cc;Z3ii?F|bFr3% zsg}4SE`d)+_SbQLgPa0v(LHHHZ+!bxc(|(ibzS@w)d4K^)d$3E&645JhP=w-7Wr@% zj}{Q-I1_X1KBJYX{FhjhIm65+bg%=v=iU_T8)Cp*)|s_hFpKDxMxl%eFwIW@%$NS_ zVEk>U#pA}SwfMdijB|EusJDaxV4RE9M1k?#&y#&LLQdHc6z5pG_7S{{fL`K{E>rOs+SIoUzLvUR>r5@Ec6`*AeUX-!5BD&;nVY)BRQ@!PB9|m zbl?5CZRGB6SB+C%UhcB`=TjfrJXDU~@dI_?_yxYNIN+be7BGtQ3!p9}EKlwl9&rt| z)4(P3lOE!$hu~D0ir+oMIpWqF@ORm4d2D!uBI2f+g>oNsaEARBnRPmUD-$H>daN z{Vt@~iRtO6htf7rv^J6X2n_8^SxD2 zn~C*|>B9Jj(|)g|1}byhb%?z`mEF%kp8uBlDxcKq(~5gzdY5~DX6(&nJEA9zbKr-Z zNsdQ+@4tO-L~VX_HZQguOMVoyQ_26`#*xAz0jUupOkAYf;XFC@zX=n2b_9C(yZ+l( zGS-Yxd5-36Q?bGC{jb_BHbUi&k^L%-_zmBBfPs3-aIPytWe;(uw_L1!e8zx1YeeLI zO)e-}{KPef`(~dTSaMEnYjv4+>lhkdG{Wd5pyC+^hyH+ZL;qJUKCg01V&cDzLGk7~ z2Ceewh;#&U_VDPSX3KHitie(gIG~BwD0h7_Ug+8#`$M}@$uR^rqupL!j`*(_Kl?{A z_Y9%O%gZbO&9wXrM~+-Mydq?bu&f2BW6Xuc+gIg^17e@eyM`MkU4DRJlEx_m?;IY< zxlY$<>&L!`?M;gxN%3>VP7U?Tup{+wEiLmL!Yd*s6|o9iCO4lOzjEtXU=|mXAOqwg z*N?-=Iti9Co)Er|w(2y&br_k5Z@C`34S<-#WM8dZpF+{MQ$u~1JUt0lxzv64c$-N; zYSw~uw5kZpVpARp{tOp!tHi?x`}{CTu=X-}m4^;)bxIYWZ6?t^uB(Ac@3tkm=U`uY zw@vX)?y|!2=r)m@`+HyI5hB~<0;h~#>r~U{yQg(#*su=Ib#K=5Mo#~ItIA{BL~=e2 zto=VWUb8+QdvGiNr{jD2VB|RQ zB3ZO8l+adD<<)~*Y5pmfB6qRjNR9|r^1It|bAdxI+ApRpP(kOba!BfmyJ|x^^mBLS zq=5%6=t53~n1By?GA(jaTEqbmqOhNprUsu{EqN^^V>uQ&qMLE zYS84Oohz7RSMP!e4DZ9H z1Y1{Dzdps+>}bb;*FBSTB~RDB-?RGuncDZWtM8w!eZR8${<+%sb`^A2ykM4pdz*H$jv&%5%7PK%@O3XL+lgPRI$JG1Dog)o;nn3hfe)_NS zV0pV9acU>q_|Sh^&^0h0E(_^RbrsDHe-}(QhauAYa;2rs`Fv*tyyvH7r)j!!*9g$o zlIy*#3=z%o+}#3=*U{j1dzo`N2b<8Xa#)${X)?W40~ypW(|vN7z%(w|URl#J*F1?J~4%njn6!M!L zLyLj@F4Y6BdNrQBUF2`vAOQ6YC0k;{24S~MPf6pRuf-)t$Qq^@<&qKTz?-CU6u?=M zc^q0^-L<5J$vU4wHF;y@>YrAr)9t&l+J(>?D@*p{wL6=x->qW)VTI9&7GcQiX@}k4 zX{cXzSy6*|aC{4X8fyt2+BMb^T>4WJlpfiY6hzJ1q)np@%}h`5`cMBwrcI@HC(G2- zy>HAFzPB3)7HT0UU$QT+jQ8PqH|-Wp(So8DdV62S<_Pk_?{MyRYp{;gUpxKGH++`ahV4cSnAQ0j$Pv@}P*`3 z#>zvs95(MNXO61KI~~4EY?Wb_QTW60@hHn&k|y&rk(}3w7Qu-S+BGjk6$4>1bxap& zh&PTmdMU?z$J%&fyssskW=6=d-@(L!D)<4nN|&-~gv)q&xqshyDJ#3hOZm>ewv(9AcHAydl4S_@m;I|tERXMOqf~L)3cEQ5NBFJTYEbJg zErxV;Ljv(>3B=CwJ={Nd1Ga14R$?j z*o8nUHwh8Efzie zor)!kCsVwga7emcM$s0?UpPms9opg#GB3{^<{Q^dLnGwbT_a-;orz$ApZ6NWy zy%BN^YkiO}Q~j{XZnLjfnFBgWZ-l<(?*B)r`*w@yzuBvo3xCRsS9XnvFq1FhXGWc) zc&+m0uJ*KPUjT1FkiQC9$BhwYqk5IulyL99!bN_(JMyQ#kTy%@nBDBfMyq)1ZjrpS zRU~Kp(YLDuS*OVsoBKN7RZeY+Bai<)eW@1YOOZ~dxy#+xyLXG&1@c(j#4O(~XqRWU zii)v*ktXHNslHtCg-B&PxM>b!9Ck?7`*3r1#7E6?${&4sHg4oeRBk8#vCrloz7@F* zG8`!PYt^efxNkC;8{A-40uJ+N1DV3|1$L5mg(jT3Q|tyxr9+Zs)0fIiTa~Dyz#~_6 z`f}yKNUob!mNs7w_2t*Tbo#PsU?guIR@TJLxg$Nd7jH`G`>UfG>bvH9c{zW4Ob@Pr z_CW0K8IMk*_dDtCItn5T0-^EvKNwU-sGP71MxunlEq<87@gZ<1DREJvnHz16i&H}~ z2gFv+RcY~hId1m`cMmlI_RMq(E2ayy;ML3D#xJF_zt~9X9Q<)w=~60rd3oU{v3cxG+GiS%v1xq6 zO(Q@;%T__@|Fx-nfQ`w)olZ{`nyY}OTiK3%XQCZlHWx27g;*FI@YhdWwgQ+4P zgf~{6s`)TG{xCRFOERX#h2B`Xt)|3ju0*JE+RUpmcfxPoc`mO*MDjc)hnUCm#wBHf(&K?!7QI@#+ z7+<8aN94`T$Q&S*kM<~OP+{s`+o!KOb<`TG^6`(CdWo~?9N*Hdx@sT0={n-aCRg*Q z#ecXhO0Sg|p*eYA4;(uzvcQXqMPgLmv*ROVk~bMQ zQ&8MTM#%BIvsaBd$I>Pp{ur_os&wrW$MC+ld(^BP2LX0sm|sfK(zW#H)==e)JtEuk ztA1R?tR9~^dg;eD$1tzA?Gb5FZXZ}O4rp^l=ati1N#k&p&9NgHOMSL$ZoU@gk7@Q* zx;oQrFU9&4tvC4WCy|QJ)UK%M{{f#}LIkLI>q8B4G85$lGRCX!=2Tb<9#w#*<7KBR%)+ep|0eg(iUWsAspJ_vqWO;@LlW)Sj_?RY>;{y0M9 zxS1FbU)np~IDa0^a&~d3NPk_gyTs+=x)^}NxPZr3S>$f8=<4)kLtP|`0q)(pNTt%b z^gzaujbv$ct_N?G&h^^Srq&7wX$f0GuTlE`lWqQnOV446{8f7VDAGWFNv2PRGD7az zBQic$1G}_;nagYBq;wPiq#ZR~4q}oso=|g+=;gSgA<*2IuJNw~~2z>^}kgU$Ky!xkDspZ53&gJVBaUP|N#_B!2IV=D;b&w_#4q zhE9dJYH&J~%Nfdi?b0>wB%7oM>C%OEZr3=aP2DD~a1RwwU+IF_LF2W(2`<}wNl#!# z%`p=N9qvvA4N-kV@=1k7B60UOCt0cS+oqkoI;Nq18ELdxqvb#1R2iXi{}_}*;S_JI zoU?aAHX7$ZDpWR%VS0t!=LI=wpe2Jj^G8OM_yz&GXK#B{xy+!;9+7>=nHT4>AD~QN z>d)f6v1v3Ku13vC&)$9T6LSCPUJ}FZB0YPXr;FS_x&%`++Om!2oe8L0T~ta@N82oB z|G}@CNi8gXGS<-0&ku?q^>5J@(8Q@Oe z;&cY!MxXl@0hj@z}z z%7Z^l)^>zk8dyfazy63Ggi=^)l;KGTiNR)eqt=a7O^WKfh)= zuB8~*efkf9%&|zii^?-WMahylRu>Xfj-UF3_7s5|6{h^5}1YN4x=Li074Yiia4!$g*2s^W_P z_8gM7`Ysi8nda5y4w84JU*)K_R&D0mkhjcp)e_LCENClhvx_Q`r=|Z>;b#}LS^U$h z63Ovxt^Cg@%`S$sEqP363E0KN>#AKNJ6yi=0gJpBXQX=%^f?LE zo5tX;EW66hfk|@Lxu;u#lGi93$ZTU|54T?VDAVt{Iwvstzy1fl_irk9*F}~BSYe9; z!C4VoRwq0EOk4N4Y;l$E`%NNIlM*#-LUz2&8+J&i;Tfi3K@kb_JO|06K@^hhqt`}E zF7W0^m#JK0d57G1n6R8YP`93OURLvgfo8XtyX)}EDte7_TkVLv5pr#Fznv<}@DI>H zdODXjNe9L$xuY)1#@5Ki!&3lf=RsCn2kw8zKu4fV^=NaUt)P?E=qGl6?Y;TN*IzURz>n1li@mqSyo^6|JnZ-mTl=GdkJ zB>kjuoD@z{eIZ|wmv9=_e>TkX&z7RFycca40g)&2OoRLi+8S_=)Ck8y4IY_7-__C} zf6YX4bB7PKMYd+Ai^KQo6GS3F3Uu6C-ElQEl*}*E4hmZy?|RQv^h!oxF~JC zUT$n@&}A?XP0Z?5AcQv}C-*k@YbeKL@GJZLjcj|wq6_8ueCbn!;?)kggfJv>hD0^l z?L|L8wh_RTqcXjps&Zr}v;{oqmbdIm+t2U4BJ)A+1TBZ`oX*(`EwJLqd~K^gI=|OX_~YV-t`%&e2-~(``=AJvfz8B zDSYqWBO3MgQ|aGxsW(FAe%C0y9PBG>AmBB7`^h_d+6JBf-3Y-)Wvh~o(Y!c&Z3aHl zy7+)%()Ha)yL7U>d3#UD_6S+<-N=0Cn4hf0B$3oHaQ}LqobtU^%kKN)zq1#cJx}G& z-z($d1`XkW&m9D$GywT|{NEuWaHp`Mo+l@NuQd!mzx)&b4v>_cGTQUxjPLn=iw)^s zHv`;I#_V>cT-k=*;{`idUe2I5Z<2+bzHAwxbbqfdlC!`}d7LvUQ*nAJv(3N?G;32> zR5Vn^E=y;tEhU0uAk(YI;T~xpbF;3GH#(9&j>|+y%o@-e>IUhzJvBQM*&?)HxuOeJ zI#5&%b~YB#dGa2|1U~$!Rc4hnK+rHxvTr#8)Z?d}Uy?;y>N4lG1CdxD%Ll8xLX3}c z8#zxnN_dSJa1I8qhE-&%sClwX_q|5>vF{zs+?aNtg9p?`Pyz2?dDAsh$yhUQxu=Eh z>Fo|*PN8M9z{}Poxc)?&J4G_V*Sr;dw=R+|RhncOQ=#&-joAE|r7si59SE@V&H zbc`_tf4~ykjf730im4H0VxN2?T9_*@Tr2ZBTIGXGG|9HPwEJT$FHm;)yE3Q4=Zi>5 z+Wkqz#L0G2h+9UJd`zaRBeRjpX^>%gKGUo6e!DMUisTipazDPqS9#TCKx<~VkB%%F zN#vdIoV$R&&ix@q!G)OKS*UtdCa7VO-<72`=%(9$Cx{Lr!MjimZLg-2v@)b zaPoE?0>{CclLej7Asp$A_`OqC@eY>mPE(-0OGTAjDU_EwIL>FY2r}D!otY@Kb8=!L3iMQ~FQNK;R7H2S`?4$>Mbg)fK$epg+5UWZ5Z+WVGcrD(_Ie}L zx-E_7MeV+fwhK5V4sr*~Jd-Qr zwYIXuB+^{*SzaRN)t@E80~P8UNVe3h3&U7NhBiT@BF{1tDyK-V`7p(Lz~6Je2g>c@ zW9)zopaXC;o&H3XbKA+z^1sBq%d-(dbDT$Z2C%Jj53T49o2*kt*rh>kUelx|Au~Mv zYqP04_??pr+spDOi___k(LfIK6JEUMyD_RP$p~pATnyDKby2J^Uy_k6(F!Ac1ugK~ zEA4XqomC#^nqF^7M&+zdw25nt-<0o@fWREd3S9Z(j=RY`xLHE$e2LxZ4u`lTBfC`W zq?egYTo^{)-X#9*ww4E$H1PJejYXRQl%E?e}%HGfPSF5MU50Xr5Qh5C`+e=v!vACf>8yi5 zS!U&`&a(A-b6v&T+Z_2g=TL(J%Xi7{4_*yTCU%`km(rGMj%I(q+HaXt-Ss=Lrr+19 z{Wjx&@rGeSyizaJnSC=3z^WTtZS4wc4Ml}56F})a7VfxC=ehr?1@q*+z2#_I1ddps zext7B?F}(UdBqXMY9t>wTBRZ*i0!khl0MJNNDg|crX}43*Wr4lkr4X{wPJa0>bMxv zedMb!pU!mgrVR~I=lTrnW#H`tOXZ!CfdS+t)&p-Jz%exwb~-n?`*h>@8@FD!SK9iS zOanCj{A?saq}=AdkIFhI;-E<7Pd+;QC$nVO)B>L#ZTEEyhe9^u@$vRn=kkW4bXVz? z4+QNkut8U8dX1|MV6k*oM5)x7`YI3kK?=|1z@yZ<*7`AR^p4eR2qzul~zx$v^)4(pCacA>BO2Q0B zO*P0T&CUK{4C}t~Z!9Of7e1v>4Ab4Rv0CP4HVa8Xv%8~R4c)RAPz$KheQ$Y2nC4j-zQDa@RQLL z|1)j+8JY$=iwr4imrdpE=l1Vm|Jzn^e{r^Xy!o;t-;3X5ziPishhi%6PyE9g*1W4e zro8*AeNr&yg8+^|v6Ojf0&<9Xc2~M_Ct=}}Ps80ez$=*8pt(nF?F#NljGqFXLUSux zXxl|vNVejQl>5y=f#o6SWW~%r=F<(y;Kk$wqhvA10mo_~9@A(1MwX^5BS z{ngdHg^PZg&cY&?pv29kC|e*5aZOqwCOY`4Fa+pMqsrgfxG(x>qD5d_wZ!2HV^C*u+V36oi`F*Fr(P2@3yaLDxxK;;IBbbh{O^RAkZHv6(Q zOBRBld5jjCWk3|#g;xg^)kxK$OPQ6N3?z?6e`u~%vJEDcz7{4*muXY3TYlTBxSWlN zF^Rn!a2Iz|u_<;04pj=RItCUJ{jB+WOe#uipA3(hien6DCRh&eDPsf)My}fs4$9jo zAJ4MOsrw{yl~e|jXTf2VVl)0@D?Sp?g9UjFvfICRu@+=%Dry#g9*?}fULMR6U!b`# z;ei&{e0=1SA2*~Q<=lF^SwsssU}R%nd&mJv%W{o<@7Ew@=pW=Xb# z*=d=2d1}js?l26S6To_sZ)ap=B?N^FcovX82jj|}vs&3*S9o{=G|H2+;7ZQOXimP| zPk5nh)hoYB(X!YY!+AzV?xHOOUT>)E?ehlXA1ug?brFxKE$ql!nlfroe+lsY-U5GVeJt2KM5NYyl6^?Jdo)r@f0ax?BdB&??hfWne{@ z!Gw4ZWE28H0T~#DP2K|;ndVa*;?b& z`8xU$OVMo`h}|(JM3`Z;hx%SKp=y&@!q@A1y=FO)bn8gtqmzq}dkSe|1VkIhqsxkl z7!T8wBPZNYmET!oUx+4|$?48aLxg9ath&Fbq)2)WQ*P=4VmKs5$sKX#)VtnBjMg5@05)E zt)t5MHKUr5-}~sb5JcrdCiQ22ezct!Y-Wf$F^|3}O`NPatF;N^G-noy2pJs7eRU<- z)tb7n(?@SOMt$jMMW;jykh%cw@UzN}!Ksyuok(br({@?>tN4-1kNkL&-MtKJLs~CL zXCIbN7repQD|pVKzI7*WUW1EUnoR|81yij-PVUUE*Z2v_k7zMp1DcSjz8NmMQ5;QX zq|ysJd2>6wUqTJctrB^A1rZCEQ{XOZn*wM_1G^`3M>vKqa>S8u-biUvXl3X%^g?;u z0|k~DISW-D@S-puk~Jj=Lk@<}d4$9~07s5Hgky5?(3)a*z(W55R*Oae1boz-eO zdPCA!WmvF@p)^Y+PnwsG?X3B{P`RRGDd%(0Jf>L&MO^Iv3*Xjjz6N8$re@nVrgPt+!Vca&v zleI#dz7WNZ$_4==^BT?7DNtE%+i|t&bk@Nw?Qi5`4Z1UT9VE-TrrB#G{=;q41*W!y z3AM)DwF~uR?T@EKqGIUP3pYr(BJq3k+TSw572G~BeFKq`0}bR)tuQY1CflqJQQ-#> zI08zdvYBM-Flmx5(8<#2%ixk6TINT6Nv8S2?PHAHwo6FeOe8Z})p*;Y%JUgW2Ip$P zlDiwx_@xuWP>${J!26~!lm#8Wyod6$@EIS=9lohf5ngRf#&pX^NQ}?^ z-7b&s@a2uq>9W${%R8Uf<=hTmW}DIvR>m#C5glMQ;T=3zppQT#+p$~OsInSO|3#f; zS#QVe8kVmhA2_C5@x$MXGs2J=9iIkl-V31s-hOI&fM%6D;*%DNXAb9_JZm(7^OM$A z**;Q?XxR_+K$rP(K?f+l>R+b;vS3!L8V$&+W$?7=4dCe+X;5;Mu2p_Qhc|5oxAfpW z;O!^(w;6UX=g|=f7Wq?y$ww;VTBX4aX2=KR;8r=K!}uf7!4TwK)Vvrpr`sB9^SFkHQzTT-!I9?`^HsiU@DEHJIkp~j*aY*EXs6>oY&r0qgR;y#~88^vL3+(>7h6`;dNIgUoXi>1*sFp zzJgn(O?viQ@^;52eE5z1L2cRz%H`zcy8h;7`Lkh;yo_@3GA?U8B_q$&oW+1M7rNmM zl{Y$C>7_dxp;I_0&XpEux1HcmY*xz*Ekoty4nMAvWEh~OM%N4u!@21p`L)Rl1QG*ehw?KEeGXv50ztQjXRfiXX| zlQ8JpO!0(H?UIa~&Y(b{<=k*KKIS@ismo;Q24+W1i>LgOjNHkj%4_g+nv>%)>s4Nb z;2xiGVFfukjQe|-DB^fJeR;)<9CI>maU0;JZtOs*elC4l$uAMJWP9GGuMyNO@FgdE zZSsa%`LXgCMrjatMhRYCPTJfuG^~@T+wYD zwoXif`Ni~Dp6wcgY`SLHj2MzY_OG z1xTTl#`nm6;QailLbl4z@1!~8&erBJW=;aKv?gMe7W7ftM}MLPJ7aL}$io57?zB*A zJPwy>H+X?1aY;svbG;;kk;(yNwL`U3#{X6dH80s%exowlC-=u0X;)=qXfJ3%F6m6? zdqZ1wzK^jz->p-MDEjJECfk;>q_lQOHd~~JN_`0@0c0Q9GGZS;+g9bNFUfGN$Qaqa zWY^{+b1`Pd3HyQp=hKt-S#z12tOdpClRDsHWLKJ}Yr)Vhx280q!C0$J(1&`g*fl*G zro6=lbqm)_IOgw`$`>PoL0L=GH;-sRSrx~xyAIhpox*3B zLN-e?bz)vF49A4vN(y<33x*x(8ErkWWc-3n2r$wzVd6z)-pbU}Iilx?YOk|7M*e^4 zwXPiZdU8hUO>Om!Uhl*VsKQEnCuc~&bYezxlW_B>dB!pKvYO`uHPGIgX9Nw&)V`5U zdzanpow?(PZ1&EC6!A8D*JgDGZ1!%Jz1yr;Oj3c}n_Pt)o)90Z&FYfs0G(8Axx=l! z`pw==vv*r=_U`*4}-2vvIoKB@fJ5y*}9?u zwdDU%ol2uhB9Kq-#Ecx`BMbgFI4zAOkbT>T_dm8bU*nCL2*^hT_G2zgN`5^)ZS4cC ztBl0PNgso01%ho)H`Vb7DFNShis+}oUR~E|!`snbZ5PR}Q=h<5mFhqqu4@1&Zq9S# zzPql3Ow(y&*#A>w*fpZ~3ggO>LF~ahl}mYPKt1Gu`nm6vIzaufd-Y#HEjvYt3*O=c zl%4A3)w%||pcTFQT3z7dDu*;ym}|roh^#z?}Z&?Os5iRh%u+#VPWLDXVdy_bHyH$Q) zmTfB9oG{n3kB;YWr+S&^G{I2XP=nQ&)8}hGDY+pnDR;n#(AQohOxa1r94fo#TTaVZ zp}*%bNyD2pmxVfOAG!<|%?W&80UiCuITElS~i z+2Czd>F+30aE{5G=qVaCZ8SSlx-uU$^9s=k)fl-IcUD;;{oYP;TBpy$({&L|Ebd%7 zA5Ye#vt!fFDgyXynj>MZ4Z(($yc08Hyq)9)Mwra#%Cu6wTqLH^=U`-AIWj{KNrrOt zqsan=2)cQMGo7P@h}RP{^0af|*xNSLcT3@D!WLm-Wop?pAiwtk_*?`&^CzS}41ip+ z1;0aikn}b@@HY9A2d4X=1AG;pdgApmHE)_pGM)0{rGGv^%x0)6d)r3;d>|?~os)IJ zJhwEQOx_9C7U6Y1z$+AjeL`@~?raxwY=QBw>z|-=- zoI9gc1KdUt3Ct&Ftp=dW`-a$ExRBlOPq%gjt+{C7P=f@=hMZU(ZJ*bm3-ppAC<49| znnT$rHdzbWe0kdzJD#i%BcLDZGEwsQsMDf?t4&I zZoVsy9$;VIwl8V5Yp5oj8-pLZ%6$FFGA!WM`-B9KEnV80QQaRa8 z+cQiX&!@TP>+G3BIwcNg4v3nI^8GT%^d6s~cCZ$k&GOS;)GX(5?H=;79A0)}EB+m8 z(aL%WNQS?%J`>4#BwIY#H&F}<$xxr05zob`!0$E6v9tOB?usz@r+~>%k7H0NG!^l=^?ib^aEcCFwN}sumWtUXJ(L!thMZ> zqT-Gdl`}LVnLy0yJw+j)JHrtVaJER3{PTzg(6+#uTRww$#+_6i)e2L0x2@6TsK@R5 z9W(6gl`A!}R7`8F9gd5l2GUUuHAj$#xk}SGAx?N21X4`ZfW@ESoM## zh#-?{(L)@Xk@m=cmggicR#VSOJ6?<1moC&fEDf8AbYp>Lzi0V8s(D|QBsJ>gwVbgB zxfX99d38GU2)sJp4se?qyQ++nAZQK~ia+YyY6A-fS_3c=3@@*R(F;_*0CN$wu#>tE z_q~%cj$ofZkd+QKkORt7&?~XThD3O!@vw%joEi-J5GekRft`g5Lpzl?3{fJ%hG>wQ zgU_piG{3$9enyvtNcPK8u=hN_TIQts22f=N9Kud24{HTXxd?1kS#4O}m1c!mlvb2w z_3_?f(F`7Ws18P4mJzzXs?J@|)E&cY2Rh?oZg?r@cfwD>&hS_wU@uORx%h$vJh>(5 zg{pAbj=8rm4({<^fk?5l9N{*T=A>UsOk7upXJ33S&%Q8euU;Okw^fqc>Pn&fhi`tK zj3E9qMc52@UVtL%C7IW{i>i%Wu%$J!*4tT5m=1A%Ws9ho;(tays6JG4RksQ)Wi>dh6U^T_%Xol}K%B|H3*HQs$ z`ZTVYG=dS?YsA-6?KwL{W;=bei42YE?u4aiPP#e)qS@P721Q(?24H_`b&ecT&k?gE z=_@-#);mx4L&`AI@{3fTH29KuiWX+%D~25f?P^L^b$rcNE| zBX5wJH^xGplu(PsmsWx8Mdj9wr^G`#4;50Zd&U;$b|qaXk*e__GkaOJ_Cr>i9hyN%KOv)X zx#^BL4Dcyuy)kdSn2o#>GIApR>A;zjo2BCI$4Y5n^-hM#cb{&PehS}c92fis1N$pn zr62Qi)6$9{rqD9^G0TF(!4&TctoF&k!#xgOO)Q_T8R?%%i}TJB~AzcTwo9Q zr`y|4&Zi=1uvQ`#ntcVu**y4jJNuU8UXkwln{mktkcJJh#*LM#_;H z?%g%!Fi--v$Zf|@OtRAEkFkB5cx;;Er83x{DcOM=FHd2A3D~{u$kQF0 zypggTNs*{LZx?y8gQ1+kP~0RGlt_laZF)=>|?Npu>Na{#AvCYrM9#!suA~$myl4GRejg|Cb+>tUH zEYVP#dRU{G;KQsm)A{(T(tgjU--R0FFsT;2p(-!7x6<{AuIm@*OHQRLZx=bc!@zok zM-6o{LQT^7Pm;jgk#f9s4uuYt{3bzau^4}K+VeGW&y#WB2}5tJT-#Y~;xw9QHeZKs zm$zz72g#RksUIm9qOCE@4PYEdvN9A^yWqG0@I;0M=wWaPjF0m@uSvUpkf6MU8X7Oe zeky>Gc32>0;B*?8qUQXLRwAir>q1mqvN}`Jyf;>k=m1m)OnhWk)dl`H*^eCCC&r3Kl|6rasG%v66O1ott94W7}QoK2ybyPV5*Qu$k+GHnS(aVcn zvd3C+wjbHq>g6q3tx-lmDbJd@JM@Oi61O4Gkdpk=Qa`=TcvL>Di|`8r^&V>P zW9%1aBY-a=gdym7yU2a6at}BGQIatw9P57N%QQlxR7wV)(GuxsO*pfjzoz(bCl1RC z*vc}Xqy;(LCsV2#1EDs+P#LL{*XwZ3g~VNNmiHIiK275O!piAE7D~-I32nPN6ETzb z8jx}-yo1WKSqRA6ZB?{BJ;TC1&*=Vwy8K03t!{6uY-p#HA+gFFucC5gr{5yC)K#<$ zj0d%X7kEq=p3*U|Z*cuC?nqIiDTJ?g&WiY7p3z=Kj^_>X0J<>8He1?X3EOKEt7Vc; zz~S;h8x;8}iF6E)ceX(=uiy;e@Sah&{e6JpK}{fX-Hj;<%o_mb*&Lc3mbOi5$VA?5 z@?4gkcyhbXYx@heiT(J|Ogf#5Mhd>5pX5Mg?%Idx$c}uU_#w(YJ+ln?9uRS!dc##7 z6ga+-DwnpwuoJJ%nfc4qHoo$v6tF7|Fl7;;w5IW0+*y`^k)rZO`;Gnw$wcLAk+T;Q zua9Q1o(vgw*dD|M1pQ6y84y!te_w8_1A@p{WvQ5p0hC)SozQYGB=K_E`MnZZw-jWWdtPSQ`5*Io>Q{pt0GdW#1 zbl6Z-&aq)K=%U=cS)&(ZN{JC@(XDBti?u;k89F1h%QCAT$c-JX@!|dG( z`eO%J1^4bU-rds?!_b?lBqB)hwzQ|^ikp5Q%U#nqQsw%jifiU3-u*RscXOta0KBT& z%-xBGiHyjBbCdAuRnBSyQ=S%=w-+AO8wsE5)x(`&q8kajs)6$YHV5siXV{TyuAPo)Q zaCf?NA8x_YS(?_jSa?~ek;|DRgJ64z;C6f2C?n%nTU|>l7BedasR z00#t@mD+R(3-L4ywFTETY%X@!6H93cG!}g|VgcX$AM!a@+{3#TvuA|a><)D6W`b!b zqI9%fiH*NlNHpE&5PPJq9C{nw0UwTL3nA5;gE+D^iw&1V1pKuc&1V52f274K`V@5p?5__)=XnqaKv`O z82C`|M#_?bk^9Oi2d}z#U@6gFtg1HlG>tJ{sKVi&p3d`fwu5p6pav=~WAxAIQNGQV zYgAcqxIOL&tFR;&nZ6O1&T(kd$fY?_PRdcXUS6uJFiC&z>~mP?;s$bG)Pgcw#@7>0 z5onKOE1~?UENx{Q0E&D<`|4F5L_AQr&@%pz1(^@&ZA!D(Br_-{q6+00mmJ|rj~$-! zZt|{albNfci~NBH+cb>=PO_Pc#GzfyO&X2~6{gz>w;Tighp{(57|0QW0>B$8ukcFc z1d?Yp=XI_4cb?)x}xxWUmIHxwTdf1nhRY>Pwo~6{qAlU8bwNVm?|-E`R+R$gb;E_APU?SU4-C zkAYrL`a?U57HWVsNL#$&@*b4Tn=V~5eS0cVta~QnKE>#m5n}a0RKV6ri%&vZD0=L4 zD1ELQyzqw0t7egix9D`0k7gL>d6=UOQ&MH@hV|(PPt4$Ehh6}7gQJKJ91Gd;jxtd?J(%Q%%U^P(yf}0ow z@V^u}Eh+MJwaCA@B8SA2-;)%%A24fM;hOv8;~mwHKX&yewI-UYo~KeYITAe%`nSp5 z8sr)@@V1uQQ378f? z-b6?2=B<5X;@rtinrDMIS$r~l)y&pJ89e*hDj+Qe2)j*llM)a5jWq?n>k2>%a`Tew zz@|P|ZNbtDsJl=kcWE}NGbjAI=c^@dhoWegLMU^qv)^|m7T1)Ri@vkl+58zGgaDWi zE~|W*8NiCjPAtGh^mA7Ik}Q$i^~y~C|6QtwTl_+`-xai~-R8qIQCp!hhU;F<032dr42t>|~>f>TIUVUHgh9CeYv#4dl0}(suBx)lQaz z`wYCr3BGW0cr?%x?e{4Dh=F+PwM!03(?qPSY2p@|FeU#z?PW_bahZ`iEGYN18&iRJ z0g>3u5VSm#Lm9^V0DB1v3N6^{i_Bo8vev3&O<0~$IWKK?eYm+``XLcN|tA2X*RWNZhWme59W8M%jioNU|qS+y8e8~ugx0dBR&E7)iLCE zTJ;u#a>b{hTp2@oLjq;dpuC~IJke@goy8fn$_=725(oc*=nfs04*q2xtuXjwiQ&}totkBeR%$-%+-rg&SdxCm zp7|){Ks+nkUUumB!&G;_=x^y@>g7pk=yvWz%L!ZzRK8J(1tKv?4-#0^YL>@0Fk&@Mep4337wAD^^7@J)yQZ4J)Xn*`e>!AC6D& zZy9NU!>H@fa*GzSS7qc6n-J1Tmupk(1T`kRizFSbDk~L^z`H!7(ldjDdn?#w9{*To z2d@Z2T10%7uSJB*HmTrDZM%(uqJqDiP^d|M=nY3BlJ_ppNDt>V0YjH(WRg!)>%HNq z_X8x6ql}*zuJX6TH?H;mv8WUe-slghk`-1oN1eH6z$Ox@BT&|d9 zTh&bElanQcaS5_xB=doE_}QWSe|4uXE9;`1+|b#|4_H`0qKc8cTtNd8(wZv-s)U>| zq4upuo7PSWo4QGWfh1)(VqYLQ+2QYo(BBB%Wr$eS3dbRB=pO9X%(BlU{D-g0yy1|v zB+JkTF?MjT{a%ZO3H~rET5_!XAgGmA?FIhI*((ApVyoO^EpliWR9_Uc< zB9#vKhF|%KBr@vb1i*S(F@Ss;mW)ycfyeTvvSA@yN|2jq+5>1wSOwy6d z+RF6GMwL5dz1&h4$+exW(vgZYQ|q)YQ&(5#4VTxqAcujF1msC($s4Y6zLc8D0~+Er$1c`^sOFIyXy1mFuYDrh=AkmIwq4@ktc}`8JlXV?XSve;Ioz5Xs203{Yr2{(O z3QTi=^b7!t+hPX%6|u{xj|U)cxV(a+bD&{Jz}v|)d`G&HoCoxFzD7F9qTbFl!tsPg z=1zOrXzc#$5mM-&g0BYU-*O{fNC(vsTTXBk120a;@p-t)>G0KZKk0abqAhilm{ zpqbpEfn7ucc_I_ZV&-B)CZf5Xc=HWF&_X?s-BV6v0O+2iqGG2`{k{ zs?*MKxA+~&B3UwK|A<8Z7tqMmWx3q6kxh`8_(Cgv}kZalvYi`8asyPdxN9YK~Bw%gGrHwvFqmVW> zjv#+|`^g>COy1*exNKA;XgM+84@@gQvRTE$!M)8Y)<MLL5Y3A+YceD2vVK#P$cOSNS43tRMN_4wlHw8>_Oiu2%y( z{2r8!83y4@dlQFLLasjX+Z!r-5~2JURwydJ(1AwrEzGAp9@X9lgm06~$J;^fIxHRb zs;%QG9PwE}@Poe)f89Y|WPWv_j`qgN4nU2%ZU+LWP(^uT%?-|F6Wl@Wu8UHFKfBt- zM%wVk%Jvm+2RR6x-?^QP{ZVs@8LQIBhhN#iy`eZFq`}%dsC;Z=dOR7^IqiM0s+Nd9 z<|#%Ej=}xR!AwoQ?jZejnAFy?8l1k+%)fS`s2HZQ$lXQs`gEVCW8Kqh?&&z_DX?5V z!zdB_GRwgq{6ZWZRkuwH$Ca6S*}4IqqL|nvN7a5?SJ%+P2w3Kt;Et)aWuB@j(_B5p zduq$PQd6d;osG3+-mWQA)6S@EYhdlK+Z@MX^S!>R18j= z#@Yf436(2U*K9~bP5;NakA-v^#@Bv5DeESQ8IG}GcJ24G;_pDuUos;rYYSbJZRoKC zK`ro$P5A>sE(El>nua@#`ce%{4?38pB&`)|KR)`2kBe$QKEuaA;cm=_44~2wsSULy z-=bvAqzwLY4FGe*4V&t0gxLRSd~KOUpIv5AZJ7(IWs>PzS^K>w{!Sn9^lq&Eer^06 zI9P%hj!|E!f%Vp!GBulJYHgYOKD$g$ZJ9MSWor7`SX<`F&n`1+yBc_2t|>#VzZmAJ zwPoI^DN_S;Pi>igu}!ww#@J9TAjc>2&FW<8hI};!Fs?RoiC)CACen%> z!D}*+cXdXcKOA!;UqJfdyK5|LPTJa-v~^uZ$l}J~iA3w7#^Rv!ZAe|Fq(*|@iY;O& zsx|v-(>kYS!Md-cd%)o*_%26XLu_|)%JYjO!0(j9ffxhxa4!$f76hFa1+DiJ}Jqc(lGY%-%MM6f{|&LhQ%~N&!XyAmpEv?rRT*FyTGv5;qatn z=f1#v$6OCjWg>YqQ}V{jxixRs10;^4Ta;ohjmd!cvU)5U!X?X+le?uq)FxA+p-6N3 z#zvL-Z7~D!jx~0Wyw{FI*&ic_9Fd84kq_iz(cyRLr=05{tvye@}|M%3X!A> zGfzoA%0wo?z1LorxdROmqsP2t-S?FO$kD!&o=@Au>c$q;`?4>`*Z+%2%KiF+O>X z(v|f2o(=6FdB;4;KM$E+XR^fZrb3GyV*r3af4>vdS3Yj@<>RcWImfEJ)|U7J4^w&9 zD>Z2kQafRebRMW2;H>_qTU_e$Wc}~QY|mt@oJI&F^zQb7{jy}Hl{VwX#KZ!%=-cTE zzgJ!1NjljZEAy+!t@4P+d<1G@YJBR|?U+`+5#^N178O(<3pz=nHB9AibtRsU;@Ng2 zdCzHtSvImfU&tdkR(`=|bGv@!fKrjz5O3^eU9OIkU}|r;Ip^!E$|i+fuKQ zC6)V-e^i!taxi>?PH>w_pmL!{Xp?0#1f)MQln5~lLb0!04Vv&_VaBHCtqya%Lw4lt zd^=0R$?^+ZFF7nF9L168e1B;3y->%XYO~GZA{TLL$HAacX zX13DHNdqG0;uFjHku4|G#QcBlkn)7_O?juxki<>Nz+T|OxkgTZDxvY4|HjqkOcsd;(vcha?bnGj|9BPsY^HVrD<@u)h%3AYB1Hfu7rf+f7AS@$ebu z|CuR;pX#UMTW8}7ZJy%Pgsc*>3_e|%icIq5iVZYOQdpLC);S=;j!HDfmt~lRW3Musco#BJf7>9ccybV^cxd=JnM_!nr*es zr&o@I=TJG#%6|bmxC#kLWtmgPZGHw0Q@Mrs1o8wyOq)o`q_*oB11UvS_OQGk(h3Lm zjmYJ3lW$Zk+GT586(2BT$281!zoK0}eIvMO6CYS4rpRh02pJtBw63H2s% z{4tfsHdNub6*!Rem})~FiJg(1h{1fz+G$uPn{6Ug&Ky|6c`%2=0h-rbv(6bkT79`W zPFHCD=VRn{_b8Wlrc(Hj=5zucvyQ>oyasdNT^$o5vy6zq{>nBLbJXgfcL0er$bOwH z>F(wWLkhWaGvIC8IxddkU%QRnJ8pwvY^swqa>$7&iUj}uVLv^;;%_)GZGW~8|yKhqjCe5#ttM$o0K-Re-)51GK*!4|j&+kOmK%D179s1e$U3REEIa{gtHWpe>U% zPUV?dect|Zs%5+awvFw+-IGMgyWvgXV4k;jAX)oMw^qzHY1mOfl#@&ZAz3ghz88Fd zIUf;sx=kGg+@&`MCU$^Td!`V-X);8Y0eiM}0*TsR9v@Kg8qp+a03aV)B~G3+Dcn!(r+@p2?muVjfwh__A4?)m@Xvir*mM1wl9-D^>E!V7U{+F#|Q0qeaM z*?@?`8uQ1xey!KrUrw{Rz=LjomE#6P=8)%@soY?^*0)2k54L8H$h<4Xvdm0pdDZvR z-rtF}YxuO-1|-=)QJYL=foOy*Ih>Aly~5IJ*F=Bpuf~5EY?KXi>y+yN81Apq;cgh4 zcK8H{-3_e+wo6^j-0C8l+90poy%mJnQ6GQK3(ePzqi3(Q+0%2ReY;~O49NZ}_Yc67 z+Ng4?p{^l|%F$ay#-6X9Roxi(B0k&OW7~WZ*TFLsKpEP(*y{%a##D_jQ-6^5`7k_{ zR&dc^=**!_nHlY4D!-vx{8%%gOV%o|6_1>bM#k_|j)3DpqbR20dMsru_LsQ>OY-~F zrds@A+JBF0a7(+^-j<$L*Q9*R{&J?Znu^4EZ>-!|)AAVuOGK#?SOuP2%qdHA;oPMCo*%`KpP7*-UHi?Z zpf7pb$)C}Q^2IK!w9vU_;oR&9cUJDXceN_lpLA(gv(9644d~SZZ1`mO%ri4N?#j${ z&&__zRUG`E)r#+L)+bv8j|=N2HX<*oDLvmDdb>=JKh!7xvszYi4a`%};tmQjv#hzn z_zOVE|Cq9E<9}T3_88YtJk-zXc2!b(0#?}iWhRCUV*9dB!YM7y+~II&S+ zVa{@4DA|Z3JwygUm8MQe^Xgfn2`ZK|8gIOuDaZ)iAl)y*SavuPauDLn?T{Czd;Gpk zAsc9J@EjY{4~_E6p{4=Cy4RmO$F=AhXN0TYOl6b(B z&qUJz;$FTtLAHoYQ2i3#az}tQ+T~4NV7H;2BWM!Ui zt%GTRnh>UM<)@Wl@V$CzwGz8VxIp~We(wfE>?nucaM`F8Z-Tr79GczUY*OBE;vil5H<5f*PUuYCdsgk2uE=GaCEXEPCbORX zwZq1IXC^AjmjEp1G@Lo(-@&tkfDiEP9~`SfZE|8v`nKo|m#b0kF&($fB-ml0Q&@%a zqlD8A@s7)u2)`IEO9w>W1bJwtk0U6vd5*n!a3&FwGOLwWm)NWI9Bw?GhX&7^AnRtf zYW5fJaIwWw*|IcR^I<5zE{C#>;{+H0^6wgulvy<|3TaIxhK>J~PREhxpt~(-PA6%o zy`aI$_OdrYuF0ZN0NVr2HnSPHBavM^Ayp3y`B~c14c3xcd7!=4*r^FBx4;*$9|N7k zUqVV2SL@zGT`0*hIYb6|1Ai?Q0w$z8SCMxQZ)Nu4Jg+9Y5TjSnB8lXTL6i2>YiH5V zQ=p#r8tRw9lg?!>AO!`L_uE?KB#GpYc4zf(Z2R)oSD7b~Q50s4Q!WpqFI)CP-z?du z=9+a(i?7SP%QG^k?Ne^e6J(P_@i)3#?@fqPjLK!E!;pJsEp!t zfWRynWOtEYZIZ7<@|#xqQb~T}qi~gojX6i!%JlPrnS>_DMv1b?JrW0|<6j7e=na?0 z=$pkjXBM7t!&RQ}%$pGmro}YWYl8e`mU-7b>Vcr=xQF$y0Mx$#&XW3%d|rK(Q)c;U zp2VRB2O{6f%sz`9lOfQA$KIGze|Z7Z0~u+24jEziw68uB0-nEK|!`kbP6{%JIc?B{#5=x?qwL@?g+n zvCbXDWP;!l^_$x{$z7 z0(mW@@>+AGn?S&Qt8I(0DQq&CbaG7_bqA$wET9dr=DBQzdZ*w4%JZ{Cc<{!Op77>* zi>;X;C$+cQPE5uH&hz~RjxuWe#aI)(vxlpkXoq3J(f=3Q9FAv)%CmJJxOb;g8q3RY zl^5&cgW@y`6|pn7h-E$Iae7KA;1>coL=AencD~)(8S_n}evy`1p$+xRmK*z@9VLrr zZSp3_bJ!SEPM&4Q%k$WXi2XD)f$RQ@ZKMe*FTVXT>nm_h-~pPx`P*(=L0ln_!ynXp$Gjt@G{Lz*Jb7V;@}dUB9`kN3|9lZs5|8j-Raf@Kze-zOiYvRWQB#}9 zAY9s?H=%=Cv>VoD3O>-Qm)kNWrr{o}2!VAiXU%MtX9q;`)so7GGkke`Kx79pq?!;7 zR-eE5%?LP`XHdbzEZn0I8p2SdXGW_xK^{QmWzUz+fN9coVHnaIi6AC}sDmrR!55V0%?6xixoDWxXU(!dkY8+)uSV!s<(u%L#ih-@axIrLvoW$28u%?L zmXlw`;TjM5E~vcb_440wvn+K2sb=vC87&ih*|h@Awp2-h%8fvx@*k}Z>#bFrFH4!v zRagP`uF2{%XzutpP$raRkBBf-ZE+-ii-?q?lc_9+EM;BGGcdAGkY253&pRZ+HI+-Z zXrQpMZuG`j_~SRz$%Y^RD>120O{&VVCahxE4vcqV0pMNPFg4@(_s(M7Y)1WfDTz7%l=TM^GNFi)@q* zt28I;r;{*Zf}FF3)m#mI2abzOoi<$M;pENI3}?Bco!0Hm7FIDqR%{U^gmmhk(vjVg zBXqsXkw80d49A(?VmZ-}4n8z3e~*=4K%@=g;|d%68o7^ATCF(=^;%i2^bpf$d^-Xh zi_Yn0hks`|3E!Q1SlY?sR^HoJZpSL~+0=;9_hPGoYN&h{S}1$=fk}c1fJu401Af`v zv$d}*JKUJ}3%4*zexW%R1WLrWajFPB1-mbHA#8#-L38p}j$6GR$jHLOmwFTAEo@jw zXmLtlEob+1xKcbk1%AG%HoCN#dUI$B6H=WKy9p|%=PK68Q-|9DbVe>R7D+`akbwmZ zfIoN!BMR#+G!hYwMak|$f`qm*(H>QQ__`Ts_g4>Ws9)yoE3aB6kIy9JoM-a};;=)y zjR_Ws->5QYxDf@c!^md)C^McMk7UI%r?ySzqg99s$Gi~$p z8mu>VjQ}~QxG`|)4|b7hqtBei(KSJ315iVA7&DY>Ei5B`H>gt_jW(}D1HMOtho%@O zR4-4~^=rYKAdh#X@u}lG(nagWKIH8yXZ)Q-v+luiROb82sneL^rw5jFG}3XgD97R6 zrOOo0AH$WLe4|%>l4>%?cc%TFLVxatWbO7w$SIu|qK{@X)6Zkzxpr&0$!nDJ9HMTp zcNm4(w>@m&q5u!rG}~;bY5ljYtg#1NcF`;^%z{RCaY?HDrYvpcgc7Zsox*ezFfG(( z?a}4~CpIs*3^5|6_}TBvpU?);YaVf|Acro^n+AYm!_rrww>d0rc+`<;!%xz%c28oJ zCs}TmbzxJBsXozeuM63Cu?)LT4o{mHQcj!rz?xWKY=bGJj8YmjFm1+R%9Si0(|4C) z2~id`U*iCq9V3;W**-gklrd1L=i8y}TvUDH;V{S$o~|uX8OU!@QKbCf>^UhwM{d?T7bZa^#_e08UWpH=|X~B{soc;S?~RjP69V;9-`h*Lg4G)*gG+f0<@!t{ zD?8xAuFXX9SCX#WWXB0dn`nt7qbj#$+#!yn9pKVpd9Bz8(D!DrjyFBHji}t8so2kg z$}Q*^N4benYKT4!V6K2&hz(1e5oz2) zv8K-aOoL@AD5zxD<60jZ271ymvH(4*SQr{$fp$8KI!E)q2+9oeo;Gi;C^2c)O!!DPFBB+5TcO`D;;=2uu_^ zi2dx&EVotVlFruAaoCsbU3ft0L*Rr=^I5r5H`GAR?%X7g)uB)PyR**Po!sdY21fU; zRl@?|Ci(bS=To}Wr*!)ANZmhO{M1fgHoD?uWoAqo$hx}T9I1l)WnO3MaJAv6bRJK% zd3451O9zUTW}F-er>I4pqD_RI0vB$|k5-q4be0u>_r}V)>T8*8HFL$5InWb1JXKe& zIy&jJURD!lgM}dzg_0n7XvYv_GS`rwOKqB;sD&iam9f>0WGw2VvE-($9E!?VHX=GeVHiq8s&-(Gx`RYk~d`{`9rEO8L}wu;kJxR8{_5W zwz>+B*L1exKGG<+NQ46pT8i5;@`3p*-;6i-IA6PXwW*CB;fVM{_h=sKMa(#1Q*k~M zD!(h!!o(D_kF9o01mUtcRe4>fhMs5zxZ|j9o_n+{4kOgyqmZ<7K`viX zK`m0k3HJ~`3Pqxdlv*+A5c_HZh-_;|;Uq06HW-2`y5en7^2W(?brHikwbN&!o~err zYk75aXDcR}c4(A0PIFE+^=`|kob5`pBOH!vi5e%m*Id_&JZ8BwSzWe8vi?=fpXpQQ zBvX0yVoC5kk-_I$oAFllUr(vs^6j#USU1_K%fQYhp8bW>dK%7(g1_FFQ26D7~=Z1s3!^Y&gxE4=%`sbA~HKUWcDC3~0sf@MIm>oFL|=U(wI z)nYeOOo?j^EVA{diGRgy8M)MUYd|FGqa8Nu=kf-M>>JwuAl(}qYnr^@QkZpldBwbS zTSl5oF!JZ3y|BG@TSoG>*M1UjHE@HQE4G6%Q=C)qzI(|HHZXOE?i7{Z`VfJ= z0E|vws*7Z5Svt$IOU2t+UagC0_O^C9mMigpCo+lMimZXieH$|NS4y1tjVd2zdgTY! z5*_U&ms@vopFz`4i2`3$c0h(2$8T}T9`M`l1PpZpI4%>(4OMg3kdxyzjAWSevS2(d zDo4|_MUrGnUPN{o4ZIf3$;X^f_MxSu1+NAFq3>@pX;9i*z5V6f0nz9AS*U#MMP4Ki z&1i)&VHcU9Dw3IH9`e16H&XeJKCi_tFpfB3HL#1&CL092HtsgZc^=7YFd9I-scwDz zz6FmDU`^rjYvF>vG0H~8`6D82E05!F7|_5Y(ccynS4L&WM zywNs!6f;zbRI(^N)_CJ&Bn!ctpZ4*6S9ESDV^gJ0wUt{EqdJHMx5)ooft&j%VNu7Q z9`|`u#uZ0>o5(-(Ev7=%o{M7`Zq8W70&hRLJHJU%ABRn@}xL@9D zl+&>#Sh?rhoVUGQMai3-oP^~9L{qdRy`0lI`DV%Pe^PG6N-oMcFi|j0J*x@{F_$R8 zwq-O6#r);(YEN8T8s+k|>-S8^4%z8z%#eCBOL}2D(y3q~Vb(y)N#0PI=mY%@;Q3gU z%QNdKEq8VJ+9MeLOq+?*+Tv_P$!CeZQ#r++RGuRluCDMJK?1%X(cbEfl`}I*1kKGE z$(Oi@uE1PP_q8C?(-b^|{@8BiTgr>;V!>pGdwFWL#Wv%R^&=sbJ1i;yYH>U6F<%&!L{$NAQDR*u)ufV7!@nG|EOQylwR~88VQhAk4ZLLIJ z&2i7tZ`|EQHe6W9yo`(-7x-T>-OgLK`1DxIE2kO*{1FAbeXDmo9NTVpJiHdCUE_|2 zyvi{)C6hxepfKD)!t8EHVN`i5)4*(}L#+Aqk^97FrvAwP{Y4Xbm5bV|`N_^rr{;L$ zvAdelV?ZM>h7x)Cob+XSH*-BYFRgNltD>e-`;)o(UCZRr$vO@vn0~K?l;OO3ysn=! zHI$BY$M;klJ11@oRz4S&v#~TjV(t+wXg3<-*d(yHTJ9=WE<%gLNV*Uc zyu9Y*J*WRBFHUXU;st3RH@mu@-N%M%xw~CCZ-l&B1gilbAAU zdtQ{5c)|fiet4ItW$hexKUX!x()r42;Wy&KChd_P%&Rv(H*dT@#~sX1q;fZu#-^{= zPEwMCBGq6JnN?lMr70Lkh#kSMZQBWXOiAakizLeCl_(kFK7+jy*O>&P~QWwOZmNe2KIxuMc}Ia=x)Fk>(JDjc1W# z>}<+wtPOFKko~%x4H>uoALC5Y?AJe>lZ~A>dM-@?SP}yeaq6S;;g3dIRL(Gt1Vntw zI;qcb>kECkkVuN6%brVwe5wjwxh!pbdEB_|fa>Um^rR(LR!i8h`Ypd!US>;)N$hV! zM+XG6Ikl9CvUwUbB|OdGw6BQ?EBpWp;ZXTy9?F!|Zxql-q3%0 zGaKKV0{L1DWHXFxi^@Y#d@nCcv%U1|P(k|ITWbcMljTy(f~y&YC0d1SiC44nWP> zCyYF_d{dj7>BTiy%Uzky%O!50Mj-5fiiP1(SEWT(xFTq{M_U5JB@%ggdEDCN1~c=+ z%B!61*{w3>7lUC5!WOT@hH7&+$Ia!v7BoRI0)8Pzk9U|5%~{sz=<{(sk{p2!_s*h6**W6Q8>Z|suY@voV6WFm>POk1`N*r> zgq8sCtY#RDqXW#R*F>UA;jH)iB(mi!Wn2kJBF@SAlE{%IVtjVi}wdMWjq<>2bV zG^tpryc;tr8#{bdV_u`2l!++yDlZ@p5b18wip=n(alIVkqu)_7d|~95L$Ivm$v^we@qI!i%-m?C$Stplhr-$S>;> z=vr@_aJSrz8LZy4xH{ID>LgxX;*-X!mG0FaCoDY|yFBvqbGv;L+Jrfc;2aCFJF~uE|GI^{WYhpQGYLWqV^r&U1BO z_5ZP2G_G^LtAk_9KV9bnS7*&XhOOC#dw!>HN)llQA)S5fHt~@i!;2NleSzKHaN!bCqM@2{J=XEO%z6!J zJW%e73GB|6o~G>f@659efYQX)yuB|uB8qoZn|}eM)@AzWn0ViZ7-hlc?Dixj<{AB4 ztnEH@Q{o;o`_5`3r)QnhPO({oiAo#-B;pX%J=q}$U}ey)p@L&eA5#P>peNE0w^yS_ z5z*4Pm>uf0MZk@Spi?lK3bfFo@z@}et*9U(z{u+wyq+%YNS*k2By;qQ8E#+Z=~C!$ zpzCeI?#h@c4Xf^B#7Nr9vuE_lZ~RcB(d6w2v?vV`dR%&I(mWm`WRmAb^I1MmF{{(L zROL55eL#0_%*fT~)#MMUey?~>b($VF@n^m^SvqV&iJ=Z}OouKEw~#8MLAF{(_nDbI z_$ALw_gRyVuveRJe0=r{xi@aUg%pW+`7qO&iiWxQYP5sa)-=muRLd2auvEyesk;kx zLwO5f$*}^Z|D}4jg@eS~U*&X-;3{6Ss0-8Naq_)e*XxatnrOEcMMA?R)Q|;t7#I}* zjMI$u$ZyNyLKPB_i}zQD+h-PJyjwFB%`rRiQ0+pNc&M?YK6q8N-Yazt_?PzEbqnsS z8zZ=j;iN%APV0l!kP>gN8(0C}8_C*(2Wp|J=vkGtd`m{Y#6hx9@%$slh_w{>H{$xw`8P|(p_3C&P_~oJvCoZDRUU*@S5cy8LK3?@rEKbQvF*IAv2r_ z_h%wrtiU*}cB#18#U}Cc3it9(ZYLQGYG{eqOWkWs9F8KE`1W@w?=2a**wyLFL~D6G z-94IA`H#+nTQYJE#pNwXymw0`Cr5PjdE<;o-h#sNXwGd*+;vH+O}r%|yI}m2*E1EF z(kjz8=`vm}>F^VYukNAhd_IIu5vl=_V{L3HN>#%*wWj21NpCr8m~W)A07J zS|Gw|Ozp;`mA&Lna_d9MZ_8A?z2vVQtvuYG>G$@+7lVfz;#v3hQn`umF}DMdEW}>; zNl@_iOeE8nS}Qmh+{gZ9K|Xg`yuF}lR9%~iyuF0%>3B|EpOL+ANx*T|JQT+9v>jyi z>!rMg-Z)ud74GTq?HF2aPj?z{S7cmFiv#UuKv9F(p$KQ#-$RkTcW!_$Lbw|n<7T3o3 zw3j?vgCsj9NV1o#G8_p=F?U>wBzwv2b(M0mjaMa&+?tVwhWf4^go$qGZ>l{SwJz@G z){G2ksPCHZHOjdX@%RW*njOHS!GUGQSfk7tC(UL{x;2A^%s5Jt*-_`Z&NPG4hMJZ~ zx>K2E%;(7%LL(Vq&8-!K35kv43u9 z21(i6a{kug1J5}Qf!1eA-XJ+D)9a1n%l`_1B@Q(n{ipheKfE;~=elkmfwJA2k)z$? z8xG^$zn@1Mu~~SxW^(dgJM^q8+e-^wM%>=ZRBSH0*NTYey_IQC))29?Dy^V`>J+dz8rajWK$j8b#?k|6JnKMVX$0s z@uRdYH3ynGC{1hj&lAZo>SaS+gG$aEg%}9e$Q5wqXx4&ehivk8rUB-{+ggr-jr^03 zldN%A-hL_{vBKWg3aie{!H*y$TY8$J{JyMd2IW0BB;Cpp#y4-4N^^GlRprl_2HP_s zmybQE6>n=fV+OH%1eq|v$@#EZI(=_9OUPvXpXE5F9KiM{nRnoi!_eE!!a=r{tE_;X z<7v+3J)cZLGG`6^Yb;KUgbF|OU5v2v2_5YXB2>cLTDHNF$H16EEj+nekzAA=(7%RuE1zPa&Q zv7x3vsL2Gi=d&q z!-xOz!N&XWcvHE*omE)zY0y6#@@(W>wrW8?D4pDu&!?cj5`!KAdLag#u^&z%Q_pUv z{sfTE#y~FVx@>%@xH&afPug0$$x6r|HpI+Nu+?+ub$Gwd1j zBLx?4&-7}uoWO17FJ&XHCzY>8wpDxB7)Bk!&>JK-S-q7V5HD`z?K5Jt#tIOaiHdv> zY)vK9@N*lymh#>pxjBQ|$JWwYHI1x#DFt{LW*t1EK%4An9O{Rw+==&*d1ZTBtMr=U zqgzL}TuR?69ev0vY)P}19cZ3=$dxnv11gYP7rSI$G6~2Gy)T7 zc5ddqSdQrmwQF^l9r+HvyoH`7PJt%v3Ax9t7v-V`-L~(T3AU5n@5+|v1yS23e?>al z7?2=4LVmjmt>c&No@SMwm(4FD;dBuz2PwCEd0WdZS&03$ z?d-&xK~v_}J>HTvO}IBmb8;D%!Z307!#z^HxBU|*6CCUy>9ak3afViJwBOg;?tWw!y=ivB3ahq z%PNj2TQJyuc_0IyCR_C@`LxW#!xWeOPGGRC6u4UE$YO1?dO%r&Q0IGFTj(CMIuzqF|qEp-)78a@O4U!@t6EJ(Pv4A;5=s@1@ zNb@&rd_Uglw`XKWvvCx(t10o68TCQ+>&~D0I<@BO8l);PBTRj7dTKsD&Sz#G016Wm zu66LHYWq)9Wb+or*L;47&!29gCn<7AMw*=B1&t9bC(h0mIARJ(qCcCOb%FgLXNdFl zT%EoeLm;P>pVti9Te=*{^pbJ3J@ujJ`6!0?jtp0iq)rqd8-L&v|MLf4@z#?FjMmOm)EnAHX%2b5mhPpvB0l)dE?}YHXmhG zUXIPI=Sxo(Q{(;ZutyxdjdsPWmzVy!$ZM1y{B!p=ucdWpJTc8(?7W!z=-i!yG<1MT zs#mq4E8SmilqkWCsdZb_*-dEs%bko#E2zruFl!x=2K&oZ9>&C{+G2a*{wntgZ!YY> z0=>W5#Ry|9AD3el_a_BM5er|eKCH{ceTiEIX3U!oNSNReGWXjRZ>)U48)X`_;Ej-Z-;U-aP3$RY2Ty#{nt(j;hj-e0C31C+ zQ0Z>8MA+|^e4gevav9mXCB`{cg{ughK5~TI`^`$x$;tt`krSr?3w$?OwXuGVP&vvq z_P3I+p$0l%18;;%x$*yF?_a>HD$l%ecJ0Du zZ{L}D+jojnXWD6my#qTS3M3{01VVxaNPwu(YPB9?>p5bA;<;*C&*xaIprtj|+G;&T zt5iYdyMFg`SUUl~Yi2&u?|QH6pEi3cF-hO(sjZ{d*7tDS7HpX1}fCJ?s>)6z^&e z@b#FpdA1l9z+NopRkBY9?^b{YRlui1c6;BXXWD=fqgqKnq;AYty+Vf4n!IZIgY;OZ zY~cgEmiwz+ZC5wLx0yOXByUh2xGJVcHPfqZ0p|)1e~2=FjBNw(KVoMRGu)R~LXrnA zwUcv5y3)ngBL{JNpV<>COKnBxQiXS<$Z{lmk3@_AaVl1ya3aCZ2B#7Q`K_}V3b2qv zbGY>;$S$HHGtdD8KZNQ2@(PY3*H`MhL7PG!!6k4#%c6P(Iu7K%sZ?HC+_FT8wqU1u z2-C*W7{?l&Y7H5tm+594Lchh-cBd))s}gv}M6BI)a%y0=%yZ70N*}iN0pr3-H>%eX?pK$@P1k>tk3zQOEL*B-iBqG+h<+hR*eY!>p{tNPjRl^B-c-Mu6rfdFN$*wCAn@?xlpTFvm;6B zrae>3%Wa$P)Tueno7G#Hq~5Pn!(=pPB&iSYncA47KDB3RPm=oFo~Z*#>aX`q9ZFJv zw`b}|lKT3dspSXQ_KxhCTA8Hoz{$dNPf0h^L(5@6LwA=Hr?_(Oo5D5z{uG+H6_;l~`KKT>3HQcfA&M1~!$aTCX;LyJ0Idq^Uu>Z2iwRV4%VharvGk4kf8q z@0mK1r1tHZT7Iy_ya}l|xG!qMaaF^&P!yx8oN11#`b?klJ`F3 zm7UNEl^c`Phjc2+maF5ZCrN#B&(wh=^_NHmx$x2wD$W-{hNWUC$-5OJ2NUmTwK>Hn zA7UH+TVNVZo~Qg`i{I+UcIMY6p@2IK0( zeJ>V*sBB}BdW}uBRP`jOH`!FXz6O%i zTlY*IN>cCMGj$|M{dtm#xDHWu#JT**Tw{i0`k}V*kL#?tWYXo6)Ms^SQS(+MsoVBU z?M+f&+B0=9N&Ul~sl!R?TYIKDhgq86-!pY`lKK%iT5OF;l-|Z9_0v652b0teTwF(& zE&sf&w`II&epmwl_lGIC(3TCK#CG1YGmFoc^@BkurRda;J`%_4; z6*P{(C;d!zO1f)hhs8Pla82!jbhO-gatw)bov5Px17O_)&aE?o(4_!X2o4DNW#J zwY+%D%QGqs;_iI4RZ=z3_EfibEInTx(;Cs%MdGq@`8Nf_D_OBRx=ek&niYJPsINa} zyJ3-ZvHE%~Ti_gM`0+P8yl=v9Oi3`ZG~#rtp2~vOEYm+pPlXZ(PTexGZFNRo7DBMR zx(59>`lEqK`F(~FS*qyv8fakv>K}Jvl)D`sI<#01>FJOvWSI^q zAgw)sch|9We`)MB;K?7(gU%fK|8BsT9$A9dneP;FDcTZx4S-?S@UrxzNHYKX?b>sk zFKov^zvt{_q(SjN!VDXcm8ErbMSF)i$OeznopAyUD)D59d7a}rIFo6c|H+eerbB6a zS%vp?rcV~jYYbn&MFnwsLWBvwy|iG?;TfjHm+0A2>$R9S(0`*fWknK?#?OGSGVnv_ zQ$z~G1u&n-lBvRfYKll4NwWR{St0Mol@%e6AfBAV9gNpGP!0yI=5H)?BOdAfqHg9G z@&*k22h@iNvkqGY#Nr1b6lMVd0z!uAOs_cw?)1v&V`n=) zab3?dVrN9&bQSLubCW|rFTlnRv4)nk4HY2_D!5DZ{CEu_<{Xv8=9Dl0+oaxDUb7zH z0>m|I~igG>UNVYB@%9{*BMHzm(w+!Wfm(=sN%AhI=t+l1e@&A)jwqWHgjmd#doum2zK}|8T5YgeaEK$sG9o9#cc;PTz+b%W?0g4&o8;{kr zMf8k`47=9!Q(H`dKcdkzdF1w_3Miy1_{lz=s8x&J~@ z?ipXT=m&rv)^8Bez?4TaucKD$nlX}OzPqGIF6SJtk$jEhr7j`}qo^Dm2Tk~7ShQgy zKrDS+NU1!I7!2r}$$01$W-T}jp9Mdlx5E0Vm_F7lVL>s4AYOg$?qO22J#%4QJ29wl&q@qf{ z+DxSKskZDxCKba98lcs}JXX7vk9W{>)K+Wu%E6@Q?@du#t5d1*9-u-Zw9#cQ-p0bd zZ~)UQPzUi^GuLM%xg+p?n1x}m*3{ISBwyrKcy08i6MF~1Aw&e~<}e#H+191Xs3%o{ zmS#DJ6X2_{w=Tu>-C&x?;X`fW1Tpcgh^#bGT}!8*ps8M$B1DgY6IlciweIPjqV!kg zd`KR}7*5j4=p46V9cQPhLjZn6Vz2}jjb;Y)#R6Da`j1-ryZBZiLO4rm!pPBlSc{o^ z^4AiAcHnx83e8?xl(uV3T6tj0mC0Rh!%P;Vuh72roi|ygl+hDq72ctA>GBSGRt5E( z!qg2G9qhu#%LCcTvK#`w%ftDS^$w@LC4qOS@?m9pbz}V{>t-V}Ns4V6eY z*x(liDy)4$!K;d?jUbT@KU5y-ET6}vu&k52K%aM8Tdh84 zj-nS+1@CYK%Hz;GioyFLC)1xCL=QQNC3e=~jM!QD`Z0I~yaDSJ@Ohc~l*knd!R75i z#ygze2Um_9XSWO2Bx|S3JDlDz3FoNyzTfk+(XG%gB4Fg`GjEdm-%=FO809|m8h~!`lBv$q+QVcx+CrJ2m zVh-gP?~Y&&U;!V_3eCR?MaK)Z{|?1JQof1`PLhP-k6!k$C-wn2^k z*`&SD7U;Od2x22qp5Wi6U@9->8t%E&Fpba^VK^F_QcMNb8^ zn_g}UI1e)|tUeI>658Dhw@+bfQ8(HhDMZv{S&HG0CNIi z@W4soM68P#;j7SBm$U`mQS>JjdFw)aFufs57F#2X|8m|@2+f;9;0Xl%3z&LRv7D~- zW*g#)AI@}Hd+mP-jd=JX)&mY{dJAY~DRWKG7;%`1v+$lryY+9z`zAt?X-@Abx`nWT zt4A~H5YaONAqdgbDVkHzO?Ra>)2T8$Pd0OM`akGS`wCqv%U^}7SZf%&9+Q4F{gjp4 z09N2T%OGbyn&~{81zl@+4(B#VZ^8x7dR}P$8Dr@~V8afU#17jXxGsd-RQC|!w;7KW0JV(7;v~5xz!*OeleLh!8R6?9)mON+--WX6oY(=oO;RC-fWH{$TVv=RLy;kIE$I?Y&V3=ikqqY_}E;t%)UOA>Toa6+D zA92oypjb3`Ioc1FrD_|>5${$pLI=$JT=maMJI?;wiY}IB$SbnAsDZNyVaF`T(*3nC zJvf%#6KL@0ogfmgB8s&0#abEaYmXNB<_qfU22jh`A71={ZopZ`U@m5vR;dQ;K021i zfBrE5*%Sa}K$^eCc$pJ#a^;xHU=0Gf6t0amkV|Y9FWL>L86AwvJg)t9AeGh}5Yx^@ zwHf-PHlWoioBpf)5^x!B=E(h>{meNeS(AB!=Ji^-xiF6R#>^;GKUlWR}s{e_z2}-glYS ze*;qv0j+k_#Ky3w(&W54RfaHZ=x#x*4xN0&;acxoOzYdhOVfx`z*kSZ^5DRY6&&_k zj+L4YBnV*}PzF{ox%!Tn)PErrk+4&}86ywJxYKu=H8A)c-GG0goe=4MR=%DYCSMtS z3LzC#TKd8|VAH5e&Sbo134sLe?O61X(|d+P-Z6ARdmr{(MtEsC(j^f6x#|&>6Ct() z*e~bK7yK%0SDg~g%in}aCH>XK;o7LW%g@iOtB%%S;N@h-frSi!C#;>OXYV8&mX<%;qlMW-q%#MX6hm3NeQ1lOH4{w@Di0 zVrz^Y@~Wcb-|U$@T$KE~J(H)`Cdd)7S{R`rnF%2cvtFZ&GE(*)14*pS~FD!P1fV+5ZOd>V&$oqwlh;B zI*!(-k&N0P7^BZ&A}bylUU}Zp^h1$bgb{aQTYuf#>>W+#jDq1IrI2YZwnrfPHFgme zrurU}G3On}K0PixHddrB%rl;lJeuw{nWTm%J)Dui;~yj>97+$Yv~rl^nVw6`g=f`wb~5 z148eD6fW!N25{9dX2T3`^AMU@Y# z${#V6a~74|l8U1&f`Z5D*~DA`eGjbRDZJ+lM$`O!&Vn!S$#&g|5BAUcXQvq&E0YeFcBRJylX3++P{_KTg2?dPWZK?{n;&>j zOvP7QJ5a!dcy7;W;M0W^1OB}LM1hmy?+EKwBjj{bEL{T9eK-SOkn-t` z){dEy`a>$vNGmQTIXasPW<#8OiYDyKWi(tS%C?;4I8EB76ul=`+I3uwO>iUE(6`Z0 z-1nbnlBa|4H+`#*`{{Vg296jE--Q`!S@9V(eZH>v82S^~iA@+y$jhHs<7AtH4Ht0k z;_9e2lL6p4rkxbiI>fi$0W`8)LSB^lf^Q}6;p3?PIWD;J}R zn>j3EidUlH?uqh&m+`9Ubx?3NC}8(!aY$BE z+@BROy+<-JRI(q-=s-r?UR#aQA`+>F%@tmOaMz^%f*iEczK$`l$n;YgU6`)uqW@jn z#UW*O;{h2J-{5|T-|mvX=P=!of~<+;xB$0}$%yAPpt5Fq>x^3O82TXs=g2((q)Nac z@P0WCZw^?mE~Eg6g^DrAayU&sBDK#Mp1QKp5J*=+ZUQYj)L4+};MZTrx#u{R4Sxl0 zWzV^q4XHd`m%J^d!WtYyeWg&j-0#=orod`ma#(|0fpd^IOj22z#mn3#kmwFupVP;ly=zWQyOEE`vZ<2Sn zEO^KZ@!%{%GhiNLA_zJ7kGilUY=FoOWXW(I&ox89bcR)y_Aa&*T%z|rd6_~wj&p2f z#N|tL#`{ubG*$Wz^}s(lWZT7glk5JJ1Zu%niWlGzvDv#ng-{$E@;+U> z01N)AY_7o7OR;w>bxwr5h9?<6Yg_D-axz&nY)F7ZMUh?MDu)?EbeM!LmNFr+Bx*A#@X6+2(LUvy#rJ^w`d zQ3}=np-t6~4*j2?;3P2o_&OyKKb{0x6^8GZGR_#jK}7h+A*0oAC(#Fu?pr_$0c> zi@BPqbhkcdXnCzyL$9pp@J^!3yqHS5Wud~ebM*@G>^zC-BX2vo1&~rFF9wG*Wf39} z0-~H#3Nt%{7M?Hc3uf|&7n6%KE5f}5H-wETrZZQ7yYBQ(qBSK*^bvJWe5G!ZxporM zODPcx_o8Q+hM^lmfA+Q^R`5%*oD`QOJXTE?s{MF5rX)HRXlY@GEdI=DmZH}tsAXt+ z+mf;QUvv&mU+*M(C5g10Z_}H}ztR1^5|Bh$Ok5uqDG24Y;D6X;+|TZ&)}~_meF{lg zC--~hOjm$(F9k(dhgYunYkdB;tGv1aRmOBpnJ)d};w@eoU0)V+4s~r^jPAF{25m9VqvsurVyBlvtWiuOQ|07AgWU7z zWt;1Y_EEW3*<8Gp{hXm}I05&=D`Aspj&SQqF+H`y+V)KUxv7LZx!={te2; zXmB1-ttdm=?2uGysG(!J{+3)hb9^;wsL8cz%y-Y2?_T{aSZqHIB~UwYoCk}kO3v5H zQE)SX@&*d?E;rt7#%-h+j(w}sjQfm-G=3?uz?LX~xETIVyA9_vpy6UFBx-dd(;n6- z3R6;d<0D0w`VE&gPz;6he8`=q71JLrD&ZcB4OG!=9Ab;pEynW4ic0yiF^8s=Jvt4w zjHY_frkL7?Ekt2~ZH%wXpe8@As@N0F44tAHp9jhK?KA^W`GhHHxVY&FL~n6<4G|o2 zoAE;tK>5GTEY}@~2d>GVB0ur5*F01m2%iGE@vj?`^e> zdfMXYXSdbjah_4|BsfC3h3r90UMHzm+j%y$ckuKfg<5jkeppouMT5+v1<$J7yqEsz z)oFss?R9rcq4kdAxX$U0M^;nL4jwYU641 zrY>wKOr6)Tpl;E;DGg2YrnWRRESPgj-TXzG`bCAUdcFI?))G|7ELPZyFn|_^cQ|_Hmu?}T2nJAOVz@7*B(6dp5cx6RD z=4cwRh3}hGVFDgbUSsOIKLO6rKpLD>QPyu>KkqfrpBF<%4D=Vp(CKRxT6&-u+GC&( z7DI;(^r1-=Wn0~Es~vZGr^fK`q>8fL?msDLuYo>N40XD6-bag}c>{f{7&@q-Wr%sR zK8lE9HTUvS6sxhw=E;p^(q_@c+1Z&4o*|c?tYpbXG%3#4MRv%B)@cfMrqlvXxFi zP@JD`!|&)48fM}a5*P=hZU1s*ow%SVBGFVMNK z7zNH>sKHm3VDE)Wy&B#( z27Z->-=yHHT{YGtKhf}wqv7ROYxvEh;g#2DIP}hbXz|RrR>KEI=Wo1D!#9nF_gt^x zx0O`nqdK$VGf~DYhGCQAiOpD4O**%hyy}6}n{-Sx!<3YKtWA?I^ z`nm=4_IVo`rq12x9n-vE{+#+|$BF9}w=~SFUsT^z*HT~jd&hAcjPR6s4Y-P^LBWTs z;JHN&i5W~KN=^M443;F!NiwOa= za)=ZC1ISB5=&qpUJhs3YuJTIg-gdztCM(-Gq%U=6nVRACvlSMe&{>P-1&HrKUV6}p zGti=xFx}a{3)Mg3#5|j)WxW#i>9+P-z#exb-Ye_|BCmvQmZDEPalp!Y3>8BY#mah{ zeh!!Gd)osTbS7jTS16_t#6U@?BBG)wMBYpfpe$TE-~nfw_@68LAj;nTpRD)qp?EJ5 zCw~_$^h)S9X&BtW_1~;MSf~8LiRmz8yt2K|b7@$aHZk4W9&kugVy1sZKd6R00b-DX zhM*gutn=w{@y98lOI6n<{qUeufd5mT5_ci;l4v~>jzGob+&bmj?BhF5y+l83N zB-#Ys-rkX+rL|R=DmeeUG5%qS3QYeJ;BO7WKZy9ic+)F{aRyp}0jCF?0^D_2CS(h& zwp@mP8HWH@Q5>Hy(!6L%4qnccSy~s*20}cjPPkV!To{8orw$zIH9=oY6`ZoCui{z5#(Ruk49~Q+@rV4 zk+t0$_akdO_RN;~3;!=U_OONT`(tQdV^o{}zQ#J<$MM3tmQ$wAp5L<1317Tu{+xzF z{nW)RbE9$Vl!j(G)AxGtt5X?g_~O}96a#=Mk3U_CXye*U7l4f=C#L({^#mQf$M zrnz&1AjoK7Q+-{bFuw_3DM!A*KNfv1n7`;W{W)(zgQ;?9-2wv@QiVGpr>H_u*rMvz z3<$7zjx%3~16$G1B&4z9EXUt?`Lr&Bm0HgJ`;KkVetJz__+CO-{kP3ka(W*o;tv|fw@NmRCy zZDSshkf5*DSz%!V9eUX|qAn&oR)FDD%Q=Ozt=5TH4Ph1%5j!9)ivrrTb4D-&gnY#Pr^ne>Et-4Hq2k7 zvo5Jyu-N3CKeuj<{7sHi-#n*IAtALz&Dg3X4xJv+`FIxu`cJW=cq`eC26Zmar3amg zM7cO*;Aw=li64qexKpe>i}$91 z7Zh1unxdlm1`U`{Unh40AJ=#uJt`&_@+cKC-6|$zq31EZf=J4MUA__=HOy7F@_OD) z?KY%P?~A%JqDt(8o!INTcq@ngfvacB7U&+clUf7iv>m#Y)V7OK-I*%c>98>>V@kB2 zCg9EnFVe6BS5~dCMrYt&iYpFyYsre&E8WQ-Q!1V|dm57tpYki&K34qowvWc9`uc^9 zEo1kOo?BG6P^uz|qR|2^Ec*x=8}$SEmi{UFv*<^*hKr%NETU51do$S{fG4-{+_;yDxO_O`fH= z!c;K>2WeJA!vgG>1b%Q)LH`Np>5J>t21?0h>p9NC#S2>IBU{RG8t1E>GzD-=y~w_> zADh2R9H(KC-FXWQ!WnvM+Z}nw@<8jM*FrQFymOr6jdDeHO5If>Hp_Xi3dWA%e)%J? zR5d(}LqE&e4CiP?E#9>%!K;5tAOSmH?&o;sEI(p!W}y$>Oxfi`9MYtin#4G8W(NDP z1c{P2*w#g|FNKU4m2nLR5qHjtD7Ags$=TUp_#ndIZal2k!SbGyzl|oubVg7GU#bXw#Zi})+YCDT=Jxz}c*}yD~b`rbUs}GELXX^nin{ z-s*(%8eWEHVY{%ko1X2lDbG4Z809CEAQ-XP{$R5i27U5vhNgFPnn6CprtDBDW?HPW z2^}e^)g%nqgbgYopN~KX8kx^$la8po-Bfyw%9zlaw@DjQ73;U%5=Aw1L=1=QZJVr; zOy{9D6Dxxe4J$(gA~6;LQhkf4yu;M@p{Xw+rqL#z<)WnNnH~%86uA5tj6*i@Jez0= zI(M4dE=8K5da?yy32@PmRZ}+FECXY*NH-cxLpIw(+P(p^NGl|ryG-pbx)t5qHbt52 zw#{~qHQLnoqy|HCkA?q5;I^TIHsO=e2_rV417;HH$8=BS-G+#>CDBwf!@|!OxGk{C zCR{8Dra-TSUp@*xXyHE*xJ5K#6RsbfF!>%s(#J!HCuV{?dvY>e^$LMIAblSfno5^R* zP)#=AqZ9Bw!c4v}{>-FN$^Nx2tK(#exsTB%-LiNQM3+;Wm+f=@*w(RlQFGng`cvxb z3iW%Jr#3IGYiz7H0#I{>PbFNXYp?LGlib_Esh-hN-y|1$a-8O63tQ@DgYzWE(GB~W z{#-QQ2r83Yu9MvR!RRhnJWmM+iuhS_oW>@_mZFB1x;Y|qCC4dl6)GtymK?Z>g`K=b ztzeJn9T6X{<7zr1pmdD;EiT2-m5q6*c_1lt0c3feN1NI^Jc6b!8^L>ypgd!|!7JWN zw}Knx5DYIEL};s6q?zpJid0gop0mXCX}lM;cfjSl25&rOJfb_>1J7g8e%9a{YD|xy ze?(v}f@{6!L0_xC`;lMi*`)kULKxh8<<{?u1VL-t^1rgx--_zHXb`Sz_v2yLhuqzC zUlKZYFv zjQ)|+eo6`q`HH`uIALM^!i5b>>c`SLc|eT~9a^EpsSvS7Vk@M6)VKL>7Lr!`V?I=> zd}frrb|xYZLKmGP2W#@TDJMV3W_~^+i=0>KtVc^o7)mhkaF4xHmH1}Z+k#%)xaTbkxHELyU!@$1R5uhH5u)%eGQ zD;lRB(K_*Q>yg>xW3s0uvwM?9*3C9T#Dz+G^I8005x;0vIw{ALcO7STooLV()h!ei zPs(wylYwW0;ay9@c-GBcAUPn!RCRieGq0&(v1pGx$B`VlLjCN;^HAKgY-Vn5U2{ty z%;p$i^|Za@Y;KY2J;y23*EiOmzPL_AtSQHtyP%=2CHPuX{k&pbImD-Rr_~$PJoNgb zSY}OaNg|>xahy}?nontInqRLIIbo;MMbcO<`n$vtxh@FSe_9c5$!K1=c=JGZ-*J}A zZ=OGULH*qM^$Q9{F{cH(=`pPF{Fa6~lqxOe*~@HaN;j4{hG(BpoNE-HUY0beEaB73 zU|XsA^fJd;ICuUc)xG$bHwP7$IZkqOKuwDm&Q{Z4qT`^K8zr0TIHxaeXpuQF(Qyj% z=gk*uiAk6hb&aPeLxxF?BO_QmVEYvd(f4&$C^(}f5PEyx~ zfrXm0>-30rtCs^1K!|f*n(0!6*%Ag{qFg=Z_7dU9o;Ub*ng<@pO+qvx?tmmZ1!9PYjlsG)J|<1;gd+>>BTlE9-#n~ zH<4*+hv(6CZtSJ$_avz4on;-k-5@YQNOP@)#&Dx;;*}}&s;>LbJ7#$v{f#v1WT~C$ zZ_ub&xV{`A@Y#U`@jELW+3!JQmZI%Ya6g(}mWH0z?}0iB5mQ3xRm)(_IIgn z&y}bDKyS$Of`b@09@F~Nc69p(ZD`s06uS7sHawTmFIO=!_YD2@(-@Z~lzbYdrUHOP zsYJSwSdSQ}O&p5yS2VMzTW)a}6{v*KJEwOreF>f8F+C(iT_`p#$U~F6Im}c?Rg6n* z#CY@`a{#~VSUp7!%ZFR)d-(Fiqzx=P4 z+lX?{jV_0lpdP6Ym+62*w+dASOnvPgXjzLGKOc`)D%6p7_+8k#4xH;f{W>868X z4mV&Th*|LS3j&XsZa65O6L>DYupq!wyMCPYJUj&#<6T9>eRi2Hhp{cduQ~-pegW*# zR4j(8O9RiNSDe^$5i(-E2qI8>7tIztx4FB~HB4pOy)^wvZ30Z^wFa@Yj#Aq_kE&yu1X&8GxdSsE6+8*UXgo^+ zm7XkCIyIn!W2Sdj)-v^AzoV<#cPYG{OINoB(vGs2{t>&`I!{BXEUreFFYI=(fX>rR z5Etx)+~?fgjEPUYSp`(_((L{RJxM!NPv0sLUwpOC02j23ocDaX02n=&-6QA?cPCVa zG@;=6bm2-2$-#x0Btn*JNVcsXp^>8zz>q^DcoGOs3+SN2EYF8%!*dM=W*R;iGkSo- zwbOX5T6Q-_?$Mt{GJ*X8!9q_gs72;m4~oa+l0IIb%#)o`x93bd3%5Y`>iM*z6|0Nf z?Fv%|b>}^wX;*89Z1*_7T({qV+lNV7hl9f*7Ug7Y7!+gPFE03^<&_w4ej4#=R=FV;y6A zCqKqPBqv{}K4c|fHDTk(BMb+%?!rn{0sIh%h8FE&S|RcN?1G)VMz{0eGP*)NaO3xL zPOWkD5Ih`EW#}DDnay~;3DEo71JL(#2QwTo203dE_CbS@s80loh+JL8VWx_^@|3)k zRJm5yOnC3OtdmmR5v}YnXd07bkx$BI=1ruhDE0_*wkC?aG-5w`9()L}jxTKwU@C<7 z8l+!eYU}feE)}z=9K1<#_Bo8nowoK%A;&Z5ZxHlbTQ&My&{&Tn_Eky7YlA+uplmKBo~P)gweJ0s@3G zB7`z3;OojZVQ8@Pc*O4U^qbU9Bg@RM)0mk408CxV;0jOnzqPW@JDP^u0*~oViBY%} z$F?asl9YVgtDp~8V(3l|sH;7oo!HU2pAIf~uxRi+Y%Pp`7XsUX_{)twMl3Hc@)+FV zJfs|olxF-j5r5U;uOso7AA3wo1L|^P?DZYeRm`+of>C-*9V=_`HYMwzRBPBi`!ulL zHt{{!FL)jettb{pGwpC=;8?pdfH<0EFg-dOm2k#m43XzikNtiN`RRxD`%f4`C61<7 zOS|bZiS4(Q@m5f~eV4FjgC*ol1bKp0F1B*`Q$?IZpFg!M>GN&!h8+5w+ORon!a{EE zGjm`@x5f73Qnew;R_`B(1B9NM7&G0{EM_QZ+xitawkZo2B4)b7E1)0mT_NZ4c?ZNy zw>Jkk6TyGyvF*$O(^gjCPNpThuonqn!fu$bEbCwdCY3pOfi>2NS& zrVsZkV0B}Q1x@SJAt>jaI)&+OJSf7Q^sr=dzW_@Ux@%cLkBS3~pQ9_W5pyRV7NQ+s z8#v3I?0y{vknUkSz!|zne=I!_g3b*RR@KjWOdrY7CWN}vKBdM!Sntr#vOek)>=<); zZdo5)FSX;d-^Tv#{h{rmO^E2s>V=d~%`Q2Tq(6%;^gR0T)DDlS3$T`i0}5dDi))-VhJw9ObU-(zF1fLpxifyOhb4{p`LJX~ zS3m3&yfpo+9ZIw;-fTcWHh09>++_;OIVx9H@JX}+YcvPDQN19_FR^Gh;^+j)5r_Ws zj3dIW++}vsp(NvN;PesYF7HiML4~=~OVc~J`*>6FL<&NPo+b9}U0m%CV1Iv2T|CN# zS-`5%)KDcmwBXX&;BvM2QcRw#zJea%N(k-``#fE`Z_7hNlDz@RPKM z2ab+^*lAQ)(=WH>Z^EI7@;9{A($-WgY%Kcloc5qd?U$ziAzHth7`tp5m3SUJ6u~=t zX@Y;JM-R!_fp1X~(xdkKHn5~Xd$x!I@JepT*%~xf-j0F=FOn}9Q>)-rE}k`?fwxHc zm>#2iOq1Mjf%u0~uRIwhoAz_q0GY1AjqdeS%$@#5mXlkqQMA0)_SKcPul|Ua*|-ze zPoR8Zd54##Yw;`{hHs`UK3t%BTLaph!m)Dz=3$SEz~OV`IDqML6^9YF!>e0s>86w% z`gglnkhNLdZf0|c)C*esXdppyl^bWd6A90*46xQiwAMvfL`-4)cH+1@zcoNH05HGq z);Xwc3$B_F21zSrB|xLAK~Mv8PNiWTz);UAb%(C9jo+Nc!9bHXGc{vg=J5bHj3^XOSew`m(3^KH50Lh7iP~-u z%~vD_kSM^{)r;wyG6io&Mj9%K;k*ni8HN7cWjJQiOi#&(WI61r#-iN|a>y{?Ix1;z zFRS&^Ng?j?ys^aKq@oP=VZbSeSX5o5A%ScMg$(VVKxQz-BR^MO|3^@^T7sA?tbc9$w@^U4O()o)7~w$ku@O zWO_?R{d^hPoI2+)`%qn{{5SNeVmriLaOhyK8*2z5*wyaI>( zB8T+#ZVbA|bOd&^*#R9H~Ek0G30q31Ml z0Wr`KH@!8;)_`q(c`0&T>0^tGz@3LoTU_j6XgDqk`Wiy#tn8reO9K=-5<=#=m|A#$ z3RCny0xaOamEHUObPTjiSNJh^(T|o&pa01(XfHsTupJYLY4?(V>D*GR6ThEk1}3)S zs(#zXKdCk{y|JX0F2swga$Mm}syyZw8u~bzz;vrGH=oyJW&F4#&LHhg`RXab=`*lA zV9E-KKPoAB9^HXGg&t+Yf6``Lm{y`g&X#Ks6@(LFdKHxZNK;y6=o}cZbiP|AW5M+M z#kF)dy7?lA=}X_5=xA0~;FRkb)`?P$TU+>3iEW?GR1 zf}RrG8x>=V#P>ft?=zD3R&@;RY8uVcJuyLM(54o-JE!SUeAAsRn7`n*9+K~S?DsF^ z`#$@9{{)CXJf@vZAa)Jvf+ZU8fw=&VCsBrJ05E(%hHtvV;&@oT@3!9$$oIYWd(hPJ zQBy!q2}22S)2$P+fU)UGNCX*v$kRL7-zY_Yl^N@v+ihi+?XM@teNDCW z3rx&=n*yc>C*kD0r)deUYAK1D z!dOBB%@~qx6Jr`~#OXI$ibYrKA49X@9Zc^uO7pw+FEB0YV8J49y?s8GyBtbBdpeNy z?-B-MGJ6e)EJWrS}?T%;?Ph z?Re$yvK?^NLAndKEUcxQMN(VT^QO;-k^<{>0i0^M#)eF%*3zLd(>!reW_L2)uit2j z_S|jj+Ei5ZQQ@cBT=<4c%u^KT(AGa`eXf*~yNr_Jp;t?_4>Akjq&Jn>mWTx&( zALFvIyUKLg@GopFSKyjWzijKEn{EEpI=2?0=O3`SSIf&J;B7z0bp2vzZJ6ohvfbDt zeVLf;GS9I52bcBH-5hf_<9$jD;B4=6bOTDUA9_q%%TP@6x_G3=ok1JVzypU{MdJ7x zmWGk533G;db~8{f3Dl!4A?GsQ=jexmb2n}-2BlAnYPlbpea&2yKWtm^<|N#%Z{?FY zT+fhtVuIv3toHM>6pKskK{f5)&6vWpZh1gEO5$m_qF`Iaw90TeEuWDzrmeUhuX1C$ z0^%pZw!Vr(D(hCWfG}+31*T7y1#~9Ibb8+`eGQDbdYiyGOwjZR8px;JGpJ=3wFa}; zrvqbDE@E0*xrNRMuycV@GfvK9x`k!95k}h5}1gC z;X%e5+dDYi$ke+yfW)(!{-9X7Wook!X#8{LgrcU%M%yWM zohSDOosmHM!5uv7#9;fp6wX3iIqh=WBV)lZe_o0nlY{fl{bCs1JOV*Dhum+HOQp0uo|sXkMww)#ij1`oAl)f-2dAhG@H{#m+xAwdU_&m73qLs3<{*{Bz4)Nr zW=sY$HuV?B>f~)F+T`KcI+^HKc!v@`x7#?ZCRqTX%zdonE?8gT+AVC>OmNaZ`kbg6 zp1B=WFaS`xdE}oJ$w`c3MY3V9ln4u}k=tG}dkePq9j12dFk@>Mx8Pf_N>$Aeuk5|% z>Y|Dv#T{F*79v*8wH042!gO5K+#`0dM1;Vzs}YB5v46?NyN*L@>!^mfTTk`Mc^1J3 zK%jLmloQtHi!*KP9S(*?CA|V~C z1Bue%7_d(It!%SNp5lfuU4r9~-f0sH1I+`hNp;i55Mpgp9<&ho=tR}yA#V{ z5Zfv?w2y9W-{MYmQmMJFuBWDej*2NCFqOp-S7$k@#${B` zQ&@pz8PDQ@HL3B-r{N9M9J@!*kr)!19mHK+r2;GB_FjYC04rDY$`R8UyKu9k?<28` zLwEA0JO_Cq4q$+)0@BP2g=sJlS2%VZ`n+P%7slYU`jS>h+)Q zG-g^kjkoJf^Y}O=Xj}Gu>~y0K$MFqzr(@BMYb2*}>UFzq7!QTLE${iwjZO0xiS2gE zaT059c#ujsWh`xY3MelJe5HzPzf+D4ZzoQmO^A?;OrGP^wKUAfpD>WuX6+@8GdKBj zPV#46OT%29x2{F)A|--hwuUBw@{%aPI^OTdQEC(Sm=42zqEi?Mxdio#t?lgmEGW{T zkLS=Y$A{MR;MjoXDa~|Cs++?u+@hXp3%DAf+vKfJ(Mvwn7DNFyhLs(4Go{!7hkg`L zNnAy(wOM?+hwY$tfz$@FCq+KBZ=sSx)yek80w$mn@ty!Cs3E}ai$Sv}h$4oiN=PdL z^K6InH@0;;ycBMIX`fbkkYn#&5!7%kL;97b-J;H@QzW0+x&_Sx3zf&YJzd95IojgO zf>~1*9X@U*G%qTVYj=4a_gE#x|=9 zd1>0>A}`($WQ5h~L$5yR#*#sS!0bOQoIH$GHo@if@NTBLxb0($78&O>ob70gV#}o4 zUdk%Ny` z36ZD_zX^v#zrry@H?l5tvqcYM&P&S$UFPu;J&h+8JccF;_S!1+ zBdWo`K)>Rv*TIF?O?X*a(g9Xf3m$sEVryFkVT{OLAoiA+I$d}zu@*-x)TEv2TY@yx zfnYZ0s6hnNDTZMo(`g-;vHBO*0bmK~?+WZ529u^NR)`MtOn6~Y5%!(pQbpY~1#^RD zbWmGB$H(;jT8t{})JzQ~FCAHMzwilbV*d`r(f5my8d8}47WQ2K6@;T2Xf#v+~ht!?k(MQv*(W+ZqVOS-`p~86~NnRUn6f)O!!{U z_c;wJFxlSucJU`elJezpYahydB=w^H{{0OyAV%mY% zU9MW(4#{yK`{@0h<>YZ1c6mO1?BJO52eKZ~s`lD7Mpird6`i$|rc{){$ZbxCS4v-r zGZ{L)!`vtvlUxtr(zDI;vH6P~^E7JS<@t0_?3Hqii%saI*{5#@UMU`Vhqq4-d)2Hg zyOVeOw9=Xb4F2BM{fJZHrRo1b>rl${wb;nKhLS7~p<<6{dEcn!l+wC(Yv48GRgG~1 z{RY_~qr}e4U<732bPt1V)4CdJ>{#P!T}=5OY^hJA6mCA#5Y@tura_OG$Ce8T4sL8A z?sB0tbiMNZ$KxK`MB`QbI5Q*>!wddybyr` zMR1lraAMlfE`kEkfmDQ@s3jTl@u>OnN@k@7wyk z3GiA3n%R*@OwY6hNSTN!2-Z-TLZ94jZvy>TEd71qAu}AprZbMX)BR_1cOu-0|DsHx z5CXQaS{cpa&WWM$DYKjYM}Vt$$>u8jbGn^etKP7r{T2h^5xv|tdg@G|!>n%7dDCY9 zi4punYm8;Ubdx%f9+w%?$$9umII%a8ejv19kBZju|Tx6t??~Q)I|Cds$K7;>FcQS{Puvp3jRO_{lp-m zDKX6e*Exc9?g95Vc44gjiimTfiIkpKI`BnXFb%hJNX=Gg)cdxr>umHF>iWE?>lw#Z z*54jfd+DNj=1M&TJG306VyFn*WiA3EMsrb@hq_tf&|0Hp&XO2g^m_ zxFODDF--K*kfoucm4W4NAXr0RUZbtlv`?vs=)fdl1ES~JYQ2fDdIfj>lqv!fO%6qF zsPu??*7StR$#k2l>~ZsbJ7jXp`yjqJ!z!qI-n9)8X=WFQ{kbXfph9s^TL5bBlLZ0E zB~~$Hgdx-NEruH{f6t;iOD3R~rrQjrClsd5z=RJ$;Zq6J%{Q1#+fr~0u2Ie32q@>g ziSTgo(sY^qek}zTK~yewq|{!qm{#+@ki<=vQ~xlGZ++WGfwHR~}oPz}*# z?Sbd3{lon-xjQkY2Vp2x<=Nee83*{EaE1VRxEcMNBR8g|4zppc`oN-pK+qFfaUNE% zx1W0iz3uFjzE#tsM($0Vg8&N}tB~h-M1R6=R&=25M->HHWP7o9hpu7*LbjK6;ttZ8 z=ctNvnAGe;Lq0pghK#cb+qujNY>b!{ov@?Kv3uAKTF@a9S*O$;`HM!%^d^)Q=ubE` z$gP>_@m5$$-A*xA866@n>FHR!BE1;b21>()IL<E8a_CrUywD$33mE@4A~}YOgg# z(&$|DA67E{rNwsAmw#kw`J>R{rRj#1z@bi^F3dnWSTyVdxCEloXK?95r2^ru)#mW+ zO|V^s5#jaxJT9H(+MNPUKj;%WZ{;q#xG%tfK_H;4!0FF!H`5geW{tYegq{(pEUwwK zxFd@u;CEI8bwb49vVYi#s%>!PVs^BZtqYiGWiaF<3$W=CnvHFs9{Y-KDIR=UhY@69ChDor9 zX};IzF&zh`EUgGKa`O(Eo}%se=cd*`+-$l%pT2HRHm-YpK#{q1?(o8>U?^FHwIv@=EAr5hW0Ll&-&uhdO z|9e=UovF!y}M$Ouf{N`5bY$ty(lF z^c%EKEUUpvUS-A;u6)!6IIoGr)z1qm!=&nq&FDoMM_35wf0dj73if_l)DOp zD`&fpd~qa z)L>Pev&I`k#eJ;-eDKor4NQV9PE4C9o+cXsZNd@}y*&E}cu=Xf(m}_^#Q}oAPAm}5 zr%Etdx7%y;=a zr1m~BLc+3YvMm`xXmm%Kd{jl5+%_sOBia2bowm#KsRFGztrkq>;X?bPP5})8E2iXY z5;^0K&UTOKl6J^AIrI)Sswpp&ht zzfprlsqcP!w>iGbS!Gx7Z{XPLv`4N^1FH1K^hAinE~fETQCTt2?D@_0bN0EQruqMsxHT=T zo;J0)X^xdEjh7%#%dMu4XM|M<3!S}Ki05fQ{+Mx*T}VWJRN)qsnlo+2=n(oc!l z$%Iuc86BbtGAI>Rl_8umxza_UqSB6)tRd)>62PS$CyBLMlH@2!VmRSxsumqCsiZhq zQ%SLS3*s@zz-A{oA?{kG*R1E#Jt?It!&x;4%788{M!^n_Q2Fk$;yCTef!oGxy`Ju0CPZ$zqUwLVw^nux&>>e!p8nVEFN4`w}t*Dp2NF2$HH+!qX0&h zR@UN_18Slee1!Oz(d+;R4IN^bZBUJq7Qb^iti>PsS@hjnI=&$H9;&USmY{~d9M6Kh zVL~CI*&TF5{CpQRci?^m3>glov5)qTeQMlA`xn$ljVb%pm~tI78L1tdql4lunhErr2+zD=?OYHq}o=YDhPHss| zXSD}RA38C0wg>n(*0WINQ5&>=n3Wjv9OH@%hjZE`rkArY1v&KT6b$V$`>fCKA+`%& zz}=1MxNfF@hg>gGs-rH*w)BWUpTWhkhA|;>^sl?5i82ik0O4qL#*ci?qI^Y0g(h!i z%EKC8=6^(ogYZ4@LKiD8SaSRIaV_%c53PaHwfO^{%X9_p=DZRYy2tO}&^va} zu8r8Of>nv6nGk2}chSY|0sRqdn*8+#rx4M_?X|3IP~0y<1Pm8&rPxJxhU+pjg&oL4 zK;zFw*R9+FNv}^YI-5DfJxBg>zla)Zz`EAPs%E8?N59bJ?S0cYM9?jBzb!w~cF%91 zW#rC`;gV#(>8B$gwo!t)g>s9CV4;dR`;Y2Vi=HJ*7OXNL@Y8^>ERy{rr&8(PR0;6= zK!6ZQNP++l-?xbzI2X}l=p3f+s_aObr~afzRLWf@r}6_QD8&WSb1EJ0x#Al|55bjC z5v35)!%mFXbn!3d=FRP}mzVr{eWDy}tf$Egql??Oz#Tng_-^7ZFcrD;5_)WG-mBVo zi3bRs5Yu-8IsrMMImj{9Z=u(OyfLU>8VkIteaqf~Q-Cc`$Dy~z(0En*uDzGpEB38Y zB_PcClX1-wWW>HQeek^Ci9 zd$mDb)J31!vLjU)OU~#r(}Y7tWT<286u+UQ$lIYef&T&C23=kWJ&y~ON9;mv#GzM$ zQww4Wm+7Y^g{)UXzgiL0$S33nXo?TzgB&~6dG^aaqF*i#yb>*f!=(!ShG1k;f?7L_ z&~ozMS;}^RGTFeu_oIVw+N%QqlC(Tln^afaOvGtHA#)8G(g<<-VXVyUWwMWZLLL3p z?g{f6Br55?=3>X=q5z!ZtvyE_`?@d2`A%lh>!EBOH~K&8wxBE4*9G$~*XY}VQAdx_ zliN##hX-PDWd#=S_9mt-;>;@c-`zw1o?JIZ>A^?c7)-@`({EQEYnSz(wRQmycwY`K z4*JHgK-%Nd*J6Bp2h83H1p-a>Y?yowKk5zjJnI{6~ ze$S;fZ2=B>Sa{P4>j*ZK)O9iHf}Pqw!+J{ytQG#ucCUf2 zSccn0t@?gV^crCWm zs(gfva#q)iVq^w!Hk<>H{U5rqT@H99bgp#5yAA-MR|Y*MB8!0Oeane)yrA5A4p)1n zbejyPOFwt)3+_Y5+XiewH*`Ta++}rClc(6^ZAj+zGnqQt!BYBkR%$oybm9$TOAIF^ zUO|t*vnB#J{wT!EjKe9%?l+-wa;Y~3MXImP4EKZ`oI~%eHobSI$oPa0%E6^Z zJbiF2*&sKKPM=P*8tQ>cOYIJ!wsU|#Czm^H!U?@oP5stH|xaZJ^If>(dh!Audg}dhn4X`&y_Wp}h(j0z!<8 zIyS+Tg#O3b)wrZ`KUe!*vltpP$Q|>z@EryoS(He0dLX`N)ELly-CTpm`e70hg3}It zJjdrA@e$5%Fau0iw#f|Jk}71UWlm;z(ZYAOzvvFLs>-&g4v2U5JVmO~B16vDuh>sHNm(%vJ9E{}(0B z=Pf1TH>4;*3n>Mkj;NX;Du5rZG*5g`CHV^3$?4^`pe74JOf3TP(1&ekKQf^8v?djc zr4Lw0Ix7`d11gLr`m_&rFCi_q?q2!BEj7368K24ir>OZzqyY%pt*Ge`YR+r#<8Xkn z=NeV2Xi`6m4DGu$@C8f8pEViaywNKBq7C@PT)}170s$}y_`5hW;P{280C4UX4ayq%7EToaNG-F)!JwDrPs%EyA@Nx1hb|)Aj07h51eO z*4?Sd0NEX-Y^gt^eok(|{MmE9ri%%ns4;UFG|V}zsQS4LO{X;`FFG%(ms=hzo)^qt zsGi2KAEUce^?O0y8P}CHH_SP0&cfz-#TDvS=!~QLx;T5Tq2-kNCXER17v>f<%&j|3 zjG^HcK7V0-b4%0WIfgzA;-bY1_l8&Nxk{YRa=tQNU3kRA`il9EW2ZUDD0Q49vCMIb zF3(NMZXLO_%jo3DFo~W^6|ll0_Gu~99ypG`ScA>(^jz5V6>xW4UWyo0 zesShhfgo|P;5g&fCBKeLJPuKEF6h=s8K3jLn~{I8Guux%ee4m!$ld{ zz*K<&Qhbk=%FWDIGu+H5EgGUPP}?rf`QWw4-&~_SZHlPL#};!8YBtRj`ayM#cMw7_ z3T=?Mdj~Qt+Lhs)=XQxu*3U2u(@|e1gLM$+d};+l=R+~4?kN*%Ffwn&Ucd0dh z?)iILyO_m$2hwqyy@Qlh?dYq5DM|O6{bTwNPYkZmWfS0I`LB)E>|wG2NAl5nqI! zhPCV_u{n)6tYI_`G4H0$j2JFsxaGzKacu!?8{LZhu+oKiIz%c{A7H;~3lxlDKDv$R zCxF7Y9%?Druo_sr}=rNa{+s|Eua)^m?9{Lmk-gav+%yBIGLusleR zVLtxm7X(ikq>7mqS_Homu zfp&x|LQ;sR6Nrk~Rx54cZJcF41J1aMsVT_NR47WS@x*;i%&?cIzmFLqmBqhsVZh?i zhPO3N323gwW>IT$4*VQopel;6yxd9fO+hTWBl>DAZf8Q|ryL?a!zqEj8O72|d4@YU zofa@v#DwT&GGICBaD+~44n)d=C}kMQ5GxPkf@Rmm;cCuD2zKMbw6&_5#3Y&)25zjh zkG=n-r|!Y@G(>m2j{O=AYcb_Q8FWnF5Y=h(Tevv*c;hGszk5H+)8NaY62)Y2tsl+Y zbaQofT2vins&U2ep+r5yDp7m>x2^LPUFR(H7`hz$$SoPD6tOX4Oxj_FcazbU&iIxs zyH4)_`3#&NW>4ew7|w!Mju@wrcd$~mdF2fKnRhUYRxmI7RR}RtHKQrH2#$&+-DqtA z#*yVvrv;c6!0|^+OTaEwk4yYsWR7%L3?27!ypn?5D&i1l3atpdauRE3#5F#c-ZrH# zP^B@+^O!CuJptCm(`W+p<*4r#4!vWT&T8-DPLTVlvzc>%oY}rbL3RPcc**5+^n6?3 z9ZWYtx`mNY>yQyjq+0YrpM&YWcQD;4IU+TR=%C2^9CixtV7e7aJ|5f2TZA_P#?%Di zErS1rOv~`M`K1s?po~A}2{57qWxi)m#^*YQCjsKpV_{RO+aEC2x66SAL5x!c6_yuI zpX507%4u7hw2^+{Sk3Q@8G5KcN6>!pI-Vu3*!ty|C|Fi}lZZzs)r|?(Ztwe(Pt+w3 zBg8S2BLV}8d5p8-bFkk$jA=f)9UE~g9JD1%hMGH!xOdQ?`8;_z95-+^<~eE~$WVI+ zwOhOR>Hn&0ET`j;wF_MoiZoGztx4QOeTa>iyrp0n`Z43BF}cX|vA4@hQs7R+pdo^I zI|b)39Iw&@7!~-n9Ks4DN6Q0OTMLeSR})c6T{yRRJy%6&6^1=SZPvto#z{8gV_Fc9 zQIA?7C!~;QBTD*MiPVm9Q?p@UqXvjI4y{4Q4Zz9NbRS}dly(gL6@j$ zLEEQsNDbCDW%9pUbZQ^M@(+1-go9d8X5ej}<6jx2!$4AE7_G%9V<|w`p0}Py<=?Y- z-&MInG^z@{dQE!(&1nfe?-aNbkn6DU!60sPU{&HX@+iS7%v~f8-fYf!CA7sUWEckL z;E;-pS4T{ZKD~zB4FVK!A83eoG}AXbJfFT^K>RAKuH+TxRo~a7`t%+OLZqW4!MizJ zhpE@8CdzIOH%#Lkcly~Ps1G_&2Kyzg++udj=|8Y&p|se8p3Zg4)vLs$>2nn_ZgeLTC)od16ce$VCPsN4uA%p7z?&ou^$1hiWyk zX?g=9GIXAgb#Y^xj8&SRftwPL-;C`A$e)E96CgJk$QA>+wGA=w(o9c6stA5nB#ved z_nvHP>&8036oPB!I#bz*sqAcn`aOesgMqwnAm^LP-Wpw*ZrZAuwz8K|8FzZ+^t;x8 zt0ON>SBj%YInyhx0SLZ~M7K4EeR!Ytuhgx7+|@HxJz|BH!2<)W--PV~kf#mg&Xv0L zkKlC~-FoogUOBxCHr-3pB@~;9GBC>$cnS79(;K^5b>*L$%6E%hR+=S7fmHs9ft*Ep z;jA{5?<%T1iOjevXDhqiRQ7_JS;MBjOG!5ty(CS$++ZIz^`YN{fo_a=U>HE!Tb9k1!p&Zoh92yT(onFLGrfV>&P*4iiwFCTWtyO0uqNmp1hfZ153(lcE(3YkKnC$# z4D#I11u$mV4a{J_eg@o7(sX{>4t9RFCGb-`;v)n;v^3yZn2kU4HGvPS@Y;9zhQM`d zjDC2Art!73rt!ivuu-O&{*=}<{?I_)G>}WqkWQml)5T-762z6$gG;wykm(NJ64^V) z5;;^Vy$IQM0W7@iL4!**75Dg>ieDh!IH2)+y4Nb|9d-o^@E!Fens0>W(FSjAig(spX@}=i2t%Zra1ty(GX$ zzi$XVM-lq2B6JS>dC#X8m*@_@vQ&5QZ_!3{@KvR{gSQ*VPYvXiCDOr6m*K`+M5R|w z-Ah1cn%*h3O6kgZmcGY8ANC!+6+C6?zuDCPjHzwF)b^Bt++rZlsrsp}w0MBqtkZV+ zDYnAb5sA}o1vf3$&3y{*MPXANz_UbX?lTCE4#>?0@~nZ}hVZ7y^LSaZ@{+A!IH_&t zM47~xD~~oy_uq-o-S~dIS!UC16J<8(?*CDXrstIjy8AD|O#yYiIze~;#Rl?61Gy5< z3n0(%goGYU60e*d#d{@Q8X?0CALX2Cd!u)f&^LOKjWlZ_HcZq+yw|Lk`q>jT5hDh2 zu7UieS>^zBO)MURWRZ;|wZ4E_G3YpYe`adC*VHy>aNlom-)A5{H;{)^ZA^DhOq!e= zHiM_xChky`=~Z~8sqIBm+fNPd?FRQ%2J%}2x!%;ab#!gorKfj+t?elt7d^v8yVe6u zx^wzh61Aoa@Wj??w|_V|AK__z}xUzr}+PiD2+{lLww zHZ*-;XzDT){KZhP)<8Zokh2X4VetFsg=O>yw_+Vv zGu3TjIx1$$+uPW1QLeXjt~py6j*m3k=9=7;;QGX^z>K5+y$f=%f3|nhd{Le?r1E@w z7fp-lTQ*N`QJ#xP(FFM%!e`(yV_V^HQN}AZvg_M-;kJgWo4E&1Zcgax*Z6K~--58p zG2Se;`0^%Wn(k7!4cq&_F;@WCBS_D_WEg(D)hWmw*B!}7js7~FFV%lnRj^cctv#qu((gBnAhv&pVkzSvgwD!3<%kH2oU zVfR-h$&af|F=rX8C(XXtZ}!Ef%`Ujf?1E1k2%evi)^Ab!BHc7<&u{B9H8w7>1f7p9 z3PvG1oP>{F_{8iAADddwG@d1(87KO+Sipcj3k@iBq%@F!iL+Fc!tZNKe z+YID719?diO1-0w70r^bgBZ|D(|eO*V?#37swf>DH@tkMg~DPo1f%r6Q}9wu8z5d_ zeBsb7M|?!cV3jDtFZvsq*S6Q9=uRh2@^~qqR*Mig+wtm1l-X?RXV_)}0VtFQ>C*y`*59L0eE9TUi`fttk zP0dqJYpOf7?v$zX8m2a$HgD>}hQic&4GZcP&70EDG;eB4L&JhOr_{|~G<`|sl*%b5 z96zt&gehOEo>DobAjVb4*ZtK38^8dnZo&UgGyDHT+t)|QQC0g^*RQfEBAuCpAn+tY zBuG?Lv}U@hC#B{gAI}doizmxxI+^ZCdV)cc3CYA{GMQ3dvaXd~T}o0)(H8ig zAPfzbmB$nCL5-M`!e|1ww!3rXaweZVu{qsaUQaW-tc&@hi4d9T#ShIe;I9+Nu5SIB zwk*=KqHC2g^_*;}VrN}tv=+x!lkgh1WHjO^3?UWr0(#Kg;WA9ZtXCJw0#SPp<9aVU)aU)T{-a9!?9hir2}?^P1=xUgzaK!5NC3w9!MK*Gd3{#1dddIceWvbLl1l z)_71TDvu;_2U?H~3iLK5q_2!GeC-cnHQ^=Z3}v_n6TL_;h>zYfoFV}hc+Ub}+o6wF zuD)y+e}`g%^G^O8v;Ew;AtB%!4sF8*=Ewrc%OW9uxq?Npxuyu3WL@J$`y_*%7s?!z zyah5xIcqwDQ1-jI8{%!f+6(&p#v31ZY6F&PVPjJFgIafu1;6EHk&RfG#qlFp<7G(G zfSU>I_ab$eVP)Aag$Xd%*>v%0rzsil>p9w9_q&r87vCdXJZsw1N8Yfl?dHu_IJPt- zG=orQ)bW^RZse%?EVw2bb5{<{AyM4v(vTOslTZj39FSIYu|yXq(~eX*nHL|QD{CN- z_{gkKN!WNu**GAJ$Zvl3NJ}8ksLesvR5`%M>OG$<;796nv3x;Avsp$QrqHYRF$p)adn{BQyxx zZ&jSM0;t+}xIaf%zL8AgKcikBv@02uHrWt}|Bnv)LkCduifXe#NJW3q0^%gB&CybV zev5d_s^I=ouGJhm?GgOcD$*hjS9+S}j*{o3c_r=GypQJ0qU$J=n89-=@5p6HYT8+9 zA7Mt`ll?i;e@7}rCvzX{_sUKhcQ*VU)d7W5Lt#)(PF65P?;$HBcD^qtvR4<8!;Hs( zn@cR=;nWe+e)1y_x0$rx6NtZ{&85kLlWs`zyj8g-Ll9v~t~OyzJxU7NZQ7wr6R&Z} zU$QEc2u`tebIT@WW&(NUPHmOnsLb-+6LnyyO$}p>ppV=6F}#G&~UB4)Blu(PImJTb+~8Z4Sg=n04{WQN;|TDw{Jju%U#> zQrv5EOS#a+-;YYW->TsB5O%Q@Jq*a8MknMnUYe-pv+L@ns_ogLGnhStU6YDz*w_2m zon$eN4VtT99=~YUoix6pk6xz`$HrEMj5uCRKWl<^)-kUC z1?N@OF%oG7_nc#D`aAh8F21v#7Ef)IiKuF9tW64@j9BNP#co zva`M0mGGKi!2~p&UnTpM_3TT2TwGGd`|AWJPUE3e7!Bd~R*YF$P0X948aJ;>iCq(X zYJyrWsd8cW`TEicUVox0xWW^h9SZ&*W7~(cKJ`WIwladZDOU;qf=`suI^v`i1XD!| zp*wlTXI_7-j4WRtv0}0_bq(cK6I?h!&GV}$cx~d_vvAolLm2m!s=D^ns0@s1j)$pRlN^3PnwD-UKDKS7~Th6GDPE$_AZs3#!I)*AIn| zxtOEqRFvY%nLA45208dpsvhy@*Ouux7vC+FvBxUnyGqwsiak~ZZ#hDVM%^hX@U~JJ zBUVwdmpBmO0!NYpx0cFy)+%D_Q3XD0Rov-{)1|Bbj*^FLTy)LY#vaqUmGStwOA8yv zH9>ck^46!BsVZkWl(tWla(P_N&_r)nREd_AAM2y~v8k}Ms*pMZW`fo^F&*2UiS71b zoK?hpcH9{E&Z^kwVE*WYZ(L+6f(vsj3Agp-Ff=J#DET0ugT&8`$H;!WB7S?p4)EDy zMxfwhRSM;A^m()#PN6|s0c{il)s z#iJ9xaq%#hhlfh01z+vU;iqhY&lWhgi;dLm1ou+*Jvu4OqHY@XH(Fq6qY4u=YOXfw zx;{@9a|O0%Dru(|Q5a9!+v7%krO*2xDyEMJ4y;ccPxs!DXfDslbTxUkXC5Gya58>; z`z)MzI7^*7RV3#08V@}lq)78*&H{zARTp*p1Bb+4+}g1at8zFY%(h}xnKsqYB6|-m z_i$o}D{?ro$feiC<-<6sh|6<0si>~jx;RtQTUXZT0!I7s+XJ_`(3;6oiAjfrcB&?J z$15$YXG%D@jp+jWjMo;n>7%Bwy&zbT6KXfSiem`D2)}#Ywy4EZae`|76zpn8%VF`n zQ zjb-3z%1&mvy~hhSM{K8hN#%7&wSv2yG=8AvJl>O&78$1oIBC?S?o7omdUCB?4UH+P z*5NiIm*~1r+!M9jp>^}XcFu2l3y>nDD-;{c$_bmcCyCz|8JKGPOI>+|`^X%PtRtrG z97|2x{{gv1Xa+k-m0l4O+@ZuE`>GvD$=i6bPQVYFn;m<`LD?WR^d;^HXVjyl(?GP@ z!Qkq+!ot`a+P_k>+fIdDT967ikW@^nyX<=%LC(nvZRZO;hQ7qBc~z&lgDSU)BCDXz zmx6V4l0s@k{JX^`vVQ~yH*{}AOPWxO>?WFIFBC=#84%3YwHHfsbm;PBkP=-NTZ1V` z9(PcGhyfYq-Io;ihe^|MtJ1{n9wyNPZiZl|qOGI_!^ZG`I>*G2_|6P*aZ^7P=Ph=~ zHb@u*K|tU#YTmwp-bXsoh6QJaam10i_a{~Ub<%XuC5IH&OqGINR>hftgH(eWsy%ze znSuYWie^+@GpobhrJUA=cZBo>Sv5={HOFX>G@=wtsl+!@=YBt_c~o~;V=|`3^4v{ZRL*`HqwAn`kI-)-nqcStFe&|hrj!b$hLp9drH!+) zO?-}bG%*MQw{1o7Uy}lzvsQX&Dsqf+a9qeOn6|Tt=RdQ;*l)^I%fHOMher;9e)VOs zatxPLS&<8`$ltiAN{f(gE?01qa*!3BQ`c0VE;EEj!INx80?yJ6IJ20n?ic4c zdO2gHRfzP~Sl(xHcuJP%&c!^b_myGUKvOtF7P=*1M?>J#=+4`HbOo6jU<{-~70 zN5U+(Zup><=wn+c@dLeTXTs|Wtuth^I(Tq$QlMe$T2JuoaXi?{o9=3B6<>B}BWPu+ z)5<4Yj4!x(tHEkoVlq2r3uLPnx18O=NYHL7z8o}!1rAkh$M=czb{jtJfp zI#cLr#_tA9k-tv~3D-!F%d2Apuz3$BMeWm~#FuUH%j?HpZc*X8Sz5UvG2_TB1Z{z? z!@i(tM0_mB37R=<_K;w#uqFbrgr8QF(u@REnF*^fc5QQYPXq3hkag!q#2GCLYqP(OE`6 zJ?&hRgsmhLbaXW1&EW@C zE;WhC^L>uGL zkHXj-rde6E1!7L^0KxMn)lJNU-YNl?d2D`A4;*kE_W)V>6%Q&$t%R6XrbmR`IsrlS z&Gqp@Oc0}5`E=4%g0_@KSvPz&&FhZlWi94H)MUoIG?>>)gxV^CceHK;K5Yfw?qG)^zD<{ zw~V{OivsguH&Zdf2ML&Tu_wsT9LYeyneay{pWdyUxReZlM=0fw=7f^n7o49{_}n>% zlb-L(P1G30f<86say^K|+qKk=VZ1Y>n|jpSfy#S(22zY#&H2jiH}2?r+DMK)dsKXT zCYpE;qSf9LqAyq1LyM!v8pHO832eb=BtG6(=j;`Ha+E6+8^BgkQfyPf;0tCcU5wF5 zlmH4CLfV>ko@3g%2?YdJ-F$^RU~ zf#hOMVL?ueBd0y%5CuB79jwl`XJDqG0*k7cReU`n{;wxK&Wt!dpY@RpU+`JVC!AZy zr}>cMW_F$acWpvP>mv6!tfa+M^7_1;%j^i0TNkmFnW&ZLYX!T^K>eW(Th=m%jr?IL zxj;DMnPhNQ9V+!5>TJm_=7(a0gThEqvVgAQW;A@@fCA2DGi^Mr#K|EK4!KC=QOY@K z!EG7)4YAFJ`r&qmy8!kNY$f%C$BE!LK=8hig3H4j|L z^@oy*3mNZ|jGm!t0I>?}nTPdtK)(5A^3Z8QADTQyF+oqxWS9|Upm8*JIc7Y1io8Il z?@6IblwXlBloEHsbN42czr<)py7>Bn|C1A6!5xLyZa!nqaD{6rii-=%)Zq&F2-jED z8A-B>IYn8-;;Gyk^$$}ivM47A12CX_$N@oLm482@`^i0);?L)WG8Fr)P`KqQNFi!P z)IN3Q@+Ba&CNm$;;in?f3xc4nHE6>I4rXRNY=tP7;L2&BYx&5G(YSYu9N2h( zNgijT1dP4sOBVO@XCMX25nr%`RH)>cUYvvP8NSrX#EH`cQT#vMo9YeS>S5N+4|TrZ z^>d3*5Fg)6RRqtmHp#O$_z_4Mhc2EZ`MPg&Ldny5VYP>8VQa?2rcxRIW%2OGu=i=5SSIEKyX!T<*ZMu< zH7s!Jq|QvMblqrzeICU!bqbms@V|ZOA(k*;3>eIbVu$#zw%yehs8nYLUISA%<)(Q` zQH-XHX+`m6)EwPV#T^tsGx+Pm#^AL;PacbfZO42;w^!h*Y0TES61(>dioY$O8GE1z zvLuI}76ON2t=EbkuLWQ5u&dvr+Cg9t_ByXgd0HvdPYIqVL`&h!8b6F^1WzGxfX*hk zvP!%PV!`_OK;mL!{WXzze~uJ-4oK}vJx+0XnMF3Rq4af+b^<%cGG0r4jo?Dp^$j~s z_?unlk8ArVT>QXk!pnAOo_OYmET)8$k%kGw9^qZEueZX+ID|Mr5PYkT?&(e9;&(Qo zE9^7SNi0d6Rty4e_k7|0*3d})aRpzZXIzxEQ7Jbir92;(vTRg*y8ruvlUOiIz-Lue z*lASb^E|S~^!BencOBv>Ii(zofN2X3Bk_WC98Jx<#b11w~Xx4sQ*Epe-72ICRQNdST{~4`WheERfI$&2km6UiV zOXRwYTXfrq=QP6t-q_`Zn^RH}@&1=dwjpMVRAg0@?b(<=n%UUip2=jmq<%6v>Uilu zGBUqR!yjiFZtSZ%O?bf$V@=}RgGrh%IY!kW$vjKW6QMK^|FyOj&R!BF$=R}XD9LxN zF$f7r>tMTT6K=Dv*){Qr!^KN`U+lTBl2dj)5*L~xFTUH5OKbxbrc?x}AAtC&bBd2u&k zC6`7wADE@rp79DkUl#xBw)P;SK~VBC#1Ns;LF`^cblWtqS4d&@FxkZaXe|nNRt9oI zu!ur{FYL1v%xnh#x=)jgQx9@p+)3}ZzgS_wA2)LEHA6K;ZHcCI7qnr?Fm)ASTqPdy z@Cb;RCg3*Jjd-KL!y6*N`Hb(Z`f|7%nnO^F)${eq*zb#I(#i(d1?Gb{p9m->iLW>=mbe9LMCKL&uI0 zyxEAzi@TP0cdmWIP4|uQ_8VJ&*!kL*k(TZ zX)Nx?{m3_%-!}$L8Sf~tEcOVPWLaPAxMH=IFXP+_VqXGja6`Zto-e%zdRAyY*RqzG zP%Ca?zb_3z8C}cjT(!6(No-li+fPLEUxnOO@JhxVkUVAFv+K*TjGH^%2oT4{eW`}X zPV>f|KhXHRF!ubB)O5+m(q_fjb4Kn{w2UCZI2(%Is!wh$7HU@FX4>c?J1=@FNl2!` zF;7+;^W>^yo?IV2nLHpzWVd~al}i1000030|CCDPrdmu%LI3~&0006pw0HrWy?wZ3 zS6L^z`&0uh9;6Pt(H5JNLVGZ3916{h!O2KT4kVC3mv%!3`bZbk&C~5ob5q^~(hNIS-Pb5~oL*l&r`WZ=`CJKwMS&7#;*6vfQ1 zcdeI2cWr%Fd1_^3?G3aqx8?x@*=zrbqUf#>uW1)W0lfW1QCw&jMN#zp{=hE$es^ts zXL-7BeQluGt(uou^GK2UpE-Wszs8^YeVeQOIeNO^d>HAQ*i9dQ<h!McFN%G3Q4~ez5u~eQ7k;1ogzOy>9Rr8ks-LOf?^`ETKfB6Pr)^G0CHI{l z;dq0c29@}?uiR`j8wR$X~6-8dhhhY2Y7)}shN3-AZm z*3T;sHr@kxQ<|bk@Wqc2erP{BUF5^cF{8x0PwP2!_`!-na~{;+#y#kt3cLcTJQVcf z(Hv*1^=xl*zw7t6($C&D_xs!2A8d1fxXt~M-`{F}lWp$L{Qg$+oBRECSl7m5b|igi zolE|1h2Qr3xqXtH&;9;Z^f!Nk{deN{kFUn>{v_{jHGWU`gWNP8x{}84pLhTG1HV6` z*unm;{B2Pj^XuME6TcPu@2;)yDNi(3C6{~36EHLz_`SZFOyHjK1Pqibfb@9q7>?6l z9H&|K8|TUnHp@PYh@r|q%^yp=X5e3U&GeWia|3PJ@73~Z*x%!6+~Q}6-(AGFaSP3_ zS@s>PC^nog-SdezS)_Mwd91Ndcy_T7F5rcZvIE(b=FhR8%#NGi#+Wu&`xE@o;rom9 z+Iny(M-YFC$Fcw6BD~En(b+8fFe@8t*2MZ9*18OTp7`@cd>ikbnwpmMcu^F$7_miB z3@#*IXOTa<%ab%=8*A3S+DsvvnC(&%-j=^=4R@C(<=S_b5de;XumLUkegTs~@>i2z zAe}uUy0W{?vJd}#M0{vHo^Z>2T)D|7(JcEbV{2bkRCaybA>Lq-e+IjRXQH9;+D{GBUu<@=aisGOVUlhgkBI4EX{o4jh{$P#ST=`=4b6^`Fj#Tq#K8gMI7T426{GjAu zZ+UFxL)P0>6<+@m;$`vGYI*g_z(A>=P-~lhnt$iX#EW*>^?F+2L2QrMyv4WvQ;0WR z9xlp}aM2QCPP4dz#(u@?@hY zmJ8-kn&+=@yeuwUEw7eQSS_7I5G1=fcf3q4Y@J3J3|y(?Z*qj=Wbh{96Q7s0A&Vvt z(!b$zh_~z)R%R6*#BXEGx;Rz++PDyu@OOY5t^Ca~_FFsv(}JJ&JR$ihOB!w#ciUwb z7L#m9{=ECE>?gAiw#IikC9CDtG6eSXduo3U!U13(CXF+^g5z`-^};&*Ow}PKNfVIR zDj&_C#UB0Y%ar&sdmhKh^vLYe&cMyah7@UUc?^82)rU#rjIZK2XU~_w|G=)mfBY)`D?TY+z;XWn@_wXr*lgWt_Kgt&3|67pfPn6F+?UQ(gj#bwgt3(89{ zl+!Ca*!@lRlku}z_N6h+vJZY{@s8tV@s8>_NA?=AMNza~%Kl^ix^hF6-;VwM+<+^) zZ*dJm0FGMasiT*%zZmDPoGFUy4cW$fIq$a@<;?Q&_Nn0+}i&w;?>Y8KPG0w#3g%iBk_u$M_%_3 z<9C*)8!e5!v-E-$s#KM|7`>ML#JraMG;RyH=+g+$e8)EtZ)}TJty{*^gQn7kmg1n^ z>xq}ugN(cz4Vd*b>6ZNrmK~P-PJWyC^Ptz8YwMGu>@?aU7*{vNE3LWRS-IN>vI~u5 z?Fbax5Qhy8g7%Aoe!hY9G=n~EuC0H2M|u6VjWLY%%0)#{Y#Nh_qL{pi@R|R!7)LJ6 z+R9n1z2W3qTk+xE@)%{)mb}l=lG*AzSr;Wk*PheJV>;ai<3OfexG>rAkU7s8(iE#5pd-? zW5$_^|I;z?vUuFWc*H-~7*m40&;N+I0_`&?cOQkI4U@_UO@;sY_f_rPrAnYj#Z+VXVYUjPAueUgk+4W1bu&X5# zITBk%!4@n^53GHg^I^4Q&|t1rdOvzU$BA_~&lj;Y>>C0Dy_fy$eSmm*9a?$A*zZTb z<^4kbjRyM@9qkXYzkblWF6t_c6OtEml1L)1s{F#}BgD(8QXJ*|;EHn|A(W>fN^k^_HjT&}4t@;M;ZHyfJ)u^y!USGT?u--n+fUuyJIF0IC9 z9ZvGp{WS5jI<(biQpjz3g*6X_%+NHml-8;L*X%Egi!7B%@r5+O7E0^i`y1kIRgdp) zabNYuf#1*K?_K37tnFdhoteXB`etSjE|A($>1Y3L(zP7-P{*mw6@2Tz5kAVX*KLs4 z(wbE8#pehg^Du5nHIBiqT}(mNJo_`kZ?zAod5#?}tB;yKQueT0UF|QnKVRGLuJRP( zvHfZMdbrh+LG&TPjkkeQ9ewU_nO>*$pnsA2t(GhVRQtq}FOcr<6<>4?zexOLxq%-z zBDncBaI(+cFR{NYJ~0)NK3ZFGU9;><(VJx-m3B&q^rHV|_P3Q@4E+99`(UGO?oYP4 zKilSh@fFh7rkJ9BW(Gm@xBdQB^mVtn-{0o`aEtp|x3S;P)^}&ga|lNT*Sv@G$lxS! zNe=qkz)23K4wu>G#VsLN^I%EYL}+HV(f-um@T;URlXLB-5c7WeQyd|G+WQ*&%j}xf zwW08|K?qa#8y&)H{vGkMxOi7dSfYUr%CYbUj+f<~=4U4Ih0U@L9WhMVvEesr$20o? zqX6Q9KP-J4 z{eRhimM54EMKmk97JT=+X9q93rVig)%%dz`d5`kfJ>>}qkGV|Ed;ZTHXS;c~?;~6m zZK?$!UTcZ3#y`NpXwAW%kqT5*CwdCEu>Sv!lr`jvh>U;`$n6CC3xLzno___FxrU z@CDyoWq+|=Ng5jG0-hK9CHLKj5Plr$u%7pw5d692`&tj96s`y9jEgSI!3~uk?!tKezU#C@tTYJZS`i%Z3BC% zjoT#Vc0&tjZAj7nDqinAoc%?=&OTP&Y4%(GNPI4SknmYNXkDwkv%BOW4NP6@+4>>k zWp>Z_1oF%l*`LN4Ieg?3{Z^_oD%LbHOM2Gb!*Mdb*j1i_6-aUV$l+?%9eE9z50}g) z*5PofGmL+js2pef-f0A#lQ~PJwPdq2nI8vR*8=lYm zQI75Wtng=B+}Hft|BCpTo?2ZErC}C)U^1`*;$!z;6Aw<%<4v#M!T$m{>{E7sj__Ii z&f<-D?k^cPRUs)=MWVO)IQEl`YjK5Yf4fS?c3`FE+5CCp%@*a$>V4nAs&BCP$E}Pb z32***;$``V?NO-ywzoWnC57CG?~@MkBAqMuRr8qqBJXGXuy`|Bg?PeDd>>pyyx6bT zk6~A3hegL=gYePstXx^qG57mf+-r4R&ClAfbtd5IOr3i?6aE+f>(d1(_uRr#xkawI zvn3(9B_z2l2?@C~%vO<0Zbj~wC82W7T`miAzbxc_8FQKY%w;ya{r3I+{`vjm;m`MD z+vEK{uh;8&&S_wLJAt`^++fHnn-gseMFAvJF!lT9+loak>3j{XM|99@XwSy7-qW77 zH^@s+d!s5+j1I2Iy_`yJUoaH`9$Yz}EF8>%K_<{9NfLwNF$itwN^>ReCbipy!b@u{ zsb=gfrU_Vx0&5h~ZTDYlwKBVhfAoEH(E}M$G-2tCEFI!XQ3ClY9a4u@BuOx9^YRT< zw4Pt#X`QF9-f4X|8*OMmP0UywFf^J0t7%E!N&^9ac@Vp~GLhvsB%Po}ulXU^7x{{y zPxFF0ku*10_acGy953ATtfU2`RUc)aE^zO0!UH9?By z>Jy35Dclwt1qQgME*rwlLraGC>p67oS~%Q?FqYTdc?tYE>6@Oql4v5`%uZnEg7w>b z&Q9zn|1|Hm$T8{&rw2d%b@-E`Z;S!UHDn?sGr41H>p96u{R&{Lfwr+0L{)^ zrj1meZ#|_nbDm?duv& zL!sWMM;vbc2SLdLWr+u4{QA8Qyg8$Q%RfYm~qViOut#( zcyj~1}F&eU>G#yHUFyLywzc9d7@ed%Jd z=#cw9ATFakb+g-+wSs0JWnWL(yA^Y8GNf@;OLqeIygji#>I_^y1!^iP-;Fzb{|~HD zBItv;uT?l71mW78Z8gR}trUkIB3{@GW)-XjZ_9Ocx%8A4nD`|Sy)<<;1HC$|oL*Vm z1|~R}XnkdraNZB9b(DyZEB*p0JJP~-9l%A3w7_4k$-#RZT^SH{rE#jR!yd_bUpa%WkaM{R?&i;<9y(#CF&3%#b zXbiIiA&`~-lRqqV?>|oO;o^|cTyJ?x%4$_a@?m{G{&#OWej*lP375l<(+V)j^w+Bj zKf|o?iKt|>iQjyw7NKJQU)p9RL}lsVcl|`o>hX2J%aBaaqw_qNp$Si20R-YtzqKD_ zk3T7g(6blx&D;ROaoWUcyoll>4of%6#;>+NYH%N=49~Ixw$Aa@8Ul!eM@C_aO=){d zm4`>RDDUbhW|~NMO9hZIBigZm$-b@T-Z4$J3C!H{jxB~5VNGwC)`C|edu!i2w-s`= zi~`_Mh-V}q_AL0VrVykNVP4>j40`gJqN6B}qf1OOZg>t*%Pr9OX)FO1Lx*EER6_}Z zP${t&-GbsL=kbzG{4rDMHv!HBHjmadxGCu+$kAMn=l%VY*IsYOb>xtm&3Pm{m~#-O z*`UO6?rY!Xi>$E%a?-*8?A1DJsQBm8VG1WP`2W2(hSE0>hq$3Lao}>$byy;>o#XE=6j zJu(Ur6SlrKbJ8li8l+&(xA@Z!ZyxUn#cIW9Q>6(mj@hB#{>|JiDoA%Q!em;0poQx^ z#8rM&I2$4UIuo9-e2=kp!r7h*^KhbRi!SgN^-r2DSUGz62#NB?YF^dWWoQs;pkp1) z8NyfY|6IYWK>{*eNO>C`VGAK@uE^<7{jIC5$$F|jR_8YK2)0Z$@;!83*i8-_1Ttum zQ>bZKROa?N9i^<*XNglm#h)xciXmU0y0E1(FDynBx(iqghqullXEScIP^@&cLP&z@ zJYvQ2JKzw|08B;G?v|ZT^8;$GmP5MK71^t-hf-<7H=2p++wffu%nRoL?}*yMT*m?b z!Dz6m{FJA9P+UKb-ZAoFBV@{=4KI1(27}i=k@|J8^WL;rh)sGREe#_~y-rEoT7$8g zE(&@xwRm!o^GKL}R?^@= zaOav+!gU$AQ|xW$OY|?~h$u8RZ4mHGE`3b{gMFA^2cwA#B z^+#wR4WZLSL1(^7lq*dHO~0Z#ca`92Yp^X6nlZ(r{b8umpp^e*{rm1 zvs3ESpIW$>oJ39;DoUaWZh)m6vP}Ct4*4Z4{L0jAdB*~ZXK*dK%twZC{xT0rj_Y&{ z4s;LAp6?&7BUT~(VsmKFLD5nDaQ^@9cIsLMew-z5Bv&Ei#FRoS5({I(*%0Lo?bnm~ zar2=!mKw4fR$%MCvm2G$4}T5AR2a{c&qT``8*w52Mg?4`lHqun-$}254p8@x+7+xk zuoTg!sCZnu4o8aIR&IRGg(rGGgeoqT2yX`{T(UT7MefKSP?Bd0{xr)9dVeJnoS3U#(sWyW;=?yiq`Y{7o{lYVZ zM3sx-;%d={WubK1ab;+*s60?=1NUUT$Kj(Zjw#S#Dq(UAbIP@5+LzCS`&;VGZy>6n zEEDn7QXxNF`eQ~LQ+vj-IUv@{N+)8=k?W!~9m1v5%w?82hMjF&YRogIQY_V`?VNQvj&u#{ zio5Lv1>WD>`q5IsxYyxManK$ZONb%QDXb36{*2g>BP-u&6duuS%cQx&rnWz=*ihQ< zv%XjV9RFnpY?p=BYWA4924*FFeYukaK9C7TYGMxX*SlKy6s18fiMWV($&WrSfo?r5$ z%AFKH_-mAT=035LP#pl)!dl$U;;c!k`aOPt?phUT01RiGv`N@xq6O3+7odB_8fj)& zSN!QJ(s^9QlkRsHV%&~$JJxMu5nV<@DXMoyFR???e#va;*n^%QUW4^2i^|%Dm=7oG^v8?tzQ8 z_FPh=$zbR@a`JENRKN1i|)G`hi z(u{@T>~P3O(F$T(u`Q26K48wl9$6^u0~*WhSKd>;>>p~}yu|w{PY8sdxduMbmF6F+ z{hMC$HRX#|{`T~tFp-zm+23$eGdCAY)B7mZAK?P1_T`_aNwC>K|2CyVl#&XlkMW<< zM!Sxfo3)ryA!EpMiQ2Z}cF5Bq5P!r`I$3mldiNHYc#}D(UcGQDHlr_bnj58nP2CLJ z!CZw7dP0Gd9!I9EhyFEPX18xe>MaJb+kT)=n@sx*Y2prnEb*Qs#k+O*(bjy3-$LMb z*u5wLg;8j8MXj`RDH5m{6Yg0iM6OyM8k_V8i|8-mcY*ah}H zGZ|WP9$-<}rlv5oPB-b*r;?qSNSKl+xovc!9f-ls5d%N8Sc?EN&ab3p|lU^vM5F z;X2ItBcgDayR&D(^nEi-gPpJaaJy~pWfIL_lfCCTlUw;#;|66RoU^Aql(RtXtgXPp z54P(j7gElRZ%8sULC>!2bdVxM6iQ?4T0%{!BX4`8F|BL_LkF?#<-eQ;^k>A6rL_xq z=55%JNqgSrfqLIkMmyc%PLA&5y{919_ulUWaKot>uEyc=D97}3DJL&*K*dYsg&ylk zAv}aMhaAYi1ni(IH#@&Ym+z_z3<*UThH=9ESO_lj?F)=@x;qRR&3LAj;q=;+$CljQ zZZr-5hMDASI!B1?J1@XdL}+HbCyj9O#!#PY2iDz*moZj(gHIp#Lw5gH-m({ zjuSZI;Z?W4-n0eFg$#sht+JXcl=(1t2lf&Jf^TR=!`mrY3z<;ITdkId&JpqSVK9%v zj0Zy9ON$&`Tp)2$l4M)6P{sb5_{ixWqf$hw=6X8kH?4wmty6r@cHikGx)U4FbURU; zJTY~(oEZvYCy^q}6ypMDzD>3XvwNC@@;k|mP8P>}ClMU)~fl&~SXl+q`c zwyn(J$0||i^~r%pqm*;q9JW>nQ}m}Ff1lOg37cZbQP9Js_CKpRozghz&OR|_g4=u+ zY9J$Of{z-!ucpo>dCDg2&Ub3aUIPlf5a3PJUOkgOC%tDHyIAhH`Txexzn z9Vg=!1Xz(RKBt>B^0CS>@qR++gd zkAt1zidKCd2ePOEYVx$JZp_W*iz8Oh@unDvA7U_{(*~)J1oIx>@A@o~rD+10udK$H z=BMry>hATiAg9Q}glh7Y{7g4*LEp|Xou_c477eGefZNDnimLxrp?`k%TWTA6TUSs6 z+of?io9^)d)<2`xN8g6k;}EM^YAoZ0ht$!I#!eq@h2_Zd5>iD=bdylGZZ-*n;047W z1gDk!JkTwEqnH#^N)D#C@uTg*UZ?^~>D|8R!W$4R@RQoV4w%CDx0q7~4gll%DMD@+U-UU` zPyiu4Cc?MD}8E3^!(op@pgeyrj1D!)P(Xg9qB(%oE*2~e2j1IcEr$H za&S99(^=U4?+vQ|Lf*bh>aBdT_S5kONzUs|wIM{BLf56plw%cSvo$_n)4@&M6j+xm z-t4h$K`;!1MUgI$w+6AxedS;B#16fp zf8(1}O%IPkcZfW0@yx1Dtj>^`R)ZQ%2%FqV`2z)=eDnIDYZRWW);cW9dAD({>I1!f zdCG!08#YhAmB&;8*T93=ix%KfShI3s=tu}v1q4h%HLS^#!7&iKyk2Zv-j`Wk!Nf6! zK}XMV{utakG-uj~FN=c(d3>)&#d#lceeLv+6ai)!(^dHVcO7BdpjKc$p6OlWwD$v^ zP`*uz1Vm*U;h5Ptz2^`#J`|Gij}bcwzd*pYWmTdDsaT*&dhlsH({G`at2zCY&E$Fe zciG@;0O>Zi3(~&&D6HyfwG*pxic`ojZ)G&%iEzI8ckI4%13xSSju;(MEnfAGvWf}X zeR*Pn0t2Pe)^A@CJk~0*tp~COnTZ=P_O4O}sBhYzBl{tq@OOAn-d>iYGWc|Q_TUs^ z0tsQ3kfaY9pHDa_?+T*S{aS)GLOQ60gdkL$>C-na4=jNoIDb$4$%cPwTm0{4ogcrB z@}wW|&dKI1hC#r8_NKShYf|0ti^q*m9*S4pIan7Jce(_(1YH<=V=UlQV$_nY^QvS8 z!_U^$d$s2P$5mNe5nQ=POdppz=bEA2!`wI*V0C%wKYxj|O&fpE7cC1$6AJMD=qpps zvu|9XE$JZ_uc#A@&kQw_;Z=xCKw|_>QwQXO>a^(@3vC%bh|TDds0XIN$=Bu6%F@4v zls!GtZD~55#JG;;?yKdlR^-c-HJfG$`|NK}tz))Z-vqirU#CZOKmE)yv1nFNblm9~ zo3W}_5A;=S17@c=Q5Qk_Y30o*+y?tJcrY~rBQxzf3+mj!eqk9KL&Z}l`|uQn@RxCH zc4;xD!(0jjg>u6WTfQ96Donkdegnyhs2i=?bkuGR6O9DhSm|N*~EIaHA*}r45S9Ve6oAUji;nw)c zTn$)lX*%WsfTi*oV>-Z@U->>jZt|8pCSqE;0QsYsfA-76@LB7VX>RaO$LFiIknhFK&p9 zDCGDqE@^M-~jnBQ7GWv_( z*~200@iNXBT;s}D2rr>QOa11F7DvYFIkn1%$(8G=4)vHpdg0t`lh1Tz76wcVVaMCu z?b%(~gR&JBVD-1knUzIkJsQ$FVWSXyaW=vBC2Yt(4lx}RFMH$o!_|j+>7qhxJ_PcM z(%F*Z{_igh3oa^wRz9d$LMLD>8@*VI)Vn&7dNO?4)@C}NSH6>Uu}1KAlH7D%@btMh zQGf-6w}<)85)_8?sVY6?It51*5ruik8r<;U`^ zp1wY`9q?XVYxVw>NG0Z-*lDe^!?LBhBe9@-vHQQcq>1J!upx3tz_cjgYttW0!Lq># z&EZKHpm6xTotZIpXRe!Y4K5U;Bo=s(y2yf;)KNR9lkcj7(o&03|2J@+k+nAst*@O; z8DpCA{=Z8Ti0bcu%*X??*K?^#Jwt~>+ibb=nPS+~-JK}(b~4#h2pUnSOj`eY$dLvL zrbp3SiE80umy6wuy)Q!tu4aCdB$KR&_Hx~Yl0F;HkTNP_)Ju3h&(>s!5GWDw!N@v8 z@yG<4tL^j{q(p%WZ0DoxiNU}njp=YB(~wck+uk}>LPCu8jk8TMz+PB&SmmlZWMbIw za`z4TMc@i#!cGD)GfQ%+n@5ioEsW*eI@oc_^RHhzt+A`6UD?o2>h`4YBDDch5lS$JP<@+*TyH<{ zmJ;bKL8bW_OfXEIB0*XMa{((5J!XHe2ry8HAB&MlN%;2^uUR%(x`$IIaz%sO!e>;+ zki{FdrE+|9Y(>v6RYOen;dh8Zo4$5BE@8sx)5gT3@^^ea7Q5GUr(vJ^lxyStjc5r( z&l6*;LFlT^`bAMrBhBjzi&m(3;^&}fYsP4R$rGOGrG=6>0Yt+TQws;Vld}YhKJZoE zQ?K|r=G}N`74MwKbOo>L%A+5hm1UP3a-phYuH;X6LevRg=TdC%H%yz-U(~hC9G`xX zVb4e;DFIw{9z|q_S@g~2A!X&5IRh(&vy%z9xoSP9``zgC-tu7s9CJhDFKnCRM*pYg zz2n3GbaEa40riOui7JeC(h^^QFQe|(y;sltRPo&>DTee$=M70e{{`n$Mjk{-tVHA_ zR8alN=qv%ph8P0ZMIGjEVA-Pv4m-Aapdzg{LGs?)ds(^Yz};z0IBTB^P#w>IsiK{I^p1z0=uGU*s=Mc}WH71=VwY zOXSn!yde1$GhrLsFbL=s9pN$zXupT~L--2+<^X9G8ReQ-%AfH&6f1Zo+V+&aA+W6$ z?>oW>K)%~r^!%?#-6?K&VKSY9yYY+FORR!QKrPkK0J6sY0DF7%Z~K-W(h#oFj|H>X zXvz4_`47Zmu{BDSTq=a$fR7#gF&6+eU4phU~pk?v!%_%IN zlqMhE1T^REg<&L}KHH-Qf-0&UgOwVa#_uuq;M@w@4X=W2VS_U~*F5ztek|4wNyhA3 z9l)Q#vqV*N#!L8*`R+V|I8n4w$>hWKx}$@E0ceK%8G*kMHF7DP!y$13I~hHf6|tFt z8Ujkeq?ZiGhCPR`N8a#FBqezpWG`UvZq3^$Y_o7Xh?Kx7X)(p3mOqWy9$$1KA~p(F zg929U0`Dj2cfj;;K&INp9h`Ekp3Uy*T`Fwz2iE}U-U8a6HhV%TGo2qhenf{RGwV%o z@mTNdS%pxcoo5Wc)`SE(2tBt~?f^Se1Lru%V^99|g+#~v4o z*}-l^AV9Gs%hwPN^Ubf+82&RI0@>u>oFPnHtdiH^q~hi&+eZiaT24f0=ITQzOk|-d zrH2ryX`Tq^3fLYlu9c6(OF`?Cl!5ZkZwhUm^|g`OuH*p?aLU1BsNpP`-?HxXbuNmd z>=zm?!*{HQA9`RzSM@T}tbN4PIFz~Fve$#h_3U=v+ zDzl!{!xLu__i2#zQ9xdO0#rJfNkOFs4@>1W^NSt*r}R(@b30UnupENl$hcy*eh$GB zmH2E7S-KMa=)gz!9qmP6rY@8G3UEtxotEWe48x*GpU|Iu%6f{hR-Ot=%H-SgELU-? z>SuZVYlQN?O0OlUKPi*VY#nKPJN=hQLtZN5+L3AD%y|;RPKf#X03|a5B!x(_N-!(VS`JSH@dzG*vyU61-1dh9^&R-bqgc(G6#$K z9gF)f{aUG|y(9{(eqN58?|XfS=6SvCJ8-)+;z7^)I~>c_I-gF_2&^)2J3qA#oaRncu_DEnw`x^6BVM# z5tMcR+I`>)84{6CjjRPtciVM%GCYZ(i@44vq)ARs@G@FQ=RPErJyu{jbT`=+E&KFiSU0ySq8%yj z)XhLiwVMs|R5bdQ()o9u_ADs_AAsf49r4-@kB@IRe84?&bR1>2J*6+Z)A|*PU@RO0 z%-a%(#PrzwAkEUw5p%k1*0#oku8pfCN{)acbeKn1h9?B*D_#?SEf&a%=%SqGZ2#4^ zW3fMaV$k7E_}b-NhVXslYbrU=v8DSKGToLxU7&rra^ua+e+#M1^4+G8Q}0|%;Vt#N ze~@yf?3t!@;iC&!I)5_ft3mVtf}HB|QI3Rrcd7FEkF=Af*R+9UC z@$px4QTf8o2}FsWP@j_UCEUK2g@kSv^{JECBh~I)wF6XT3G?#e^dbuQqpubc%=Z={ z5+$kNYfgvY1liHH_lb#$V=pZHj<+-71deabO?<=Oztxrj>-x)1_(i#hbq~IW_Pe1C z3~ZD$S0U@YcB^)SWAzWXC_~$Ao5Qt*zCC=wazJLbLpka8R&f#HoASXT*;?W_)-=%CliFL!RM zpNMylJq!5&_+ZxIdh!ED9t+>59Qn?G@8f?VG##igWX~Ca{uUZg1ul6i2)ToecaZ3Gc+I6dR66CertU4q zAe}K9DNTWhMN*P-`@ZvEi$NHPYymrd%}PflQkz`nyA#&)qtEq7K#U)_eMAj^$2 zHb>*bZy;n~!e*%>60q!+tYCiCranPO4JNAPn!rJ-#}Xv!a1fiB1He_y44SKgr3*2?Z$72#>{wf5q*E1uK^ zxVqD~u|te}{}$RBv-HPRIj-?lE=%$EobFZjjJ{i^9-lrX|5JTp>5)k+yAr2>SV|6u z3H_7kZKy_?B5s-6=6=Jy@}ks)G4^KT8oV%LQ$y7j z_I-jvs&hq>H?oj-od(sW5i0>F97()8^*tkh9os#Dg_!HmL}ZHCfYi)>Nr3Wv2#-Rz z`K*|8%KFsuJd}kk-Xc-^YG&D=msDaV?IQodjd+z&%B3LbY1aFMrRLa%Jcc}-cj?tW z;=PXP;UUJ1H2i$dg>WwnUv{oskr-2|^Bmf|JpH^eKNbIREbz78uCIGDFcsvC%*s=5 zy^lYxWf!J(8grb-i#yFB4j}Kys^bdn$ruw-95;BxhX@E)87O+ZqD}b(&jn2fJyqxe>DXBN_x~&5bZH+$jh{MIbLU^ zT4n!dck3$W87{b0GYt{|JbAAbw;SYX9%Pg$JKGS-r zf7&qR^llg^zTpu}n#@JrLslVo7Q1lt9#&)?&_YCh3{ZFmJ@f&+83K`_%)iPr!2e_B z%HFgpP&Bi2z$_=%VB`?05LC}o&aUP{3oc*{!?0l(`Mm-^BHZs~bPpWzfJ%z6OFmwU zJ&w>V3ZWsdL5tDNCP0rJmp^&$tO=zN0)+I*(@T1z@ozf~LJOQ~=r?}5a|j$wEqmQN zjV)c@g^l)rp_RXu zm>YsslZJ_*RSbOHMyjvtQuEmZH1qCte?~Sv`B*?KxOI;YS)ziyb#P@@ef*a_qm|#B z@OVaGUE3kp9Q9y*Fviim7C59(g=qxVmeExhdCMRmC95fzT8)32ATFsy25{)AdY7VFZP9t;%$qjzlWG?X$Fzx@>Lk8^_R}; zE`6x@3OV&CO@lM}%EKrg#32)D>vYH6lS7$egcj_(@h$HnF4o;uu`FV-W^Jz}j&4$| z1Ee+vHWk;5R4d(tI3=oDQmH1C(6ZV>rQiw4^TafVU_*qA?z`>;)06J1LPeU&J z%Sl`KfjE;4u`w*!?{(|@oWOl+aOrsT+Z)e$L(t-K*578>Uu5;S!my^bahUXyO0!QW zANZ2qqmMb$?00St*KR=6da)YP+{Ox@kB=u=5gW}L-2d^PaIA+%q>h;eh5)+h*-Nbe zI{lN?6A+wnpVP233~7MpX69(g**FP1*2@UVq>S>yX@fSF++c2~^2n#w&D~YVH1H=u zebqVs-G$Tlaf8LlWnB%VH)O%Y_}jhPg0FZCBWZn%V6-!6;9$+)Bi|)vV8y6OXY#zO zQM}>Ji0#@bhQDJ>{reZ)YWK}4iOWXZpm@X}n4sCKSUqHRI1EdGOp`U2@gAtBdN8x7 zDD@KolzshHZ?mSb+&oq%Ct1>I2zX)hX08<1^tUFR3P{b-GLxKo&uoj}wCyLylfSJ+ zSA67Lzwj=orN22F)DFYa(fwHuK+!NzzbxB+Ky1sGUvuDcm#va1AO7SSt#2wYEhQ64 zJ@?Ab`2rv_YBu#e#(UBKl>3i3&EGDT@PH$M&6{Bgo?6qWjM8FARtD88weJJX7~q~P z5n(6xGDu@uyp_2Y5MX7>ODDOvm@7Rc@Lm;HE^B*tIWyPcyg_mNSM}f^OE7Gb6OD&K`1OUwbGj7aS>7 z!*JDfo0muQ%q@AY72w=g9y|Z{;y5S6KX;q9xTZilU-jqInBdZp{B@_toNCO4cj2!( zqPs4UX@?%DoU@cJNSU9?UEU-dC(<#Sq)v6kx&~%rr1GBRJpj71up(BuB|*KO{xD8X z9^kGE^IOK)e8&H)zm#r$fB@$0bt-a#Fuo;g&bTX8jn2rHV^eIGPG0n7;>M#csY36? zTBK&_TW#L9hLJF)yNz3tTe}PkiKl#Zc^H5=yn9I@4Vo-k5sjI8Ls&K5n$|Y`{c3S3 zN49A@NIz%N75kAP)Su^Ow0_K`w-T%^Pj^9DAYuS z2Mq815Uo!$jYd*S5wx1IZK+b;Oxh|?W5=22x7&uo$C%Hy{*za#5QD&MUGq>&Ypy;r z2?X_lJjG)t<0d)CYirp^?fnIo=`!)^cRt8>dv>`m~h!=pp-DCxz^zCs*3r9 zN%qsUGT*2x`l>EoU42V-LHVf!J1 zA!xE(CeLg2!*YO zK~a83#HFnSAp=WOQJCNYawy>aM$6k)&>3`Dqt%oYP`oYukplnD=(G4$%^QN-(Ky4= zaMLiP$?tfBaJQiP0o5o!;l7TvH0a^N{gx*RUloi)|3_=SMd62YSP^8CF?F_zI{l<1 zQlwYsh0#B;7@E4zI3niNnogun@@pog%3{t$R@$HQ8x$OQAm8u(0$OOBGH~9^EsE*2 z@~@9Q2uH79cRa`yfS;2auOB^bNJ(;~a7{(;YMddI9H=qn>@%J7L!o)$>w9+ds6p*E z+YeWD_YWQu0h5?xUSB6eqTb=f1#?m+q6zl=8gPe?lMjCtn!v)E95!X(0q&!cjxo)&vU;mh?6-Md2=uoxnkf6yE~9w;hbQ(6x|TAlo~QTy~+6hJ#oL6aIF z^`H2;3L>#(?MxK3v}78x5%IZsK>FDi^}Nb$DmTG%Gq3ckt)2imF>KZ{5fmyws3F_} z-VbiE>;8|Y+X4d@n=@qcJ0AYUN7ZW2$9gNt&7OoocvnD?qSab+7j6N-t5B*vCHT>5 znEQNpG&9ehjmsc|21;y>$C^R~CHq2;En+hg-665E-vmC)YQZ0n1TFT-Z9iXo$5g)A z#+8#g?i0Vky78`{`DZ-a2Ca8Pl z32z}PV?uOWeS%GGaze^r+9P3AdZ)kh`o_Sv3u+6ButV1Z`a`lY(Y&TtE?6lTW#3DB z2`snZ!HnHpv2l~FbMi(4$OhV7q)))4`XT9$^SN41hd!v%h1sSjM$t)C&3|9~;l@^3 zq%LejQ{KzXRVrik9ds!dmky5pQM=(o6hB?#5_7opf~z4jmOrv%W{0Wx*~dDsEfYQ<3vFMc?8EA}04za}zUglyo*b;qZ4W((#sIO*+1cOpsjk#*k)>EM6OsB4whhzWh2 zhrUDZhv%?e)Q)9?qQH1@fSI&Q+ebpNR)5A6xO;M50s{^zT3llq3~FefPEPRk=1GP! zC7zHr79%+E>JIZgz{*CUS0?!X8?D2hevpmqbSFN+XMJUNH%{B(GVX|+ejXhpINwRA z3Hwp5&#JT3yeM!l1YcSQapScVAgr42&z=Y`Ed!Hr)6~Tq$4p$oG}%;RETr|R zE2ns2qD;%0daS?!*Mv&yo2~Fj#oHY@8czI(9L%HE$3+)=i}uzqE6f3fedO}>R+AGo z*6!vZ{sQR_#9YX9ct%39e^5MScaf{L?lxR$nDx|X$F0*>o44g|x0}+1wIVgdCZI8J zi{FL0yavx*tWI&rud&xCmQuh5y<%om|3(WHG;P+QM@bnQvE1?n zD>17fAMpMyq=lJU1(v^ws+id2(CfTyUjSXLs7eB^`PpRVRhO^nq4z;E4AYiOqmQ8K z%5DVm<~?^Du)_#8+UDu}0@HWvBBW)s^2f4!@CyPZP?4PbWs{qReXA%@Xb=vAh1zN1 zUVH0?yW=t0(n-JK551xt_t;yAB=Bhb+L>uMKPCt0%=i#`^#(_}aq#P1$Os?>U_#Hm z_}0^%w>3+NbxR|ZYonLwJUD!jtjIk;#3~pT9bF5PNuI>8K4aw|5oJulpEM zbBpi=z$c#2!}b6c#>vdyTYlm#{5`KWJ)S*p_{uMF&%QLt$Mk?SxFfLts3LXntv}@z zGIWhZXnC_-8vMdjK1cFO_Ws}or!O@Vr<{B~b+l<N%Uiz^z_(@jiG^hJK`cPBZDmgr=sjj&-@Qw-vUYoc;;CFWYh z%#G~Fv>JjZM@s_C;*#fYEpc7`7J5=Ok67h*&d+*Y7=v`4eHhh5o zjsJLWXyQTd0#T3-!eYiL5$xNh&K`}?1HpflLR?juo^kK7CC?Rl`u4fU-2agyOa5DD= z(t3IFI+qwc)XrW>ktQNmQ==Q~{mPw^p4Z9%gc#BU?7^1=AK_c8r}-D9Gi%j2YOe}c z5aXkh-9RJ5Ct)4K)0sr6uy}wJG#&6XFWg5^V5D-$DyV$Hf*Fw8Gn-KGH|Lb=JNHb? zVmk^Ra72T;U^N3}Sw&ycETyBXF%XWp zpoY!XoOI}$EhAa7G%N{X*Y<)k^nDDsuM|KsMPG9AYk0o={^+G{(lV+gGgxJ4d{{bH zA+L&~-CF9-{`86Zw$zMD_l~0bXO<^Isl%Jp-Cic2SY6k75X*PZH_c3O(|4bd%C5Ov zo%aS+9PtB-;gPookA=9Ee&c%Q%=gME5`0xGW%(vRx%qui@86p(J-j|m6>V27dQn3Y z0}7_v%nIyCXhk@+Nsb6Mr|u$k{7pbkT!Ukxvyvrr^eQzhge=bvsWWXhhe|d zlWQw>wt{dgu|}_D5IlW-RfDtG?<^?%+EPjUL6w#!(7~_joBIlU;3AKjrl4g_JI^xsZ8&63f|EY35Jn-;EG4lUt002@KgF=usR0HC6J_!xWd z>C%grX?Z4WfMu>q^<_Jm$80xAYpzk~HBXv;tFuTX2A~BsM7EcHEOHRD;zsTzS*c4TnPRKO=sc;EltcBHV9jZIBvrHPkEzjld7iZpO zQSK9-N+Ptn$d|D4H|mX%>ceL2#Ujnpsjr;o#V9|14LF}ytjx{$b~`77G#g>=4VT1x zNG-oKuQ>Z@g~EiJOBICVNS^1FvC8}5ds58 zCzv8|U2$}b4A*~K?3Sri&WdGbC!#lWoB}AjbkE9nDG?UQe0Mx>c`2aJd_wu8krOkf zJ8Q!FFE7F)_gAM;D}nli{RhrfQ6X_GT&5>3Y)8}eC(E{sg@X)fZGa6ie#JZQ4`}?# zIZSCxg9i83%IqN_pzgS!t#MRN z^zq?zxa8yhW~aHvhx}(U_^uBA#8RlLsLCa6tVCZ9>2LV0B^B~*%{S&*oK>JMKtN0~ zJio=1W$B`YP$e2h4Bxq1~M!D(6LaArR69YTv5 zy!E}YMCCcd4(c8F3SozNbhS_L=uQV7`H7T;RDtGEs?-XfFdxY&I=ky^@8om z_lIAeWO9HYbTOso)~+XwIqi4?w4Unxcsi=6!1WPw$Ns*%0}I9c_x~N@6F5YJ>BNX) z3dfmHHk$Md=(^|HD&^`GHV-7WSw5UrdJCiO76lBsVoAXKerMkFt2}SkV4>D@@qC2xIa??@H{$ zm;d;&CS?D#$4{-YVaA@|ozdNbLJwn)r`p%W9aRhs5}&HLxl!-PsJ?m#d~lKH>VvnI zrz815LM+x**4Eaavy~qFbyG$j>1<8ABO-=30wNivv>@}Hx(TQ7|Mw@^W%p{W3thHM z>Qn0U`R>y*=N-=I)NBeNb0MlUhh~ny5$Be2W<=Mu9F)Po$~zi6xP}$>8xCw!UZ)_c zVRsyaj?OqBPn-!iv8seZ*!mD#M(^0Vr_KZs{JnnfH}XM?FzsQRCB(P<75tbvR&C1j z?Nu4(zmu$hf%ss{T%)nyZ`ke}U1XZ*DH-*_&aLbZt|WHSCb`9@s9hDuB5nO6R!h>z(H;^60f@x z`*z-6bIN3P2w)@H#X()B%J-zni3@j0FT`F@+g~g8B~9c>`bbcJ<|1xC{wEd@lHHz< zIX@tPy5MW@!Pb!TqZ8I>2;BK=4D2GzugiaoATF3wpR|?94jxqwhA2dxOmG0u$ysNl ziI`WKEZ78njq9wVRG89&dDi-vg>s{b)8Z; z$L+#s+3CXh!gtRk`i&A6=6eOG_p~Y|h(|4PGWFDJg22OZDo^;N%k0XZ!>&%R!1taPU2lepZvty>K zmuf%q*KxIcx%?im$?!-M!&&RF^t!WhT+49cc#JYRd_-5?2IOiucme8@ib5wB(f6gY zLbBpH0&bllVn*Ky)v z6)*iERQsh7z3mf+jO{L%FW;e5_Trmwl|5N3*JzEhtvzmb{J3A zSM309?jqI3?TeW;y=0jC_loJcrIg0GiPN_+HSR(!x}N3ED(?SX>o?frcGM5J1aqZZ z?u+(Fb>1ozKQgbcE}Z_>b5RcA+@;QslMd%heVvW|3mHG&M_Eh(f0)UX{%2!#wu@zX zdnEIx#By4q`m?E+)KdsYJ<<*2i=)rGx6<>6)L6meDyS4^5Ve0P1vZy)Fn7O^5&l=a z^~Qo~Pt1Yw;eHqWXhd=;XVbRr<(lsg=y+07Mo(yPg=ahReJGUZSEdlxV47bPn!BDP z+7<^o!p|*XHL|uWs#w4gcVAs_lsri_`h<`?8I643Dd88hN_#+YM4|UbzS$@87X5v? z=7N%KSps4u4tz>k$THn7f_%<)n+Uo~tiZtduBRn=aA1sXMQ$n3sl^PW5}zR73e^Zv zj4L90QV{nla(=7Z@8oGXR44Zz+O&1&e+sWkV!k2CQnV;Qq(RJ;G8zi)3tHlZ)&Au_ z3ElKH*74ej0+{Z&@3GYd>T642j4$QpaodnP29_iKs(rF1|1N4dj$0{YgHJsIddMIe zK36{NZ$<=$($C*oT~6P(Ga-)-gctLPA+mngdr-E^?&hPIDued+)sv_eKyz_QUW0 zdENjwFIe|=bDigL9^>r=KxHD>wqsd+u``L{V;SI=kK>fiKS5{T$AgMA@*tVlpeQ_{}5@1@*UL5F5Alq>% zf0?=T3ux$aeP;@LV5~ER={WJg7zi(5X2z=IIt-d&(JG=ulA_W9Y!MAp9u=zHeJ}8e!qXr*P8mxTS z!1ZH-Opi=adrXpjdJB@0722AB(S^Te%TlU3+`qGgJZ+Q@3Gri>#sZL}`Q!T(h`Mgg1 zcTF2OT<^MKD(~ih$tPR@9*3^-gQSMP$)7wZe{om+ z1ZIVOc1M<=wchz&D!ww#UR}mGU06gmERq)8`!3t^n!?%sG=120Yr<3EWnr|sNAmJ} z5R%r)?6sPoF)Fv*c0!sDWX6WBe%$h1S(-01fwm$oKuhSrNGp9wYA5kza34o6YD=G( zclzgpJy7LHeQC2Z??Qpj7LcyuhR$N^MXSmC~L@NT*9 zo)W9W7)_1Ge%+iD400-`rCyDCy)Bx#UCK%B?*he1v;|JYQ5Rqv#^h7R!w+eI|G;C0nPAz zlXBx1U{9#k4UVC7F%aJ>3tE0JFIP67Z!7f68!%_CjDLh z;PiJWin!9<7pjMR6bm-j9>R&WI{9w5KzhixEIcF4-}UHA_b7{Zq0XYf5p$-u%Ka*Y z-AC4D1_zMDRR%sGS@MeNS3$r#v8!8?+7 zRrJ)Qi+;MYp%Ed$A5vh(!LOe z(Fyj_)?&g7=~?7zmUKh&&VQti=UoR3@5OQM;4)9ojMUAuDjMR!K)^@fa3#EGKIQHy z&zN$tP4e1)_KJ-sHm|ESZBHJ^}k|`b>d%@Cx2P%7QhI>_dwgKO7Bwhs$p5;VdbaLff!D8Yq1Gb zia3U#EUO|TH&ZyrIXQXSVSgO2j5WVR!xVR)O43h9dxTdu`du%Do~yHgRD*w&9dLhD zB{5gsAHE$SlTv4bnXYswb;`*IzRQ8i&luJ~V`&~U9Vg7vY~K&GRvaT_+#)GFC!f) z2b#YnThXi1Nva*IKA^DBGjbIYfG;gq5~QT!KkSjK4Y$H(*`t^&BgrYu?6l;>Bp|^m z72p8t2|A|hh_;e#WB5DuA-R=epnd@R>L&<0mN9PV*wqkeS&_V!IZ7TktL4W^K|2xDJ zz1EBjzEZ{vule_ah{9Q7w{&q1KQJs&+@{>+N$mQD!BdE|&L86m5qc zU(i_rw1FxU-xz7;QZp=53*zDYH{e+_iuwCw5U46esHuCG3bRhQp$`a zYS24zoK}EC!kXT{#e7&>ov~NXJ?SWY#B;8m_yXS=gmn~vO;u_2J^vrAdh_%9#|To8 z+xn>T+5J;}#20mQTtuIs=W@YeE4bL{{cabTi08tnAknp10fIT)$#_HdQ#5-Za&0#E z#C8hfr^Lv=Vrk=wP;J}EN>V5A`G-U2*}^ysyJ=hJtvg|+8#sN?Ur6wikxo6GVwt-( zTP86&2zkliv#r%M7X-{tY$vEvEQJYA>N|nz()K$e=FsJFEBN{!Hk-_zQUT$$+FH$b zcBC3%hF!fFp};!!KFZe#G}U!+@oaC}R>YL^i_*po0U$Vceen-WC&uxBykb7HMJSiq zb-+8SdF;%R!b7CxM%%9W+zepuR6x`};W)ev;Yis&fjnQn(af%sFW_@WtzWFwMc`LD z*04y~epIG15r{R!3FPbyL|+HO6KSIh#oL8ZLqK{?`Sxk^k%rpZ5ENY4-SE^5(Tep16@9lme8`e--o%m zQnNvfpGfd`^LF$NtpKHzZ4`S-C#s^UP6+?XrvPv4x`_Z)Y}FGZuSK8rwsZ zuty)AlnL3-Bbr(5U}(Kph$$b=JI?K3F2U(VN-(i1MJpKhVGD_OT@2^*A@IW7*Ab8HSQ{mmyiaC|fHrdBDeNN6ykYC`5^Kz6gu$y;|8AkW!Ly9VpCDNGTq7CaK zqnE9G6TBi-yTC{B=tNs-5j5SlLqL78F^O{Dl2P>&Ht@jM{Q~AnVkfDq&d}J;_ff?0 zsc*GUw<@hmKqrmH(aLmJKZr$@4WTLmP~%a^bYAed%Q4uc?BRa&4b^7&!87Jm>-bjL0LD<%tUUg9>`qM*vfjjd zft=~_c-OkN+AyEy^v3h>`X%e1tg0cD6S8>i{HC+hy)&_TO6Aa1|Hrs-6vc0Rz0EHb zG_S9`Q`qZUD>412(R%WG+bA)QhNcDd?^o67wYF zwyouStCZlm z>WgLk4#Sd>$!`_zX#^Btv3F(1p?@IpTtVi!0IH0}SLB+Me*ixEcOFS2O17H_)uk{7m!s{Zljj@eELf6(Mr?sKW%HXvX?BOmZF zZw-xY!FMv}{lCDaxO^iW{)#*C{NSmpSI2=-AgJ8*S$~nGYa!iD4i;=Rnc`^7maPT; zHTFUw@-`+{2-9(QQ4omQ9Gb|cd_}jy#`S-71VI%*7lPZJRu(;HzI)B|)ipmeHxIFT z&%&G6-1XJprX)gF8{0bFnEUpfx)@2DBF_89gWpfmJ{AkKS>N%SH;;t<+**Sf+5Cq1cuhSe*7KJZ?Zm*IXtr=69!#F7NK>@ z{T3F<>@Qx6FtD!%Lydl<*jG4dq1eJW)4;Q;ijazmH~uv}?HB*gfqd0)YyQZ`=ML@e`Y)h zf*&FTB=2P{qDp!H_|YP{Tv>hl^56~7$VtXNBzW^CYc(k za_NKY>Q;K>@Jz z5Eet9?qDs~tz7TNi=yW3K8z8_7UqjTb_iQY5l|mP^YFZOl*$Gmox_+}OW>_=4iTj` z)Kjx$1h**5Q9qoQjt&7L=?U-&O#Jddr`y7TATQ6uJWaOcfaRP`q$N?>>HC;&VPFm7 z2DS;5g1C{I_2}-0(d<1ReF1Hcs)cm_ zND}&VrJ1MCqUhNTnR5nbct-1m8OTYl#leO}h452e$sAL4+-ijJ`QLS()6?3`oxX9zS@3!iV$b7CY(c=59=tLdTzCqVL^ugE*ovX6WUHdv^`6eF3)|F7ivC|C;3e6r*HdpzoAkWm*&5zjG zmzm7hLKv7%TgBwOm3mBx36kbADo8OHM7#>-P;fp|87s<%-#5O{R6FIB5ZmEE^}Tm@ z)8-|sgqB-K_=yTtJ%ZT-O$z6bqQ=VDT_4%f2gHa#op4NQq`^1BJ(%$A=-fMf0{_6> zS0e~5#Vy}wT19aew$20w3|1NMQksA%=}vYZ|J-ER*P>noU)k2Q8IRjbF@T97RDobd z0)#IOWS%tm>uDNX0>mG>6c+KDuuStG(MZj*Vn}Z~8C?Llwl;M-fy7#&fQKtlN_tUP!$P|3!%VeK zC41bIGjyu5-qSEu43vuLJ5~SA_vW{T?}s1mgKepKMuh_n6X;OH6Sk)=Xdlq;`8SCZ z!X?v#l;F)4hpvL2oH6MLdYp->ARf;=HDEDkx09gj{L(3pvj85!+}^Wi82FC~PXt^O z5Z^wgAcA97*WP7Yyx;ohd1u+UY4bQ%-*4{2PH;W{kp0 z_j0U#0hH|bBFTOx+j_dO2~4}aJ42^@!=bFtkS?d+)=)))=hW5(7>rQHi8jao9{W!# zC!iFCDz)~}Gz~*!|1}@s@4>^m$l4crM;d=RA}Z!O84qnB#RY!5Y5(on&h$gN0*brl zdq2w`-VgYKPQq9Q{=sAzUyC-y4h3B##``AYFD!Qd%vD9ATI={&te-PuTEd`G;rck) zdgVgc+2y;;0$?0!SOC%OAl_*3$j&RxxK0$j^Ob{3I%2rmxwjKOdbKAny}Yq?mqe&N zLI^EGp$iPh60pjU`oI@>aGe?b(@w4%e^#*%JH4ro77qDL27x?3e8k8V0{`WqdqHH2^0Y*BqYTfJP>(4;3Bxt=Wkw-DxgWUnE zr@94`TRF=Ow)*xB=;rpZSTVVf8^mbLKc?%P6LGWmd)Bp=8rY5RO_sRcH)O}E-g$=} z+pWTgFDUa-5#S7OC1CoD@IRIL4&AatlS>56j5*`{Ri0(d72j*kWmZuee_3h`t-f%W zTP;S|z^|=?#Sp1dav7g=y@ss_OhiftUmyzfSC!Y6*R@ehoW-1mg$SBfEbnFYGXwT# z(wx&|&ZoV!f3ZxRZS`-rR<>iyBE;I@6zE{FV%k~6k>FfJ2yaQ@^M|tW;G2q_r9|%t z%-3Zd!|))KBGP&+RA}|{uOl}yQ4-~BCphtr_E4DdU`iRFC4(5tRt`|y>3vYd(Ua=l zRWPkL|0aWliVDzhpQ)}JLh~w7N&~f@8b?>aiI8hQ!>e9GLyKP-S=c%37QzQBXFezg z*&4_yp=5Kn-nkS0#DVrQHRXvX4wuj0h?l$F_YNA)-eKxf5(;eJ?`S!lh-Nj;hgmMT zlOrw~e%q;i^ViKKl_VJFwkZ3J z*pLCjHx% z>|nb-A~=WZ4o`gxrP|~QUT}G#F?=E%{yeY-iB<9b*w<)PS zV^SSrs#m;?$k<-yUrOBh=!>IA{VFR+Sb5&8d)n5#T^B4^Vr;qV@m@Y!6vW*sqacAd zlkVv?>cvBcTHfG?RU<$VTazz?41zNtUjcMfD&Z8>laU`|Ug5KKu|3k^#bNOTSz*Xs ztfKK`S3prrP;hMAf6rFXpXSd8SVWF`0Auq_emPUz3Y6PxB@N#v{?V2Fn78-GPnbMc zm9{SS!RR-vqm6j7?kcliF-#9J@~H}g5{r(Tni+gqiuv14K$b{{!tD$J#^QJc# zv*onK0TQ=P4ZM_g8myr4(^uRJfQ#9-kgeZRE#%Bcjo6eHL(h z=y`FrjyDoa?Or$``kFlw5sS; z67*K*<9dh~K2k^2xIOZF@U^%q=@U1u-QY*Eq(oQ`gRbEz;mh+#o_e-(bN^H$sq)XIKt{=p(4hqJ4-SjaN|ZcV~3 z2sM}x>$2RE`^mrr>vJjKJl0j@U*xkrE&R2FUjfUhz1+z*XL;uox~birs?SqiIsiAe zj!5?2QTe3~1B(p7rPX&d=4as&-2K1s>xeExKyQm*kLmK>{^nVyKYrvqLoUSVK3B)y zamdegiS&{)-x@2Iz}Y%2&?PU1B$wI5HoCkHfp6BCE|QgHZYVN7GJ^0a27jDHjQp4xZ=nb{BjmKy-hzM2ZzFYE~UQ4QD=RZN`N8ao) zAILe9)NyA?!Ig%(2WvPTnA;itR=Vc%i{24T6Z{r+swJ3lG9ym<;QQb{%nkHjJM3|Y z{)M2mccL0km52RO-dLSM-!&+kMYaGGW(LUdRb6wpd~ztIN-_5cD+%q+KGJcR{^SFF z*QfYfisHK~dYQl@&!e0hQ)x|JWt=}9lRa3&Y3G7H`0C2RLAwQR-a33%xSupsSp8c@>$`VZKrY~}mJo``uvzMx{c-GhNo5uC)r%oULjuKV9 z6w__E6}v)cBbf0*ifG^VR%O`uP(U%zJV%M&>um_JJCOqCcNt2RXH|XcZPZGo&Tf z>+C}n%H~=zMsC;m{l{0brg9=D0#%Wf`cLkXSjOBG0NBSQzHUuoe z9MXImfd?ncud|+8>{%p|q`YLKf})MO>eGc0Ng{963yZsHyA21*rF^5GuC%r%Sm2c) znE@^Vh~uX|PMASuM4-fnN zh<|%E{`O2{Zam6t=Bn(A=V|t3+n~r?QMgREaCLFkPEzZ)!v-NMz4&k29Ob9^rs;jV zy`-QA2aB;SYbi%D8xKogJEVO*8NP6GY3!AwnMiD1x8p``;A>|auot&|?PL1m^|%@c zPWB+DAD6`4c?y>SWM>-y!*=&yL6CwE9{tLFn$rUgtZ_)*nM;?5g_!=;1d6WQn{ z3!|fQ3RG2M22UVQq?i z`CDj>JTGBVU)Vxy`pkpsQ`}9nVSkbQBjJ+=GBoE21ouhE+aqq5Zm^+9p zIXfZq+XpR$>C02X_DQ4k1g=6ezr830wK2`b)Prt9p7wU2x*>M97@UA9=;q^4*JDuX z0Y0B5BgI=xojjY~kJS{mHL%}+T%EC5>0}F7=(w0yuHqyXALf9}u-`N|sRd=&VTrmM z8suBRN84K1Q0g33X{SByxeMVj!*lPjTgF}+Jj}nWjW<>^C(f9w=#g}!_!OKwR!l7( z(b-MFb;F~m@r)iE`*fI-@}lyH5PAW>$t(KSUmXlHURi>b zfxip!hE@;pq9MPy|G;9v$QHRRov}VYaCiFqbZx`XTiNEJ=yXMSvj>`=t;r=zc z7(Dyw+^hS>y(0lvDM64S$<4AOeLa8YX~(HYmcCmEql?eM)j%_?C+Bhx=#6?B(F?Z6 zuiH=su7eo!?fJKW<7ns2(=Qk^M{?!hOWypN{S59@XZn?U6WW0Zp0dH;E`(F`vaA#A z1G#F{TC_DM;E{~0skSMs))%`_+t?!fHjVvX9LfuLIHSv5{^*CQ7vXrKTN}dA_&(lY zG&}khTA`ZhSSNu^tQ-7fWQE`p&|bjv-ih|m;1<+vcb_ZoGbhY^CZ=O(<4G= zZ0}ULfJ|$JW#ozdXTw}W?SklkW)u+)efN>|0#VH?bVmXAjQG2%!H61pnjx8o%x9vKLC)LDUpQ^;=TVpHCoG3 zd`##fztHnw|Jd~W?}Esu(O?6{9cugA)AgdV$8XGU4v`1QPkc4$6#w8)fBVqH+C!>H z%e@NreoF1H%l6Q{V2Tvo+~s*I9~K_k4Yq&?kj}wj!m~xj3zAP_8Q;OOiA(-L^`nm^ zNQ!Xam&O>VJD?qP+Hob0{pmlS7b=`QtV`b@f)Ib2=zqeL9 zyk7FvV~6We&M!5`L=|6XFYr3lblB63a~F-I*qv*2iI!^CYY@+#OvBiN>GAkDJV-rA zrtp@Ir1EP6wFvDhZUSp1!t@wP=MU*PR zu)<&IR$<**OH<{}#b}hi^%tn>HLtzvTA*H8#k!%MS74*BwZUU%O{0>TLHExcXF?6_ zB_?$Cln07k0Mc*QP-r2D3`8!b4KV24)=T4krz!eBGe@y6HcZtMhRE?W<;h+%ZJN+M zqkQ4Abd$?HHHfiXc+yj;yzM~Vb?nRF{vP+wMqke!>E7uSc1DMzTTCvJt|oTk0`1e= z2o>nW1z;e*wqF;k`-D~|S2P!gYmceM;oymEU=}CxodhW!+ojRqSV$Q$u(}F7MNj~| zGfc8PC~DN>Z6JKRS9oFhi~Do^r|5+pBuYNbeh6i?j0+cf`BKm~c{GMD3OzfnH`=e? z`pHiPq|j;$-@rgB-upTA_9;$nmqlchhs-_CKS}qy0>A9Ndw|L%ohbQ!#=ZNQAol0{<}gMDHH466*xjTj zRw0hS$6kD>zUnd_9*hro{49p5#zOEpmz7^RPruFOZHntiFx?laCqWLrs{4Gst?cEl zwFhJG8ozl@u2HOBQWVbs9aI5n!T0lv#zCr}Oy|gHxc_ru=={0xK{tu*d3$xv%{`hd z@jQ?091HKR%Ju1`Xa)WKVtQ^U!tvD)9nPHr%Y`=qUw+e!yHDLMG;E;`0S9~K>+)b) z!awapm19aMq-oU%G}G@Z{aFV~eKGX27vp0_M<+RqmU5=j*`tSX&`WMq5J5GgU^Yy( zj9QF-51Cg1jO8)+r-1mqw{yxISDzO$pgggQd4bj#Q>^RExjWhP#XGw9c3;+h1Ij4D z=fJy*l^^~MCR_XP34Hv~mv2=QNSfcMZ2S~D#Pc`@5`bGB=13azmml-ZZiM&reTCaz zXN*7biTI`tV1@;XzTD-Ke%wnMa)bTy`OE@1jb1>7!f1=@qqx}W#=vTuds{l#SepL;| z{HY8~5<;)w8GvZP#d4+a%03=V`zg2-)3P@dth%el(M`mdq-(%HuPEo6paa6~PDBq2 zqpOFSID-al0-vSt0ReKykL*y0Wj0_R7%@rU_-WRQy3yM`FsOr*rrj(w2FPB!v~!J8|<3}b^8YYzf9K= zG&LWGKFQ2QsPox(S`g&FGuLA%F#a_^FG3?UbSA(OHjz?4@Om|&70sW&7zjXYJCWnc z%;$`Aw8;iy0s|kdte=>}%GOi#x_Nf%)w-;6a0H;@mp!+>fm5qVc6wmdL%Ib&#TAXq zoT!7R5307BC$%082(dQ~L1=zI1`P=DAKu&V6YFOvj}`WG;jM>D^n#4=S)M&!v~L-j zHLqqq^m@GWI27;f8&8cV2O!|G@Tk|yi-SU$gUx{(4deuxO$y|*;jdJkV12eO!1N;f z9{g|t8G7woyx9(NMkl`43{pt=7;*CkQ{Q{<5#*x6PJiKH)Y+cz9Ga%B!5luQL&;AywI%_9~)?y@aWNar?l7-s*DsT}Eh(?nlsY_Sc` zU{0A?P9Vv?)~RnOpfwu?vte_?QMPM-X^fAm5ApANPZqa>RY6)_RmoWEw#F3)6j=U; zPL315fCS^3*Hu{;c6trpt{e%0z*UK2)hTr!al(XusN?SxVbacz=_U_Wrk0U^45M`6*<&GeI1_Hu~Z-<`NGSIa(( z%7{UWVV?~@Z)fUG@4RzNcj)=*=14_5D#28ipVHre+9|IoaU0fmdc2)}d_4SXI$@_N zKPKTPRxd$LM|@NE`8j(v=NNFBcYN82duJZVW83k{_R^^8)IYQ@UOBwWf~PR~BqjSx z2yMJLB}Kqn2sTn&nolfjvTV5@iTJD6u9<;v`W_Osgk! zsFWr?fFU_&;-$nZ^}^;MK2Cq)?B_%-;9E#YgRjC#Te)FHsVA1#l$%JPk?aPT!cMLn z-dhp$MJ~Djk*LtKRGQan*dd48D*Wmo^M=_6ck-XAn&x^fmC|zyX3P4X|KgqYE5RuuUazl?5$ca?Zi}oO zg)$nO@~eS{O^;uj+YHrKw57a!ICmaCxu(i+AJ#5H0!hUt^pABLhP8UOdiHaGw>{a6 zAP}ITqxvTSDT%u#z)&&SwOno>{yX}iRntlh9<}LqR(b0^x76UM&c$U~al7AjkZpV@ zT|=|IzdAeOi;2a7&8G$`n|e$@-OVWvjn!nBxq6KfVIL_6ZZea_j}9%PV8>wHF?ndu z6#4e_GK`%4!`fo9uduX+J%r+^@wT(>(lYYAu8?VPBU>xwPkJ7q{;~?SvE-1IXHyq3 zJ~+Y`e_UczL|ojniM?!txHOX-e4DZuJ?CNGOz(XS0<&FH#n^GX^^=+>nXiIJ6&e!| z0Jc0ina%6#9Ha#jQFYj(h=F?=468arNx&WZl4lZ7K6UCOr3k(5pFY!H?OBkmz z{r+_pJrROL5B?y@_3H1558P$1{jakUloQs0hRwtNyDiZw3{;-Z;gWO`BY7Xu%DXk7 zW&N(F?R@UKi&3Wb4&|!88`3Q^p)O?e_rS-D^wS_y{-HL7QUQO;r?KFv5DQ$M2aR*d zdoK}020>NOh7$JnUk`LYcwbX258=PY;11jgLJPqa&+pf z6RhScyHE|Hnz%-r2*ao)QGTCCV(H_`gMHuyV==xy@H_iZ98iNUG&E>I;B)aS&hM%7 z$p6oXx>r=?xVG3{<+3B!?A;;hdGYXlJBfvo8Z?`p8K6o(p=GxCaiFK}3%$4lg+8p( zoHZVtosfzw_YsjfSU$D3|NT3)9svzRf`3G?Dk|U!RRF3ub#88OIaie`DE9M~(D&je zAn?o`tUn~N7wnVQ^rGtbn{Tro-+u*nDSd%{##`!prZ=@}FGk*3rbZu=kv~EYh$dWT zzm$2J()Bw>xyJbWT+bhSa=*!QoK%K; z33uoG@?qH1f&s*Y-Ls&vOZipSOakX&LnQ+l(Rf#k@WZv*a=%ZEPA-g?!huyX!**oQ2GIi_G&>CFgS%rzH6kA0o~Eka4yrHIs| zOP<)eMs*IWgnhn27PXpUvV0$|sdb)awe_+GH==#2ciB(P z%j=A@JvL+-oZLL>B1!MdVR+4(GWEH4C~;$jiE>s6{-2qDSLNMYyt7ZZ+Pra(0%JBuuU z+k>5*#k3uh&$|iI&gS8e1OdTy5$mO)15S_OuGHgmkLg5zZr6t_-$Kml_HOR_33_$n z&WUG)?{=npy4?4O_mPhlJ**IBqhUo$3ZpKw7tiN5m)!zBh5muX?4B6cesOpyS=4x1 zQLhk}xYv&JYWzr07V0{bes^=lIK|(BCnY;~V$ShRo+0HC&LvnBsNtJOHRSt%G0kel zP1J{{3w)#Ssr(6E^IpWl2j6S(23td<|Kr*tGvi*?;htRw!*oEmv1acoB<`#~*0=nx z`ZxXw!~~m6`%wElzcZzg$b<^fitzD!7O8HVkKm8c!Ws6fG4R{m^cUMnuMkkY8o{Gwv_^-?HX`b^to!jDzp?dg@*W#c3=iMT# z8*qv77TwfJ+sWgv$_(8^o_hJ3)e zOxc4_Fvaz^EdrO5zjmf>LZ1)Id4+{nAO5Rj1Ah|`LQ|xFumL%y|69f8{e49g!MPX+ zsvHi4fCI~J!J`a9)1Q9@pwFo7m+o}8y?x+;cJJxA9H?TQB971nJ@jVpT|Hm8<=M0$ z#&pNE!Uxdg3j;DYJkK-KFoP3`JD0ag_J=Q>-ZZ+aZxQ4!BYfQJgjHO^^gf%RGZ%BNXnbjGk%+)VK}UF1y#xPDSvSQRfE~W? zc3?dyNl!jgDkVfZh@bl2?q*}ur>*-hyqGVCoKIsw$I!DsQDvm_oR`L@DeH78&sy_d zVrePXqbNDW%a=X5Uz$Ku0`s$IEVVG#pB_Q%cg{3R3<}$!}7}j1S&GI-)$yx|a4c(=r zCF0k?7SLz3M&Y)?@9U56eUnCiO3;!S(^7Jm4OCujULLrBnuFh9-ReG}`RtvgnHg%@ z`4b$4NPQn4ke%^4xP79>jh1heg;BKEDHUAW{1W^_fZ~1Ti)4ZsD$Tb3YyU%D3?i92 zXDG@s`NQIPEx!@~o`;tf4^nI#A$eEJz8_gow$`3IANY!#d8WcSb+L^6%zvEkyf_Yi z9Z)#iDn4c#oRJ0sW5Egf>@O$k5AZ+AA8t9?mIFhgz??7FOZ z;47vAx0NrPX~5H-M<8g86U1EOdz1BlYMT>IHt%Xlr><6 zf;&LZt^pZsT_UxLUMZ3`?w6Pb2i#`w5}KH9EAmP&9zAPw+S=xq9-c-a{iop6*8|{| zR|x_m+D(;9zc~bXSa0Ic&ehWj*iQRWHL1XW)6=#M{uhqJ`9p&<4d1`_9V@;h#tY{Xkp-MMj zagdS2;S@Bv2L{&Bt&o&+s^1Bn=HE_wg-9FaX&r{O+eyh~;)y1-sj-;pU&t^lZV>Yh zbu`?p!}wcBzq*(MOEb!UvASdtEgXthgqK6)SS64*1N(DzUl2Xos~j{eI|`#GJdW}w zRggH8Vz1+l(M(wnBYh$xsFMNAEjr?IdCQNNM4j^_%zyV_mEXO-6|28uFZB`=sawYq z&Q4e$owzau@j15~thv-NoX$=5Z?AiBI+x-tY8Z%(n}6K1#CSUeLNrm&3iAhn_RxX!K@3JxAE7ugk@xR1)wpOOOc$s_u-gG+ zVJTdGi8uD|#_Q;V_zSyYohjl(NZo4KW8jbCt~`+uC0W8 zA~3=#oMJVe`$xuoe)fl`P|=S5IIoEq&xl!+^NjlPK&|CFF}I96#?#zdo^9DnU$@K& z{E}skQnoJu)z&zATDyj_B>v6Dpyf)3Ej<=LI8jE$%{&`iQ)rjl&AC752OEY{b%X;7 zO&OB3r1k^BuDCI{>*#{^EoEcKqG!?wy{HFe@>SB=CC_@MtFuC6KYTProC@bYEm)@b zM%uJZ1vx^jPZqy6d>!8{mF?-yF>7dDH<1&LbBqjoAY5`ucYY2i9@7P97i z1!h|duNTRG?&k|Tzme22*K6A8ChjM$u>v-qYON4c>64mU7pD>qiNA%m!UC;u~NM6Svb7Xd*SbEXBniG9JW zhChOdt^V#@pQ2}fDdrtBSZyk3-(@eAfWme10;;iQbatoR`l0kCD&=f-J)kQ2uwGFz z(QQ!|mrPS$_jZJLKM|+c=)vU8`O*yDi=6*xM`6*p543_pu}Jdp+?nGcu1Kca-ls6| zE@BLJn|1dxrr{ts@G?Vw?mW!~gkFn#jzWnTGFA=`~2<~5%k znj+#vwhjra*!`0eb~*1d{}25@Tfbi-BgFKnOR-LA0k!epmm@<~=QFj& z`;uo6uX67H_kl8v%;SQ`=j$_*kI6;C8?dHDat<#QtRkyN)EbpTN?W=k)lNkLO}D$Z z1Lw-9NH4@ZI8eK9f774DgM#GRm_J2;7ePNk|K_hZV1AJ5zAIRJq{$IGf@0CUkrk5}k8zwV}|2}_lbeeGmXE22G zXjXZ-Di`g<^{fcJ>)%MfD%5VVi4T@Om%N?Z4SjkPTKd?~)n#l@>Bo5zKV7XT%IPN} zjU_yx+HLSGXV&9d`~Gt+3tx7Br-@El26l{y`>Zy)`of-k5NuYf&;AznM~trYxiY9$ z1OL{&isTuwa75hb4#|OI)+;fzXizvND01(;UR8pJ>c~Fw*PN^kFbnnXc>$#5gf3U4 z`1<8fhFa7M*LoV>)y#<=k}KstzG{U1SM2j_su3Pv=ScOeeynu=Nbs&Vh|?8txNf?0 z_cmwky%ZOC$oYPCR?2oKozLH!lj^cCPTj3OK06PeU~7kgJx|=ss#i!L+yX5%2^~1{aHme5&uEGhPGEM41hdTLVvGrt162RE|sgvxqz?hZG zsGe*my~}0Rt5v{`?_6wu?2o&JZ#Cy%v4Z|_A3weV3*V&LyXj2` z!+Qo{XBve9-{deQd!9ic`epK~o|dik>IZ@*O*;7gt@<(73i?N`DX!l;_t8H_+I{RUz5a-L&k@_I>6+Ww^iYf^ym z@aIHhS8e>9?U(0h&KLBrlBeRFTYSNvAp=c4#r#LVV0$UP$M&Jv7jPjC%3^Q`7 zPIEb{-Y?nSq>w+?y*heVB=^f z@TEP=ID=Rh^R$MH;Qw5 z;&-hluV>q-7@Dd5v!rMH*ASArlSL`koR4q3!2T1R+L@&gY-z|;-cf-z2>I6i6XDbT zqpd(7j!0Ot!}c7mq${@{g-5d)H3nTd*%kcsMenz57aysw)9M(wmeA4&Z;pFoy)%Sg zah|3Xlca*DNb-E~D&ZzCvA-lw%wWYkK$JodYpwpqecdD&e7pDy z;gX$cb}HYVa6-_$=6_}VLXvX+2G!DAdi;3e!GZpM+Vn{eTHM zvycDDeoMbfe-pede$zLf$>t!%vn6E|n|MH9OO->@wO_Q(Eo1$JpYgvq&hy2O;HrNU zF1=%Oyxv;7VBHDWp1*@Y*>2?D=oP}v4jYz8hdY}`A3Rt;`-!zMPi4)f)tnp^D$YLMW(^_mk0W_b`Pz*Z}3r(q1Ktl5tM5{m+KguI=;iStT+bdwv< zC#>Db&HkHNKj}@AZyK}i(ZcQ)ozEQZe9y}S*Ez=imf^&^>W=g+xS_+PczqkE>@uCz znfeL1{#)4Jl*21RgiuO1Pv?438`00_I*n`629{$i=eRv~RTm%Dx`F zo&8kpcV8_;=xzURY`*%OW$9Onz8f09*gM&7`d*vahcZmep5&c1+K^fR`#qimQ6M6# zfHvVMm;PM5i~W@6cWduB5PqJ{B`h!PTAq)@CduCNIwA?DvyvSz0GJa$ACdg@?%rws%&4uqNKt(-8V#&ca{UyOQmV z3%o{L@!h)Ti0I%@|IC5y!>>KtW;;pGn_PkX!z7A*N^ghtOMiX5ceTJ`Nmyexf>+$l z=xV~3@v{+M!o^m6NeDLFdZF6fRy&<**iL`bMn}2k-yzbP9Iv<5jvR6sVwvH(WlZ`s z-(?@(#C8%r**#;-pJ!!7vI#`**5&U-6=0ZvYrJ2#9r9xKG4_}3ZPCfx%gZ<^?%NMO zY@=Tid~0zH;aJT(Aef-^#^4tASMKN8Ui1FZv3iF{E8d|RoFZJZM~u%4KNUz5dhsNe zp&H&sxb!`8vwKRr2|9!}6znD@g+r?i1Cc+s(m4FNCg#_vj69Iu)k$K4u8-Et?$7BMa}w;?j(F(fB7@VUY`7!>-DV| zOT6E=@Op`F^-f{2C&KVUeh=Vv8O^u*N!CwxtkstUJyg@NQ1sb+&wuXl z`^|UZ<>#7j|5F@CveS%GfMHS!l9;gc)y(0N-DKm3{h#N%cQ^Y<^R>;Ds8e?-xzB

{2sFSe(L2(kC|MgqA{kgn)i_h-)4LJ`O_v|zCu11@jKux$Y<%h&Ue^uDHljr z*m^`9+sNxBId1YFc$A#d{084;yJ?-xPKTc^*YEGK{tCSazc2<};G^?B*56Ogch6DY z+oxRP?|XUD??!L1j5HqApL+fD?vw4mxzB@@*4P2n_c{LLx3~IyXTUp9`=f_hKk=LS zu^Hz8>TBAc{ebmX&;!Qr)q}tKKV<#nFE%|?JS+wMjxB}wa`*`05`P@2uLJ$iT}~4L zU1~nv9}zC;J@d1}u901!ex_c(Jh!JFSo@;(or*KF0ji(zPuNddN26OXgZbL+PxY%u zSwGFk)*Vt*bSwUudi~^2TDmvC@z~0CjUV85rgU!J8=ZOm^4-*(&7*f7s9!ln4%$)< zLGF=*RTVkyl>yhhw1I3G+RWi6bon0UPsh<%(-5bIyG`^wS@&|I78?-EqcYcut>0S}^)O~{Omi$V33h%3I32>t5 zaQGzQmiYv3Wln*M01}qpWc(E2%6+c(?XiMzs#2b66Ca@{ER(KB4w%B|ntK|HU$dV@ z5s$F5dG!7R^_f#11hwT^jxusM7gP>SE5jm8C-mz48TK-FXyE<>#>K=wTkqR}@Z@(G{m$Wve)*lMCdSx4DkZ1vC3W-dDEreoj!v_kGXBlu zb$K7*nR~Wy{ zag}k0dJnpJS&gqZ3ysWyOyVtPO@d`OB2gJSZDnq#s{aH40RR6JA~wMRFI978a&s?h za$#y=XfJ1PFK}yTFKusRFK2ITVQyzGZ*pfZbZ>8LV`yP%Za{W0E-)@JEoW~rE;2SQ zFfL?aa(8KNEn#wPHZ(7Da$_%Yb#8QNZDlWVb#8QNZDlQIWMVFGc>r2WNkRYs0000G zvhjEUti20dl-2e>ydDM=WKhaWnpRvizy#6E><-?^8wp-2ONQYAM&{f_Fmt;_S$Q& zwf5!N&of(3CW*`a}F$Z1^gagfO?=j=dC!>@COlP(HKvX-BmKs?s0h? zPCcp2Gbn5D(5&nWk~00}HlJRS~I^{fdt?_`_1OegJ+ zC@p0U(br27h+_Oshh6t)6?lAsf`F~qsgJI7+pjG1WckZ&UOnr|tHMRg8mCt?#{p#q z$RF@k+5<_~CXLgplR{d(Ac>)&g-O?4MOr^CrHZJ>iUdlrySzk)RwPik$iqDzCu#Qt ziyOi{E6*Ju?df>4JINK)5Px`a0wsItnfOayiYJP(yS!N=JT9-zr?+;?;SLNPausRm zEJf7H1WKtUPLr!9+9p!Q$&K-pT#Y~b7=EU*a9}9_n;vg(P^*6Q%bRq@wDNvCa?Hc#0sHfFM5z!zm(`EB=6d@YKF$>Y~dS5nagZ{NVfhf&Q!@zeu5v3t` z>Lqj@C<}=w?ysetDT#={YcQ(DU2u12Dw2|6L=@{^)|u8wsBMtcLPQsIWIV97f2 zZX%*)DY=-4G7zQji6ZJBtHSm1Jw(*D`E;{$&4+?JIabTGO|?0R#-f8AEJT^bt@(Tl zk?QbR^pnzTh1Fi2@*ZBAK=)dR(u!O2M=eC@*ZZ=w*ixSEn%WpI1u)UYt$Adh>d6)h ze4yGplb-7NwF!3wY{m3+*J+5M0Ax9JH!beU0w7=Nw3X3QJ$v1fKx9qSAQeqp@j*N# zMMEO>_odN9)IvSnr5dtAVzn<=vuwN z+vx@rrFB5>AfcV+q2SUEN}hR;$Hf)c#B!pQiRFPs+^KhWrdlbP04s!vwL}w})evP? zFwC5o;96ZZq8cf=%Ba;GqiXeL%VJDo2UQCgiW{O40PA{rNq zu>hF=q|43IRGLw0-!hyO+q6~!wVoakOR3sEx^)yG2A z69dK;TPv6wxSJPPh!8OlTv3vNP<25ZQQYbkov9^Wb#t(lDD}-1ooQ_xQS6|VMBW2R zw1A{AiZ#q}Z!e-GtE%@fN+g8mTa|2Rg}B|Cpb~EPb)($|sc*JBtdq3+leBkK($IFB zrJm7l_1&E*F+sIk09m*sC#Zy{j_vQO&g_vCOmBt4DDH9#kv9mRagBxO%1%TX3oJx$ zcS7%=`Fg>W8JhnC7-;_NcwjJPH3_B?lC&|yxE)TczA(uclS@&vl1$B{OEqJbS*l^0 znHq0SGpgXLcw?Sfsv*{4(iueBQVp?=;vPW9IxBTxK1_YZLNpp<`?`hbI;%K*>_@86 zL`hMrNCir8$Po^A6DbbQ865sHI0U5TJW0bi)Ch+=4Gu#Rh**Y-1Ve_8MFvS*IkcfT zEIzVIE8X@%rovi66nC+fX(PIL(DDR&AOSj)%r6P7qHNMGW_6K8mVW*KHc})057vB! zAcfN0Pmh?Mot_C?5Np2Hz#YXNso~I+8Gpd$3(%8X(1HS=WU5agvNmBz9khVS89j;8X3>qDxvwXdJBdVT<#ZF5 zoYV_Tt{N0o(Cu85ha$2zpv*z{a#>9;=CYbIv8NJQG2Nt<(|uer?^Nb;5>Z+OJhBxX(@&S`3-Jf4 zZ^0j^{?qAFeZUz;^(LwQkx_lp8B+ZT{DJD9;SW?lHA$*Z#+Hico*UaZ!tzzR0}huy z&}H-bsmBvth>9`^J0$MqKR?If`Z4BSF_>+Z&FLs}Px0n^<_;#UHcG>GbLwmHlst!) zN8yi16-#t)puEsiQ0^!VkakSdQn-Z3wG}4^ZMT-*e}c`MkyYaG>GpufSDhOm?fob% z#YJ@0-gv^n1MidCukO>mPMcjX#N0sICqeby0iw6}#Z$8G4)`3ppAK_`NOsWY{Kp9x zn{pNC0n!cyF?J9OW;SpFMpOKH32C=m%$}V9Z#JCgF2SMCzM!VdZT@oAdL$<0gGqZy z(^5_Vvj=?6F+NYFH_K%U*vrQbCT*3brIZmpxj&x%aqS64<@)0*C(ohBwDzo~*b(sS zfh?cy^32gkJAD2CX%}mJ^s>J_vnhkzll4;Fr@QTXMplJhP1*xdEdBeCK2Yr?x@upm z4JCU@OZ|F)9*kTd_l#IgS!Cvhv(&Y{x<13$zqhK#3V? zEYpwcs_xWXq^%0vTlIqg>uV^(5a&-Kpb1OWUS;0GmG{ z2e$Qa2Cug=NS;G`+qHMMK9@!DllD@S7G$MP-CY(aCyup|#7Ga*J_|zHGSxnY;2+g2 zLol1)TFD4zkt(}v-f?>MD4);cBW*(mEk#T=#Y@D;T&>YLY@Em1{N)oIW#xf;V0H=fQZk{!W&T7JpV|ZH}XntcAd00HI^)_LdfJ+=|=FxGyx9W zqb&ZHv_Cs&DLT<7?}XTGNNuObT~@$n-IrB5>=mPCSK6G8Ky{&xpfygzx$%#3Zj3a! ziY7rl$<-O~0K^!~i5$;fiqs>b*{a|7k$bxfgt?ha6K-0z_p8f>uXA#9$K?No;9W&DFzor)Mr{Bh1EOT_>JdC-X!vl`lQ6axxp% z`1V!|PLJK@9Eru|1Q?-qSDcp8TGsw@Q+nXX8g)`-V7zB8`>xHDqy4V)+BT?Od27Aa z=Mo{BpQz8BVDp;G^qPTcxvtgW*k*B*mWgn46b8ug&fVRMW!@o109OR^ssSNyOv4ivL=Q z)UumiikiPKp4=%o)bC1^=F(lXf$OHFVuP8FLI*YSqWMx3#r5k-RNIvZJvmDw${633 zXn878YGGHRO~Axn-IZu;mg;l9q`+T@L+?mBlRbgM?GM`k+zC@WGX9@Ce?~A7*PDv>4uC7E4-IR7_Lpv{bC3@ma zqKsW#i9Q7?ZeLfTX2IZjK!(uac6ZSO6sl{ zmx1vW-H7&}QgJsTOFB`;tZqbSrb7;HB45zA0*xlT0HKLT)1`@+3?gnKJwuwn845O7?Il*58+q1i2b@7(Eu@sY)PTJZO~oE?nM27i%slK zCOJPGc2Rc~Wim!}C#uLIiksM-sHTT{)^QI`{;%mybVnAR4s<7aCW|Qb*6u|6!JFci zcPDD-LEP>;X!qsrM8AR-x4S!0vygUzWa&XPWgt;TLJy)P17RY4dk|%xqN=|N%(xyz z+XfQF7WN>TC( zRzEPZKz7qDljQ-V@Mhx2}MF#C25)t#OsW>6bQEN8u8o3l43MY ziy~s{b}xY1qs%@;~cT>V~iPb3K8{E)$@VzoI*5YC{f1TQ;4PwH9R0kFv-Bc1C|aI z4>%}2IwEO^2Yd?##2yeW3Xhc(is}n)@PKjW;^FJ!Q;4#KJWNu+@Z>t<*;9xX0LG5u z6?q1L103bw=Zd4W0K<;r6c%1d!73AYI7&Khu*7XSg{WRworj09u}4oKs+9_>Bt=C~ zGY0h}nmLRp_WYhi%Y|y7(DChXeP2(a1;dC^ujxtjFxpXL(IgG!LI#Y*&S5eZe*?sk zCh1f)7MB16J)e7?=(%2+TQ4b^gR^HK(p+&KjDhvMN9awGLipj}20dQ~94jzi8p;C+ z3S57lDDWF#Sb_Os`Gt~#)uq5gfvLFC6AJ~_3AtWUz+g)!ibG{S&)h;!0f!+Y{M3eo zp?6gndY8k{73Uja=v@v&cb-oax3DKst#oA?M0>0!(fad=`ajo`Rq}FAq7q^1loSG9 zh0#7ipi~0#64exp&QtM1&C|_Fz-AWkJyXDD7O?XLykak=Pol9{9fhnq9rXCB=~y(@ z+|qA3ez|g~wpUZcsDQdJ$cBAyLM_UPSd^$FZcoH!;QqJ;Q$g z0}N)$xhVJf)!Az!If+uiz*rfY>bW6-- z@5EEzORz=CK_M2IbqutlkXM;~#2}I+MUNMwTUce@3>dF6Yx*eV;fe~cGMg@uRpzNV zP##v9tAs_pq+oR)SYefUQI6~`76BhOtQS#(@Vo_>nZ1Y}2d1nS(N8%M)3_N`oole~D2WS`5cLiwda@+M=9EN{k-A?kkvRzMM*An0&>Fd1gwc(Kx} z3OHLjXqf?`Egf()ed8FSOfPZ>FF%hl7Q$OPAe4~Rdj?1M?HwL{HJ-}GVuGL@T=RWo z@WjMMuXad$H=erW5hWayF40A*$ut}7?2z?tJdMJINoxbx+32kf&%YZ_n@~j7W>xk{ zhe_|nQ^uu4*6bv1^wSR0UW%vHC@De-LfmI8*Ke1?7|B|TVn2P|q4K?Wnluix5lR63 zj)5s}4hM2`tLtizN__i9jD00kVh?$s{X8c!=H7-HS4VW5p2&ixu9*e4TM-y682q{s4^wS(A`(MUVs8>G`tuotJL}GD=a^A3}8mvitKTkkyE^OC${mWUqh$ z#bW!sPUwcN(7Q*$4e{l3w1}T5Ny_Sko7!sQzLdFRS)i!E6E_gy7Q4 zMDp#-2Ro=H!E6Br8>cUsgrkGRRMn{sAfZ#8CKJWs=pa8;wPh~i3nya)xg$AZtA&!H zBhP~aI`Xf{5-5*xh4#;z!X0Ah5YrK|hZ_!oOP8yaL#$?pIHSODh}G;6*CDSQVl|uO zhbUl&Sk2Ee{wgpWVl`W%;Slxg5MvA3AvSW=WraZwv02cjL%hHR8&IK)>ewL;6cSl$ zQklgXW^t^LC}DiA5`TuItvuE;k27#@Svf>gD&KFWdZ-W%F&s2@hy$YGqmn`u41+`H zQ^X+_PZ5WpzG~3SW2b$9U}OIC6mf`4reYX*X3y@c+I<4;!Xb)5V~4=eHQIUvB$(Ar zQ^l-4V+?GkR;uHMFWXLsY2pxx(&s*shB!n9U!`U{x=`qbuF&FIvCUPv23fvb)-o;a#&Jyi;)~$Pf5TPiPvUOiE*qcwIeu zc%2_T+ zWK0kEhO76vlBa|{QJ4}gxsu45cqUib&sFSK;{6SA;DeI3@;JaeZpM{%a|;jEFphZYiL4q9}0VCMarFcPujU@@sM2M&m7#NG|D)E3?k+x3K5D%yY17$c{ zgl^~x+@^yY4ClG4#Bff!8U~LkAzxawX+-P+HEF89K_1W`m~!xe`r_)Pmx9BudkZrXR@l~NZhl)jy5ctIDelM;&zo9}|f+1W*g z$#;PVP#T_u`>7kA?)!>lABO!|caT3dO8B{D4=l6lZN_%|JY?TPwu}rIZp8$2Ef-2!|oAjVhJ6Hm_9T zT0?*3duCi)4+v)VZ%QSurQ$Uc71x^kt5N?B?INy?28{;Vcau?caT$-Rf+&@`^GKU-2Zz&Jq<3kh#-!#d?&E}IU44=ru87`#-Xd?c|t00QS z9f>CC!dH^w*a54M4BVHvt3vKeY^-RfE9U(qpN>11Mn}At26ss3Lk1mfzh7ZEwvOH< zIzxJ=|M+q|jd5c2fJ?L^_!dzqx_rG;z0s=fB2K{dnwR*t2wCy4H_bs6fIZ;E;I2HC zs3B9mw1k1n*m)|^b|&G>ow5`O7R_hwe>jOUaOOVP1qa-FD$x-ck)x7AHDHjD(3@zA zizwri-b70nk7Ih04#!FgJc#YYN4Z)jGaAB(p|zax!P;A+vHI3%(BIQ8Lw{>HFn)o& z3XE!dahlr*jB7adDLFr3{pE4w7#%;TbQ?jAFBT z6V;0r*GLL&Hp0hjy@~b%$idVguM2{`3>boG{A`$lt2a^YKsDa4fOL0nqAVt1n45*P z4I~WhtxQVnPL!CfYV80CVfW?P5_S*Imav9!fvGxz2fC$y)b+S z3=wu$fyU9QNl3dvLbUn?jIf-{^rIJuR!M`DJUf9wv|8$y<>bh5%3KO14Ovcp3kK>8 z)G2hYq|oZ?;D)2paRHb2YG(TB?VjNz})rOw8#Qi>M$2)0)tFfx{WWeJT#YG=@!1g@+u%XxJ=-n z+bvj6{s&~km6q~7l_6DN>+~oux|_?URq;r{iAp4jluHVAEyAK3PIL`$>>bU6m0c|c z3EpuZNE`%fq~;Py!QmFb;3+>b4BL+7Lavh(u-gE`Bj<~&<&pC$p&BTd1m^w>JaV>G z%OmH7)sUZ^BJpe`*ix{9Q#@KNQ}_=6u~Xo}4Vt?h7&yfR*T@uJd$#gTa|*xu8gP!q zgTDPD$^l8ip%xtA2=jqsn`{&T?gj}q`T8|tlija{S7JSvI7A6~KQIV8Gns_d3~r}@ zGm~&N;jwGw?0ye0Y|>*VD6<T%pc*KS}T!v5psLu=}qL+Jfm)^#aw61Gmo(0rC-NWOEzhbSVCr# zwQRmKuY;}Xv*}TGftARndei`*vW7u0Ae(N1SiW` zAWk-S0c^Z)52F0@lsr!X11Ecbfy_KD(*5X3%BltNzvl7;0A|V z0UXaf`9dy~6tFKf=o~Z8bSW*7)S&Xrv*$*cc??vMpdgYt^L&4!%seOKGciiGS^=(- z6l|Y2WIG2qeloaU$arJ`kpO!UFgzJ_0>&|;Ms&rO1Q9cqE|i$@DlqKyvEqbQNx^D6 zSi$Mj@tqmg#)U%8krXhz?y2VCky2VHsX=9J%)E&x4)busdCE>q4J^1xH1IImQ3syQ zBJ&530mnT%Z!(`PawJPUUm3t51JM6w6L7*Q*Xyfv>xHdvFBl=Hrcl|83PpdZQ1lmv zq9r#Qq3ACTMca^f5psLu>Gzv)I^zz|Gtt=l3|d5#?$-4Zy@YOx9&;d`$eMFLhmFP2 z*b@{k!X^P1Ph7Tuy_|04n|#kK;#hMFUtURB3<&O`%%UZH5oPpZxwmphG|qb*$a46* zm&x_0kaQ6Oj6R!IM`K#wwixz~lOB{i=vi((?-pEK$5jm%ZQ_=f-2$Ao24xP~%BT%M zC0>GfNZT28(yc7tHkNP5t*8>$s}E7_C2B1G##oH)Lo^*Up03L;QyL9O3X{M`RXDsX zy_M9S^uZeOdD;15D6tLcaA-pdFjN>*Qx0)ox?Z-i4 zB|D{Nm85{30S<`a|6v#w(TzghE-7GX_=iK;PAT0lDF~pl^CsUeq0B%v2?~8T9eL#K z;>cyUvl1|;j4x2Vn=UEX4mD)E^>#6Umr;SwWST|o=K_Pp+F>+(eovwq!l6h~G;J={ zI^IFV_XGRi!AGyyo4@PLpHOA6Qsz@R$AlM2cmixJ{$WGS z$yGKy8WmxvQ4NCWj#)L^Ul z`ACeGiFc+RiNRY_TbCLE^bx+u6jw`x0Qv}Da~e>q-nqfcR!{P4RdZ0nM=pG%PEYau zr6=*hmxwzmB@(U~y2t6&ypcCVs-GGx+V3LT?<`sqM5)lLiGJt%Odane%ET%S($MdG zP3q!1jkTo0kUFzy%?u{(_ZYRxbnA1m9Q=csRo+R25|&04twNEfg8pFq3wL7OnTywI z@Lur>zUlH2J}SerH=fjCth?iq2WD?=natjOmth%#E9_&XbK@liz6VD8M5>yIw+#)| z)La#;qQ^U^(R;iDM(@gHSiKI%1VqpCy_aRnkkPB@Wfu4#vfP2!Ie*?=m~~LWQ$cTZ zfMQ;_OKAeSd6PjONeSyl6=}=i5eY5QlfKiGOlclpiSF~$l8!h<>C+XMRB4pK1a0l&wqo!-A!{}$P)<`vOx5XkLnw^yyPThW zH<7g#7%`OtT>J)LZe6KE6}6zqQ$YthLW5m#6^N{jsA=}`rdafQ=t@ldP7mAc&9Ug? zPsT!s7Gzn7mpFgkDx98;3GTw}%xc9dbj}NkgC1g5o4Ke7MIvz{^Z0O;8d4rW zRrD8%JQdU!3yG8O!O_}SUi!SrtVUvgKvtadra9%}V8N(y_w2Y9?SbQmz+&*TfM-5?>Fcf41kdH;LiI+zs4PFDst zUQ)1n1FpkYdX3WjFe+c^od9mQ((45`UUSq)i%n<|YmU2s!J*kkU}CY;FBe`;Nzvk7 zw8%0y2#3zmSgiCWdLJBcuzQR-K<32DmCW5F1&5RI2{&Fs^_0@Sk{VQALY=W%mQc-k zVN0mtt7QqLuZ9Q4=A1^9E-Xe$3Rb5;FxP2Bi&qn6RGvok1bAM58d2h z%~ExXq=01sh9%c#z<933SuRK;LBd@7A(OD|YZCGBRVNnjy!%AF>?@SkFO)Pyyczeg zcz7&C<5UMfgAL$*)D56MMWi%_Lr;)&kf5=v|0NDqb~ z_slnkZ;8WWqJQD0Q^eJY$l&LEmCR4)xsJ@fb%@;-in_W>?=2L=4v_?ozBFU6DfK?LaW9H1YG zoBCQjjYJ7ez`Q_@#^J8OgD9y%$t+sSwO&OD;g%)lm#5+|zkL0mG08p0`J{)8%Lkje z&NSrJEb7$h-z?2F55aAS;$A(C$U9Mu=^EI1%V|XKgUW04x~r9z)=COvcoZ;1$dlGc zR3D3rT^u1B1-D&N;2+}%i5E4D(w$*cHuy)t4N-j;xUs?4i(pToMHoD;U2^0(_hHN< zi0aM4>#(F~@oC@@dE9`p?0mup-1hQd&qqlEN?;)Gu|aHl)G&~Poc|7a_Qy}SPCwkFgE8Pu@5IZL zF?|>~eH@(k(Oy2aK`WTvE zqgq>})VE$z3|SMJgHhd%3Yd1cqe3jE-93V9k`(xTrcoKC`@^VgR9zmIC^{H-dD*CH zq=WCFMHrO>7(~%}VA!aR2(K1N(c%H%VN~w{#zxgBtPX+%qw4&G7}Y4;lq8FMWFgCi zXZCj8?;mX14lb!^M5NWl6VZ)$u8U7W-N;XJ=RKkJXgBf&m=BP{aMF$ZbhPg}-UVfo zrt+k=L^omgfI)3K5U>(e-q>3Kp?8C?gXe~cI-ieGn@3~Ln z(ET#lj?YKSG+g}O{v?*D*^^O-W%d0m(5X-1I(m*9{~2Vgqp?fbz-)UyElcZhdi5lqql`9kvHfWxYYplG&MRPb+4iZY zVPfiSrgh??4U$61*xK>iOnU+3x0xCWh)Iv)7zyvp_im84nRMKx=0$IxVr8!Bl7baH zhTmpd3_Mo9`xuXBZNmh2p`^gW9Qa)6XNKXifk7S_1X~9&@YrAgUtAs5i>O&7#GaGi zjhgffQ3l?Pdh!{0H>yrJ)JqCa9E$mQbuXg$XQ7lhJo`>8RGq~b@=2BhG+zA{2?;~T zZn%Rs(B*sli!Paqz6PM$!#{~U{fci5Ar zC^7iyJQjh+Kx5y`7ZP7Ghi_i7URF4p*CRk-p;#{%RN__HhwF*>9d@r>4b(hIv1l>Z zvFCxO^6pha$DK8}AO?tU#lyQ-0>@!zr@;0|3K-74h>z`>r1YSq29=NPe%>I*b_VKz zpdb>AkDq<_+$hI(7i@%bc=dHufX5^STfU8t=K)tUE+ne>{f;uX8BFpWN&_mWylDiL zH#w+m*{Fibn;cX+Y%-qNy~$pczlq~WHa<{=RfWWnPuV%|-6WpK-gb-u`!-=WImL(s z-*I6%E>l}+Djd6{ifZ_o__Le&zR`Ssg8mLN{M|bBII;xqy3s8RO!_yPRTocg;}RE2 z5Rz_;!m;}E$Oc#DkCf43zFb`MJYE`_hI4a1TDT($M++Z6&kL&C8FKpz2pyM0I$FjJ zJoAF|3}I(EKeFE+RK#It1s9)&X`-di@F< zN3Z2Nlc`=(7?g(R1$Y;8;7g>w^ius2)>DXH4T4)QDexBHaVlQNFhnndyd?-04;Z4? ze!w{7)QW^wkZ^cz-3mX(0d=#G5%dOFDvBGYWf>ntoN9c)CXb{mU4tx}g0Nhi^ z>5`%@FZ`gH?%U+f1-5#qXli2^?o}y$Hi$mYhXzpVN~2b@2yfQ9o@>S8iwaI@FCZz} zJrx}AMTG}|;|RJ+$aRteb_QUGpx*$-!` zfY+6SW|pd#1dSP>wn7=E#xDtWkz1sE*}Z9png{qIeCDghG|&a_1K$6tF$>7ecitup z#)dWVHH6KQDmn+bvyltXQ0$kd;%^Dt=Yv>{0z~ce5VY%FBTBEJLEul;{Hux5N-LdC zN=0~%-`N@}sV^p;T%&}q)As|vxjTsxMoN#SOUf~K6c(ZBsCF;z!jZKB>{a}~82n${ zi8g0Kbjk&>^4yI!o!xdK~vrz?``YL02s;oifRKC5u&P*3VQg%;5`Td<6KyF5?Jrrt(npDe6bNH>UQ)Eu7p=fn`|cK7 zosSAU7vL2JV9o&sE7>)`;OkF4aIcT&f+ND~CrN?FhfR1cIPZ0t3!Y#+<^mF>L`w?% zdEjsBP4p(iFc+BQAlUhUVJ`RqFrEwYi_A2Fc-{_n&pzBA$~;x^Fjq+ zJTKG;2mZ+&%nLjBkow@vH+y7WXed&;HJ75D-;jA>&>J!@6uyD61@P zrs9K5NsCwKS?BkH5w?lHG6s<_Uog3-jyQJen=&t~m5!{JG$ayU^d<{{dn`4=0k0$T zWyyn5;jpBrh)7Np8+AJS5hi=e7BhvS@UgCp!KV`yq4f(+CmJAB0|h2{&m&{j=|nej z>uto4!A1-*Vn?$}HKt<76iuCpO~H}aBX1HvM4WL4c^e5@I-@RG_tCdx;X3OZV1E+8R*YsCMOEjFL66*j!L9)X>27d(kLL z9kht0d%$P_A>|fc2X99MIl_3lq^%9y#-J~`1r=1*$&fWjih;!&W*k&H?Zcu8L51wf zDjl#mT+JxkCqX60t_LB$Uk2bBg<2rjX4P`Mi<4k~M;W`m??2=~}HsC)|;2bE^wfGb%XR4#o- zf{N=M2`Y&tO1EZEsRd*~ccSOsk)ZM}ZgLX`m7EeaM!2}ZLFG5lIH-7qgj+wX^FHs2 z&a2;r&Jk1^1hWV_K~SlCSAt55bmW+%aBFj+v=t19gG#e-zs0H{&9^H3y{#L*Fx>Ch!SR3m=jFh)nQyllQ7e8FUv+N=J`=U-_bVCPtBhL%4$w0J*2NTj=Xb9omsWps@3Ex=Pbljk$J+lL5GYVI#8 zR~Gj@k#f^>KNL5e4^$k!$n!`M5a4wV-hVWnD;hOyb<9p7C zJ|O)L66W}^Ou`XUwnIt6uUcV_zX2q6$8upBkQ5w_fdl6Ft$?vR772$xLBbp#cSziE zz#(zRT1S{W&H)7Oc+Vkm$4q@=lw>`$_RO@+Y1_7@-P5*hThq2}+qP}nwrzJcZ@u4l zf81KDR-NRelCzSW>}2m}KYIpVfa@|^y1y1h-sH?;3vv$BGT7un^J z8T>kxNAfB3Wzue>gAdt7iyqfJ5fxs){h+6h|Ej4Q**&7rtS+K%g_9@<97(FSE;dUc zu5tT!f5B4#{@7Q!bFzZ}+WtmpTRD_=nn@>1H zBxc3JVju$kCduA3>25iUuK&^lRYyW8z%eqtOV}pPT3~vjMpHfeVA&X&z>h)n*42Va zvnFQ=O$yC!D`ZMt);dP10$rHA_~h^WU6>x?_Uz`sWEZ?Y21C(Cu8bsjgYYrA(u7CH zWRC4WXl1;3(V%T}fACImr6tS$$%XypQ@^YfcxhzfO@;(n@nd_el0gFcc0r;u!R?bd zAGGGNRAheH>F146_d9H}&dTO{b*^dO-7l&Jddm)637%h-k7Bcd*UPF-L$F8g3Y!abV~dW-9{ctAo0uR3CPp z#ax|eE4z5O%4C+*(uslUP3|vx&RVNq!<%C`VBlP7>be99gy~Orlm(~Ml()w+$ItXe zQ(00uH6-KO_eny3CR0PfkMj#K5QW+@y5lKYo~a{AMcag049u>L zbReR(RGa541BnXfGae+ji=mDyYyZR$!UauMKi(ohO98BS;E<)Y!j_4jR# zsSSO#i-(5`L`46k%qFBHVR2!)nBzplT>zrMdIz-}J4dQokOUWHOimG*oo8{3>8OH% zL*V_bF@y;0&i9zM1izS??-(S(^{uhTcaeRl8>3+&a;`M!5868;UV7wfeyCs2H%8fj zV32Vw7LjF>PGfbAGsp5Qsrfj9@8hgVrGh{ZV1Z;DLJ6}{rL7G*^NNWFnd38J1{6#9 z?HMIeV)na#hC4_s;R+D6CTHGkSp2XZNvh&JJ){y8$iqLyHL=zDb78uLVW>(k;|PJr zFA&RXqiXQj0EuQ*%u0|4Zf7QXPildo3pf}md@pJN;Q`Has!|-3Yg9RHQR-uv;u{xa zrQ?EM5J~Rl*BUG%BgDM~rDkfZ_Hvl?ozOsdmIL|1iw6|d17=LF0!b^fxF5R~NonUMkpQV_^L!Pi5gS6N zxs5M$V#@$7P^4Z?A3R;>Y7P+am&jk0#~0sQHjz6ECoO@d3ga~O zXGZOm!_)g9E^B=Z=O}BeGsj-C*Y?`zYk#&LL>Z?svxQ2RP*KE($$OE$^;07(R2(Mt z@)ZocWV5kZ0gl5eeamr-zCOVXonm{8Ak|P$-?TG9{8d@YVZBBTzn8USMmd04P+>QZ zWP3~t1mni2{-pHSwd0pdXN>sj7%Eg)3x(atCqW&MTBvw}6wG0}n&gq}+!@t@D%)TQ z8sdup)R{>Y|F=t~kJ4tHy-X_CHEkzKdya?aCG;`BR?2w6QW%8)fnn6t_&IO6ntOx1*` z70nX(i_gjwt7x2fL2B^5yR40|Oe;$;Xl9mc49XP8FkDenYFxqt>JrMpqg|M2`#*4E zYZJ;0k)4^ul_kvA2n3jQKj_lOhD$57!}U0S1Y}LJ`i#n$bcB^5O|F3-OM);%k|F=m za|PjLRw2k`O z->}h`-}Sq$g=ogV9f?i1zfnSxl|@!dkNWxa8<$NIiLAIsD~CUZ^bZKV0XAq%ec3bm z2TY)XvY#d|PDx{phXy$_gomZO(c;h_1HBvWC`k;L7dA%mb@dErAFl$A@Z=Fc4($AcKsjzb8X(E( z{Ra0)J~c>J5ygLcV8Ib)I)=3T;~2{>c^kDoA0h7nNvlZWMvTn`#)4>M| z**`W-5v6daiM->R15N(aAG@O^%yAa!fash~Mra*~VV5p|CavAsnal`D?wA#C%EMrH z>Z8u$gg{P!TNZ~(ls5gzgi&f&uW||p1c%mOKoDOP{x{}w9AnALx~v*}R&$Fd6cN=h zFyy#Ms9^(RX!`9yHRLN62SMf|MD&3`6~hiJ@_t{Sne?aoki;-SH4Aj`PC!t2HUxM_ zxfxCZu80WH+7bC=$J&x}3+8SN+t$}Xj_>xSSb=0ZP!cn14V2JzB_ z=Bmza9R@?R9Xu{JN0(s!bC87lILO-fk1ZtPR%a`EVZLV6Ir0nqMCtq{g zlN4b)=IFG!szIown<;fIo`@lB&e{xpm6tE*4jQ796K&*XXGZnD;!^^9x-gn{S4s{Q zV~I`k740b^x-}J-wwX+B<6f??+o3=9fb@94n8g^8rTj zg;%7>2AafZY~|%+z%kRV@^kGFxuub{T5rN9oy}8BLIC%rQ?-Y?Y7}yCnEySg^;0c7 z#$j(?o2!~G5`ngY8qv-{PzdeMSRR^p5d&nK*}9Of4P|Z`C`(mfP5lOKg?d3;wwL#c%aArzg}n#Pm>y~%1KrmmWSGqndl0ZL&8q|5ppiV2)W|jgpZx=z`r?y# z%M>J&iI@89%upL~XoOwi8n?9rTSEixFNg>eI;Av=kG1}I`}!@0f=;BZLi93tdQtO58N@~ zn<=0h#0BUF`q0YCgtn42`aIUJx(0RJP`+6}qVp>KlO3+)OYm-}mftcW!QMc$2K+9p zm&pXP!JWp0$mlHFItC_5RsL!R~m4Np4d3-Pjy%G}04bYpGvd=ko$GxqERtv#83xFw_(fc6QeUhUOv6Ohn7x=qAb+|>^K%T3-!Byp_EVCR7na6;E_qQF2HIiIu{>* zST(Cz`+(F-3oLFpnyAtcqY(6SH80WI^&!7=f5<|WCDi-Qw_-0wcMADwp5sw zjt69^Eq6pMDQ)uaukI4}DQQXZ<`4fY_UE)oIL{)S$56f5vgq*HMC^!Z_L@jE_Rt2C zC!1GJ>6%d!U)fnc)fOMW;r!e}d_TsZEo!Y#G~2y){R^QNHU_UTsGBTKS3JZ>V|`Jv zj*+{UE#VU8Q)u&&cv`G}oKrZdSP?;(U#4-4lV7IivYv)z%&3%+4$$D1N7M0}mlVqw z*UYFa=l(Y8nw(S#F{vnC&n#plA1{X=FI+R{d_CDEjf2m)AyW(_QcR5WazR-m{DH6R zI7IpL>GMYpULJhTmNZ#oTT^&*{HaO9xWYCCSdT3%T<959rINcSR2{#`P7QHGxo=W@dx;dS|6`26{{{owZJd`?WV09Dh z5}>VN5&IM7I?}Jhldv*5mnJx`k77*dFD7-C9q2tCS16|bq~h&vx_z>SS7{3?k?v0> zpc-t2`0O*pDpi1&Y|oBHe&xp*zk*-^YI{T~<8b=YvDC?Fut~sF&j^@bB!VDdkA3{e zD~%@Au^1oL8v@ZLI?rmufj zUWFtz*QFd9S(8Z1bV_! zW7l-lp}D4un%NhNn5%irF>|M}5ZyGlISTL$mA=wMQt?cr(HMoF6=6t8x+jaA`Ik5` zYCDgh6jg|viYGN8^P-|r6kBT%W3~_+uvHP=ZiX|Tx^EYhnY zU9rSP`FnDr0;2q+riTd{ZAchk3SUMP$OhC1G2g5io+^!15NVf8uA)og)`GK*M8XCh z6S|~=K~1?+UB}n0OdN|gqA5jb>tMg61+G9tRfM*hxw36oHyBM2rPwdP)+s302Yej0 zV(vk@my;vWG$JjWxD_iyEmm=5Wt-RLz5Uq!)86W4XrfXux_+9lB|4O!lDZ-Q!zWQ{ zKJ$-$Ktg$5UHQ%S8gU+Pxp?wW7=xgMnkSQ z=VCVbP8{Ip?UJgSafNH`I<7uxm0X*M&7xH%1^BO8>jVo4A2Ma0C5JvXOqt|w^kG~L z3JsSAa|(e+>WTY5msDk8?%B`GdHnrG7TgmTkY&zIg~!M6XjRuPo4hDKS?Sz59Cq;)m-A}a(8v&R}J0$WN@z_;5dnl zG~X&Mpx?tmoP2t2-kP6;bm+i0@1X7dzaINGOp}|yI$xb4pk8?uyQi*dnJFfBfjMqA zPh2Y;rGba~$Ebn)-XqnDH*&ZuKRz*K~NlSkCPvpVQ$2! zXk;sgbIhXB_!(S9xps?U#UXI#zzp)k->8SuIKG0YpWX=M;CN-(2 zcLxdT1J>R}gKrF?dU8L#T_Q3d@m0tWToNe_6wFRWT*2X{@fo+Z&8V~Ws2x_%npgJE zVtT&6xF%=+yNBhDbNkZlHxLcd+z5q}>4kF}a^Ocy<;A;&`${e+Ra%hMT>m%5rE%tX z{w!@}e;{PB7xhqfAUTatpV*TB-zObeNE-I28!@@NCtXIuHt%*Ws#kH<>H!P`&txfV z`;IaF!>YB+P$3-Vy~uPUpknLUGpD+vGh8(_4vJo1$*MusF2Y)Zd*S-d?c-;L*l#r3 zAjL2pEp@j3V@Da>$;^H$X*uKPJ!2HB?rX#hIXNk*T?@@~T(1JUYd-<>oWIoa>Mgdj}|MLyh07ZPGs` zxK>Obm=YM+k<+~VF4N|(Zkzavum|YyE7Q;F|7=d46tZmqNi;;cHrYxJuspHRI640y z+XO1Mv{83?i#S=M9JwUc-NK$NvlXu6&*uHAYq%!=JM}o@1bTTiSud)&YtEaoLQUo< zK>=(^XqfUEy2Z3wbEqagJ-u8`?JG2{EvSvTr1i5|foCVcflFw;mV%xFKG>prwgLnF zIMaW^feXf~y?R+e5B)g%*Thp&l*=TPe^6UVud9usB%BG_b+#f|IdAUBCdd|h-?8#) zt#|6`9VG5Z-hjJde%nq$xapa*x?kYa2&lw}`ef~NYO1)-Rurk?m{^z6%Tb#o53Fs8 zDj6B~Dd>|HrGjfv2rdYO17liHP$mJ)E{YDU<}C9M8VP@bb6Ie0d<(ho{sRU~p!FJK zu=YX*6nnDPjokkTT%--8+X7q{8#+6d9ZB0*0Xr^lR)xEBUMH^tU3RwT7Qj8jGji;h zSbFdwxBVZNSW3v6ST!G`~e(KSNskg^`j*>_H_KO21DX58qF7MvjXb@|EgpywE*J!5>FbgNqeaE_5iv z{vSU|3to$>VQ*=K51#Q=@X1Qz=YNT9-xXNmJGq^ia~9u?NFyPqJz={++J9)HRm(0Yw_YO`pf zpvD*C@#{#W>VP}Z&{qHs1|*0#LwiJsd3*0)DXnGdNNOllb9r1lyWDgf*<@LO00*iN zgF6vTeQK)wb^I?cHp8#zz&>B_e-n#rq_1-f`_2sHAP=S_GQ>Q7$X|Tyt~AJ2$b56O zkFLWTp2ikLuf)jBy%ig{Y)w5DP$1H^%102MuZ4EA!(4TWw+Y*o&>3^TLqD?Pcp9oxk_cJhmMQ?4ID z^HUW{s$j3r>{6tg0AlZDKoCwAYp(<)N0{=AkmB)bX43FZlDB5s2kq0=o7U00B4+WY z>Vll$xqH|e6x+`6!$lzvMu~jnNDiyIi`(&O8<3#GO(T2-jY74F0zBUIOc~y%S60KD zfmZzmaLVNQprlrtyKWMZylf=c~e! z+2c}f&tZWTfll9_sR&jLp*DY~(b`A^*2K;0vs{E1lF?{`W$_+1J|mTD#O@TrS9{4r z;88DAGUDe?7B5faC3B-&+~Jf%yL)GBG?wGuJ%e&9b-POaagOASDSwj;H54G9K zLi9X6uW{ahSpS8WGGH@!N6qt9i40LQQEkygQdb&Vup+!eJ{vGhNyfzWSC&!prloNy zlJAFPdw6}zP($6ZB-Z@k@D0i-bpe#oK14`%@BJ*-Ej3ls3^@b7yFd{*J`+O`Uc$1O zL4IgyMzVVNm8)+lc6OFA+%m;N@Pht1_<{@Y@_Fh>XrV}>s<3o3EA-7L@+MtDPBIwA zI7y}bDq!*%xdo$sFU__~c!Gwkyk*yK_~I0UFGj`foIM1uHW#p}d%Fd;1fyHz5jZlW z7~WSgb_iwfZjr`4&BHi5W@?#ej?Vogv~w?4__mCq!u4)-03{^bT*1;&5$L|v&g^$? z=OEi;@#0!Re(@}kv%@ILj&0>ObP9{bD>(7^eko*{n^a|1G>9Kt$u?+q2)h)zV|rrj zIu9XBG8;;WRn%9X!2c2KaP2#j_fC0{Jc2UHuB2T**>7R#mmGd_k?tx-Ao6i+$PD zC%Tav^)SP*`4fQDZbultd9vqi#5{h9=klWjx(#EWd?q2hjoAd%S@r5#?$&m>kDO1wCrBA(;81S;E!NpHG|Rs{z)|npQsSt9URKb7UKL z{Wf8RdLAa4RyW;Zg-_7)rS>=*CBtf`)Ia47D;foRpO+Y08OKsd^57CsrE^dM`!Gnm zD$miyp(io)`jh^=8X(yg5=3E*gh4b%{UW+!s2y^_k2ZWS?t8XVi(*?tgX(Y>?{iku zoS$wND%7SBD-hw(3`4LuA7WV{`hDL*IaF%;s@NzsSs}A{e!Uk(eg<&Yt6x8}Xo=UyA zzZnb`S`@5*(@&!&)+Fw6;xc#U{SQ-OpyE97>Z{_hf6!A#WH=EL;Hb_nViW<7x-;vqzTZ}ReT#uW(oZ6w6yCCWf(cF4o^_x)r55Jyr|dK zEN3325;!c+)ne^AsNkUeXjDjp_O=&GPe{e7FS@@=c0hGbhId)R*y5Srd&odqe-Dlq zm;PtTO4|@=RuvrTc_w7Yratjgl~xRpwA48YqbAy`BIV|Jl#bro2eWGR@ud|uAR1%u z>LNWD!9zi9SaxbprxUVIhHB{T?!OV85s0KfrAw4BfhC?N^MM$CN+7etpV%sKb_|s| zmI)`ss@Gd!FH{Z}n_a-^eYM{jG?pjMgcat`Vi6c4qNy?BFe*WBYxr3lZ@B}j^4uV? zHd9|;@c?JcH-W1%ZfI=V+-Q-})RET1$-j*AF^%Wgm|ekgco6jjh8=1lybHucMSKwd zo?t_K>XIs0d}l+98rk5qxAa4|)`v)~GY5$~VTxjMwj$7gf~}~E8RZpJxE&01$y{&N zA+wl3DwIFSDV@4l=mHYWmUeVn6$cM%JcC()~9)HdQVMf}tjO^jw0 ztISPJ!LD~hTYzh1qc~iQ3(QatE>5iOUpQD*0&1m>B{gJ;G)iz2u;(#gqpHj_ZDJII zk<19Ll$>R%#ExC z($FC+)%1JD0^1gVa%1lEC+gKf{wIY&YR>kFVxK_DME|>2^0jUX(59e4dwt86;xq)~ zygbNB>6rqfyW3BM%EZSx2)A0ZII0n&FjyUf;SO7~qF>5Tmvp0;ULJ=k7$h4c=^Mi{ zJM%Xps98%v@7C!oGT9qVgBc_avC_S}O3hf(q8C=BR|;LDt+2NfX2P|}iMUJH%!35% z_)|q{^9xRB%4ytIKHJ_1ME+hG%lM<)uN2Z}(1yhQfC@60RY0!?*$T*u5=wuFck(Yq zm-e_-0@&|%4o1yqHx$iB@&2^DVQx4ZIdIJf|4$5A7x;=vIG`|G~Gy+eWTjC3}3atl2Lo;o=^BUOrOZ?-8bD@Ax7-< z^%@imB36$%4FJoaDr#Un03dk{ea$s@a5}Zv$D2xb%HkZDnlKK4Gy7f{_mmuBP*bwp z=BRY+GxGf7*9Q-s?EUyPX@ipk7SylJHm8selw#j`RIdeij;KX9z{b zd}`9}b$S2&L`1-`e)f5hSa#2y8EV~XFml3e*dpE42XS!*BJks`3=WL*qz*S1bpD|_b z;r%Yq_CJ7$oTLx4qe3!*=f3yiQ>xOXpiE?tB4%b1keA5=PCty!Gq22inI&QQ|K?vH zb@&<{%4|^nkJ=&lPmX46xMuPHvKy=?t`ffhEls%B^|3z@ZD>Ohp%b0D^1yfD@O;OL zhJSSSS&%+m`(PBXyMak_fYt2b;GFnOTy6nLsn_bth?&Vd>C_ z6%6w!4&gir!91U^ziWTX+kNvz8i0JnzJD8}&3z-ZDJCAjD7{^}SFiY`y%D&4kdFpZ zFll2XNrvl8^3xlE4`A(316f;p_1N~GzSfTFw_lPBbBd;^1)bwuttd}C-JxpilyoC` z4}EYpQ%sgpOs*%ojzo0R1>gF$W;0}4+3AxfT7_XMY~ueafJ&WBeG_DHw=o%60$9|B zXYwOawq{yC4Ue#lIbWeROyxO7wJ&=$1$go2Ole+3;S2DoW=1M@ME5+Ktdd1O7a`NS z-ZI;Uw>c$2X2(R^bsg3kDDKxifATd6?fqPSgs$vA44aaT1f)jb@NzSiIT6dV=aGts7gOJ@8-GU$J!}OB$`IB7 z-I1wkp;K;&Tg23;ft#k*z)n+}HrcA@*8_wMHlPt03IBYTH64!(T$PkEJ$prE!MTQo zNASNDuXuyWyQ6)@BHiT+UBZ;AlCIWHOGe^38aW>a@5I~}t*ZKo2Rx&P;Kq0d;nIbp=_8top+fxI(m2TUtY}n?V4e0LK-maIh-CwEz%syp{h940VhSx0p;tIJq_UG z5qY(Tz>8Gxr>7Kx}xChTHCE-sX;8tteldqlY<8IshE$em#X(yrh>h z7AcE3g1dLwx&bc&=T#tuWSnWK2Rj}o%^(BBbrB>yU16>GdmUv3JtuNhf1 zyn2z~ESix>PK(8 z{TrxZnIQklgw8u06qwU1`h=^sdcYYNX;ol{j=nt_BrLLrt+m=8)FiT}L)Urku4Kys zy|dT9rL}5++C39=vaL4=4C!9_wIUHN?~Ydvmy@)-Nl<`_SSU~Indo|(0hag}Wq{C3 z-!#NlBC;w1GhYfkO;G)`PDZu5^IAM8~cgu0fCT{*Ra&b{z8I0vMr4Vtv+V z6c9i2_F7F+%GK_*Wwppw`PJ$F-+(R@#b<|#`AgH#vtdA$GVmQ>TT_8DYbcC;-fCXe z>5wb@-zTZtKQSZ)+dx=1@EDiwclHHNj6Ew-PHZGn#|(qS)FA6LkTETgt*ia|Y{qi280H#pJKL zo9)Ut)-!?3fj5xGL;!+LA==yO)8-IqPwt?myvVtsCs+ilgp1qdZ{zk1+0=8_fLV99 zgSk?_jsx==T(OqT{cUh$P$6G6qF!% zM!hOODgmdz42~AGICj~DoX!K+7$7UEJkQ`g$@^9+f1Ulo%I|O*Yc!+2DvwQdvVv~!7L5p8Z^+Z%` z#G%Z4EHuKi)VwzjhqiX2cJ{jM0!=2@;lG8Jh+0}k4C)Cb1Is;U7EU>NN|f|AA0oYq z(@uR)kEq4>m|kfMN4d4x=pUxW+5Rk7vU3!?k5fUVNN_p8e_8=dWVxkv^O0VRA|UHT6+-KgM?UOdr>fo{mck8K zv*$E;3EQ2+L@@7i5P2e4?S#UlkI-H1M>-Z!xqJ+3U%B!drMTvzDMHr)&QX&v^<1kw zB6*|WKj=#7I=4bdKu^&bwmI<`yZsyDaUAU4L3FEtOW5?Nn8{sYr+NjAbqmz758wR3>w#;+Qjfsfp8>5oxqEL;9sxV zukmPt`Au4u{bGZTRbIqcKL(x@GzOxL%NC>jbZ?;1!=MhMzcrl z3k)@fqosRlX1xvyM22LmLty65avl7NZTL+vFv~Y1S47gk*;`;Nm+r}t z><0oW8F}KPLo$|Akuc}Wo4?`&1UR{1n(uV_O)De0sRBGd4ZKkwS%54ZRH zzox`_WFl@G$-A?LFf<R?o*4sPn2Zqz@7@*DxG1dzI`Q^>%9+MH@rIvgF!3 z4Kl3o5$jF!iAb=2+`+0>4^?IdSTrD_KC*|2(?Gd2l>a+}`Q}CFg?6FT=jlag@kQh# zqy5n%U!;l6ldoNQ;8?Q9*_QOFm7d?olZWoiXP&Zr1eAH=-)@DCYhP;Pk-SE@u8v+4 zN?Y@=Vts&$kU)`s)5nsxNAI6;YwF>4NiM;J7f<`h_{#;5M8^T_;%C53ZjAJLSCYD$ zjc9Klek!9!{KCd-D85po&H|Iu3r_x%XV(?;Y2lMclHT5v+hzL>-x#)?>!x>u`vGR3 zRXQx2^knglwriU$oGs<^1B~Aa{YV~yOCY=@#^8#a!uf0#=_8ver(2MS`733Ctn#ys z)n6+$lT!ban_(XQp~j#zc#W`zi^7n$EZw$JL}Ap9`XCAdT;Q2tYYQR5wL0kZD+#N} z#*&cJ;kS%}AK$v>6Gd5!gx|sQhZ`1hMALV%2at!9*=iq zcv%09fMG)Z(jM`Fy~ZXstq>~$b-`t1g@UL?D8+&@+hqB2TLwj8Oo=oxeU} z{SFTnNemiWz7Sw~#O?PoXAn1&Vu2khUG6W5{|-ihETMxAU%3jSG)aLf$?7 z1g76|85X5*PKPk>iqC@!!Y>x*Fz=(NTi9=3_Pwux-$?=WH5L(FeQbA=Y^yr}5$-Ls zTG3;G+;i1Vi!ZgE9sZ5o1)v$(b1jrlPOhNIo0057IjNq$w2Md(LMD; zmM2qLNF14^E9Qfq8G;P5o&$I2ISpP4g&Bl5W8wm+TkB)gt_%vE22{FM}~ zF!|edf^S16o2d#sW-ufxQTTaaFv?6bwZIJEXO4Q_sreGbQ-V{7!D68s1$e?3kPaxj z0AWZk`MO!7OGVa^0z5~H&!tHOF|ARMF;$XM8ea&1i2syY#S#_$HK=qC>FFVGN?r() z+J~^+hP88HzS$K+S+VdVSZ|Kad5OuIl$19=N_!9b330frW8zjAT)7cWxHShFmh%=2 z`gRGXfS?+lJdaew`A5A@3_hUSn~~-Z-*50or!Xu}k7eW^j3!+4+1vO8YnOEGUqPAyc^Ak3*5%f`T?pN}Tx(9msz( zP!!|N3JsNbaK(zd0VazzjbOV6*y&{J!V4ESf%f!hti9!>9~v9_Y`ifWlA*AetY)W( z!;j_=%m#ny4AKo2OdxLw0K!I8|45SP zj^Xk+{H;D$7BS*QaeyJO9W`wX!2IJzW$4}#fT_&EE!#=bhMRg{03U~x0G&A1T+sjF zc_cy^*LEl?A1fx$fsRi@hr#N0UK+dRF0GEl1sYEcn)qp!HbR>Fr z#ZO01fm0+xQl`B)iIhNwCX-!gm5ck3!^9|yDZu5-WNaYtMe#H=C|1Yo@;FHuq?ibj z>6WSYjc;oE<|z4Z5n=@U4jkVuP&$wphfU8ekZ*O*>AH(ql^C1etG!~T?A=lINlEp^ z;o7sTrwruB#c2Xa$1z8r3s&UAJJkiXA>HP7MHpRUfK8E-R^&2^Ohz(-pQ1YJx;zJ^ zL{C<`P3Z@rbVH?xhjD+qy^QhFc85UgpQI-Jn_zQ|-6QSgjj3wNYjMS~U>X~`zifrB z^ycLgHJm*6_MXw<>sz#G_H$CJTU7#W-Av=I0BN_O%j@KtXZxUtbdl8!5Y4K5j;kVh znN^M?mr=R4q^kwYbY>-+Bx&`FtosD!a;Qd|3NR`e!^$i?qqgu3PbF_2Fy2qN8rp1} zg;JaxJbSR|y2}k*xy?R@r85)#GjFT;?|nv+Fw*pkHD}f0JIS>x{$N@qZPN>RzUlNh zW2*fNoAVH`9OaJC5|qPp41klSH!55+`dT)-Ovxm#dgr|q7Arj$Q3oHflib{`}**n`|Y!>!z436nO*d} z?YTv%bNhl0eb0C`_h<3k#zHluYH@QE+q*s1Z!SC2TVbqbDpl`yB-yLh%i>BhWak6I z-`pXg-g<_IPchzes-hhoO?$kSHv9CAVO+#?ZdzBB$iq2uQu$c-;g)R8K?zQPx#a?y zaVmIbHQGB_Z|XV3!}UQ-R5o_~8VSmd7|5?xBd_1dUZQW>hdV_S@8dJ!g4S1kXKBoh zMvESbfU&#HiN_gELi70pxux=S>Zv$q`6Qxlr*KS4n`y`$tI2zr4Gvsk6Fdgbm!3U3 ziRml6aESn#gX?uxa8HT=*TXkKSk5iswT!K%Kmc9h)~0qxALf`U(b2lL$E!Z(Sz3&q zH=e$UPRtlLm6A?WynTlyz3fLeLl&qTHO|>vR0#42Irr*Qpc}fx$5l07x+`xNcC@o# z@e{5Y_N5JXdN8N{?0|Wxsa(D@IZ-z}&e|3+eFtS{rZ;kty$jU6^WnjjPBO{Iu;`o^ zy$8wp&5WRv>)I4c77E_!QCJEJS)uio^TKG{YgdO$E+g@w_X;Ji+_u9DWorv@{TW9i zpn%yo!VoalVau5Z<141@wk0yw`F)+mCD*&Y{4RKK?u|(H)jAl6%_>@sPfbM0XH1^G z8FI~=k-lm_O9moSZf8ZH`( zqpIZgmCs5GdS0Qq9CW4Qk2j2XgXW^x@jAyLNug&g()KYBoIW~!q?)a`q^q%V%Qj{1 zQ8Sf1wmJW@-kG>oa`2P0odrGa4ZGoACP(e)oe64A(;92?Mna_u=Te#D)AOe;7(#`g zji_5oZpM0O96%r6(=6a{C65}5u=03TkYrM+*;=Q%!$ET#e{J7L*iC)2bCQ82?wSbf z(r%krlstCjPQ&OI`EO^#g(6LQmhoXlVL#n273AGINJk`h%gPp!2E#y>gP&G*zavm^ zed+y6-f&ITheuH#X24C)y4e>8gtmp-?S#^&`2LljL*jx(qWR{1?J6yCi{Yi=e8KHJ z(>;ht+!MHRnWJK_WU*3VY^+x|(5M(`BC~9NE;{Qe`-K}s<#Fjt+=N)%L$v=Wr>2Jz zY<1qqITLvbcpCGdhdWy2c%o7hyqF7=EE(boVg3i8RpeS@8cke|?x71~Hf-uZO4UlDINYw)}lAXqOa{#{}J%#e`X% zS2`TmoW8YpIxUKkJ_sf49eEY^v8m%>Nm6UGvGnPX^SulDn2YBCv%%1Iz-UyirSp;x zMa1GX*3p1~a@4->y<4GUT)mR}r%f`&Z+&ncP%dh8$?xhd*FPoCItiay6$!AhF2CJE z8QQ@zUJF|jNYCHpTom$yP-)_yyzH#KuJ%ONP;)h^LbEcRE@eGAsjCm1wns+?!Um&P z@S)J%54C?u>Or+vi2cpO4=~mKPTpK}*(&U8`a*QE&M)mr z#UqXQTd+)=p*x^hIHS}&dNfcdHyD3r(S%ojq^+lz*gw!MYUu#JaAcN@;PU+dt}-0s zS{|h*P1Etr?4Uu@lp3hUu1CD7xO4U+qmhuH2`iYK)9!o48RHXo!eRD1+$KM|IGsb0qmY}Xb|Hv~%1EjHFV5C9=ysY0D zs7i`%U%4E#glc<=S4rQaX&lW!YR}B>EMmD0(8@e((SKK@+A$~TYGIbjjHDvqiV$xdAgIr!ySdgRa>yE zu=cy)sFpp@|7CrEe=TNqMOeXu^5p(pY&)F-e506t53e?xRBt~ZdmEPD5y3)i`}epl zBiIIamkaXT>hZnJdk_4IyZ~4|rwCSX(c5g`n=0ENa(ytn0Vf9WvhI{&LdEmDTk4Xh zj3*63h8y5p_UDlP4F0kFrAxDC`G>j;T?<(O#L?d)k7iZf#DY$N2OE#2v{ShXop^G5 z&J*$q_6M(%-Xh=AYjrgr#2~=6!*12~A7y+hLsV1h@6$>k;$iM3J7DK#2(Nn|rx9-r z?fdZ0Zntp?>@sTXl?2R&i@*0h&AhgI%>AIcon>Q+))jlsLx8}FzFfV=_eG`d=zHqT z9~7%UD9_fW1Lg%n9gkNhAA#9xry7k9OQxUwxB>0cT3t7l__c;VU>KS!xE|EsKu%_p z8ZLC;HOq+<`cxZBbPG+A=!cxhv9DEFUbjIZir zpe$n$hJeGJqY2HekOdR%lnUv!&NmcPM2;_`P1w<#9Es1m!?187%|sGiY@+h z|1*Nazqgo%Qt?Kfk{rP*pzyHti36jn{1nGy+fJUvN=k}z!vT*L$Qk<|08~J$zfB;W zEa-lM2@PR4HWF5&tCECV9g!O#Uz?D_Am%7*Rmc!+VpLR_mWam0R*HUqp)mCYeXfZc zoF4maQuR->3l(#)QF(K@Usvgtz0{x1@tdyQP^y_E;3T5^RF18&mVNTt+;F4ze6YMZ zi%J<-bSXw$5on`9eZ*c;um*H$xMKhRbL^m}OyFo;?3~cG|c5%6L_WRy%cB z$;SPI+)w=D5~stE zxAQ+ya_{!#B^YbvR}^7vDgRp$$Mn9wh~s1J8;UqSsNPt_aSZx&5yv_GZ!F=My{|06 zm^)Zy%&MatGxt>`YRubLmtY)7zo7){1`no#OB1aL4Ov zYwmx&ggshU-DX4e8xZTmPCAFcJem*nfg?(|GyQU|Kx^(ptL3q9hM7+KYQXHU4^9e% zpjq|tt|gu=@IdfkG8e{PvhNNJoe!4>KBZ9Ays8hJwnFR>AoY5I^-2Wd5D36aHHA+HB%BURC1#JXK-iIwb7iY> zdRJxIW$zIYsXND#ff&;^~?-H9a%(o}*YTw4 zgQ_n=$7$)`&*3iUKw}}HE@f?KrlIAeIVKe`S1iI$VQ{O>D#>5U;Ap!BGL&%{QBSLu ziYe6NDtT*Ruu2f6yxOo=zP61@nW?v87Wc}FY@h{<7*;NH9z=`;J*enTFsVt< zPiA>CcGUYpONW}D%VJZ=fl9sOz)SNZl+s&Jq4)h#-T5=wk%=hTOznE=rV$sJ8uq_v zK?D!L&9h=>h)Qbp{smm_y`qDRx@~D|IyKAf=`UuPqAkAg56d&!uVmO#RUg=m zdl}@Uv-|7A;Wk_CYeg8HIZ+zVVpg{UqlZ^*FqZFxBDH@G2bMq%k48&1Tchc`zYOz$ZE#K1%v$=}{R2qPH5FCc#+m%X z462iUdU$t_%I~WBf;xP7D-95S8ViyhjqZ|G+7pu$B6`AY)eBIJN{yEX8t~0xKW#Zr$P+-I9~gO45x#tr>^pP zR^d*&9fF1IO>N1@*Hz5o0MvP983&k}{KaV-^PWPdzBM0PCMYW_4p#Tmhlk^fR z(QLczCd8U_=@K!tQ4iBlvp!)*B#vZzjzF0qXo-g5`&&x=^SaK{5LhhWcn!1wfX0%d-Y}9VzYfiLGgrYN& zqa+xrCRizqPB=*xoZxb`ZM^;`3m1imj12*!0GIjVya8d2tsGynX2&+xyGnOT;vE!y z5k<@UCvurx>Vew%*!VYsQ4WEKZd(yx9c$a*CS$bWHH6lSfFt!*R$TgU$_*_obn(iV zsvq^^g(>)O?a}MJ%2)w?xY^muK>~pc`#w}v$9EgZiO~d#Z$lzye;e4x6*hEIiH|w} zN2s((My*f?vvazUhv=|ql5lm>yyvlOeW+E}O`RnB?&pz`OGfWnXwYWl3#QM@$Rm~D z!y*PEq{MpujU1t+ybztdM5(Q`ROzF(E)kWg*f1)?0S9s{sE`l^mYK4Svg3>bvN&Mh zjJlRlAI=e6UcddF5{j}CkwKJ|iUSk9ncWeT@{2fMxnw)gvs1?L#n{U;+J!xO1Nax5s@8mX-w272eM_h#ofW`KKc zZ)y(CPZ`J{J{V-XuZ}y(?BR<(Z8{kXySHYLWDh(OOsFaCScO9>BEEnBK0FB*bovko zp5_8I()i_cIz10Pt`$8Vbjz9z4;}&Uwmd_3X#p=R<5+mXt~`J0eu?K@*zjDEbG_ji zYKX8=uWWq!evyrO%LepNgmklE)2+55oLrLf5TyD?qmZ3vhYs5BH}qQ1?O-y0Zy59JuHJ=8IRP;oJzkb4$oZ z97&iGkg=OR`!K&zTAoJFN@}>Uo0y1N$#dR;S(SvOt|{`2HT1dC*c5!o#)LQU3EaAB->2S1Ff$;O_aY~Fj3r#s zPnb$ykaBSYGXz5Mpt_FhN6@X~f|Oo{Fi=kv?O>CBlq9cfAqo#eAxc1brTb3`I;UkD z!LTEdYzFq#U`B5NN@5qftO>Z6*_5`BmgR;O4=Y6(`t3zY@^uFEaTANC6J>`1%YtMp z1Jhp#$FL%QLv2Mm@X_5$vgS$;c-g)pm5l$pWd;NZ-R@;Va_Yl+j}&7M3ijsZx?}*R)ImY3@wdqVHL-MOLKvFnnW-i z=BTJ^0g5sB{DMt2^Po8)0`mwmv>k^wnYZ66324}JS`a?gO;m?O5~fsT_(4kWU`gEq zbUZXnB`1P%YXwt@2`A6M;hlH_I%!{t<3FM3>N>f?FeFM)Ir3oUG~w*z)14m7Y$fa{ zGlJI#2{?0o0j%Fjz_Ie`I8>ZM!AT1tJFbzyTS|H~*0m@y?4h`jt;NE^wiXLA9s4j% zo;X!}1i}nv!Z|ru-cg(nXFy;cCJ~&IDSEC*@iLXp!Mv>y^))90f3rl=i@p4eg6$A_ z!<=9mcQ|sK1aqVb=q4lO05Gq;h4L8%vq=dmZ2nql+LP=xn2HvHvo;c%!*bviI&vHh ztE?7eTJT>jv3a>$_u4O)!O7CV+SSCtrLgOYJ50RaUSasI-%P8|;Az z-ucu*5v%unGu(LO;VZ(!ajt-`2oFb)D|!(g&IAc~D?IFy33w|!d<+SA6$Z|(%{N2U zG%oaYe48_d!KqfhXN&`#7yK$#!s!&;e-<==FZkhPVmVRRRq4}dSs7H5w{2%HyDHrW zV25tL8Ftz<^G$1{jW_lN2x}+P1Sv{@Z6V^`i^cb&WN1G-H`zwD)zta5ZYmtC^eV#v z{(SRAez#mdmwtEX#N*326f}Z0o-#7=ZVl%P3AsscWjD%NZGw-s`DUQ?BMT7v^bP4R z&873xPvVlVzd3z<<|ZQjCaBtMev@)hcfmBms0d^4PVWK;1%k_FCq&o{%_=Q=(2 zbCr=INPIUyG_7N3I!)q|U@!H`PY!y|_9&|v>k#Q<2{@re zM-{>;yoes{7*wF{J<#%y}%oT6A;&9{0E?KXQ54~7vtWz)YtK{3|e#Duc zQ|3GjR~%>TvV_Lj;X&D9M}*-oF7eEzPiUsTzipHcwKc8Yr_b|#0SrpkUnJiY+ou(~ z`lmQlSkZfNdomHoCRo7f1@d=O#|~`z36QR1?yBjJ4Lx`cGf=wpT`9CmSLY$fO*bN;oLU0L|LYS zYs`XtWEM_xf?L9%ej-~KaS|s* zAX=qZcpX0jP&;V^g=nr&HAr<1(y9$~ngok1%!*A^(d1X8w-@sDdt_l&?7~1s1Q$X4 zi_Fj$!EmK-mX3V6HY3A}F!7QgJJnasOkX<-PEC?>Kxw=PrO3=ESvnIaAtT9E zbu()&jG72KI6YU=%Li)@IU&n=yd+dTA;ygr4e9u4l)s5`o54%Bsp`l3dgj^n(m zKk8MZxvu1t9cDaKr89sCQ+9pq=o7wyI>KEBYLz&NY);vwnT$R@#ILMRC%D)(U3k2} zbASaoj916Ho1|qbT%bsq(hUg-jYdR1aQXHta9^xwNoA1_#jXmh$l@P$>gt^xtmoOn zUoZ%)EaG3ih#v+k&eJa%)oLR<9nZT9_2gVCfWXr1LOc1CrGv|3e5jUhO5p8Cu*ut= zTK)=$rxV`j#)rCZHqA>EoDqyBJR)VeG=hdoiQWgtf-+oV}Do@|4-S z5k1>ZUXtY})?8L%JK@1^d{9Tau}Y%KiC-OkaMt?JRKB7{OL1~=U5%%vm(um(gij#j zgWenKRES>AycH*XwcAR_Y21+i%msH^22}9eYEs z*~}0B)K8#0A5yMGwV;T{eW2$Yo@_y5oLr5Dq0DLA;UR zN{;(YeibkHxDSVFR{i2BTDP5+rxPfCzk+05QQ$7n^-bDe0y|HDqp$d$0IiGQ zysIQzgwiplZFX8x>1YR-E?+rfIxZ7|w#;-qeW>WBlZXups9XBeLx^oVnJk@_XcHJq ziA$=Ijmw;Is*Uw3RJ+l3De}ZL3F*C)4+Up&)D4niH?cd=P{iTugBD{9SDMDIo%XsG z8w;0_kd0bCuK{-$;TFG;WC?c;BPBc_WOnB2s6A<}_#7F(cp)6>ar>1FXkrp0p;5KF zJ@~fL$@$I-j#vtf@+1to?C^j-{mb0;qHlmy8)ZUe4sm#=q@xEdeRk%qoMjb? zvz5pr?N*j#gek|-L{{|P>HR*{$QLZtw4G9x>fDE| z89(JkKlk)jY8+ECkJc)?p*KGW0GIV&>Ab<-q4|AjaBjuDYPzjNKM`#vqxX;@)pB} ziXNs*${qRHHdI~eJ(OE$8|bs4;&ws4HxU=y&-TiQA7Ggod(1(Mo4>-ZrS&FIhC4LbmbRgsDOG59+v_ zvry^6+OQPwfM6PGlN`rG+tvnSYwsD9*>LoA72Bn{DQG}eTv*CJvVt>~i&hFu4mLG} zAaYn?H_1%oAlcxW%e?isVLU8rfEQ}Ky?$*bW5?~97RapCnpVk7@6s(=hO|8T%IsG_ zM|r4RFK!I{vupe^TVgl1n%5|Qb#sFr~-DNTk7b_ z@*GeKXrS26+@^27rLocLx(HoRDE%_fS8v_u$(xQVfs7Rat*g~~A@O;5(v)h8)SzPW zN(4^J!qpcPH_w49CMbSJh|bi(lN*}W0Y!g=^3st6)ej9m_`@d6kCOI;b5(p46J-JQ zQuIxPf$~}ljHLnhLZZgvK)EFj2G)}dkrQ0{lHqb5AbTqKRI0&gG^7Or9@ScffEGDH zFD2SRD&sopTM?wc6_TZ(>U4boXV28?frW`yaD|0wtoADO)@U!mdx2BBGmh#}z_p`( zob?Ebh?(tGznpq-^%-i&181Uei3CX|J+g@`lK`EV{et92pQ1dpUx^-l0d0@sG9?Uh-gdN^$Y>Xuak3Xws4 ztpIhGKz+rR6mWS7WRzd{f?DXSt!SY?Pq-vn=qC{jN=&wbfHI3?1MN;NwfWS?1 zjdETf13Th?4M%2?Te@dvR7PLg(A+jvFCQ|&VsFn>hx94NXsu`}xfNO(x932*bxJDR z@og^&Mz>>a@fx2`3X67 zE7i#5mNFJ*a9Bj@iw$H;$EcT$ljTv$Ds!YRtC7Slih*+oq(euiM%zgRaAsZl<^3HV zxexVW=~01C#h&i@mSUP=6C=6U- zOb$JJBHZHSVO{D&XX(?Ik$>z=^nnVTA_#j!eK6{B5g3Y}h%uusSIMGBURxl~6}>1V zr0tsYk`qN`0!vwE6p<6m(U+XlPQ*@PPf^UG<|u%U1ihb(+!;S}7rl`u*<~b8v%}vkPm~9GvITGNwC%X8BG_#(vFVT!zFu za1P_>4^BxX1U#~)7Q} zRO*6?;iF_B3j|ucr7Wp*7Xo=CQj0?lMMJ1jE|uW=wcE=V>>?1()dxK~TieY1mfYpFWil!g$;GgIau`(+SpfjEBhwiTUB0GNy2^5K)4GI;(A6HBD?S7M1%= z&)vC$%xVPdb~#+X=D9mhWU*wa<>vI;pSyDh9mbKa?<=_^c-4FGH+v#<@^<0jTT65& zPv22n@&?~lm|vqdA5{MC5_ltwHLC|~MZa=X5mbc}XaKDkp((9(#e8dNG1s!aCrd_cxf=eW0#diJgoG1=&lg0Oz#Jp~SrOf+ z(L2lem`@hY0^ROo%0s>;f9Gw_W^IWZyLmm(f8@D5dVKJp$(`uq=Aij>f$MQSXio2W z3j1=}*5lQK=FM-*`yk`yp!wnw7gr9Nx153A9yEVZ#xDoWPoII^95i2EhNpw(9cAd& zg1oj2U-#kb%HXYG|DiHG8TMC}SFu|&Wj|UboQ{Df%Y;|U@dJgOI9@phzWNL+O&%yeRU3L>xPY@^Cs`5rU@3x~d|?L$$3)RJ{LTwTW&|45W1EhGDBH!*_30RlsBA|mPbnjY5uEK} z*FXv0aWC!B_SKORD$z}WNvbgxY^uR8P%PF#XvMib@7 zh?+HiQ+EavbEDmqEWawl-i1;_)oID^!*k1q^+ovWGUPgKmip_=d>KZ?AiN}~7yW4I z{&k$|K#{FB`_QRbTIat3b-L#tg;BX#*D5v-j-$Lj_X8<{e>yWa6C>tShyG1jeNUz)56ykKLAM&^6;6sS7W!4wUPv%7D(k<^Bb2O*k)zrT(3hB#oLRyQ(zd1wD z+MTAn5=$N4(2bT}!@5Q zb3p1wMaDO>h|Ohe9i^7J;Jlw8W7nC~!Qfjm42-5pGryCEtuucz4_s&diVToYYtk6I zsdhy^kTixjn)DlbC88YFK{~yQ9b)MP7NunAUuRRgHDhoPjmGwsHeozi`TTEY*$@r+ z|8dnbCx*c2zh;azzxC}acSQIUUTxT`GpjHSb=6U|#;E9tcI4;Va`;hEA6Rz4UfyZqv4d`R9W!`(=Kn+IY|w3e;3dc$ib1!H78E|7 z=<>GZ?T*vjLARYrY{69zy6t%(PVa)t8*5I^5LQd>lgKnj?D8N(K9fN$M+3IcLAz7| z4z(y~XFV4a!<`efn?{j(MpEwb$U;C3af#67krxKtT{DI&ICIk<_tJC`8>^s4e-ue( z=C<^fIYGBA=yCR3o0p?>nWUW|beW``5#)MG=XCsXS(D7BIwqE&V;J-?v6Q2e27~+D zSG)W(eP8XQ<_GcOs~u;K;7a&v$5lRPJ$dv7!Zh<`4b|r(jveV9xFVC2pUwy%z(*xtgd(<^Q)z9^hA)jFdXSOz9sNZ%wpz2+`d>XmGW z-HCjq@L#YSUQO z3%ogoSSX?5RSU6WC$`w=!17mq5tbsYS7uf}C`0$@LDjqw@8+;ql zsA4m}6A+wb4QcPK=$sbj#qhO%$J_bgTcZR^HWqRqcs;Gr3H zuWM(iKW&ujuNQuxj`GcyFDU|9x!zTn+$#Oo6_EQlttE#1jeEG3goLnH(zWo*?>+ql zR7Sd>SDxSDd`*esk{18d67o`;@$o3od-tBsu9aSU*1s>WSWD~rI|X3Ble_x&7bM#6 zH1CfOlz{ujyG8QvD9pZ3pB(w463{jrTRm9@GNX4(jE-AI-&sUUlKz7wV5p^*6h{i$ z)_tg1lS(M5c!cgiq}7z7Q-O)OfhH~zlQ(woK!_m6bsN|aGs^Iym0&W-+}7)Z%n|w- zaYT6n2hS;OYhzRIfsYmplSvlrwFRJ^{9|zof4rcb!v&kVj})|MxZwW0ktmE7v}K^B za3Ml-YC<%l30*QLxg}oEYcQTu8-i3^7=E79Bh4M8OgTyYfID7Y`%W4 zG@IqL|3r!6qRbyE(M&BS|F8&TQ~a@eufT~VDz8bq zHK*E$eRxk!Osi?gJS?5A6ttr~Jhh>=ee5hHb>GN9PF&7D(>LwoZih$K)P{qm9#;&5 z4*v9DB|9>o6V;c#WY=G$P8X4RC}_sh{cbpq zdkG=YQ9jetzN)EaHm7)Iu}xxIQ7^8@A==k7A&+tag5JatNTu)1{VRyjXLTnx2;(`I$&Zeg3{;_HtbTgVNcj zyM`P__bgfTsk`}d-L`1KFKg*H#F~o~?UH(=tQWUv(J!g(tdN2u8cQ#IqfS6N#x9XA zI#@2PYnEmx(8^xYdJe)8vP_qgA(JgAw2R{RAHt?+IS-^9V7r`pNU%&a_0`D=B|%43 zPcC&sK~p+b|ChBl0dJ~G|Ht2Z!d6M4?i-G!sHKclKu4W%W~8)fiFAog3o`1oX`7Zv znuH{U;*3tkokfc}jtgpCP{a*XcGRjUE}&IVR6vR#;$HU!)c^B&&p9_q?Y-sue}2xx zH0RvUyPo$f_iVSKq8zvF7U!u?j98V2Wa}MYHc9vBZnfBB<$2ebaqc4eGMsL?*6Pa3 z&v$tlVpWZJJB+&&UugrUQ5JQHVWnjic@uGyF_5Dzk%jo22Ok<^;3<~CB23MJm(>UH zU3tZLmz&I>6D|2vR*Ywa<;RU&cI5Kx5-v1%uQzy)4q zjC#jTT!~i};QMxUviu^uYh0yQ`p7E0UHN&X`7V5g2DZIxDUHwTGUgyFZ)59a z-ng=Iv@c+0^~GBTmb(foF){_v>L9iPV*UxR)2#&KLX_eOj2UQ+kq3s#UFR||+Zs3# z+XNxd^21Xue3lOgtF3IV0$jhwkQXdn>N*ei_=AmA|Dc*DK&*yau0mbakF~O>kWVpz z$?KN7VC+>;HmQ_hmTfh|DzP;&U)C7QPpG=w1?45o>^)1F?qaVhjBVB!TxeB<`*E4s z305}p*cK*QHKHze>BPKZhFE=@-sP6{(rO&!a$}xBy2xsui_7iBE+uGU^$mAdah_L( z#wV6$V{aETvHCKi@1jd$ZCbq#b;^8mq~?oSn7DmiLTwThxr)2UV; z9d_ZHSrz5%uvK?7Tj*gO!(6@FFeL z8e#2iuj(s@*g#~teqbZbtIB?t9cHpD`qykQ71$REjjiStT%~35ECWD)w$`~qH#b%{ z+JKR(#8V+X`JA0j<*xiPOeEmTxDdP74$K=GfjY@XId7$GHAdQClx9a;Le5j|vek>F ztIUIy^-7PFwZ~Fx7*~i(m7%uf7dKt}6+etA827OyJ8bD;h}BDy%hPSZn`>nwpErP3 zmLq!H>saiT@}9R=sk|(|vNSOi-(ZIs`pQ}#F|lgoF$}OMUT~_ zqsv>7SL|ZdU$j)6=Skqv4{ad@*dPX&)t)B|KT7gSCzrdF`_Hn{yW+fxaq7kB7%L3! zyp~R1XQ|etvdZED`C&{bX8E>yp64PeSjr8-t453jq(RU#plJG$LPg&r46vKn}J-9_UoJq)pG zM!oJLe18J2u^Qb2Qd(HX7^`4qjMUd^qy&g$Vm1C_gk)mX9eLeFJdX~2ttJAz>WCRI zR`V~wl$RAU#_HY9>n`HSS@5x{f?ju#*Htu;F;=g~z+jOKe5_tZftf5r%W7ojbr<35 zC(OobU;|8fo-A>zH!Wbi6%u1pV=5}#r7X+pRSSG7$|@OS_15Qg7gd%QF~n+)1Q2ie zXvSEz=n^xAF;=fzUU$*N3bP7ZH9TH7Hb&)-XI56DRj<3~JluAT0<(Gn1g6mY55`!H zuDp}+8AUG+AMwa;Z(omaUI8*W+@PV;Y8KTed1d9PwPpjLHyLO(69-^ke!lD=tR~n1f{Oeh8@a$}HD{-wdBr$;qCPZd zwV(iOVh5hpG67KKu5o4R^I=vKa}s8LSe0$8P4zE*ESK;bLv%2uld7Qjl(dS*G9573h2hGvD^@1xfsRJNqHW%CTzJp30#pYnWg1=r-BynRiL~;fXKC6 z90a_eQhgNLa=izrLUSLS<&qCzURSY>4a-S(;1Zu@+hJEUURMRaVr4GLTh6zGC;COT z;oWkQ8#t_XY9%dai~&Sol)DqsEvLo+9_LoqHd@ZT0_Ama_pOg6Sx!*`i;sn?vtpJL zU%=t@uF{L9nG5?%JBJpzDiD}ug$3%vv)}fDE>G+-`mPr!{I3oB-VTdaRemu2M>`n5 zMo8RGb~tZ$n|`)K7v`HIQGYzufN4u4F=?zFOf5JDk^5T2LVm4_|4E8(&$$6P9ROuChgzCpL~9ZaIr+(WL${f{3g z(dlwZ=7|HvbAUZ*hsK~zS-Y$koY~MW?+vKy=?Yt*yQth%QCTkE_g30L@g;@40*r(T z`O6V9&{x}$6kxvR9I+^t&wb$0L){mS!#9jX|gD`45{ z?O=REp#FOJOLoY@6zI!#Q14`~Zirs7!zI2*_^K_eveXi#3~?ukB#?UTD(X$~Sgc zPnp+!UX|CAH%a!K-`ar-6AFB1hbt}g<`=6oD?iu)3&*2z;!yd~4p-tX9q%evUD3~W zNUy5|OR0$ceReo(&s3Kj@3+Hw$D90qvBkMcbT{y8FF1WW!Ee3b)VX8Jvw&!)OT0x| ztmPFr3a7f6Lo^fb)(o$&;f3%d3PGOq+4F-Rh=yd`W?1 zcTF!aZSA$a;JA0>aGBj3GHK;;jxE-e#C2|O$oyixfI6=?Xp#cg^@hcJ2b*<$Z^(-B z$tJk9H?XHn4G8AjB6&^*i^Is2CCuzT$Z3f$$X`1|be?gCe7 zg}Y+1o_kqn2j**y3#!UpMeefFs{HY;{0Vx*e!m^v#1hOFdA*h8so!2-WJlz|JMH+Y z@<~-VTTotLzIr@(U{sUcuHu48Fm4M9r|h`_X9=>fpYCsidC;^PNKkyL(9 z92iB?JL;4F7YcdMewiH!8U)qIEVskSMyqO)yJCEm-hi;ejzEQa{>1#^a!ba%l90n}ZEsjtdAT~e|5R^Kb9VXZ-jIn2=4b4n z#qKhG_i@Wb3hNu7Cwr^h>KKY1hCgS=7`yLXUN2sPadHL2`{(WOJlm3|UYOU}B4sdZ z>zU{tAUAz!njKu9ERD($d?V%^M~z(vrp4d1!||w30XNwJc{r(no9%#7a_Xq!Ejt3mIhEpVI|`+AD#aE%ij>JZWyL#o z1hAq|Bj2?{vN9(0Jv%5Cg?KZh(qO9{I8pHudpoxs7(vo*(Cu~P3#7c0kM*YTyTUK;~HBuBbAH3fDh$fs{=~8zXePWAC%yxcihbvBOLHNuLmoN{*b+2Bv`@)W9Qd#+g zbNQW@Bk0TCu@hY7rAZ@;E;|Zop{gfe*&(a&Mfsi@-LL{^_?9~T3vN+nOw^I{s6E_{10}}D%>2DymN4W?`TYY=&{5vy~B%K z6;)=>^lR^UZ!-S3-tpjX?vJ$oJjP_NG8X(z{AG4$2&8ksA006l{jA>c;BVqD?;Q{R zh4TICir&%SuFzNZjs|c2>Ts1E+>^9vZuS8HQ!B(AI|_`zQ{i*%;5-aZ#m}?D^VC{O z^}Nmw&U2Be`0MTP7_Xg{if^^Um&<+rsTA|=C@^v>@phxQ!Hxnlu=hVz$c=U+m@)Ho zE47>K2zX;}s(}C6;dyUnD*k3Wyo|S0g8$hO@NUgi{4I8PwZk%%;#NC~#2&^}qTB3< z5Zx0~@weOIIlQ|OEU+U$cvO_7O1Q(0fHzd7;_tM@vr4H1ci9o3O1Qg8XtN{mlofXi zjl1pOShp(e#{ZsP2+H%j`Se~p0xVy^id3WTvm?PYiJH$yrCMl5RfVAXy`=klp-Fj7 zOcnQlJ!wf^e$sxhMZJU3LnJnkJ$OI_@=b;N!^L(4g9ddLpShU{ zD|^NciK}hPN^yWef~~)|Hrb0$eHP<_4NPc0YsXcF8*f=E(!u(i9YKYw*j3`HD3{-X zf8Gwt4#9S4bVho5b)6kWN~EEcOniU#1v@(LWN!sl$8iswTP^>*Xh$%y&|BrHEH9EP zMeFUL-a>bkF8`N$1A5#A8vC*xHoq*d*j?dPvpBCD5HwL+U$w)Ry7KXvo{IeO$!_0Z zM^fZ6cjb+%RF}eUv?ItbE-S@;Vwc*4@tPeNFAECydhfW@p0~q}K%SORtlzLh7Gfyw z^(yF_woqOdSIAAhAj>9c%Qo90(RM19Zyf+o&9l612gTd8V!6c*CpUK}I@KHEC8WaTx0ZJBs{7SGCIy$DN<1|qwrSbjBlzFj(NtBHy3VUix^m=)13{-uy8n0}6a}snnx77Y zCTZp3=L5kPxV#nRWzwhn4uk;XCw(k)|A7!-no|?}Vo!i!bg8R2QFOl^7#=$DwNI#S zza1D2_7XAP@@jXqt5v2>kfF`6{90csNwVdZ-&6xhCAK8-GzM#Qx!pZC8aqJ_GA+N| z1HOgbM^snZaW3?ltqQAgl^xhz?~)CWCvS3bS)P31y~d7??}h$7&9!zkT%GlVf?2I+ zA23N$y`E!7goQRaEINa22~tWr57IBPmSG&s=ARE3NRPoDjXf zH!$yZRFep;cHlB^6<>A#`y!ZcM}|-ACfyEogB_MHSXL`mR(Jn;0~h4&K;5{OWhb%91b-wGkc+c;h!zk}kSs5jWAJ&?W8vDVk=hz&yvXuof&{~rei<6pS1u%=HBI%* z_9$C_pEnU~Jq`X4gYAtwuctC}aTBa<(qs%h^9%2#z_ z3}kh0onmQj^6MoelFQFhO&PcvD>u!y*;h$-J=zi0wA0EY%@#;Cb76gX1=n78y)P-= zdMW4n#$<3Wb;txu>+D&V$CI?MzTJjZ)>z56cd$Pm4b;ZiahCT{G2?NO`icH%ED)9z z+P+I17peEgeX@nLkBD)RdQUXm5cM^(4)$&v7pZrLVp4%#*^XDugq3geDyj+#@+Zj= z=A{;GNnV9ICaLfAu#A|z3O(6WRajPxtH4ohEWdY=1W)+@kX3!ptEiG6){!eNR(`^2 zs;gWcKDF`?Ft`{O`q}YOOJ-IT4{Z3u7vN*1HbA&r85paO0mh>~eQPxy1E#!ST%J0c zXITmoS6WssJAF&{12?HW!Ou?aq;iv=l?k$nz$%o$ z2vg4tvMLJi_>!vfg0j+LIknJY6>^Nzaneo)HH3q{P(yAw+HgwqDY2QcQ<_47=3IX? z>dND9L8Uo5`VJ+7h|Ya$AQ?7zehr~CN9O_J<@Ynv99_wHDk&uDL-=oi5%o9tn^Y(A`pr+#bY_SwL!lxN`~9W3J}fcOs6DFO(2fu*`JdeX^KrD<1)j@sv+vy zo=#a&KhaH{>68_zB|7Vwfs_>q$B0WN%NK1RI;4GI&%9z&!cD=tiGf%kPR8^E$KNv= z8loB3Y)!B^B{nk@_ciAZAK?zg;yJk`zIg4F@}`iVjOPtDV9GmCjp#oc6I8h&|BP|U z5;8^`Lu+UZOc)xTN@Kcdsl5^iLAgri38!R5iRxf&4FH5dd%w{suMDKD7|}QDlVl_u z3TZRr{umi&Cdd_8l5e%7kbj0Z9u0&V$oN|V-;KRC+#Q<~j@IQ*L5T$oDAnE2V&BCx z3xmD}GX8C3MMq0dWJSlwA4&2XeYH~pA-@>_-y23&BhkUT(YZ<*_DpMy9kxplo|5RC=2;LzF4e8$|mDCVGM1g7RRtX z>0BHw4vX>#`Dc{-eRcjQ8RHUOQiWnl7@km17GPCEyHKq!LL8n$<5Ud=O|n2ZLdHa9 zi})EDj?kn{o080K^hX=~h%>K09?Pw73e}Dw<8i~t3i)Rghi4E)15QFBFBRT)rsP)Hkz2Rv5{DyFDY>F@9&prRbKq!9dn3K&6PF_exhLP1w)Vz(u z@!*tjM8g+qiYPCW(P0=_k!ZMq=#!o4logH!8Ui6-km$9|=`=LZOm7&V9~ub7;$`~b z8^+GJQcA9z+u594U8friv!&F=>dNf#MPq(4e&}cD>Yh~}^fknYZcYk5jUGM%<6kiL zMdSJ5P#qZ$LSv;LC~2|r?WS}Z8mXm+*jqyb^ss@D)}nJ8ea*TlEcS=U7^`iDktRyE z(BpiyQ{z!z?Nsg^q-#=O`LffIG2m~GM0J~3gZ3|Xyf5a7`s)MDWW3pjZRD0SBJ(&w zeVamo+Hjpe*BvYN$MG_2{3EGdOFS(IG@wNukn~VRLexLqAC39TeW3<_S+vd{_17f= zL&w3h!pN%g#eGEX&(bN&r)pk}{5v#2&j~Kvi|Bd&L49i*Be})?SnT(_hHy;H1D~11 ze@W}o=&uX-LV0x;`D*>4xTQ8v^{h>7hA$G6jfu%I42#BCa(cLDPRg!SjTYE@7Hl-4nR89K*U&pValJ4ku^krM#p;H9R>~fa;l({ z6{1c31@12x(U^>fCygGTd#82HTQ z3R~O;+rlw^ynZ0v+-6YjrO{Dj%r>}Z%npV_4gOf1=5V^R%^>F}C>^Bh4N#1@+o0@7 zEzM_sC7cS!=mvf`=pKWzebENGk*R}7qB*1cxRtp69)p}q787Mh>2(7m3*%mcvS$S9 z;#25#=5WHj?52kp^A2OqPhj3*%%zO+NX$;g+?~MeWXw9ov`EY+jQJ>m`GheC-3M9i zz;Np}OPsPRoknslJwlWn4%X3aTwOiLq_jhWf-)bK!{&KJlrt(QD0ZJgIcEk1J%t~6 zK|%c&8kBQRP|&&fQ4th$`$B`#s)B;r6!jILo)r}IG$6Bsf(G1gP|l4(LFM-wH0+L` zpau6MKv-y}B3lhI2z>@<2u*vyAQtMy4+wqX0fR;V^ZODns(SCrEnvDK;aem z0b1ihgIM9J2erbA+EjQJAW(P%en8QM;#T~CUAyoDvML`*_yom3OQoc{K#O9y z>k)(4s*mvlBAt&ak*7Rr5DmB=g?EaqgiF+4G&ddK@G6S!QG;?e1qH1GzK42UV|I7C zGIyOW`N<||sgRX=sX(Rv=uv}425N|w3iN7|A44*RBwh>7S)%LlQjX83k(P~;%(!H! zK_ly8@q7$Qmx_QtowhI4bzP<@I~Lbn{o?{1``?~0D7%K%2(%-2Ac1CYElb#sWB`d> z>2&^+1`WS7Ix3gDyvkvC6XSFs?yI57Nlyk~G8{n?{=IjlE`b(qA1UpwMnR(D`e^g*G#2+qOviWS zB^SUIf(zgZfdZKGtU)8$fpmp{2cJWR^q|W52|4IP=?bn6qn^V6!`~eDpr4j`4alnn z0=?#)NCCmabh<+9{V<*0dd^7n-d76r=FaC);hrGMzKE`3-UZ0Xe3B@;j;e-ZcDsETL@?^~zZ zLGc0zdhi{1YL~UUBnw%2o|3M6ie2~T7YrIXjp!-%TLdW#WS$auPkVrqaL`k1SQpYf zD0_+kV?!GIro| zEH}O$Az6*AFvRa<>HkA++DRcnt;=LQPK7(hg#^8}9%F%!puR5|G^{)%$n#RNUoi4j zuZSf+o-r|f(ps48o2wlhR#)>}M(ia+j)1o*|9KR)FA-e>{z;D4uZ(qJCCKDVrO6WJ zIb;Vk&w&o;=9kdpM2Y4(FfldYWh85mM1QoClQAUGAJx91Tb6kaj1u2PTJ}dWbQ4LI{p0(Zrh|vaU=qC zmjeyy^fwQn|7%B(1n73=z4=Y_IJF@`R3v>qT>V9f-H0ykvXG!3-ZW^~?2w?qCbV$e z*DaS3g|~&A>_$OzHyN_i^KVw2-bb4a8t~tcplyohknvImcuzv3wl*Z_!ndFT`o@#h z;|tVZz|6k!^tS;+-#A4*zEAxH%mZ2aMbD7RBdtJVH~wbc<@L_XE>oM7x^(wM}uq8Ohb>N>Q>sxwemt=xh5RqKn=^ zTTFCq9|ZI&q-u~tJdb$Spph}85Y7)Hh3ZY$_CYvjy@w{5n-`fe3mJNNb!{Jn^AGRo zaF%HV-fex*2aMdtP0H$ndLfC|Awa z7&-7efLFyoa!Vs9gs+jCgVT^uTU|awdlbeYG0}1Wd~+SwPQe%vm2Q%#|N%jAymP@H;Q~ysj}v z@6nhpg?Ul)sokUaJOWJ51;dl`qs?L&dqHyXEH;djP6|(@#m1)_2GRgJ@g(Jz#avJs zKkPB+#FN-Di;XisN~fca`N&8JQq?Cv9GPh{ZmCKZ@V;TFnW^^;)S$n8qzmtT1Cz>> z9ibikEB=u|&ekQ~Nu_G7dxi9+yA)x#vyCz-X$zF8=^WmzcTh&>-`$H+8gE9c6Z z^O)E^B%O}?1c6J-9aSHv(+x<}PVv`HrANiskJIUbPYrT<*cY|5%7OZG*{2x%wIMG+ z?cA^5hpe>Pu%M2$GUQH%-kg*9l*}24oB?ygf-dk$jjPpPv{z5!Y1;CzpoqfrYwpKk zK}%G|C1{G${tyw=rE)Gs4y9E_1Z6%grQnrTO3A2^QVvspA%)*$)BY0? z2wD#ldUCXib?xRzT(Xtz_>*vVEzy%aEdJ$FgYee*q<}9Me`Zi_cq$oB%JM$xqzFAJ zM(j;To#3fbxlXl0V6iE~{S~T(^!<}!=iYQWDu^rus|0?*z%!SBr2>VP3$(NE0+mv* z9bB-P|NI)!|1|nw#9diaoStD-9N(bGk(U>3pmxst^EW8fE}#RnjmPt0&KmU$LX~!N z)(xB$K~}UWiil;6eL?DD%31^rWW5TE zloe4R%)4@VeE|^cI@nOM+7!f;b($e$u}?AtNm=Lu5gMkfi-CcxJAk3IB@scRUX%*q zT_tB{L{K}P@uP@(^e>oG(<2*<=>kEs`PS`7$Bg9m+FB%19j5AjtGjZ zmsb1)Rnw;l%IhO2=cs9dX7&*@EPI-uE%-5dnxL${AnR8+O_;iNnmXsyOcPYpSE%t) z0}o%jn{48%|D|np5YYpMF;XsPK45_EqP~JgHiql`c!agthmjFfD>3u{50G9rWj$cv zZ3cozlktFo-9i`~%c-$`{RCx4{j|vV>8*4sL#ol&jA`J9xr_f8IlQ{Hgoh9l`U}eD z77?#EPcr5~QtDgfq7<{pra2GG;IAeK{N1U(=y3j zAYL@@PZz}Nq6?I+hbdANM}M@2W`H&ZLT+r(!XmvEkU1+^&|I|~F;^gvUQX8*&gJfq zoTFYQ$_~Adiz5aYprEI1UgHKFEZZ4~BrWwU0X}^HmuaN1;-`{kVhf=V!Y8DQSwu^VL=Y zZDUu8t;>ZmG8T^#E$7vhmj`QOm$R2qp?H7X3Qi8p5X6;Xg+SfQ%@A1p7%dk=W*K@l zWVBojnPu>DNZM&pL9I$r?t{li1>KziBebH^n!<6vsaogd>AVlm;5OrBs7UlOE4V8| zD=5=6;J6bVbd(~PtNx2m0f3j3!+RE3>`AE>aK4}oslZ{1X@^@Z{=kXQ2% zTDjH~C9jc6Uc*Wrcc@nC8lD%lw0eq9&u(aLT`64WO0IL6>)1mDab2Cu6RNeQkhxOG zfV1v)&=N(zPW?rp&4c0PcRT2+Lj~ph<8BANa41S|Okc{}Kr{p^yO@(Xe@tKc9FW>E zed*-G;1VmJ1VVNGW>eYfE0fEsRVv-eN>?1Flx}6EXCTQlVXZvIyzMaUDVe4LXSO+L zxZ*cT{e=VjFi#}^m!oUt;ph#=sMO&{2pM6X4YI*l038uwXC9#BQ7C$NE?WejaW{ ztUHDXxky?v1P<&L>vE^83U+Np^|i^?Y?Rh)bOd*$Q)Gy?W+Pj(dWg1WBb%UtT|UI- zpf@>t`w&6dGw4mGq2pAi`=$e(?l4SBTd377z_ntL6a}|R1-G(-g~w@yw#wkMVx5R^ z&8;Pc>ki3v2XhS^r^03jhs|75$PRlUGA=da))WD|q<~#4;4v*=7Yle3Nsh~1?6U7n zfii7rl@3fTsx3w2M^fZREb^#7=%W0HWt{Z~?YWO+U?zD^uRE6eBSBxSKQ9=Rg9np; z>+Ix^S)>+g7V%=uKOra1_OCRDPK^5LQTEV)lA!|0 zNRS?77iC0{0iVfq=3_%ePvrqG%JML^>1<8bYP~9n^(<%0P(dSWz?RmtO9y2Ml0~zg z-^zP;6F+o$O?<^&XZQ$4-H8U{`fQ0ZzQ7z@)asbB5WUB&C0~jNR>Ks8lB)tn5Gahg} zAa|7M*Rk`H{kyEMk*+;HNtdCFE09*DI=erD z@ukrMO*M9lGGB7GzlN!nsWi7g)^H9UgO}Z=M!KaxmbZ$Ktnt@}qkfvpGA}}ouO8d5 zWyXo8g2LD6r&}2CGSX4(Kb7eXrwMYlz0UI+iDt zoQrpvlGTEK{ga>(m8(%!RjUP!J6%vtaJ8Uc@S}OPpu5iiv984aW^6}~o4)P1J=w== z`m2}3H7s}C8G>?Ibwg%QE=)Jpu$qM3#&M+JYdIUfhMh#iYofkdKRqK;*#TO|6la_% z$XVTi{;w(Irx*KU@$j`XQ6q4Y<3$Gb{WE60Ja4c=wsO{Ak;Spu@`g13MVOD+jQ&}T zYM0^%-kS#f1%dpZsGubZydL12qJqxjM|WV*mHcRFRM7Q!WO^wPl7)d#oov#*O*Jx7 zw9BgRNOs-NDm;JY@O<$v2sEOf**CxZMF-W-{V|V(D!QM;ZSq+d+4$qsG5`nI=fW9r{!uE!UR2jYQn$X`bVc<}1qQTDJUigjE}%FKX}v1el<#2;#iPoeS1n1_r= zV0t)C=OeuZ=_!FwoX-FE73uV+v6##Y)KMG{ry}JGM*Y6JnaLvK5P1(CVlCMe_TE@Q z&dg1iyb9FGE$S!a!J}!bqk=q}h~S|M(G7kQ6;zZbDCav&YT(B&Q937Rzy;dWF^P@5tTf&5P~K`X}z%K2MN&{kl`EH&*Kt8&xj)qYQMxm|0R1;^!P zsB*j32;3*=*KxAouH{*{rhGJfq*AvaB|WNg*5spN`)9~ZLlRML4~{gh<(XO3;W^Cu z{d|GFb~15TfgrZML)ku0+kQrYvi%AG2`wW5(MH1(ms^v*i}kn~Y!*_|1++}y*y+L&9i$RLX}81#d6X@u!gws)lmzbB%cM~ptS77x?b7=5TiJ^Bf6#imoH_3)II8!ZR%Eq*lMaaVWJ}-?Q^821XL=cLeNNQhzzGY@7(u@ zpxY{CAv&S?4kT(wCd5RHgtv0hq~qy{$XJ4mi)aus&Yy^Ql$LIjmJZTZ%%7+%eUd?r z_{@m^FS-6{xpFZ(*B^>UXPSDpoBCpD6kF?v44jGBGEwl!15W%rQBZD_4B12fo+sb4 zAZJ8Pj@6H7Pb6{1Oz3fw6lWwVCfReoC|qN5YU_#PqR-N4j^>EOqp2MGD6@x;2+N(h zDJOC@%gjejWwC>G0dK8(1DyC^=mK7Gq_j_Bf<|p4xg<$M!w!oJ`gW2a-Zfjjji?_H z9gjyu1C?an?B%Z;7l_NNoigR>zc97qG`|Y>E`k_BmS5{wv99r8zQx9mZUK{W^(DD2)Awl;-gCmoi#TbtYtl z!o0mW%@I*gvFp~g2!>a3^7gHw*SlZGO?L4a`eQMXWA%3F&(A;8mi(YRo5PgOT%$@XuRJSp@^bv zQGYcD>{d5E+BlGoI!{$yB+fc7rRomsk-0bStDRcZ%G>1hUNobbyga#T65fz)?6^k&;vPdZ7&YQ z;_??6#~N8dq6xSuDnfjtN!E1S^MWnpL#OzCbu@&32Ivns&tj)~C>$YUQ9_%c_4Htm zN+jE>Zo#=$q#A((aYO5Aw&=--WO{4_PHhn zW>)MBQu5u zXmn5BcSWP&Xg+R?ku%$5oN8qGiH5z~Q{Y5=(HKH3>Tinq$+%5km~-K~>BP74VCUTL zjnzba=M?WNGv*}`BH)(enw_YhuH#*t)+~a4UtJ*7pjDmS&p^l>pnOFv7cR+I(*yC) zSW^wP_v^`yKrDZXFPb0p#bRXKe2`&ja4-<}M}0vpW>?SG5y9HsENEygebf)*HtHMW z&1SLO{H8b=KcyMwJw`Eqg!q26EVaJG`)IS|(vtjLA8rcOksP)6O=)d%1F?y|V4yDF z7l}7T{n*4x#%F0p7O&Vp$N4cjG;P&>W*DaWyl@xJU4`1gNTt#D~imR!HHadE0n__{+NH9=8bF}ocZc&r3 zKg$9&H_(ZOORWRz4kds)8c7XG#;U(^G-DXu!HvEM6XcFEp^@=&JYy`toAEK$B%@6z zO-v@4@;ef}jalQ`&eM2IiWJFB07Y=gKkbo%dk zg3_ie7nHG`_yC9-J4mowrRPb&ZPXDUzQ76eV(9U zgI5UJjvvRY5OmUHu<7RGlyQgJqpgjKOiea!vVmFZ$r!-7C##KBlVRzNll27k{F8`s3Rehv@SlQ)m97x<%|C@Kld(Hx={*R@%H@KN zsS+eh?Sz+Q*)iJ_{vp{8IA8t+TOVk^wysLCeY8rkZSBtXt>3XtJ8y-cPKAqu)g>zg z9bYYI*mWxe1*#RRj1Q!ScpYL5TdTWk$gCm?H(j%OPqR9~r&zTqh{Qx;89=x(xB?g6%ER+%)Qbsvl>3s(6e0%cOzKA zuJ0O@U1v{GcC{;rX;)!D*%iPK*flo*b=Xl|3T-;-v4CRu6MhU_DQMC~iu0(CrIIG+ zi!V}~@4ydm-gA-3Iik=3t!mm-#o0SmFvPttJuwgXOR#xsMB$M8AxAOZ74$;ZJT%usvcD8KoG`4IT6YrR&EmMkbgW|BP zG1|?tPo-ra0szbIiz>_3M|-g>7!F5FgGNP@4f{eG_5~aENmLp31?x66CMemkFW9in zF+Lhj4LT?xrj3*4kFl5wc(y$)^K!b2^Tt(xH_qxP1zqD{aTR5gkS-78-{5O(GMYo?V+})%tlqlH}ou{~7p#DN5Op0?$ zlsjFuM9tHYZxdy_QBZ5x-jrMxt%B=Kt3bVZZaT(GM6G;e{U@Ywb5aaBIBt6B46S6V zoE*vy(2aZn*0LE~4D&gAc(W>o7UkuGv5<~p@HFd|ZWsm}S2y>lI8UwQ$Qc$>-BHuE z+I@)V2xFw5=!nPONvFllTB#$x!A0oQ@9vetgpW1Ja@L{sHo>P#@bA2)CtdiOfnsdc ziGWMQT<0gchX-kA&rIy7)~^Y}Q+e~Wz2@j?PPT5b20QW{~P+ZGN!49K4dXL&g{~eA9LpQoH^ZfZsYyKrY2CW%4zF@5ur^Iyr zJ6YU(DQ-TCd-4(`Za(w=6iMaW`2tG=S(j?(%1ud>J%#?qyK`KZ3UYQpwJB;_QtPGC ztv52|6PLoRXaf1=;T9I~Hi*W42{!QHt<32sAZ=X8-(1i}WVR$5V_v2L-wpYXGG-ym z^8QDwF4Hy=8htZs{Q2P!ri8VC$gE^Ro21t^v7p&3h-ed^Al{3VYNs}FJ5|`CC2wL^ zPmfQ>5!t-DN$%T!`f!})T)u9bogxlB;bk1GX5F0(EC zpq#=qb#K31GBvot=#n;cu?-Jg24@jLR%99)(419* zs%8lqF?y9C8uph}f^MGGgF20YP+>S&SB~A`T7l{-k`>vHBils#S@g)uwXHMc>woMSJ*Xa;fN`t`pRTkvvFSo(QF4IK$>FQtu{KFOFnmAcV)55_pEwzGj_1?rQ8RlIIraCcfYBU&xZP%PQy3*I)yW zFBUJCYN+gW*J#rvA;xW^$+*cOCtjj{dWijS+_mUf`8NLlI?x9!Bty331u49qY=U|9Nw2qx}>l|&! zItLmoN zN^NJx7^v)I9ZKfP$(R^@=-BXfIz{Fx|FtUreWLxhcWz2mJAPgd&PxQDFUb2C;(k-D z&Uwk|eI?cViq*S*o>uQGQ@w9ly?5tn^?qda_Rmx5wJY`d;EW1df)}q#Q7_~A9@MK1 z`hB6iVBFNDLSvy~l6Zh+KR6N9a!)`!F*76nh8wTrW zIcNR@ncPOUeI~zVAltXG1HW`LzV+!i{Lzf4Wh%?2~TN7>~l7 zsWI2xq%fZWW9_4QA8$a?o*8vtvWGTG4{c-*o$+7kA)<}!pwNG{M`Ri|ltyK(-`oea z{*>Dg7uc3WZ}Azm?Z}XO{}%K`@s%xLM3gr5!FtX1_hAP#?zniL`CM|JAm`Zcpb<`A?PA(v@8=RhuDX_SKNrfm_d|!K z`uadKUBOyigPf?JuHsbt{m|lj-kaXi7ftF9;6yl}^8+-YJ2-dk0~q6Sjx_OJCLgj$ zkTc^a+&UNK&v~rm?7J5wq(03#pCTvfr*=*adl1|}b`h;&{{KKu`_CMBSdU%Z7cJKh z526Y|nI-#Jnb#P6%wp`K+>d_I*GwO9UNiEle_=y*an9z&a7mlY`h>F%dWiX@$M2DG zaOS=8Z+3uwVZeU?NOP?cl=-U+OCLh6d6l4h_z{Kx)#?#Oxg6dvJd{%L_nS!iO)|L( zOwdGJ_HYVv<->^ZS*rwj6mlVu%T@{c+arR8J-td$^&`N%vPw{^!Yl&j<5hw#Hy?F$ zf3%SwL0ck&Fmh@K6?Eu&Bt?hpM>T(sLcS%4nE%842$i%&J^BofP)T2ZR8_!!kHMvu zja1a%7@qDgPFd^LwQ$M(()+U8zquc`S2y=Vdv)$(7>fCcZsv96+mQ?*g$VuM5?wXr zQ*ve)hHRd0WBBSNIC_XYv`z~-@5rUlla1*#q(}bXAYx-mmnvg!0f;e~LSnKt<^zp6 z`Ef8hFoWeHooQ&~!DNFsNP{=9!L^SogEz3Yx5*@G0&VDrst|fY8!Zv!tQLm+&czlc zng0aJ5rx&JbT8Aozx0H%?>7_EsW6i@#{Hziv^|v9?xANa0k?FocP~LoV#-v@=l>S{lWiRyb3ry$dm&)TEJM}YC+CXeGJm) zYTxAS;cIZRna|a3<&3s9Xtr7b3{R!qeAM*BwR|`*M7#Oi=y*>0npJWtl4{Vqi+Nm! z9LaPyXY_jtOsOw!#rD%}1JFIKdPo@F7z-{ zJn*a*@-S0OeGXhH?VDACBK>7eyv{+CbJ%J@-#sTNXUJ+n=RPlJ*znbYuECEns|B?` zkJ!a|;&qS8;Mu1BA{Nv75b@dKUC*bSEzVk}&K5VXLlr_Ow<)2=fCS+?;Dwa%J^KX+ zLM2(ZL<%}#sr(B;V?c#ga@`B6Qtbo2d!xbOuI?Dn!R}cl*~p;Z-_JPlB*gCiem9($ zP6NigDCoo(UCw2ianXwynGIk+{?hNvmj==cFT!t@Z4(PrFFjgN=9*-IuMOZv__YBS zzMD?L^%xQny~bs-V!a@|$dd6IAK4{r1WzQmybTE@zCrZ00cf`7Mm1^Pw~*zX;Add)NI~bbPYfG4o_rj^Jp3xo1CFb$RO!HuXB&5@`Vig>~?n#-&)~1WV-oHL20{J3+httF6v8^ zW2_PM_M3u+9kNEyX`5`V!7;vMW3>2|^~q}NkjCs_V=mdGd%ztBA)d+UNkh}%A5FVi z%O^K+BYP@+$jMKkfwNU(zUIt9n;A2PzTxC~n-NJB*s6sk7<=AhKEfI`T0Qg8 z3wXqhW5$b8-JtplSP&f0IPTr78pq=?;Ot%wiT8MJ7s@eP?D0kYA=3wAUzW^ekz78I z3--clyEa&ifTcZ-2;vR$!SU&L?9>)ea#SB{hwm^NjpXm?Tf@b3&19f2C zK-8iW-bL40jjWlpk177~uAuB@`hgRdyo>S3K!dV*KjM2#^dvG{kU5?9Fzh$5Axayv zMo`y4S+GB#SpKv|P}X}Wb37X5l#ll5M=pMZb|j)6J&s3cN2c&2P_?N?d+-Ph=ipH< zF3!hZRMa}tyB)73d-+x;m+q}jlA-Ymzm}Nb_g19W}Abq=#Wa>qW+?y7xW|ITbKUanYeYyvZl$6-$l`I zQ^YjMvq^H5HT(@}+8bGltF?Uzp}y+!%RsTgL*8zb~C zlRmMlhlV&13dHk$aeqTNIx{aA@WuQwQ<>_`$x8j^G}3QUTnp3_**jmBj_ADfLk#b0 z<4wMx%DMl;9t761h7{vq#`! zqPgj~MQGa|Y+zIB4N+XP19Tmq(~RxK{y3+BA=35fxX9>@z0k3HVW}^!R^YUVsay5C z$vQq^>i7ifnD?=uTzpv0cq09yE7OT`>+wo%rg#u#Ji%&^vpU^C>-QW(^FBda*DAAK z;H37W->;R`Rr`tFm)gsi-jdGk+Lm;*Yb~G3 z>bivuS%FlPXiGYpjr`Aab(LvdT_sjk*DdMXpk>F|t(Wnc+kKzuvFdjA@*h4&596z$ z%Lbu9Dv_+E57N;}HY33WBp=?6bewjwK6{Xe`KQqyP9FKCZVF`D(}mrdC@qX$KWABA zbzybkzOB;YGq=gVT={Ms1gj4F3dLdBCUDO$-KX7_v0E~c?z?voyYJpXaNm1hOZVM7 z2o>*yZ_xhZz15Hz%18GOl0L#nd=SP1w|t`=^dLLzb)@AS=kh_QN&UZt$LKz0I|7MX z^j5TtO}Oe?yo3!h2uIZ49|Skne20_$VU_v>sgNYu$Rrnj57)(MBXb=31KtFpQQF9w zU58{e)Ii($>0LkKP5`jy#rid@;;BEOYP2I)F@BC2Py9(TevV0vpB3ZhnDL~aQ5Y*b zrT^Yhe>Hin53=A8d?_Mu;vSj1T4*zvwpa^1%oo%AP^j z4aU&u%wI69qiZ-j^b2OS2lL43dVc%_a@vtIB|xo%QA{6$A{bUF=daMNyLz%$DpqUA zTC*i78a@sUi5_Rkr~E2tWHV9_@eicrnD@YrwgV=%0e zZ~kWO7A$G|m8x3Bv3rwM-6~bx%Bp_%8v0E_9XpwG_Vzs!okEP~P>{S^CTK20l5W=l}q*$3s>#N1eG&$R) z*sC)T<=6DJ6kFtiB{iAPnq>EPkTdgO%pQj61_qte-@&b*Of~k0sy(|%4k5I1U7sc^d6!i3E>`ls z{!+=iGSFG|Pt!`ur{ru=3f#>K{5?%7AX7f1V)1C8&aZNANpq0110=O{KU2J(1|^-D zhw$KM3FnPG2zfmgt@5!{U#s2ud9wN|rTQyb{pN$D`YT!e9H&-aJ~e&3ise>29ptQr zCc!YR;jGJ@(jb|-A1P&!%-If-B_OG#=b2)7I+S;I$-GUR_YmiGGbqI2BBNAWKwl5cT^zCcl*y9qO;KAm_q0g6v%Rom>!=|qh zl%L@s>esSHm<&1-jP6_`XljNgyXs)nDWZOh)(8{T+NGV4vwn@B>oOda)3HX-Qv7&l zjiAjL4jR_EM$kbAJAN16ytl2gqVP zk9cqBjR&LNtv=X6vgf~%b=?k;+|SZsvY)+i01@}IbcjQK8e<`T;Ks-=4nZvZuH2jB zybnq1)$w(*ehZ|23s}Ds4s}p&Eg1_~7s`#}Clps>sRdGNso))~VEv&Esoz+o-yK@N zYY$cWy^0^u@36z5U%yXKR7#1AZ?rNw2dxzpIZQj~3>MqnnBnUDp*lpXsas4g{fX#e z9wT1-+U9h+(d2hA^UJQGf8k4n%MWu9--Xh0m_eYqgva;YT&Jb!j}s^M9~PdMB`*)>h|be93GE<4;oX@{;A z)ULGpAGSuHzE;q}!yPoNXsw_x4|foyox4_0#^F-)+kgqL6?Ebe4jMLNt)THo00TZA zg}Db9@R@pqgL3}9R?sql!SEdhgJGKjw*d@>gN{@T&pQ$fpTEmNoeFbb&+c-EV)2~Z z5?qRmkn@CNrn6dqNOsx=gYU1|z+U**kq$!lxPd=Redm!n1UB&2T0*1o1OFU$lx!X4 z7g`FD;;OUJK)-s=QRtwKz~E*Sy~VtKL{>Ftji&cFYvR#}L1!y6FQQMG?_A_LJCM~# zKXcY&M?1)waU}N=`X|oDQZ6*DfZ0!QU)-K>}+TR_m?Nhh)WY^Fg0>yvS zF)AplkCa6#FX-tOlyyyw5gfubWwh*1mbFM2xS(l~z=7Gn9^)Wvty?5e8DhscX!xbk zQMr+Ll*Z^!pNz^4`a?8^8%6-DKvchH8!Wzv9^^f#i;qFGeiS$Bi&^)gV;zJq5I!VO zoBnewT$$GYZU=Q9B{d$2om(UBcF?RL4$2vGw}Vy8{RsdMOr8gyl;Ft@ z89PKegmr^MP6Hz0kQ+}@4v8qB>5!~!<&ev=l|$OQJ0xQ`*9MM|l}fD*>MxYf*A58} zR}NXLo+hJA0CP@Z{iR_IRkH-+ai&GQ}gG#O>`d?$$~JETk5$-2QI`+!I|yw4G#Oc? zAh#Cu<|!$HMvqd0JPK$Enm0-b`V0_sgiDU^X3_Z3EQsTCw{mos`itV20HJ6E9vH2P zc&~bzj53SkmC=bJW-oOqotO{2^f?fT;wT>DptSz~A8&607G>4`kKYgH8D(TfvDa*| zdJ&fa6GXGL(n4{8G$qkA&0>H7Mqy^08AMyrG_|xSTePgOO+krUT*60Y*glJ1HM)PtMqkrcpMupwu7*!2nMqH*6TT8d>puWi2abOC!e0zw>S%->^ z#ql(a3HP~`NXF^bF_suZb>S0r(mQlxB{Qlu$fkzad8+wSes3-f_~n;es+|k_@aFt_OYDJOg0wx;cT8T?Qf1 zXpC(x-<&}0a|NYey*Yu-&lS|Ad~*W1a|Mxo+U5kxR>)(3{PpGpngis6n-l2wTtPkl z*o;w+xg~+dzB*bb3Y|XIl6wR2^+&WUFd3Ge`2+|Ch@T5zTRY;egCiC@8&E?!nRf-9h zDlVIVd}7@T)SXBc>xNV@Pnjupg7s_+9zULK1q2BU11q+e{3!@vZWtZ{-k$@SxOk{Xqy zFgD2=U?R!4_yI2cFV{($RFaa|B)-d)V2|Sml6-6>N$DkZ85*19ALCV$5%_^5OUA=) z@waa4bMk!r-}1WrF53HWW3-&_G;r?!&GABBzKy_-<3;r4+v-AVO;O(cHad{5r^+(gRl) zob%YnPMT=3QnwKtM=yw_*-&~W9^cUv0#THLZlasp@BY|H*ITsILPH`}C9)N6uKGfL z{HMQS6$)8A^1DT(2Crku;8iz4&iNeU^_Fc2O(7NfwPP@uTdj8&Wwv2;>RW3 zNR=2DGur1F>Ger&`Yv-V$|%2Yc#!WmMM`~Dx{k#b!U-JH5U;~HxkqSt&|eh@F-lFm zCga_qAvo$8l+p3ZaN{7Qs)<*Ir{0yKjyQE76jOXVo?-szf;=g1ytMwnNKdKShKnOI z$I0rC*|iLmS@F~>-80l>45-7uY8@w5rF#ZngMr93|K1&++UpC2t@yO#(_)=L-dA4} zxp7|S16BvD#PPOLXY`rzOYw?Vx@W-KIC9(Gc$qOVi!;4^XJwpBDvoo45#luhLXN!7 zl;h*`7x6v4h-_m1d7ZNpu~HNg~GA%{xUki zhUUCL>UftK9nd+2*mx_e)SL^5k9QePZN%IH9h;Xxp&1>+uv6kjWj4(PH763*jD z;J6M)wLLB>AJ<_bVXsq~jt7Sr;52{vUDi z{y=`nQ&r;U$m1$%Y)Z?cj&C1dq!4o{$HcoAVhT7W-gOW&y0nzz;`0@8`9V)HN5-q~ z^OO~odrQMy8f>|0%Ut*2r&C7N%;rA3m zuM69Xjw2Q-_Ifp9;ol*mUC_woe}P;ax1ZcD>h6}JuI<+LI2nrlC7#}77g>Ly4siP5 zMUub4Ifv{b@E14%Pq2bJAY>PZYo$yV!ZYIX1N{2p{gS_+1)Ov7h{@mJoQEe!{syNX zGW~CG`XkGeq|7Zj2IM&(cKjQh-dQl@-ytDK0)K~uEUEoFB;-l`-yy-+jemy(Yd8NL z63m_K_-ixuhP|`?0x94Rd1q)xp*tkCmK+70w9x?^WeCBliXym zu)oqwa<>Ym}0GJo&PKIdg-U6_&)Dt8AxB`HBqnY+kK zih}L9_daAqW?B#2_cvxqm8Q;0oBq|Qg4#?Hl-_)* zpo~e_U}n99bs~xQnHY;mgGCJWn;M^Z;4yd>m^!}v zJ5f_#xloh+Ke-2|x8ZLl_P6Hyt3AQdrDLo8WL#>bs^@ySG3p%Rc4-tI;8Q8mdd%<# z8JwQU&pjE#jMN~}%zb!Yf{9joO1y60ppq%>Vvi4RNGBMn@)_b0`*2~sq@5b7b_YU4 zL9av8_6JKm!9HX>*>ZxS77_~Ph$s3iV-%h%jbbdraJi3uO1BS~@1KMXm)=TmnEIjb z+5bv`+1GRMd(xF~G^XeEg*?HqdN$4?n17xojQh}W`qdqF6SV>18?F@mwlbg6{Sbj7 zl7aS<1vzIZnYtI`z+2i{h}r4pjGPt33^;5 zh&RD^fHD;bVGlfqZbwKS=a7I`uM*@6`u$-#8Q4dz66C0d7{&Y|A8$!{;%bN@4N|V& zL9`?Y^pBRLyOyLoB*`q4w&)Jh0R)Oj238abQpZYMh)eKpq1`$)_I6w!~__AX>bETD*Y}uh}id8wetOiUdJC4m^T@ z^bQHQtQb3b-J!wy8SFt=srXt!qu-<+jEKAvGG9kRL#(g{PW>p#KgcD?FwA=5-a&ou$Y}Z&`T+G`ah`0vVNq?SOGN%@|0J1QE#w&ZHaj+SqmCEqrd?{%+~ZyU?!oFc6% zLldUxnci*6L|)p-c#ljGj^(f3Lub$2kVM^b@X!474D3P?sa83;L%IYXogN393^#zseoCT%||7?`$cXpA)V0+yw3Ta}(hBgMCuzxxDdn z4MIhvLht$n>8G1zynecQZUPE5o^HOC)BWaS-?^OwXZQuYs+I#Y5tynTb?zPz`swD| zIF{@godh*1O-{kG*|1K60s%qkmvs_!AOLNFL+471oT_omX$q%mn#RdeIHzfxr!~%* zLD*K>NH*M-Spq|%&9lZb&l)z**q}7e8Wy)6p(0YDb|JxUGRwGo5DLlGeoX?}`!_<` z!zBjUOFIcFR6_QJkky?8nPEZcvk`+Iw{#M8FMizFNzj{Np;FbVROjkcwknm%jUPz0 z1V4~!PnAy9pi-TuQ?;6|QWfC`QZ2;~q-vZVF;UPn-4hIXLulD7l$zbP_V6U$@=y8X< zzA`JukTE5m<6;u%g_KAS(jV&OnSt4e+Y<~*PD|ujfaY`mmoWyX#{*aVju5{MM8-@* zzi<_anuceCaK{W8r&!Uc3?-`EnnbDmqLANLF_Q?bTxwa+9Vqt}kG3FWQvzpJ2XVZt zgqO3_f}AnmFjA|D0z1(GZ4q&t59!EQ7>yZlhs%4FdMiBmkvSMkD8^17L#zTx`_@XJtcb%Mn!^5UGb^Du9PCp~j~&>Qxsa~j6h z1S7SS=$(~L(#1H{N8B#6ZyD@cMbwah>lC7f%b}UR;$CVLoA2il`Xv5~2Xu?PAy08w z+IX^6*<`6oM?Xh}6B&ls;iQ2x)BqiU!WkzRp#LSuGJlDusHzOcukuW$=JRCzmfTN% zV^|K{;iQ{1O9UQ|W2tj8O|GG*t6C@JND^0?Sx4npzV=bgYv|SQ>~PX9(Bb#1s&Otv z9uL52gSgWJJk%olncW0sooDb%3Nz5P>4jbJGX?Sfxxje_<>3?;Vz7gA(#+WXb3KAU zM<4Gvc~#}71V`b|mT4xgsS%WTW_LlgD#@ej3)R1c>gRSB^lptH`$gRaHT6T6got;Pj_WSy zH>AI+yP!IiPN(lfM3+g}#mKcEB-c|9^)P-5FUyeTfJC#r9*I`Bm@ zVUtRDR3|+BCP8*2%s$_UMqapFP~d#Y<%mJlW8HG}W`WtDFbh!ykR7KIn-wNoHd5ZW zS}t`Ld*onRYo66w$|&7vu4!briRPNPPVw+fa%?I?{pM(U%r&`VA+uTqB7BsgbvMK7 zvsl!%iDtFfn~cRKN*B2T)wG=9&&GJe%gO6Eb`0@d^cb59&~v!HIp^aQJzeDlppm?s$ViJJvE>L9Hr80<~YFwi{@5X%i8XO{xu&p z(+deEy`3bb-mktWcNxInlVDQf0u+}MSpX*1N;kwPzfor5M%FNU0mgoqMpWHP+Q@de zWr6k_8CIFQ(d5iMj%zh~Cc#MW(qfsNEH#j>`if-z3bTF*tgDG$F{_t2>9ED{6&64- zd_^*J1%kAf;Y)9UH&v_9=LkX7y^NP}t03pdc9P{8inXhhUgf0s-KsM4RYv~e)|TuS zuZi*_tw*CGD)sq5`_TtXk-7zf^da*)^@4?hx>eJMtX$%>g^KlujJR~6An7p!q{mD| zrov-7FH#~VQ9rpjlDnbls}LW+Axn*R`+fB0=W zhYzt#`*1#VnCK zToo#&A8q&Tanh-GVG17jr9S$}_QNAiy5=splQZ`eX@%YDi(7@OD#FFIsMWKLPTB#i z#3|F-(c8+(&FYJ?eGmR{?2?ltnYHSR2tFoi-_c!Av+{)9aGNi?3pxg}_U7(_vIa_z z`WR7mm!N5P!+*fBP(|GgVr1&hf~q<>Gc(Kl7<~7!GBexCRO2sG^tLc-qEf_HPLfAy zm9Myug_nQ2)Ks~s&B!SA5glTietx&0ZXsp)U)l1Bt?m)jEhd+aa4sch-(zJH{c1a@ z(MdA^LGAD}`#}D^(S4*CwINb(OO(P+(IVYqGrASit?bB&JMIaJ;^%1{B4El)47aOtt!Zr`6eqqJ~C|T0N*%wR7#h?rek9$B__%6nZ z^0=Whyb#@QjRLjdQvMljz`3n?R5iDC-dj$3@c~^a&TWml<@oI2nGj8(rgJo<`&x5W z+}9de@rgz0zSdmjx!}hhdbl-aI|e*xWy;Fd$dnKOaMgzy`-ul}*yQ5)`e-dju73!2 z&T|QBRMvPH21=ab5_F}~ZHD@ys6PT*-Qq&mMUZ_l-UrB*xo{JDv8!Ezau9QOYm>6F zWiABKJKxaSq-)S8CRr)nl_J%xS6>i^EHZYdf_|aCh=oVUj4RBR8TSH&LB>5$r!sCo zbBM|~p7L^6yS?Gzx-t$~rmpWIWsudy4r%foY;v(&P=@q$+QC+JFW1?*gDt1p9TjlY zW*gG=Kj7FU=%G6rH1InHz6T%|+>}95gMVucIzFs47{~Vi8x2$)qvg-;8m-69)<(DP zQuLjzU3;CBc-_O?58ch0B|r4AAp46hL4`_)*O24KvGn++dxL(TMZCFNG?QoS?4Qrr z;h$eWtWEQb9W5eE)0>QE?9y4`iqG=zlt)y{SBQ)d! zK+sK~Z&=%DE3}nlSXt>CR)Sn1cQyUTe)eY$dS?Z!6b#b$9OYao$mOpJ(+^DM`IT7Z z9fV^))XbNQ-}fZ0ml}*}mVRf*<|hTYrcS3H*YN|+;-&WF#5f1u(bQu z(^yIbWC|@|$m!1jE74si=v0@qK$`l(gg?WC)pddfJ|n2djVN@9N4A=DhQiUf7ZS-i zt&|3n9t3V$1M-IK{p$oZ=Ey?uJE)DQ6Lbt11)wHtfpSG|j{1W2FG$aq?lviCv-%?T zm?l;DEEKScQK{EgGIWNgxGL-}s_QukfNe7;V)6yv)KINpdSE-yj1t?2a39|p`5|li|AQ1c~D!#MQ1bOQP^+->HND-MT z^wNTsqOMo8G(8O^P0ycpB+*m#S{e>2-{}dqk0YGd0oVx9VH>hlB6S=P>5DwC8 zBNF!F64Whiu*pcyP~rlU**xV-j_e_-GV2Wnlb;hr&g2X!-P7vJ#rizNKaY4v0BK6R zK^h1_u3QZ-1&DY1wG$W z^4_h!Fz&fP>pNRe^%_Av2FwO7ylRue(YWxzi-8Ld+Xr0fVPwBDO;A8F9SDpWX@YvK z6=a{2Ca7+x!B9V9>e2+wT`MT@=`=xy6bFa0#0Vlf4Om;#1bwttP>&rlt10o>;erk; zwB$^w1cV=q18J`ghw_Psh6_4X#r0QT;0ystXRe^$>jWj9mMiEC6|1oz^ip8qgzRm= z>fgskDREdMiKr{ECiSt=x9h+&$_u$?R;s6kRKub@vB~mC6t0_1BjUYkR#GLVr>0^bW47$$3kwdhOykS6Gz^@6%4r$c5& zs8tB-HNxQtLe#Q^9*ztR2K_-xs9H<7sI7sR0PnOxtvnzwX2S_q9yC!5Y65B{(xREt zM=KshyJ)YI7HHZCG{n-58a71}wN@Gw%{uipmKm7in9rns1u0%-Q1kG1OxiaMi<}CK z8J5M;S1SXWaC0n!h0Nge=XH&vr|vCrt=_PnPSpgm&#|n4Wu5Dve9K7#Ejrhu5{PoW zwCHZNnwZrT@*|Xp)HcrR(W{<%964))sLr6`@Qg7$Zn3B|wLs0dd9RahdtT){0^314 zNQT^c6Xi~!aImU4oN{f7=HT_E$OX3xEs(z|tY9hETl@lQmuAeu(~7-L`cv~o;GY}+ z#^bE<5tclwLN9OVZBf?yKw~(3xz|a9H!95#@WgUxqxBdX3}Os2#8cs(Np?|ZbmpZk z)Xav3g!O)Ops^kOw-iRWxzrhl;0Xpx{6IO}BFfNUa0o}&x5#2>5T|a2;d~LhEVqMqJeLBj6q8ZSN+=eoXq{P7 zxs!o^Qe>~VHcEvz?9l|?F#eW8=jr|mdXtHD_R=2Ng^05&-*Y{@!e5jRg^dSdR3}@g z!dvVK@n+shW&X@ixjW#=oHSXMv7Q!0h9?uYJj@{aX1kL*`-soh#xuOc@AZ)}+b~i+ z6`o4s-S_c0?xGNH>xg7RJVst<5O9{n7~w<~Q^;MS4Hi!@r^4g*k(R8@L# ze1MD(vH1ZeYcZ4Y049!fk#M5!~0zm_y|qnX|zeqwnxw=9p^ zOBR$n$EDZEz2he)^>oXmlxi^~1#E4VgG{`GVO20$ugi)?*R=7-@?4_EYIs9qqvw=q zWCYx*F3Ci3osZgCu5WX=`UOFmm6)CLl#r2$`a@kZn~BPp99hHV;&qUEBl5l%F(#Z! z=Oget0OM*_%0W zE)tKzZzRq|;uklg8%Fkj4HcABDE;(df|9oiO7A{Q z(5|h5y7V3<$k!mK%f-V4J=%cI`afq2>Y&V&rM@uMd7z1oJ-rmuu|HMeXxt7&k&gWW z;1<-egz-7Uc9pfd&X(r4qO<23BSV^xu7TPoy(}mrqyndJL*tC|i|W!`e%$REm`df) zH82jjO5I+}g5H3b;483mE-qiCIecHLcRP+7c*@*ZBv{C;$g2RcY;-3B+P?}M*@P_S zYIM%4SkcVmvHr~r+4Cx9O=|(F@Y7PJeGuu&y(%<%2aMoo28xfK;#6}1<9n0yFOeDd z6pW1b30Ce<_Yz;SQ^dYd)+*K6vc<$5LK+girN z*Ofvu@uOpTJ9_!`mSn8(7h!MWNInvxMXY-_TGVfGd`OquZ&}#GuM5g3@dVJO%Ik6; z*liWv5Ejp~c?^b0IgJWNpT9zk7wxX>wa z)-XZOE1YOt55`qXhY9+AkDwmQflFk6c9@{-5z=vvfZD6W1l`#vDE+Nrg8IEBDE*^h zf>QPhvVS>DP|XN=IX7=-N*pezcCVoHe+(CN?mj{3XAT#1*4u)*oHJa|ZEp)oT=hT_ zHN2g~ealzWH<^eU!R&(vlIWGUk%eR*I9yPJk|Y7LUNv0MmxyHbv)+-}H&T6}z71$l zzwR9&74NHJqp>W-KYNEc#d~yxzsPcq!Ve@DImm6mreAN6sDbmM>$`#?mA!&lX z69OKxb|u5eXyDL3gggkr%@4oJQ%t)Ou!ec&d+Z?easpRAc%G(M0=Rf z?)TILQ2cyP^z=^hNUl-#vcL=9$I%r)q=Y53F9CJf_wTE@t$oZX`2*M`5t}E>_oV)( zsxPd}S3zPUMJe#?Q-%ww8!0^|9~tn#a6v0M3R5DDDr$m3q^3d0jBR^i@yrrWsV9h` zYj4Zwb(ZmsL@Z=qr^55q7s6Z$j(ZYJD*q5d$K@rubec+&N+pjiE|=xKoM<_h&09az zLtq(Jvu?||98%)m;es|P0uAbm*-5@%{Hx)D*0jVP8%ZI8W?lE8w5Al{F-wHUScH!( z5oB12@R$_ARb4_)Fy5ITsoZ*kV{Z9K$cd%HO0H8C7s&TGN);xSnm!U_$HWpWh8Sxy zDRn>gANH|P)};o|v{@5KCHrAgN*s#$C`B|yeL-Rch)>QH^f(ZC0<2I)2hcN*9Z=tiJ1sX9gKBSeAJFJCB9=l)BR z+W)a6(cu&Mg;ZYwAEX{-GzdRHxnj2Z0_{(rLB~#5$cuO6YIns{%Y&0Ym(*o3e?Ngo z1m#5Ub0s|S--0r{zEVE`sOQ%M00P5&06qMwlMeq|JN5fqBa^E5Kjwi64-C8Y8~B;56jmOP_GnOFm~(1Wa}ZZ)kB2R_4O705 zX{9Y!;O;{lXr&cAoqXgQK^fThu|mLX7k{hGwnFYob+MgS3YdBNx5{cOIqvgs#c{b9 z^telgc!L;^hb+$7|B9Aiy)ZILyg@b)t!E*czSCUS^L|@Z1FdH^s5+l)hJ`5=F>f@Z%%d>(ROGVM z7eu*ZbA@8hQo;5h)@gXki($gjK~Ug4!IiWX+KAwLuOF$ zXKeeq45vW7CFBRz)}P^b_N}>s@-H(87MhH@vpHALXFm%{|07q>*}n)%7kPrJei76q zDNoQ={OFJ;=*(Z??Pui)s#D2*NIoD>P{FT)(ud>;x)wN*Y;y<0UL52<;lz;6u;d@2 zGj4%lSJO^0I+7fZC3k3=uv{svA# z-^xw$I(bisd0&A}_$|egM`@6LEDmKXv2o;eE zwf_U-EHBR1D@flzVyrC_!J^M`^6`JLMP=Z&Ka@plm6mI@mfQbOT7LhBwrGRG*`RSw z{!`%${PQ?V#2pGhp*^%6>VJy1$MzH0W4lp(l_1Zb(jMDcp)U|BA{FX%L|3QVEmxB7 zlZ-in3l+xj(ec+gWj!FZ1q|8GkbOrmsn9Gz|6)*^qtK1ru6C?+somNDw;xpo*Z?HP zX;3)-);OPPoYu#*A)6J>#~P>CF-2!Qa4eT9F8BLGo_u8P3GJutR`_$Y{pKa;(%Xki zZ-T0J=Ot{oTF{Nhr2Xdc6!zx`6_Ek^I@3OYxa+{oOo4 z39U>@|0GY)Nc{MBo}hbLnbPCva%o9^WeF|80id8Iu(w8Uk|}2?q<%0HDEDryC_jcD zp!^^F0Oj)&73CV0$f7(a(Ufh>(}@=4dWF6GP~L+ z%A?zWvXyNycQu?a_jIlm9*)kvgH|OX`yQkU!9inFk)Y}}GWX=zaT7vCq(YyzF-i9v zBL*CNx5P$WFi=~#o;g4uNI<~Aw$Gb`dEpnRj5}=L{ zz|q!}ugh|%7eZ1K2`FuAl8eVorvUqbwkA2UAuS($pwj^KwL?{dE?46fGMmmMA}(k@ zwNp;pDOuytM2VB4adMIs&Q#!7n#bI)bi!=ZYU{JM2KT8QEvxTX>=Cp$S!N@@(|=NX zle3WndF|oB6FDoKP^<=k8UUsczbE};dlR$VP_;>^(N?Q5>LjJc^pk*;6gx>-;6Wch z><{vtS-s+7aiBXE4ye`gWVAgDNV> zE(-qws{;GS!B1V-c47Z^paWes~iJ-_91(0)Blx60+L*)hGU}Wui(&Q?y zFWi4J8QqN3kS9R2bd8fbm%Bsd)cwM(Nm#;mA@@56{MFQhA)#sNhbqP-Kt3devDD7r zDb<&g4YS%986l!sZ6N)FCz+JN%w+h$KXl_Zi+9nYo3M_5z1IPCiQ8KdqV){;9e^-x z;J}$Do8&6aqBU)x?l=zhrnMYebTZs`lEDafG3Y~ua5sle>d4)ZdzjMI2!u&)>z$2& zWGZ*{ZtQ51qaL*U^Z?`U?}&;j`AS}?e3C)O03t^rV19amVX3D8tsbB$^d^Ib0+cwU zOi<5Pq=5#gFUpDCMwC9bOi=kLCZ%6lCg`eDO`=3knV=GdSFXOmZw>sJWrCKUW>WgB zGC^OQuJM}`-dE}ie7Gv(x9+6yJ)KY$L7wI-_VF?9 zBCl_RI~Zueb+xDIElqM)^6*Hl2bX5|8@^zQphf>QNqQd#Gg7qk@yb5A%4r1Ld`pT+ zE|h~%i1Gk9002q>htBS7lA~raSC1o6K&m<;uNx#}3?NSdV!s9Va$P02=;B6#^o?bL z5>rj;vaL+ebo_X;OwdyNc(+W@9{gy6e{?aa%hzRsin_qeNw7yKGgRrWs2E&TTIvZF zV5%C78g!&TFvjC9p@rL>bfd!e2l9M;rv^h~5yw>dJiQriSlA!{cpru*90xy#;o>-W zUxu3sUg@5Z=L@rN$MiqV`%15*;oCq6{)+TtObX{ z_LR&li)^!XCna73N;0DEn}o?xZOV%#^Ye4~qRITxeQQaH+lK~K?NIo3&o4J+ph|8I^}Tp1|xm&~-tq@)qE zmph%h;ANgF9}W~{7FP!3jxh@-D+c!;2GL!ADQd{LJ|8wJcEBTaL_z0Z<^H|%%w z-fFBz)9@uuO7%*Lsny;R`KxmgU2R_Apw2}BDnuRFd79oz&9xA2^~xMKHiwM&+Zd@~ zqT8;=on9O#@BB*740*yM{l#Q_U@%6XSx!o=@D~$3z_>7Speii;A7ng>4ZczwDP|-C zwDsSQn4xokK1i6-6Gr=QEkDxlE+OLrHVX_N&qdk#jR+HoQRcB^Txl4c1N4Len~{J5 zX#XuwYg8U5D{aGwpr|7{8X;wcbWOQ?>2X8dUh=rne`gXF;Xrwmr`p0xUKx!)+{o}( zg^7mao{c44Ov(sI_oU%iVLJqf^b+ocp4Zjtl)`T@iJFTam0eA8byrE7*qD!YHOW3b zC@AG>xo_waB1&Hz6tn{|PXqWe| zAwd(mVe%~`s9vFr#gP(a!?fs0JDlrrmw18}ubLO4travfxP%Acu65l_$_Nt$QBdDQK;=9I z;WX80CMl=kn~?W$7+WG`e4BJkZQ<=H8K2a{BuCTL2DxT>+(Eh)RYXn?lU$X4U$~sE zJ$RjyN)aw`&y=xidSFbGU1*T2++T$~xky82BaPQbb2!)q!H}m|1$}2DO^O@)uFL(w zFfHb-I~&h7$x#ESm+suA-bwj*NKo$#lM+{j1f{50jdcaB?UgdZSsQJ)wh;XaDUlYKOiLPcbOmgJ2-iimlRNlaw)IWa`$o}-Aocn54>@-mjC z{{Y`T*Hl$OnpGkBW#y?cX{$~Fxpo;`d+st&eReq)%|-@NWtZH}Cr?4DfQuU+xnD1Io=GmQH{NBEPa&*op1oX{ zKL>SxNW)`yt%pZ!d^^A7v+-cN|^(?hrZeO~<{=SpRoM)k!>iVt`R_QL+C zL7IxOe=0;x#%Ti_bX9*$1kwTHxeFaM3kY`6z8xi(NUwU0h|*Kqx1+}qmH2B&P`;vD zqP{TaUZAxP3;LwLNnK703raiRBzu>zpln5nAFLi57Bu2~+` z)ppaQIu$dDo5n<=$)v9ozpPC%Bf+-`2|G2J^v3xnbxChBsr?1;=^Mj>8Wo9e;DX#B zHg1+FyKa$RP&lX?#JA%j(b&iaF}|gfjumO<`jGam8q3FIGk1mxU#Y%e_!ALvGuQV5 z)y$=+Sd9hFRx>vq`uwd3y)D+%AaygA8FmyH^2m(*7Hm;eu3>`QY@)xGnPnv@w30&3 z!@A2h%pG=DRQRmurWiDAHJgr2W-gDD9sq47trDrOo=%eWk`@jeHV6YNeH-}h%ResA zrAUUU+jM%Ki6(ZxP#10~up+97)yEM14f~agiGJgveA$I2sl~?Mc&};rLe1tkE>Wu1 zh$fub+ag)`a!*?`+X(y@kLX@ zEBwAPu3gaENR%+{OIW~U3B+IqI4Lq%87foN6o3s%icq*j4if+rZ6H}FTtarSt1_gZ5@>!n$WxUGiZTOp+)&sH? zc*5zKs=78-2?Li3pT8M{Ge*h}dVS#`o>F&JML0yp3Y%e_3&cAv!>WA6kx}cU$vrp` z=tho!_Rz~bXYp9-v8_%@Ev^g@wQg|YFz7#F9I};GhBNu_e<|NRE<3DpxSZ-HLg9&A zY#cdgJH`{Ja2I<9RaB6%%pN&t>#rzLiG7}G`74qr$L|Z{bS@beMN)*SibCO_MrB1t zd8(mHfhSDH4!eFdSarH*N9?>YBRM= zTySP{4JU7cv&xoVmN7Z-e%WTE-4M*m&P)Wt~itHJ9^zZ$!?IH})2 zlb`G&`jvY{69?)_{8wFxvySo9!$EkjCR`uG;HYG9lo>3w2p(kyT3qb2MG*;EiD_E2 z&FX}~fVzPF`?*n6GXAX&`nu$Xg(mb6m%ZFmUsrGe(byv$r_u z!HZx!&n!Xp%62O-u~0Wl&`TGY)MF*C_|>vSRdAPkL*6jz2rXSrjFi|jT}rf5p!DWl zY|72(JH_J-PO7-rBodr+qv`)|)z*>7pTMO%Lg=HtAa$Xa)f3Z$bld^Rd z5*q6THD{ZYZeK5`)1Vf0SH(zcUKftGz)BOBoy`o;O7pyzoRqk4kV)OTL$Q_So!y+2 zoHW>^Zg5w6hD~&8&R~-q$)yIlJoE(LbA0V!45?n6CFoQ|dxH8xLEMqs<2ATKkN5C5 z{nJ^34h}XcamuuIG(chM6sHjJwNE{8cm)4sR*v)Wvlrt+?lO<1L3VYtCeK@W`8*4H zTaHPY+(sDBb5>Gj6(`q0M>dr3(u*eAyytREas&XGLYo=#We&{gs0XZcI=yDz{gRW; z9D*@Pvx4n4H*Rs#TYxzSR7q(n)E8;q<@h;6K}af+7HQ{WTXU^U;S5Pe)^eXhDWXqJ zl!bo7uu~!8)I)>85H*?Q-JRqcW|AYN44DGpTpNIk0jyC#n#cEUI)+0Qt{3l8=Brm< zWY*1aD%6W3fX8U3DuMc{FVN;|wAn!W8`YHmX~hCJMk{y#3Rbwwhz@YBZ5$5&i&yf1 zne?)g5}OfON;NiEBRMS>bD%}^nb~oZlZp`H?EZptdnv*)!zG|PPYnRoKq|jRk83e7 zI$dUOxdq`i2E)~5d@&q39oI|o$KD{nE-5Sh!u3OQo*DrE!nMLbE>-Z(DnMzSM(^N^srYosbG)fYKD6{Ku+g1!SDt5E2b z!c-s%T)JGUh}XvxpLi*VbE+nlDxg!{p;Ki|i4$pOBvs)Q$%_>SuVa@gjm{gTnAXK7 z|7jGMvd`+w38+khMl}!suH9oaG6griH!3b1b>JaEC5SrSol9hwTh>0>e+z4*)nq|^ z8h7OqJxjSeH znKCC@*n_PNw6F(Tqa0=D%iao}EX;>ysLi=%vfhSWLzC*H9T9WolnshAK20z<@tQHz$I{m@&wd3zrKD zOqE%GND!ruSuSYD7?bQcOItV9ptCTy3dM05+l2*CoG-#yt8$}JePN(qKmiwFtST_0 zZ@7D2;b>gYycnf#1g=3HYf|JQ45%1gXNLk9f|XkKY>u((eTi(>`x0T-d&i>Z!ZZCu z_py?10^~*r11RS>-7S={XnqkG$=?1jBq?BB19gH?_bz0~ckD76Hx zn;Y#A4@zCZP%3yE*>2|W;6xOYT7XKY(L3HZ0kDOtE+00u43~VH zq$aaqmYv;u77wehoeY-dD%yv+V2|jt1}zc|1Ov%lEwgI$5=l za~a|Et0JDjW4gFPo9Qi&2M=hdWbnR1&pABQmGkqAm+uyIX1UDzdAv@v2O%5`s($R_UQ!W z)90{H3+2;u6Yg0e=!j2hEFYu(a2JWWb5Ga|AV4!(>R_tdjHdxk!HD43&Hpejxd8_<`g(Gfm>;x6Rbavx71(`OGGg ze*heCzw{c#{a*Y)@&ot*?me%K&V!gkjwj3todxu8bXGL~SB1aXSbo2t>T6|IG;?X( zgisNw(5DD-QD`<+-7hHNx=1T5AGPjwok@<`AX-Xw1oDtE|DzgAvGnr(se?8ml|H1XyTdxN2{JX!S; z!um=7!w8gniBC>;x&eKSFib-|L3Ydys3Qv{grD?Z28gAkJ|B5PCAD%{k3}olK`Z%J zEq;f!4+#4525c$w6Lo+%r8lb6k{nuqkkm^88USFuJG}gWpkHq^)#1sLk+@flNsce^{ejr9;KUT-UoS;Ibii<9QgQft@< zY?>9jhRt-aO6m1gc}DwqU6j`tNk$Ew;SEQF4GGS7`|uQV1U;b*QyLl!=6WguWEWY{ z7d6XBImcudP3juxGe1ot6{T*B$6$r*qE;Vj&XHCLdI!)^_xXN2m;t66CG%r&bnS9O=Q?D(e;UtD}&R^pc`vWs`%00zX z$w+AdlQ+bZ@nh6Na_q9O5>p~#vpM0y=8#$xE|!mEN2IHCPxTBc$jQqqz!LH+t&Qjl zpi|u;G9KOJq*Sc%poibtOWRw+89Nu#tF3XeFhVM=hdWg4_40asv9*5P*xCRYdI4mP zV;^P1n8$+ymg-vc2%lVcC|~XmHs+g=)p51EK`&i%j^GJsE0c2E0W$hqgd+swg^*Ue z+QPKNZiNX_?LBm<^de&!Pm}(H0>XFa^X0oltuT$Ok&+|Gm_Y70TerP@kQ{MjaD_=pB0R|2z5;#=8rdcBrJi)VA+_;||0L0MA_jM935 z@(89p0+fqxl9We)^4y!$6mxQwK|sU{5Ii-%@+MSEq6IdbqZa-8(b0UyGoSI`^E&th zh{i+aljms5j0ama1vQ@MP55!rY{h>98n<3};APHV^2d2Si+6K=v62Cq8npo0;IiIkg@f7PblmJa z4LG>}_A}r_a-EM$S;{mQL@U*UmFfYdY`36SS|zu!Fyz23S~I=^fm}Z713{PFisrqV zwdo7M)>|PI1TU%pxp#CJj~ST4pTnK zv`ocDWAScsJjt}}4!9UoJFM``agsTx-K(ihy;D*9vjr@R+LL!OwcZUTg%y?S)ECrN zfZC`AlfGs=s}|yH#nT2lHybBN5#;10who7|`4{=>Sd4*@eD+`$L!MczGgs~#CTIWg zGWi7cbp@%BGk4To$cU7g2Bo<}^d7gChYJ z`T(hp)(N@-Q4zzGd3*(d3UAmlNaIAQnKaBJ3GDBW@QsYu-GkDKdk<0{Dc4lbG5DpWCOPODjDFM=KZW;Uy97#Z!xd7V zx78Q9%%e$^;+yVMrFiiDa0@HDFTu5bT9d$4l7iIeOCj&|3lV)OP){DcUuuCcvj3%J zTIMe~j4J3qJP+OJ0ko!cM3irG(q#{z3_@nUCGZ!$r$NV50hOeuRtg116e=fsdNxXuO`Uzj%-#RgYpW-&+zcr$-q7 zy+!-JmcU;zu4ciEqvS3_s8!pZz%M zfK96fWhrESx%=JKg4`U1Ef;kvDid_zlS?00p3_(p?Kz3%%LQc>NrUxOUs$|9{@C$& zJk63K4M`x`Gs~YPDHQVDg7iIwGZLvH#dWNIq`%q|v~na+CK<^R`<{*6<2@U4Vekr_ z3o?woB|KiC_xV-i(^q1#!W|kH2zY`DHgF}bPMXE}^MMWd6IyAKGqFT+8KAzdV*1dA zAWV z)2!|6c+!+ss-0sH`p>HD!;%dO`5=(-4F3%pIc2UyRwhLb=FgRse+4pfaJfdVRmh4m z%6@}F{w>!2YD+R{yCZ=5UHd3~H31MGX4S zv$8DFLJkgC1+Rx;0`sI{ZowKS3^Q+)GI{+xBiiIZZc@m%Yvf%TIc0tfGJ3)J^Ce~U z9J%l%)tkg+&Rws$*C-tH^_ctCdd0mlmLRy(0!eVO=6=m{3ORd040q&VKp}H4f|J)P z2u2pnD`mAx-W;18?Pb}mf6{fmhoeu&#%!=B*RSzvlGd1;#%!CGE9`Dhm*y)hasmbJi4@$;6sU4 zOpDj3(C*yGq)%yOm^v2BHYnegNH~Plc#H8rO!y$VO|6_2_=7R8NO^tXp4M4-uP;2A z)ca9=UvT2cfOlQ zn()*Jx|%y`L70&q9~mDd7^y*0$MItn9_oYgJh(EN6D*A**iz8a%5ZvSI9TN?c84wg zI}-SyNA3$wYX+#Zk9KjNv$L08;~zYQQ|$Lm&m0s8czh+7Rr#UnjYeuoC@htXNFNFx zhdS0Tn-XK8@!uwaHdeBYG0B432rY0zGBTd{Z}5jz_}yVLw#DFL8{LOVlnS5Z`^zxh zRV=SING*-AIVc|A}%Tmu+@Z&0TH_w;h+>eM$;@sPT|?LASP`? zWSnOqYm^wTlUj!&Y%LQ7J*5?%;&A5JnE}t}QZl}5W2AbC__~0|&1~)>uZ6MB-~%ea z=uInNu=ePBK!b6Jet}#UVyvHAb*fy<|X_cx>cUi1?Ii2p!ANB^)T6215dX%cE)F1h zb>*Qdyr5^gCm8aKar?@Sf8kA5c#-Gb-hT1_`n;Q@B;R(+ea^{myWwk9ymCoZpVb*1 z|JIyLzu7u^6eCI0SsII;Jf3p`I+0i1V(7GRHYaLDB@}g0O1_`3yqN@8X29>S$SHSw zeM2k4ldU!=pxUCC<(9kFJ1J|eT5dt004-wFv!QSSluxzAs`0rt+@PhbmS5TCF4p&M zES9cCoQ&66N-fkzD+%`EoML3??U+34I2}T`iB55Oob)_|z_AB|pQ4tB6Gp1^bJQ19 z{)x$knbX?Q=h%TJ*?NLOf3VbIm%l+viUeIEY`dU9c3UUc2fKi6**e`D=)zO3t^m1S zTkoVd*O}xB$!Wuk^=il5*+4n89(~DM4RTdeR^~=0{k#EP$$UU~?|(mR{U7%{ZYl)K zOZ`y>&e@1LiD22Ns!9oXe4|N@tc3=SaSQ3 zy+dxVe@;;2Lb(KcF2+K9s3U)^OkS(LNS>+fw(14&;p|G|XLUK#(q-ORwkwRW;Rqwc zLo@>FPJdBbYy_;Sav+A1dYxC32*i*Y&7?+y)KZJoXc*vlQXwssCQ%bZY66p*aN9;F zZL&yBXj!UIx)w{*sEwg@CDXbRv_7?HU5Tu;#Nry9dpP;-k`j#NHHoI@rM-=1C(8ws zQli1RFE}Y-lbo^Q1(WkPndI_$s?n3-XAN!wAmA=RJxrs~33~$&G=;-H*x;mQgaV4! zI6yjXf%7fmq2c9lt6f{7GLr=ixJxW~Xj8OYlMTo<8FGEM#pFGalhG^evQ>_9!sS#1 znjIQUvj6a$pzK8kL7^hJPMg(&u5K`?%NeT$<-cT7mu{;CEq%!(o+z(bWKbs}dL9!{ znBm{_lA0(#Z?lA ze8oHr=FoKgtEODbIB5q2T(nwHt-_fGoTpX`nzX~D^fjvm-D%-8D4c6G&JP-=+fGfV zS>f;$HR#ORspvcpoESw*E5n2R{tCR$sx{2s8m;LpgH@enK-UhtaOwaPD`=Jb?ZWCE zD*iNSwbIJ)I60oMh}66sP3#s)>=q_A5yYy9ZZR(3kwkawG6~mI6jLoX|2q-(()}Db zhJe4KWTGeNr!CBU)@vB5SC!JOd=&oq*RUFMJ1SV7#gdB@S+Bz;m~kq+U1t7rRCo4I zR|{%V5PrAp&((si1#-M`&^Idc1T8)CcSP&6UFx%)^?BiSlc3Lb*5^lr_%ST?R6%&R zNg3Wyfw#=(DFF~B+ydZqPmmupz|`CU0CcZzy8)j%`wgxE%G_b^bPv7C=?(*?4bs3b z*A0GC8&K}ZTZpii{=)`*2LV{?b+*%=4VNs71$IihvbuXGf!*qz1h~}`jk02SCjm9f?nd2K$S^tT zm>a#vL#l`0!rUexUUeJEF+j5KklN74jFq<+MVjmnm9?YBJ7nHZMlG|ctQ}SCH7R{} zSvz`duSs3rD{Dvp*a!4DUoNm@&fgU+`@f{@|6k0Mm!m7^1h@Z5V=J@+Tu z+x0%rFE8a;fu|5qd%6#D^Z`KNe|HJANMk<0QpKIzBHhO`3YUG@f(m7SPRMoI^hehS+6(-6hs&|_3^YdTyGQD~hxHxuDHa2X_6Ss)w|$CbSlTO4c|3;zto$x3 z|MsUQ@sfYN68k-f5wf?VtR|DvvG%{PsU@=)Tl}-$ik4%)lw&{3v8qYRv7hDmcN5F; zG0R~;pfceA%hB_INr}&_5%jduYNz_bG@nDJx7G;qA22EXqcwt7AAl*pSR<%O;r*$; z!2cKUjkSW_0e;e2L8pG!f)2xYjzWu{|9-R@-%2&UWi>AROg3rX3QS)F5#r4V^ey}4 zJpjlG`dij*BLIOQrZyx-^Jl1@t19V7RyOT(lbrMSN`7CduS-|e#{_Eqr@lnzzj-k( zW^;#zc#6H1?g}26(fz!h^!b+AYr$+ zf|i=7KbL=PQX0a8t4e8!dG!lURH1iqVSA8Mq<;&GC3aXVs6u7%eD#Hie}suM)(Tqi ztx4(q)(TpUA9-s9y^9}X*9vO;ok@v#i%bfrgqpwwMDz+eCsP)g)cZS=(!+~Pnus4a zFEZ%{{J3M0N$bCZ4EEx+g0k7d9U%>dM8n0?OH*#3T2b}0ufI{C2m+Ns9phe zByK9HF&hrIl{DxOva!)`xBDrlsW`;T;jrVpbNzG?7f zJ#TWt+#h6x{3bWwH~xUFj@SPoK)X~tUzD6?|)!#ZsCRn zf?i*^)Z*9hQ8eFA37KM$oM2$kWZu_ zt^j??(%;ss)hUQb1dMYSe|`nw4RF5!AN0dt6-971oF z=v@}v{N%c^n2P;Uqx+z`l6goFo+UEcSa)ntH{(m!l>6Y7w(g&^+ zlzAA|z}+bIOAITDq^M0%q}X<27#p@rSt_~T9K{{*LIuA)3XY6vQ1At;HLTnm1!3a9 zSj4mLlf-X{B7!6V1z!Xr(HWx#$X13l#bnH4!Iku4fePWi-;gmxi@A~C%pnhkc8j^^ z(Da)wXfn%7E5lhc&g*MYEc{9uQ??rqO3Dv1W%0YD{2=$WEqQ{x#hX_Rmua7f>BLGSbA7>3y z{y2`K?1yPSfp4SvZI}GEGrycaq>9_QA6ANx?1ydVe%LJlNM75S*E7fS%JEkQDm*hR zF4^Blb9+y6dyly_GC896#N8hz(dmDhq`1Au+@1tLa(j=t9s1KGN8NpR)vKZ;9}j}< zXA@j>1U?S$%)ehc*2i&<)u7;?wcuE4ib=~^_(Qb3UrTwvW_hcQNb7vfgPbKCV%Peb z#e46FNr?sP1T9o5uT)>ulfR(sd)Ep2{Rjpt>ja&7)TACW*I|X1%# zZH8R3}$y~avM zS>3^gjU3JQa|d=lcVIs^Y~(7Y`DW$TBpQ@pBXZO%(s`|HWPj^4qO1oEj(JS>Pg&(UmEslk1^qKXA1Ytb$|hC5u9cphq zhHFk9=UcjVF@&Awao&S=lifzHDq3OU?zk81HnO9$(V(PAOHdt*r!_P6g$ZQ`gQad( z_~%6FhsGtVPU_c}^@GNRQ4n^A)7!v!h^kXj=UT{33et}us3%gEOM(|h7)6_>*c~n| z=QBu_SqhItn`gH)&u%u)mNquZzytfVoA31c8DVLh-E5qGNjA!;^n}a(CBVUSqA$r7 zxgSYRzL2AEIhK@yL3)Gf)F;`o|Y|6Zbtov<`qy-tyYtEc~V8c`9Vh`6KKU!~1fU)Y|d>T^$9nhQ?oacZmq z3JaX1LvW8XSAoZ>uSMhqLG+5#MqOSPM4~MuLf$tj^~V5_hmMC;O7nNA5+bg`AI7K0 zq`3+ig1%a|g1BKo^wrL9Ym@UgwJIhILtJZbM**;vbQ)D$5z~Pr*^f)g#h`=wY6Y-4 zszybXFf!b+UO~!RcE@tJucX3rDLYWe^2C$`xld6R*dL@v{=gpjbz2lzE}lQI2d1>M zks?z<#t%G)aXi4$^teGTIrc->Cl4$dWdg4IEHps%G-z|ze4FVdZ3;WzwG z>H1js$wFK5zv0*0!Y_8n@mZZ6ctw~yi@JjV{mC`$Gl_?y3 zrIL7t`npQ#JzLkel4ub}SEy9;)z?)@AJ}kux}Kv)DsZ{_x=QII8@e+4IQlw8bcy=9 zO6goeaGnRaR?IUsov|uwA|*i3V_VvP#uceO;yW zxeZO{)g1kbO0{2o(@N=Iyv>=&xk)8yR$o^seQ85SZZ?uSvQ`=Z{lwn%3_yu5t`n53 zD0NU@IO2EslZbmUyDDg3^+m+@sJL)7W^qRw$!=1La=)?a3$(*PLqoH%qiSdhSH?9o z{!>&#a~FP~q1kYXYH0c@O8wOr)xXuye0U0~bu=`!ilo)h96^+lYQ9RlR(&DWRxMS^ zsY0dI8}W7v3v7sqTrq@bMq<7d-w0zDkaoC(tPP zgJDkzJ=f~NyPUMRlZ_ntPw^;Vbt^2spLvFjT$QxCRqwl;bp07<3mnZ7VJ9OzdPdk$6KN7L<8G~G5`nnbFf-O7KMvlT~4RYa`zz}`g3U2>7pjoS!<4;U* z=UHGvjz$Tb%?FXw{)x7#u%7X5Wl$ABTsjSq8E*O;6^;|c8A7XNs`h{+{=*<@ZX<;`q`qKx3S#ZJ zY$HqIpT>}945B&(=>$k3y4p=ilEPQ&3kqjiv4<7(PxVFYKRI@L0+GWZ)1Ioni0y2} zrYUGo^+jwK#Bw)1WsTHjGDFY{%8!C@FKAc_{2NiVD(Wf=xj{h+8G?R)vx1~C1gTQi zN)~`H!S9$LW+w|(RJtJ9ExfZcc%gq)r=l`&`KOvq1hr-b?-8NDPSWoM2=`;t+Dn6E zsV`K%SE%ufj_j!-QB$bw2H$|9wls<_^lDOYFLSx@p0?C*lH~HX`hrUkvG$YJ3rb!u zrK)5IDC8^1bqoQ8S_Qe4Ab+w$)A;g!|R`9VEL6>I-()TM27a!W$R@;`ItL zn;}pw<#|bbR+MUh*D3fd5qRRvd)m_elO@L{^##XnObzNZt5ic70^w6OO8S7X@V-=? zn}sTBc!UKfX;APHOa#Kv3zEpCQJ7$tt>Bk2CfL;|$QYDsbk{p{(d-&jR6!IG5N}rS z332e07bVk0aqwCNzat6`)(r|ihlK&!^7V&h!%FS$%*?3XG!Z8 zsxSQT2V8zF$JI?LAx4PN{-3g0GCJ2{U8o@aqF6(udIi5Y4&J2TgCcO*754hV!-D=w zbc1!{RM=GtDI2L3tqtrTUC|m#Js)(D-QZDGl|(DJcJ9~JMwvcrSC(PQ^oH;ruCb!^ zNexLvSZ5q0H)U!JNAeaPcRbS?&D_MUHgY6yMY}?)TBBRIv@63(UmqB#1$yuN7-Bg!hziNP+)E60Gb!76o!I#jH2`G|Q zm!SetBpZ;rC;@nsd^@P4B}wigE!RPPL0wfdXcHP$!c}nI5eCtlpvo?j^^#Ny6KUz& z4I*cE$kKPcpgI+`6{&`;7c>%4GWU3|xU_DOCurqdO*bir%)vKs6|Pz+e4{m5ZGU$g zWqR?RIvHb3*R7{oW zWhBCKUzr5>U1;a+qt|0IqEz!;L6q)ZFX&E}je2<3BUrUwP{%YIrQf(-(E0eWV7;K( z_;L4oL64=`i0x6UXiUYNE9}uhy`~)*xyntzu!y!qpnJ4MK5xxKyU$x=XxA@2%BMeP z748N|o9uHo*-rp@Ng?#;Rrg5a^7U6Nw|_mUMV^;h{y5ahyv>s7zv zpRe|?kxORDS1iNV03cIj&~dg+<+&WZ7P(7gzFVrLq(!UPELCh~6?>hHz9si0n_FYF zT6(tXH)5xcn%0t)oHFi@T+?8pC3&07vV+JYOtd_3wvBjEEMLV~6D@B;MoFhm#r*_2 z(pRP2mEKUu>nlTjqXlkEj~4tFDfllecv4SG@LyQ)!97nPc#~rID;ToiS+7XJ4{O0^ z_EdrgRE#BfeNXmKuH{wSZ=e$`c!{5POO#sT*7c7T`%fwMpDgy55L^4tpX@(pWx&gb z{^WY}t_&Nwq?giB&hC#HQX2LI`K|*x#$K_omyH~>oeSV>K2`Y#AhkQx%%9OtV$o^)Lla zc#r6BlPA1y!4I79PCg$4cI=xu;~Z(jH1&lIm$8sAen64JO$1VrCI#Vh`aF!yZj_=t z26h<6UU9xnt~&QWSJED!zDeq-4PJHL2N8G}ThkaljAg#{ismDhB=lAk7r4Q%oE{yI zY!!v0={h$7uTk(PahGvam%c&4XM%VO<;*Hxg%?QqWn1>FoonuA?XRtO()}06GWDz- zeUrTx*mzI$bhS{pitl3XeIZKFUc)MG_3Y`7T&O&~o;}?dU?WE@u)HCa{5eh@8h{+- zmrat-mx83JFEZ&lZSE}tl({>o*i+Rv#@ty0k>i*((9vDv(1;SBzo#t?QQ%zl)x2ak zF?*lP2As}OHf_OzcppyLuK7YKy0qbDX~WHI!?6RU4L7q5ZyktwVqe5sTi9CP3{=+I z!q&>Y2=xMTm)@3=G(_bt;Dri~qXL$MSgX7s(=!cg`I(qW=2J12a5V$8kXZbc`?-wF zjZS*_BAa}C4S^3XvPD0>W?h9_G7a+^EguBalp8MA90n56#YReIz#Vi~_Riw-Vfv!y zi?@6)DEVS(mWwepas>iKBm=J@pe}mm*rIG3#oX+igQop=1&zK`c?cpV4uXl_#&r_< z+U9C7*)zz7%btB@;c|I&<&44bXYX`>m?i}Z`KQ1gC9Sw7u0?zoZ`rOK}{;T56Ms6AZYkd8>Odg5Htlikv!Aa z7q^^iI=7kzMd#WA>rT(!oNI=<&~|}Q^&dfthRSSPz#Ht$VKzd($&d>nX-@^YWSH!+ zNuVLoTE3KdfdNC}{+nBwbmcG`xu&SVBg1UOnkT<24Yx>Z{^~HL`Paj=b+Q#st;TT- zS2(?fYn*_>!Ny9q;24clW#QB*oQE`>B^u{N;2dvdz9hP3Q)?3^=R})&)d@_!%4lj9 z^vQ5(>Q!uyF1gawGBgY!X;lUB=Ej&>qQRWcaq@+^Y)%o`Y`LhT3)w!=r7u&az=_#kVfOcSum%(b9(NSjh^6ib#bXMyOba+UMISLmGhA zB?P?=>YJ|}RHi2T%ngESl_uvwljR!(jnB7H`qLW(-Gd)%HwfB@ADcD^+MlmVY=cUa zfkdeKj{yZ$f9hrMBc|M}koo}$l!soXD9^zUP+ox_p#0ipigL<_()bqT?|}l!$zv?a z`3kAOrhLvAMLCEcpnNxefb#QW6y+L~$fEoaP(b-8Q?~Lf<|^uzvrcDOeQtEt9kl8W z+0!7^W*s!re-YHVKxW-RF5IOE6_E-pDBzZxI|V;+{L=+Ca@2o_mRsF4{fmLM(me$> zN(;JuCH_kKS=}_9@)5>%B{S$SCq0OywSf8grs>}pnl={ZBRg)IZu&?X`4=Ke$4%2C z5Xm=9C-0Y0zhRNfGD*3|9XN4Ks;$tJ7j2C+tKY?SfuW3%cHXaod}F08(ok`3K%j_Z z;D@m`a>*N)`M&7(<6s#-^#Eor0_9#h8-c0gY~;w^57j9XfQQGS3ayb4zM%CxKrAKn zP0}rAsTRKZl4vD5wyZ?QKYkU|?{XVu;Ns|x;C>xKngA%Un1U8HDonz<5YcHwg7y? z;z>O}k$Se(I;32o^vt<}b%+^D;1(4vqQ1^I<~|0Td@H>|jUo_Oa7D~0A_-H69>2Vj z=td5eSCx6jdrSDjjhdR88k3=3UwAOC9f7zFGFg?^7Y>AjQQ$V976=T(F&De2b4EX{ zjQM&V@lv7@irh3o9Z!1Mpp=hW^ED8GPYlZZuP?H8hy9h_ViHm0GShnCoH8!5_6I!{ z-7H044g)(H88e7F-r;i6)3D8ScLh;LbaVH>#xe8?s@;KcQc;Uap+!xN*W4u~L85|! zo(?()f#2G!3eEU0MOsDp*Ly7}I6TatSWNvN7FW z?OsWXR=ewacP)u3g6S^8V2UwdI-yyZlF)(()q#Njv=BlOB?JiD-h{=E@<(F^iF1Vrv_y#D_yhdXAz?MuLDiHnQ`xWSpKy|@4AY`!Lp)`6 z@8`Z`(D;A3((Ne%T|{TyQRJ>*aoP)=7Z$9t3UURj>thMaATgS6S$mbmb~j{|Rr(CV z2@dyG%1nn-rKgo9tm;VAEY+Q$(lbs9R?kj^A|wuB;uG$K4*wq@!zrbZY4;6C)H`Sc z=ugo6yNdX}GyMxT!L2A|sb9GyPMV3ZVOcYIJ9@C%G-5^w1soND zWE)Y##v&>+jTVdAu~tZfIN|<UorNsJT$tJ@-~vrv8s3tL+nXH9cB=@tQyVgFK= zq*H%?w}z{%(lAY_P|65aH+5SiE>xF>p4vAA5J-tMU$d!TG+)(=LeUfur7eDz2c6qf zM4*%Cz)qmb0fuXILNNeZKLeObsQPo(&%-IC%IGIKk>^sVE%-Rn^e~sUVyt#<3#)3Q$%IU=pqkpL}UkXkvh8$Q_AK7JXE)&2i5m4ArEdX9(7D)2V7v5|-26gxid8`!S&ys^nI* zsl+gWL{dqTg~>F~ob2vkLi>;X#PQa2srt zsP8H~0Yw5$3G`xj^#N1eXqX)&z850Up&+?5!fhRi2#MLk)$a*(;N=^}Ru)UdTe-~V zphr|@&|KwI6IOLSvMK~NZ*!Gswo+RnBpMuzGQD|86vVlCZhB5X3-29eD%lAqrK9oG zUs4UT5 zOViPpC~L7M&w!GG5A$Dj6EW9AqK>-@=q9R8 z0{7?8vlIBm63;g}63ZNcKL*)^<&K>|;&UCFRasn)SdCCmBF7j1;(qOmUw^wmN$c{( zv#iEYs;$Yu?toOnBvC9fH!M3PhpG&P#-b^s`j|v4-C=MI5Epsenr0b{c*I&1O|?x+ zq)5!&F4<%efe=?k{{vOrx#2g|VW=WwL<(|BEXE#Ag;YMf&_97iI80`l*TQ%#8gB=Q z5h4(Ynhp_&p@^_uST1{uS`DT-H!(dCizPZq6b*LxiiJqj1loW8mz;ApW_$vPFTi-h zs>WnEs{gd8Ng@tinMORNSaNwE*lNC0{U@4nRT(ao zsAX%Y!Bpmw-4=5L(A3=l^^@pj`^YI&AYn%Jc!FEE6(WulG@RFhXoo@K<>A}sK*vAR->9qtq?I(nxGok{8bu&w5xsmr?uIwQq}pGm z#y?rbRw|Ur@;`eiA!6Gl?57ect57e{V+&UEtQCpFMWDrUxn(y9+CCEF+z?JB!ZD^$ ztkoXt=nZ11L?K;kn$_1?ThZH*{s*%r_Ece`N1dY<>#>XHaC^z-V4i?;J#mk@9((80 z;kS>H)NrC&!8sgtYZU_a6ZfAWpm#oT>eVNCNj-Xc_-{u^I^g6%8=5{>lR~SPc3=!6ZaP(!r3duFs~v??F!&0?gLA_?kDa$5X8nN z69{iaC=`RWce|P3C+`2U1nX_@U^&%(;(l}pD@^_kKjl%$fFP*v1S~`SVp|?f3VA6w zX8XzymM)`hp_(Gs1VpSb^QTOJ+X?4{s$+w$l} z{17DR4gBzvq>*7Sm5(M#v+!d#lJrm*eAugXynh87SN;b!W!qAt&uvrg$MZ3(n|VAR zHf1mCo-ekIk+gF}8P)l`pm_2}Nc%I(4ajxgxagrX6DqWt-8<6%oMcJYXO+|<;?_AI4<0KiUc%4mM3c}vh zb*{6i>te>U@f0?~)f{*Ufm8Iht^M1*RMD=tdu?iWHg&CHQ1b6hlD537-1pAd4s;Jm z`k~!R!AD5al9-o*TS(G5F*s`g(|ZbdoDKX(zyP?mLj$*U*uZrfn74}o^CrLmcx+q) z&xxaaZTH<_M53Yph3~dYw71w@viCYATgxSDcMm3T$=ciV?)5yptE4O9s$|u}kKZBG zOgeN{0=2{rO-W|9mfh>Y*0PHc!zF*OXY;O-UQ61wNMR%YUr17q)+Wx#1(yg(WlOyj zY!{MF!w*YHdI~>I6OxKeF9pvRl8!e~7|7nIv-7i($bK_mkbN6|Ap0!KOPu{u{6O}% zEjv3d)kP!E8{LpSn9|hG!4G8rBYuGT&S_1(Iqfdptes*1gX(J+Zhwipkhi+Foek{@ zw~7tBOS&tq3VAF0rY{g`CLP*o849^0iSXUKOPaha(?k0w!=JFsONEW^qZg$&rG3Sl z(#~9#0C+%$zuA=brQVd*53p#Yi~hw~uSeFJ4{)F`)n=IV4afdz8PuybrS*KED&|+{ zgxw`1{TqqADXmW@eZ$)V?7C)eO8Xxagk1o2qud3tuU)-%RN3~e+*8sXovIl2MaQ)T zfo4*HYY^Z<*mu&NlKMNH?Qj)<{RZJ!LhY5S?&9vS+9-E)mzN3$px`L&2ha<;upO@S zue=kh7BMd&#!;WW9qxaiK3i=yW8CT+W2>)b2WC8`buUSKbnBgWV<6Uggs}6DLysY( zciwTtzYxJ19h2Tm()d&Djd(jDvvDd;NI*$yBObroJ?S)~^UjXxH1i*U_0`Ad3z4{B_U)q>wWPk~b+H<#DCTo-HZ*Y?}#dvT}WPbTf0c}3RZrV&>1hE>*{H`To0ypBF-xV!nf$M*sp~+hS z8FQwW*|Vq{m_9nwOMah6Qr}+%L7%6hOs?}ty8SFKRo&`AX_L&bo4Km1*5R0@+nkI1 zhdh$r)RKIpzrfoGyf1nr#d_GxT`M*iJV&CYgJxi^jSf%A@5cJy4U(+xQ3bpk^#3#h zYLQ^KU))qg6=!D(d^c6#b}5M6ASw=}w$UjrviwV&iK8mrw}wu zGoGMwP&?!txX8v21yyhf&%uJz#&h5i{wCN^;t87kd7_9eT<#?*+B#FIW6Uh|t%%ZG zaQE?YzFBiF4%`-=3!DSUA2H~JE7ld!{^#mqKLJEnQS%1wr?CtBd^ma&lW}Kaz#7|F6!p4i}MK<0^Kxg}zY+U+zEgMOH7P85VY%-8-aF9(#`6y(A zc81Kdocwk3ZE?X=T8!su;xw{8aS?8F4VkS*id4)Q7u$@cFjj17o%>BU;68!ym8`5xfs$m1on&B07DgE!HZ} z@3BEbdF54VVw>r@maZd-`Fi(?O}Au$twYe?sP6jS$f)iGt+>&NN#8UkdOg!`Dxx19 zf+|dfTmFvgu&@<_?(tv|v~-o1U!BkyH*voDRnXO+cP^%ye_*PPs1_TY^ADAhKGI4k zZBopD6+X_=Atkk}@=~x zE)PH>^EhJs9bQQ_p9lh*2SFR>dnJ8viI;+xdL`AZ@lx>DUP%|?N55CnYippE=e&|i z7Ag_P=r1sghg$N#_e$!~3b;&vk=g+KB|b?@F7;CRu0Bb3U23~D$68@M*xbXHZ#sJ1 za;zez^mzs!Uq1sIdbV6@SA;X5TdFV(i+;s#GJJWdmkN6zaTrVN=kl27=(VuNa-XDr zE%n*p>PVlY)LJj)&-6(uT%?GN(O(oeE^iNpeUh#K0?Lp1B<-%#_tsy?xEvbrU+R;z zRpY-HG5L@9Bu&(Cb@~f<9CD@n4}6kl>(~YQi_}$|cZpd{7wFi_^cSf({91LYS&S=n zN`^@){Zy6XT10?_C7%i!M?_};4dov^Owtye|4sb`o;66$j|`LagN`MwZltatqVkJ| zNqPv9=dT?mX+ND@qrU(D33rGh0r_N9`s!hsE0WPjtvb7Y+vVsinQ1H3YS7`!I*aHX#GvAi2W-_9m{?1N;7$+Y z?PdPAhDqxCR4|Vf(9OSxNxJq5FR`oK`l)i2&qAl<&&!uo`k4Yg5~Wd_FRA=WFO?sX zFX{Lzz2u*sFRAe}LFGg=91fQJt@)CAbgp`kSdlO3g)6tKaKUOT+8W2P6|IJT{Z$1p zht)9$>X>?!7kAdT63sdFo+2`?va8-4NQq}i=lYXH^zc<)%0FdkG0oP9+VmF%(g>0A zugI6QT*t20U!)#`)WOz~jM`hSY|ctgb{E)5%;8Dk@NMRh=p^t_xz6VAB<4?*r26gu zZL*m9v~~<==cQyZnd`iizhaT*Lf-UTE3*u@#Fn`Ne5pIw}VnI{ah7f8;GLRpSW6=dgJF=rT!VpPXjY% zk6%*JdfPC3*F$^N0)A%2A4#>BENK~^=WXiK3L|L^znMTaE@fZtqXm2 zG~V8TM{@0+sGY56u__6%9s}w)cOK)|4P1DQjVL|@BHazOG4y6(G8vNzO!W>XHOAB| zVQ)Z?N9a!0#wBiMRtO%stq^EcTdq-a_AB_+q`y#n*bN zs84hCf&Q}hSjp+ewJ7r{zodcB10%&J>5^``ndopN8_1GyqmpnVOE`Ihm&Tj8k^uYtPS}9a zD2a_~lR-6{p4iBIL&AIc*SZZ}DoSaTx9KlS*vILQZ$PI45+-$`4UEF>LrrOb%>ayi ztDp%DR~oWQqWa8a4N<>%;vS57&rH@L01kD%@7D~n3-4#Ls)SpOxM4=an1W#y*eH7) z)L`OyrGn>K1%=lu6+F)b_P^e?z~|jGo>w&T@9;~i)3_q~3ygn+Sikp6ieK-g;PZY- zcj3nyeo0%dA6!7ZvC(0vBy39!L@z7WUS`&cZ&3F4GTYyAH>lEi*=>I>E5<4e`V;^9 zG* z*KmuM#&;53=)s26wp*0o7kYSujWWv%)##|gpq2dV)?2((G*P2$(_c1AwTF_t8;e|HKb)MOq9J5@<9BQ*%rx|3kT*qu>nI#wef`o&y zG2$-zTa*TFVGZnZtJ1(NjQ_A(ZL7S+P2v_sf~}G?uD$gaQdM9`^;^HB({9yP`4E1< zD&M-*E*{<@>#(xbaI^G`GnRdZ$FjTM=B4pT-W>Ui2YY5uzRhOr8O2M5v+ea+&aw73 z%+}CmPW{tu?s58N&!BPoW)CK!$iH~Fq?&&!7dT50m7h6WlDOSV{)>i7TJlfTGs5vd z|8*!Q4f;z+yJ@(j^zB{>J~Uj?ows`_|B2y}F4Ic6Qh(92IvghZ=5R?b-VRmhlCq3c zQz{y6@17b>Im$@3xRvsbqm*}8MBg2@4&GrYaVVb#JeaGRc?YC7O&Z{o`|r>bHP~8* zrpUou((0zLF3%oguHy%63hwmMc(pxM-Ap(K5vm;4<)Ivw-)Zx(E)Nw523bpDT^@Iq zDq3hgM?ZY0=4U-8ynUzUNAG5J*jSRq&g~96w=+8v@3M*A&R8|E+Zm@ac)Km?G($;j z0JIVIMBQ?}svgpOm9Z?l%S+?2-~Cl4(vKiiN3Swo<8GU%3M)FiliI<$_kAdiwoNvz_xUz- zSWykR0<{RR{VIq0z6w;d&<8#++q+2{^9P*p)+R6c7ZynB|3bNrNFGtJxj@n{?*)tY zasXI#_oQtuH#pp?^?=iL_yYZf)NMfdqDPX+C?Pe5g7eJd@vyj!fjZoBf9hief66?s zz1J4{Qy({GgK_FgU1XDri*`@lKKm^i`_jTtx*WW-aJL!>m}Cib0A zj0Zb_Fk zCw=Hs*~#Mg9Ez7vc(&&rWX^jh!SvD)RSS4V*SYI zbVY?9wXy1{#Fi-S2J^Z3Q7sRz5bg3iFBK*sPNF4B`vBmk-)Vq-z)sg=UMkwEIhIQm z8x@Im+IJN0sqROS`0pu@)c2JjFjqxH!N&_EmG^ro_*{XcEBd`u{%V1w_Q$jwn;D|y=_dM>U;J89buRQL>tQNQPUn|VjwqyJT*~<$h^?a>d zCh-giSAw`Y>4Bd8T=mmYUQbGKFIb6-#CUA}UIZW3{(I;z)L{@Uf<7Pky{)`OFafG) ziA3pG*zl+)u!A5LYPIMjgiqoy@ha-2_>Ja>ryL%5!b|y+3nlHR$xhT?C@BObH5W?y z^Ak`YYOiM8;c`i*o7>gs^GAqQ3sC*x35Va+JoS0NlU^F%9*r|Z_@*LCBO;!NPsOR2 zXuK6UQDApIX-l@6hYwT{ju|0yu7;$?8S4EfAtm)Pi0>&(J11hXP;<}9@_j1$4z}2ieh^x`9|5pZMHoaPkYH9FO<~y&35*HZVNmG?}B%f+SBb;?$^R@ zcD`n9e)hD}m-?FZUGt3B?$drP;4WI9vDNrB>qs|0Uo)#z;q(ycU#zZcpJ9Do&fWG` z5isc|#{4Y;7Hty`UtdJ~KZ}~D&!)|P)=NcKY9m;$zpRjNnab18!slR3u~+NkTNoAA z6nEc@BvhS#os?&Hk*oxJ%{oo zRb^Iah%5CMRc0*sLY29lAyH*ew$t$&2OUh=td zL3-}dpaIAOJzxDtFI7R$O382iQ7f5V@DeNemoF&QKxB3JBB+jgJF+&O?v{BI28rlo z(I(F>zb>NRyiuzobagl7DAPP>(!&HE_ZP-u9B0@|P7!`aqY&xB3ekcePHHRXFrBjW^K*)p1{2 z(wZ!ZpUM;CBScT-b=_A)*2}iIPw}`%X)GI;I;>+Osmnt4v_keYBfH!|_H-Uv1j>Kl zh60+XGaRPBsBurjLbu#dKo7nQ161bc=6^wV(r`#M>YS>?o>3&9VUlk;B%k4tXiomI zdBesrkcDfr!nK)kZF6vK&O>iSSK!S&0L+D0FD%+9YtC{T))s|&3!@IaVyk(JOU-ek z6UVw#nK{|}dfaGU!2`4*lMmdz!g(8Baaw2YF|dtgSAm^HB)Cth=g@TTF_K8pzR>qsuh|>^jzRFkKVjwh--0S4G#(Lk zucHE*rdkQR{B<<(CEs)UT%Lko>ZI3fQ%Ft#5_Hl(ddC)IKnwCCen61Y zceNk`+Xe|z|E?CK&A{ArA{O8$R;Zl47bO5A!euy|kA~is?cIX5CIZSLyaP z{e^CME(0vx3GB}mNE-g0W@!n2fTeTZ(<~*)Ls{d7Ti(+wZGMkgLJ4lrEL~s=`w4)6 z`KtFd=Ee{7h{XCpV=fhgFkk+G#{A$1z|8zFKVR|xf&PO3(`+hlA_J&Q_^YO}53QDb>2@dPz>yTq56UWtZG)lFo<}w zVx3NF)L+2xx*{egE_P7a$vrNxG^|mhf`Q$n)AF^u0IFWlfwA2-DA6`NqI?)eqt0`s z{vt0nAmqbH7U)Pj5qTYh`4K!Sm(CN=xcAL=Sp6%M;9@=M!)P%+$%phuqhgo-&FQU= zMrXbGZ?<0_4PStLh5?MCuKF8hE}|ywgxdciLU3HX!{D%wyj0jHJY@T&{ek(=kI@;b z)U=cOTLB!00>_&lE2mRShl0|CzoWcfDw5QHv10d0{RPwL8U#NslC;nP8_>xg=r6$b zg9rIb`uw4YUuUmVKgUj2oj#!y!q%Ht9ON z^C_xL5=HRrRNd+Pg8D@fREHkjDEt`psh28~G-4cX>B+|`{#Gr-%vA~_@QsAQVntsq z=i4AP8l+Y)=La|LR804M>UFPO|MV$WI4q2B6&}Vm1}+P0;ANlbHSi@G(FXkm!Y<^gs8SrrJd_+UZ)fo_{~Wdhu*o`kn*IU| zn|v5-KL!KyGj;NM{RJ3y!>}OpKG%ZSP#0*Z9D##Z#5{Umi8M}X)Ja^oF|dCgNjyvK=+He)ZL5(kQw$0{P5q`8<|F+D!;`?u ziyle!UxAgZa>3-8gJj9oHZo+Jin2jRcY1_qDvGt`D=a|crZk!gtzPq$U0ze6c%{K< z`1#IPcts!6Jo6De_G|Pcjd0qu3`8zO>`x0pdEz+Qg%hUr_$CQO9$>|Xp^<-gU|@=LzO5!E~oRm9UB z;4lh#Cw+%m>=4dhw4YlJa%4v2Qhu>RjzUzf;-DaB>b%INRWo*)h;*$FTcQRMYd zWul$Bc__yJ{PiOw)p(UvOhYjpJ3`V=-+L(-8X@VxAG~TBtk>(|3NC6#TytlA=Gt3`iTEPf_O$oG`o< zNM)Ps)k)lwK$7<-)C?pa>@~TKJqsfOIo63xDzsjizRK&^HDHaEQzF#%1!aX6v80V&A65MWd4k^*OZunRN94~dku;#Q zq`}g-5=moxNGdOpR8yd^Ekn}e5=j^Md=zXbk@OgTG?qvb!+cb}q(o90KVl`4o*M>r z6ZyMJBrPdWWyX7nE-I1q*)SgkFD;R@Pd>nW*Of@85bB)5P@9S_&uhG&cW+>^@p2x| ztM(Ap9Ih#pa`MmyyC6|juf)__gy>41MQqJ?O7Th$yuh{jb}6bbinNx*l^*y5Dx^XW zKW0Q%dEjXV@_kge1aWa%&oSfuKB{P@bsj9V83-734QF2Ohg3Z0yQNSmZ!ddt>gM4- z@;_K2skA6lYFZ-fzIJ42bYwxNv%sDdF97QY6xgg6pr@-jFJQ*G^_Yz{MF+)(EL7uc zRO5gu?VuXxLNzXfswEnWVa^`Rp)G3awj6|gH`w~diM%2)zQrOc!tG(dDR8u11Tvo$ z_{jfqiKLz)4`Nc>{eMKG`O+;t=h=OBA zO1iwzhmlOxNJ-ynnSRh;Fu*uxjFj{sa5j#V^d^2BKT^^*&WVQA-su2&904}mXbZA8 z5&AqWM8#;A0!6l_i(Tr`9iDw#EE)}IV@0~6kAdE9Eb^&RKq0854jcd0o7{BvuuEbO zsOUR~)E+KUdjKObsXakz;RsD?FC?rO;Zsd3H@$mhnqqEh_u{6P?Qo#TLl6yTt;*3p zQL&HwEh8nBjM&Z&GZzv$4A>-3Y}vMykyCX(c}LpE*)ea(l)rt1#7$lGXFMHrtI3`bP~g2vFO8IR;7GJinAEkC zmkb8Or0%f*)8?5Gf%`%>3q3lYSdW>dos|eXLxj^Du6E`v3giP@Hhiy>j~Tkw0Un3p zxFR~=;6tMNiZMG!>8*B|<7nln0&ggvis{kENBa0tG7iuvTPLT&Pg4GysCSy^R{aG6 zo|D-&QqqZ|d=wluO4625@ROOd$O#MHhRe?(-tRDhWo7-O9;DuPv`;;$hrmgreObql zQ{74RPG7>Rc6K#8xcY9h1#{uVCgjU(oCrL(1Dq%$2hEaS=Rk+G9?uxfA_51G8Jfk+ zJU;bkWR17yK#KK;q>U7TTX%reeH_q1H;@bMhj$gx+csJRcG@Y6SM0>8tjb(Vsy1Y! zF>vtJ-QlpSn(cn^rE0d5wg16h;CyT={z3CrV&fw2H0uraP&w`&Or%pVS5Vite%rMB z?0oBa1JCH4HxL zPNlCSA!YFe?7RHq zb|uAhZ2TKCbYMbk4J!o1i)9g8y(3~nl@@Pza!72=kQ(1Y^fHU|kfx%xfxo4xeTAmNZ$k|E{Db)g4kb$u4SCwD{-a=}5d$`&MoN_Ptq4)*>G73H4R zc_ux;qYAjEpWPFlkL~5BYaG4$%lyJSF?mx1HO`4GIuh|s(?TpG(90_TXAJVp%s3EX;Hq4#IG5HEh{rrY03mQ14N2;iXW9w(Dh!Pdibllu} z6`|2Cn7`1DVKhw*4M#0*YFNNP%nZlWFI2=i7suGdnVrQ87tET|uz22_St{e1pw=FZ zN9@hU;hTySHbgpSco54N5zFhMh+&L~X-b6KIVR6RmuO7HVjRa3Vi`AcwK_DqKR^*Z zJ8Gq}lMX?WnMkDOgu0s4{n-}hVZm`{GuFT`7RMWnPUMti(uhZ#c%}>BhOTfdZAF(E zP7*VYqyIT!C6<@}$b@=o8FDc_L0>*-gPfSugX6*i0r<0;JnrhO`V2!CFq7(~f1 zZ3xE^`16(whT?Zr{CPH<{COXJSIXsg@fR9dxXlQ+FVY9s{CSl> zm2)~~=$OY=?6Rv5h1;NGx>T`pzny!E*=mtLuT;ll6_In{>7;ZD+w2=dsWz0YmX2ko zC3_HGO@dcYom>@c+Fv) zz}qWlUWccGvZ$kT9hs9(8C}8Zh!Hl877{HY(2*wk#Rj|v5{ppVG0zlG86Fvp;a3iV z@dOG~?8H>idJTpNC*u5s4xS1~i2$yBVV@R=TGO$n%gP^|$uPMt-~fcE5<3K~tb*** z3@aSeyQ@Ww2qYs!f4m`=%uU@)7cyHZGto_Aln7WvU*1qe&a(o8`Ee708%&Wnct=F0 zPYYH@<4UKS^)nH;195x2P7&DP2M#KNgP?dWf`>QH_sdVS=8+iiia<-8==K|3%EMz(=v|k^={+x+yfReN_tZJe?QGD+ zl6_W1v}X9(Q3hN`qXalMlVOJfRI;{=M2p&_AW8%mIf^ErAI)(;W-++FK-HaQG-W7V zfA-($S`AO|s*r7+gLu`?m*{bfV>nQut;+dQ|lR3er?h{0}vhRm5x=DBsWQ z_m_SNhrNB7Krz$dboIJ`Snd^>M=;=#jCt{fENg3= zE0r}Qc;%5OZ?KSq8#^vgd(>p3YIIQIYN*5b3X#LEyRL{HL06uKPI@V4JN$#{<&=1Y zrEpL#*++S8vA&RqW{B|}X`&f$obNce)w@ZBJ;ffrZ;fW50ta{ZQ3X8eT!hx`jv1Aq zshgcR2ny#d4o_=-XI|6;yoQPZxCpGArwVJSAn1)u{$_B)!n33*@>wU6MOBgi za3WdOn*@X%v6qjE@-;_K>Msj>52K&9mlpN`4z})H{Hr()0{%Jze-}b^XctX0j0iyp?I0UwQr0`~>SX29lnU-pz0;Zzq@f|o}EYEe6$ zP}f$!6Y!D$?UG^|DE1JL;(D>FFG`Aus{W1t^Yca)Q&Q{Zbp73(h&L+{`8`QVB_%2^ zAKu*~Daliw(-JaL>2}`{D7l|ousS7J9R%B_+(-I8+d8OrCc<{7tqygsz1$YIP6><7 z-88^%F87gt-=w4^nnB)?R-cqKW^W$_=OiU9-rGm{CnP0Zsa3l{f1wM$!m}$W>CwF* z&-NCX5eRLZBSqhMw=|7Pnnp;o+dhsojo|PIgl%aWAx&r>Tbjm_~w$|Jp}OGi6^b&2P0n`}H?Vnv3^^G<@gU-kQ@o{RJGhndXocwQp8DnsTH|R@+h* z5Ya)Z!W;zidv#yzr7==O2f^U?s_@Zxs0w2fqJtpj$rZMk2PrX8A$ZH@*%i4e1gJ-J zSJ@_9vcH>wIf{WfV4&Z@J|}~nw~c;N;oinEYS1>0QK%y1KQAeXMtTTx@eOqT`;wAs zbPV(pd^ahny%KuN>84aGZ8JEK#pnr&(G$Svs!Cf8CuA6n;^$M9KFa?`Qqo~MbEE!( zDT6SKgW2}#*vs@6sk}83X4@DXlEEe;$6+2*$pQc2$ieN>)Y zDrvVWAC;fFRMHVu*pdQimuM0Z{Y5+U95O=M5WtA!;655m$0crc#Aq?hPBWCObtLX} zBp12Mll%8JRC1ycSg_%*6iQJmxxHCl1zRUt$us*8SJ`@7$@?p{;xd6+66)4Aq+|kx z{Ubd}kE?hi=9$0nVT0c&9l4GpALYn8M8@fQ#y@&L<=yFehFH2EwnU!0RMOT_%9?RQ zXzNW@jfcw-;^|~+DGM+O-Ywk`^dlYq!L=9Grfnq zU#>f;gbMceQSi2tN~j(``c5jLUi|p&NhS1`{h7CErli{rP`vf&FL=9&t4P|E^jjVK zr2ZoHW^i@0DQS5PRCKQ?sZZ0s9Z9d4l5VZ>QSdEO(%bm)S5uOx1<9NS1_Nw`bh5w{m6q#7_3#M^%3viTvBVB)XLh`c3qxw zG%ehs^c}2Ql$ViPwI~NG2_pInLvvb`e*ujAuUe8eXe!G+MCCtOl6E=7M}B`w(w4Cv z8cW2-XNS@^Yt5~1TX9<)1#agarlphIi_kL`Hssbr6~jv9E4@nPE4`?&4H3S*E0LPJ6`ht&I{fz>?v~?vEywj-x+7=mv&7^f23$>ULJ9bc1b5;ls2kC3nu468BQhJ52XdevKceE|v5= z(6CK)X&R^NZG@&XwwzKda--?# z>B%jN8&gkD&rjR74OSV3q;s)wlrWj8T-~MFipI!jO9Uq4CY3I%uS@Q zCeK4)fNiIsP}lrLoC0p;@a)pXHjF7Vrp%g4{=6Qk76jqtL=L!FbB~!aoBVmbKLa;+ zUc=l4o%1Gom0_ldx+unOkS>)SDK*6MKoj$sGSons+;$2r_O^ydxi0N66jWroSq z75=>6(%!tjI`$V>f6Z2`auY_E5ndR#(#d4PO!1QS2E8UbP_ca;Xv3JEj)$>u{cp9l zo7{T@3Hqrv#~Q1ZGO_wdV}CQ+LmMtsySTUP{NF6I+9)!MEII}SlrYu*BzEEDPxFle z_u7aJ8B25;CW!)(<5_(hM(yF49!ua=_Guyzu~Ni~Xn_`@JKrdvKubr8sN=B$3M5TD zQVvrNG)GfSI0eh0989LnXa|WI0#A4M=jJYH8ZFVTS=MaBvKF+3;v{-JBG9agfODepw52jloVUy{4ZT|JYf61 zM+A~>QH`$INQD%_A#%=1$Fu^!6(W!(!s?X^K{TGi6Y7e3AZpEmMj(EfN#C(@hd?~d zh^0bc=-OQ34UwraWJXin$hE>FSp5^W<0vCiAj#mWI0o^y(gStoAd~J$$7&CrFdGi3 zcEV&5p9?N2mK$^KZx>KmTeP)}wzBs5_v}O`{c|X386z-_%t8c&Wo=O!#Xn*R^&cDQD`f}i&(am z;r9?sYAXY_XFNg~38Tvt$q+e}l_BG=JZxp4;zHn98OLW^**PB2$blGoKX}r@WYREU zG0&)SWDMQPYRbCX=vl^D)*YkG>Q`5H8$HMREbHoy(ewOs2=u`F`)s_xp931YW|N1I zH|Emy5Sf(8BiH2_lLrx(dNTUmk;z}NV@0vWL!g(k1sY1bp-6C?=JK=g4Dqgkmugw+j)74ha;s8sQ=4wJhjNkhoN-42=qT zJ3^_p{m}qcFEB&VSTx>xykX)E%R`y^1%D|Z@_oNTI^C_(i&VEutPk$KQquR%U}_hh zMQ{zP?XFN`$h3@w(Rj+q+qlxkfoYEY#dwQ|?{(VqNNlj@J52a}%uhIM_lY=aW}Q8H z+n?2_mXP;H7oIA@=~SYng-$}snRVC^5$ZZ76r)x|ynzUQ-Ovgru(BD2>;Dn4aXJMM z)2H~T!m6Rk5M}EW9~ExJ+)kL<(NB4Bs*eim#^E}VS~`af=FO>Cou(L)w@t;^Ie(v( zlDf2xPS;;ZITdrRbt@$uSdSSYfQ?z9)G$te0fs9c80=Vp4W&&qT(%NRRx2!mm5CcH zEYg=%oL29s^A3UXeuyJ%eX6*(>U~rlC2@y-Lj}{z&V3efdV!-WrePM^Xr+6(y87jWaF>~yNq9Bwsj%DCR8a$ zCrzZUn60lG^hC*5oV4?F9~Jg+3DI9U=P}b&iN3{nS0KF)>ESl|P@vS_n~qY;q34XD z%sa~AwdWEy&)+Mazh~Uu88*+~vx=y~3U$)ip0oZ~K+|UUaCO(OxNSQXfi|OyF7m+s zu9@LeMstDI#|r(0H56ce8bBoKrj3f8zC@C84FFFd=wT$TA5cb&Rqx?6=*rNe0^8CG-3Sq$@pOyYXl^Ui8Xo71t^Y5hh*Afy)u-q{}$)AOdl^g7dw3w2un=!THj3 zg4S`|HUOAL2VKpEii53VyLt(x&-7D*yLNN&iGf)>7$iN=pX;7+R`m%B>}rsVZgJN?LN2BB?C(VWq5x zSy{hqQp$RmHE=dC@30PVXV^j5zs`;LJ%#u^M!f3+>_>?Zy~mgjU*IFZTqUVdOVa{r zDpyHbvcN~dnpKj{S%3;~$SO&_8m1L6;Z>4u0ZjWUNt+k=s64ew(kJ+F#wtmHg~)aO zDoI;(E?(K(v`SLLLLUVmS|zD%p--u@WOt=X3y7i0i;+85)zVLnO;064j+A}Z+0shf zPN?1COt{0l7oy6CVnj1BWpl?O+ab<`LwpfIJv9LGDU!2`|<$7X3=Bf3tuBC-|r+cD0hdWWD-USadS_ zKGRR|5fu$wqhjy7R(;vYT_CPPpOA_s>eM#<<_=UN-M|qpcBPTeG+@q|vDJR-x{wVhsytms1#=EBjPArlcOJB{Gi z2&0_oQGt>kILS5%MUg6;qWe8#9CtEYTeQ`p$GM;mJ=vE_td@k?5lT%{MQqEo^(MD$ z@7l7x%d#y#*+=6YV)QOky9tpjj|xA7FjJ>@nf%`op{)E}*2t4fY>m9j8YvCg0x2?7 z*tolg*nGd_3_T%q(ip|Z9C2ePx2H1`>3F2cHe_3tx?j6xI?p3;ANzUijYc(N4dkC}g&+|a}`-L$kh{geOHA5`pm{2?wOVHhn<8H(lUCCI2HZsTu zh)82(t{R-@c-FEb;-kVXkTe~o7a8yYL?!p|P(??SUgoH65g%2=qIy~*U?4=VGlB*L zI_OOfoQ6P}1~_mJ0*UqrCOtpmPU7EGOgc$Fa(H+PG|2vO>mD99f8Ox`|2V!y`^Sr0 zw0|t!bC7?$TV;n+ZroF4zYx9?PPwqvN9>eOY1K}7C4RsuKhX-R+czXF9jOMVmyGNRHV+QQE@=%yY$vof51wyI%r>>PdGNU1G27G> zI%Q>>81|kRwg$J+dWJlx!$*Z1_Vl3JeKV(T=|IubEgU!|KBOem9SOGdxg~ktmgISs zWL4aj-*graBUAy~i!dr?w=YvLu^LTar&%lKoRorTR1v z4bFT-pvKUrOmscMT&X_IL!}C&ZP^sUA=o+AVWaf7Zg!SS-Q+HpFp=ZZj_Q_67(rW? zZ3N3DjNm$i*<&r2u%XuwMhzxfF2V12h+uvdg8FVCh;cgjUD<8(t0)pQ{0QUj>-G_k z%{E9hgOg76QPJ#8irWkHx5DDC`5K9SXveAWZCxodM3*x0-cwPMC8cb6_p<=^BFc{e z|Atak*$&692n_3fLQ;+zg$X!rv}{%NIw~7MM7yvL+-2Qc1$5wPPHF7|;g3a_h2I6r zI}JhIS)g5@u4@tF^imk;@zZj)Fg6pDCOYKn9#y&!%tjfrQ3f_X09I89Wng2s(|x4a zC<7ba2-<9vfsOU2`>5iDO;nHM#}Kfn0Xh9=@F1q)(8mhsNc^5g$K!W1es`N`IuTIk zBJ|>URF8?c+s^P&A(eTkqAN=KBKaL8pQ-~por%6XRUu(AegjJlLMNQ*qoR)P&Os_di)JDbVAi;5Vx! z&Fb-yzo1vrhJc44GVtPj!o*%lTXeR4L94A-QViLwUP-Ha&|~lEl~hx%_^IJs=&mi% zF+2x0d9|cX3=HNs=pg zm2};&d{p&$FM6B)(U(Z-(d4HC^1w?Zy#~l5E|FAp4zw}z5=ng;=48NhULtAHIX()W zeTk%1__5*=Nw1&dBmc%rB-QPs#3@3dymg7B@6Yj3`9Cj_G+{aJ#att4g9iC0u7a&z zBWdAsAC(`pMpD;u7Ke#$)lf%+X!|9Su3qjVrT~jTbie*P`~dZ@fe}O-_jM6{^%6-( zo{QbOmq=3j6l;P(^zUwS&$&0CYvE13Wy60H8Y&8aUraMH_yn_YaCiQ@t(V+4lk{rD)(W1zy;%z7-Z-!t~ zqiZA`aiNc@madUR!PD1B3SH=<^7Gb6T7IFA%2%(EbnAsa z5}kArk|4_&7x|Pd8!pO{<%x^5EFa>>>NS%57i+K5r*Sy4>~}Hlsa-8;9)3WUWq^f~ zq^d!(Ty?RMrSwusHC2kwi@{9&rIH@G*hl4wOC`O7{N0yI>d_#1(AzbaO7g7mQTg+i zO4?_IkMdu?RMLssYL@6Pd=qc1{rXZ#Ggn}W6ksu(oYG%_JrBYFYX=yS-*=g$eob;E z5^FD$vCh49PJ_@ETlk^JMy7Dqf->mSF|AxyXEzz($gY%~^ zleFtfNX~eAbs8TIK=#8|`Y3o7a;@|c<2h%g#0-9q7H5n9A|1d^En>4xO>e?qaG`s%Q?=+YGn?RkQpDPhk16ZfXl& z^MqUPA1MWV#60e|S}EWo-dQkjwXFaZriv8(o#9rk=KkHs40aC$L$mg*X6kkQ1#=(6 zKGDd0wAx49$Q1PIwr*-Kn%6&FCaH8kWwHMN@Yk0~>hASX@W;y}{U3n6YbAXJ;2c7> zM7xX#_p)uVHau;MhdcO3u~m(NQSE$*V^q8YB8@QH6dzK(5JI_Nkw9A_gCOVp~+*h_s>5lhsl(7^~rQgku~9w#DT z@kpc#SZ+obr$&CoT7DM6WTKPm&_Vv`QXdudJ*k8n(BBG^mh;Sg*=3mQd;Tev{FVMz zn6#Rcry-eV-InOm@6ul==}1_R>QV0J;TXJk!Yx4SWx7Xs(Pi54Z`f~;v%Cwq$v<+f zq%Au8aVW@YL?HX5wUWNXk0aJf8oSm{QP5t4Ek#B`og{&vSr(643q+a3H(yu#*hcd!Ni#^cALE3hD< z_}dqNmtWzdiZlfvkQ0tD9G3dYW zN*_VBlfnAO2q@J~21f^6WwXw(;O<~>cg9sYeYYaarD#)Ze9o<8IFYv_kis*5ibv1vfhz_pHlf0VV*8I7X9RsYkV|5 z#21V$CKYwSH9o42kYHR?ZIL(@mO&L!S`0ysxrPNfdC#}8x;X$5mE(!R5nXo;KZS4# z^!I!G?x1$W|MeP-XiN9^;Gu+NNX@_2M}>`uSW2fO;uqIKdjD&v0{bhoYDFFWZmp!b zNb`Fxm(;J5qDb2Fa!E^(l%sZNrG+e`w#HFc-4?gn9uvHx_!!TJUv{mJpti@@V}5b1 zUDl7O!mNly=tCYCR9?qDhNn3GCIE#1OVw;-zMw;FP*{#Hcjc23^R z$#(P@&2L47?&joAknB3Sky(XrFwKN>%Zn}NR-GSrR~kgSbiQ3c#W&c3?!w0D|9*v} zEj5|za(Oq>gd3ot%z+7;P=jgC$KFBS(d{~U;7V`mj;BIh4B$F<-~xbQakA^Yfje0* z{F!eQxX*mp5Yf2W*o_L+pM%3$Ka-yRIRqV{F10)i3Wsg40^aL0S?Py}tT z112z74n@S9u(M8y_d$*V;|(PalN2sM*GuivZAbsU!)knt(Vx)&7WnSi9PDAx&) zZYvy$#r99fqg^yMG2TNZd#l56rL~^_;%B{E0kKCGD987 zL@XLNGPE>JobAPI{0=dhMhKTYwr8WVQPgCi+Eox;D|5-6GNbWUD;7-|GBL#u-Fc58cYqh(fW`21vz0PQ#Z0}?Zeh5?Ocmi8n%nVM z0gbx}D~q)>4}nGmqUy?-Yw$ZoM}wPw{B8+FV;0Rv_}iQCW>l?*(d>_$yWOnN)F9A= zK$L0$x&gma#1{oTf!_+v0Z9Az&FG#3O+s~s_qYXZbLj*R251ccsY47EE?E_9dl}AM_V`37~6L+ArzXxA-Vn*)QqxTQRK!_}I$|KBd2a z=j(C+|KP0}{#yrr?^hLkjs60DvW>syHVvP+4WqN#eo0#=co+t=?L(DmeLCfoWaFO{ ze!(Rg;}VUbOK|ILC{frF2S2{eN7cZoNaXO3sIJt1lK zuWgPw*#B$W0B3*+s%WF5VENUzV^K0>wOJO;Ld4OCK*iwD)wiQtt+=0!jDNiyCeo{7 z*cf-a1KM8HFKNpG9^Rrh3ng-Hzof1^d{n-=U($#8v94c||4tv3-_S4VNc_07U($N~ zxW8Z0OLsyqG%uHjGSl%?w8Pln=!&M=5{Y&iyQ$XWXrg4I=fCjI4vvhEveAc9iH>Nv zy0U6kJaxc?V0C?>BN;M{De*{CXDCTxZxM(R^}SF)0bF25*nS(p?R@-eM+h-uoHQ|5 zJv$MKkia>nG|^=*7VI#S=AtRZX0}^~__9zeZD^tyL$al?1+1Q*jypP9@)z4`xXuXu zW-E_@^Y8Le{`F5v>eHr0Zzy1Z9*o1@uRe)c`nR5xbmUz=s`?v(l>fz(lE!GzQvF4i zLy%?9rz9>vdU#wOovza^&|kptM9$W6dDMItiam2GJeL%iGvN~)L6VL@f{8?QxJ`dS zWC{%Nqj7n3?p;1&YPbDY)M_0nJsPW6FY+s%l9bZnF8u|yBSCHcQ<9zowd0?X^chpz zaS7==Fl_<@8lmz)^`huX9r1|%Rv0vG^0Ngr@^0uY)A8F*Z@49q&&b}S&&UQ|%<86X zm%&WOXC$H#^5Ll*SEj!MEs)%0Wz*^O7{GuYOyS5#L83`-g#ym~FMCI7yogGuq z4Ap7yzFo48`EEyGL7SQAB!Ava4Zo!l$9-?e%hBlxw}rxOp~+?_9-*^a^~Yh*tYd0-&77jSz#TwDs^CF;@v)~R?O64 ztET6Y{QNJTmGpzQPSP1sAFv=#oNC8PS1LEj{@pf9YMiJVX|Ao)HcM(})6P_zoczu= z@PMSlY3W;Tju$e=3&HUXK=!k^SR@+(d4#;X1GfSdvd_GCfLDfLG*wxhYxX&>}ag-AP*(W)PQl|zhZAk14i6f z7K>tp=OFJ5C?aZa{%B#pDm)sBoZg2mZn#;VMx*C?2Exth7NXIZ&V8iM?guiop3+m# zI8`KTY21;&E1ARya|?_DGRqMtDW(&BWiECEi#TIjdVmF zxyLgF^x!5(+(8mgB|9i1M77Mjnq%ySJYb#--I`pMP8VixGxtCYZ zMnT+u{Kk>BQ2=}YUM*hZBz6DLC|kV1eOkN_en7n6+_wYqribIHm?M%s#~}E`)M#~4 z6c{Wvhy5g`?;t>R{9Twh#=QU6MFgFb3VzW%pwbA(t43ue#qyE{Bq(_9+V% zH%zIoZ)j@bD(R}#gTbaxt6!uZYdJGhvj+psZNw=@{C>9{$wXU#oo z@!WaS=gpoyPu;KL@)=q13l|J}n}yf33$8%oKa*QLAfaNmO3a}B6dhLwsXT50T;hj=Ktr*f_vCjB>Us*NJk>(w#U zvlFeYm##MpZ|wFCy|<-SKc{h~1drdYhT&;9yHAY?Pi>THh9lnw4tiEqG|@ z3?pU4mu159l14leLa*5lm7Z~uJ;*+U&zQG?bf%fHS!@i&qv3h+hOTI;#*sg{m(qjk z_l*?l&i|1-h2#F69~@asS8ApGpugzxqCu#t8(B=0KWk($t=CC52&Bip~KJXFSr;^_FP{?Q_y?L z|LA2&g*wfK1*!wE+f=?HspkP7<(Ir7sYFBVuD^h)g2HmK31e`W+p2R2X7XU0$%Db< zjbQR74e+e~GLwgZ$@d&4SLiew7UU1HnJoK_X0lg9t=C^b4ap>I)M3uqNf^R4yrq};t4ApirbG5*;JYniP!|(JRy4X*w?V5aFQ#lI;> zfiB|KC&OuFRa>|X+Au?5BNU04B(569cbq-GPoWF0C5;j=pig%lqrj4{g;$J=ySW0wzMIn`0tx5XUxB15cD(j{n_=l?* z(;Cb!lU&om{}6mb&odQ;dOjF~4K*K(Ul|@Nt2#p6&Bg+j9apfV%wV-;q)7bD!#evK zD3&SR4V|r?70CI{$>V%`Ru=O5S+))x;f=T!{-ccW0SHeldl&uvXJwz7NW_Bn6!hJK z=dfmis-A;}&6ioUkxy&>oDwHHn0}>HmvUjk%2h^o7z;+N;-ZWx1foRP{gZ)eO_)X~ z(oN#sJP~M35*_zt0R>Xfv(+9=+IQCI63tx>ttf}jGv$!MURl*TJ8GqZ)ycHgMq=IJ zZ1_KbXPZ_QFxIe>PSN6)Fo5k6ETllIVl4|?;Wj#wscsT$GstrQB@;;!w`V}gT9b6g z;ShF5D%a}=YRzGcvwV=jpRl^4Ml3?&dym)=A~+>f0KF3NswzCTdu;~U_TG|?hq-qk z{+-Vk-#t4iyN<`hc+#}mh^NeM66X%f0R-1JuW3OW3Yf$N`?)H2B$1u?_Adk!^4>3$ zLR_VtT_QK(+NdPaSqBwTpo6IVGq(>k;>(UQx*L|JLor^S4ynrw1I_B2(VBs6MNKIz zYh&+E!X$Bmz)gDbtVW2zrbHs9VOiPGEe!p{!ET)-W1(oAH!RsWPi3gQ3OAJ6TQ@77 zYD$?TZdKJ0(>ib-Xc_{zZUj@sTrW|9?iBJ6H;O2(ClE)801|cHP)y$f7FCOU8Cg-1 zc(ZeQ!fZ6Tt^39+0$jHIUSU~?zU3R9%bM*UVf`b6q&^gj8KyPe40RY5iAE8?HVebF zh*rP{x6skM9OmZHoJ`^vc5rA+GKgRc3*(_!v^8!-FhhQ)a)W>>TS|B36(633W+I9C zKZgeQiQSZi=H|TVjK*oC2hZjs%Wmumq52Ga9nmflbNE6_4834E_~L52xtI;c)pqqt^#w)tjZYKf(-w%O6R z!39`LVxtgcQGG072iLZr2-)$p~g?yv7|GBG~kusy!^8HQ;+7Tn$zxB)bX?sk_7u4D&M=8? zeHy;KJ4y6$zq{f_Q-&Ez8Hl=q_*G)I#+8?&cfn%Q|SIcOtIR zaRgASz7|$lll*`rHsP{OskR)}m4P->-ASVwHDF##3yCK^B9J0_svdoSmKLJPS-dG| zWMODrGMWW_978*n7HW?2%lOr^tcI>6F5QTbct`T?m$oO}LWjbs=rUtYB9e}wPMs%3 zS(46|dros}k~{Q~L<;m9NUZUQKwD^;p%fVCO@hHgE=AxjVSNlX<=w1ZsnpgUQXRQ{lEOR6k~+3WqFXvFbBqq5WwP z+E|N~3WeJnL*aH3b44J|Q_Uy;5`D3FYH-h2LobXc8GgP%o2hD3hSOB2wbh7dMO7!& z+z5$TB9KZFo&L+LQZ*A{-EQL|#Bg^QCNQW8uB?hht<+2-6qyr>r$ezI`Z;sKDBdD% zGeQxfYr(PZv1eG}i!#zljDbj$sKJnuDQg@QFpZ^6*kCB&<1*R#g7ZcbkN3<@bj}W? zjCi=4#Cf^KC=9zO6iv<6yt4CKrl|3CqP$Nu2?}K$HGIE~dHoOWdQe%_f!oj<%|uHy zW(2G8wzat0D*{O~(L$6ys*nO!DrBbYp_5bBbT=W>&7T$LEgDlWl!AWokLkzr_ z^QI8)KC4f}Q${?6C!WP-k0^sLqCa@h8uRZ1CeRC>?EaBcv*ILh17>{t(#w{LofC;K zv*bdb=PPe^pU3X;@IeA`z3%X2s|=kdj0&q&uY|;nLgXA3azl$-n*Zft4}@Ysw+P>% zD?IO>!Cn{LTWrjzQg;@invB6zR<%UCLa|sP9IS3J4TD4*cLz6x3dy$4vSoBiWU;#v zgiIqI;w@7o{wBGRS8VBGt+u+YDL^`}iA1r(e%UfAfzK(!s!j=}u``TV-PUx|Fp!~^ z8!P~#Y_>x@Gu>)5hFT30p&bB*!b{Up)94`ay5tV(pJjGowHm28?2brWEJOfZQ}(aP zB&`&Fe<5zMUL^{O+;W>n%m`UPie|yd(wd}!LD=I)R|@h>4_PS^KMvy^0hoR$OSKtz z@$#o(kRKB$=~#+-^1pn%fXYw;(MT8lih~*DM54?19xs@~@;pbM8FR4p-e!c7O-2g7 zdu{^$2Q$HsdxbqE5Qv0QA$AAp6DZ_LN!#u@}c7_cXcHTm56L|hVApoz3 zj>IpU;S`dX(Sbg4VxJu{QNADjCwMcc%38x{nLhTSRn6e#0V!J}3Zx;9Ie-X>>x64i zqhOTDka%RfBq!Gu+a=9cHbLSXbVg!C*EhROhO6;3-1j4X%vF)?l517Fb6V3O6Ys%` zRe3J<;znnp`4j_hS#KCAGNqOg685G%B8hWGxw5u|c&%Xqi4klCxb3X0i#@KI6t@Dl6snJvi7@L6^i3krco(>oCvVbY#h7S5Wh&1yzUC1w za}4lA8`rbknUt!c86q*%oL>g;;NUzJn@*SL>9aHCiP^8YWjOIp_L@MGv5tUS=HG*HQmCLCy-dBCJ^q2q)A#MmN5@D zLN{3|em6ZAHpPn3y={!0!5%mH`FmD2gA zTZOG*pbu8V*(FKbH7p}BG#f#6 z)PiNepT6Q3n1suQ!&T}e)@7<~hIK35j5ZRcc0y@*c3NVXbI2Y$!(7jvj;Humw%6lzAo z%Bl>xWXMcKLpH@Jyd08~Vy3pLDS;+%6s&GGTBC6imkEI`+Mipn>XE{w50~@#&>a$8 z7`K$5hyEvIGdx2@PTmEU{u+7Gz!&DGNm@#*G7yS*QbzLRn|R zjLeF3k;%S-Qx+x62w4bcsA2vUzK+BLe(uj;COwcs=QNcJYq}Wu65kUZP_WqemBG;( zZVF?0(+I`WNfLk0Ll+@T^x-Lm6le{15*^m&c0*3Bw~|IU8j4Mi#v@Hhctmb3NnDLt z0jK*MP#aM-FYQrW;ET1KGR(`-H6`<7g;ds(NXH{|ZON$q0xTD8C`tUoUD1GTX2_bG zXi_9C5>FR9Er3!F54fJ;r`@5_XNy^OZ6nlF8>rc7(>j-t#D-F!BNIc>Fj~1zT z{^ka?a?VRsdgVO3QedzjO2<;@qaT=~k8ZPTbzAcdex?kN2X6;imZU>aemtRYNVcMQ zXF)2Inr2``OyW%`0udvL_b8UZAJbcMO`?#>BJ{TWKVFaDflWo#k)(Mhi7Q;f=_!@$ z`K1s~nl?zI%k z4Mn115?5m;m+0a@z^x3OBd&XP=Vrjm(xFI%&f`v7rfN7UoIqmp&+2e6XYXlz&u7wF-rU?(p^W1dDyfkKT@_0m~By`NvNKKE%L$TetzdO19LAMYp_DPBzA==x3_KG_;#xIhR(o0@1tNO-IM5O5YBUpJOC7!| z3n$Zb-7w5txLHg!b;rZ=c(%k^80TJ&>Y9A)?wVK_>o&Gw6e7;{@XSbdEp81@ZAJg4 z(KK)hk;J`4B16VLOC`?DbH%nK%!W|7t&zlIB_d!NAuAClI`0EK2^c4O|Fl9Erx;GR zqF0!ZtD_cxF}qLVnc*VPsrVQI z7AjM)kL7|L%m?qF(F0!M`ROF_E}9Jb$||ednqzdJl!MhVK6rVNmpc{17r52XfyINx zI5$;~r4$lhVK;O8Gg+)h*uC0Z{*~49>8BHZZ)1u2rTsbo5dVL^bgw=XgS(L8g zvk+xb#I57s8Cp)0ts8ojJpY7pED{fRGwnV1F(SkZd;z?;nZ{F_9K_7@qh8ERyHU4> z;d#tNEM}NZsZh!Y{{Ptf6X+<5?0*=)Pv@aKAsxjXXLN=HG0T`3c5q{rMOmWYzR~G^ z(rMBg{d6{U{G#Hn5K$Br1xHle!4;Lk=%AvaGU_NhVP8d5Mo}64pU3sX!!-&WQPuks29_8l+7_muWkHZUa3mg{4^{4t~u3FY^pF zLXlvAIX}alr2r?PqZKIw^Csnyn6Hku+?VruMrFg%L}jWrJ}Ft7t_-Au)ue6jrxmE5 z^yew=9#B*gW@%8KPJ;u%nnCcw+j#~{1z-y%^&ra4$Yi-YDSH#V2H@@V-Y;&37iBI= zao!8_ASh)bEJ_bK1y`0n+h^=&H5EB?il3}$j~Y)bQ79_m7JwBR2 zdJ$x{;sOx`^Z}!|ES+k^T9mYBl@sLD-_K$>WGrxU|89TshQ# zEveaRyA!&$LLRD31=OD7PcZosOL~FbKk`*KI^a=b)1e8$klT8Z0>6e@bwph)QqXJF zPCb6P`)@edhL?d*2wg-Cq_fo4FZfv?%laPjc(P;xQ9LJxb(p}HM4`YE2S(6c;i{Sl)Vwb)kAe6ek2$T ztmafNXR#Hb`>lRFOZp05!SB@2r3Vs8kV@JuXWBXdG8L?$ThH_zacJd1+FF<9Xv>Fi zA{j{4Se@tdEUf_RTjjuuX3~g_;)noN5z5EnHkJ~1J< zdOfdH$NP6>wrA@sRgri|O;CtU4<>OI7#5*`^D(tw#wssAQ8|%5_FQ*oI|vm9_Ip0x z2*TE-&C$esh71KMq7`=>G^5H>Tow*ubx7K>j+&Jt9}Zg|cVt>(i4c9#5er9IMSiM2 za7yherzUXnK`k9RayV;WOWw= z1D6c3=@iOR&~Rm%M0CegHV53V|MR%aQse|dL^&p;#u)X{M&3tOGlQpccyPl4y8v<| zUibM>UrBl{2bRcuwnRRc1CyMaB6}Yol1`HLM%!%wR)R@tZO(M#$RLljdk;y@Ocyyb zTag1}Dz+uCD1ttQ3SZgY{y;JiHh2dDTX6q&w%`ibf=U`Jfdvc{YEXM0`~~AK+1A(} zc(2Y~5vw(mwT=*csM$i`*m#vVe5AD&2jK+#7UD(-RdKdT;0P`!KY)nIZ#}jYv4o(T zj;)UyDYH6~wAL(_X$1+Q)U-^=fIzhZha>V;HZa_%R&C6s{6xwxWZ+c-)&e$V!_Jd&NaIz^IJJueg z-Rl_H3nC%q|HMXdn$aJVCndJ`@$c3cOiUslYJ~viJ%wOafBwyK|8Tnq*uFv9-wul6 zF&B!ttx%|@lFpp0m7e^yhsR0uWHdH9asl=6b7W$ z)GIQ~DCR)lI8@+p1``4Ap}pd^bBKB8p={)ep8YO*Fso*2)D*Nmx}-&9^v6U;H0%GC z8>DS=m3$CRwDzu)V$XKlyxg3FA8jHw7I(tfYutaGr4?Wtq`x*PFAwmqZKkv1wXxzd zwjpR6&e95ucx{a6ho(#kQ3WX25O0;O{jXq?2{WP=5?c=fpQxOeOs9B{hBv~scf7no z1{rr#3zd|);a+UHtC9AcrWHhDl|;`@b<-`lZH=M5*B)+Q9$R%mJcx}qj18+@nko>X zHLR)Jw6tTBFpczRBbG=t^0JS#9UXax5|)XANL-l}V4|fx9o3@AHMehT;V{{HXQZ${ zOxnU$xL5%tYSZ-hj`(>f3A88Z+!IM#woH9w{*VIo}N z)lMd{ko?@i$yQ$t_X2RCAZer`iI5UiIs}hlUEqUgychPE(q?Qok-f0YNkEdlZ^zZf zu;E79qsn{Y(N?Yym{sxIvshj1gBinSwQ2M8z1WD15DgsX?%aI;&&W_iS+qN8lc4|( z4$(vTz_t&e)K0Bd(6$V-(YzpD%MN-&+icD`47S0ri+9{1025VJII~q2HB57Sbs(-@ zw2yWt3hd6Mf@lx*!@*osqS%KypXI>3Rc&Ve47SLbMfjm)o?XQCRfE(~CaX6s^lEUE zMMs5E8DTrkY&M!@ql`evdQKk)p&roeoNm(8#>!Y#r{Xefrlv@HNo|4OIK!{ZRj=@K z=k1iFvX>P(0;Q}7s9!CyL^-9Y)>aG4Bo7doQdZQO8ii|}i7z&qyyGed47&o056_-Gu&cW%O@`~GLZJAfGQqa{n~V6EiQ@YNxgU!7-Q z+7BDR_Jh{OaW`D8468$I$O3Z=TvfVmf+x?|Xo!a4r<+_WLX|OiK$w*5 zj!DD^@s2oY8`?91*bZ7&)?ta%VBSam#_`H@GfAuyJj|mg$2PGzo7VQc`5#rkl_ouo* z9Dcudb!6Anei;%Gj3z28jT8>ZkoKXE$6KsJ3*bg7?26DbUmGox3|MPZK9)<`Q?A8+ zoSx2VEz|XhR81wkwAFe?SqL6cwQg;A%7l$4;sQRieQ@R{Ec?Y}aNFWdi0cp63b33d z8UQcgM7kP>WbixRq+1S$Y-I2YTvaIS;cUfWZWI~NSRk2TX{ zz7fVnq#{OV{jS3o&vIL@C2$>!Oi$Ee0!>nYp^|?&0AZURw%3T84qV3uF1zC&9%p(RiM{XY1+si4sjT-&GCtG zwXs+r)p)-0g`bO+ol$2*mCJ*2b!WB|@UEEkJ3)M8qo6vXemH2YxjJSR#Y!D=4|p+) z`}-Lg(?$S?aZt|MOJ>k3+4J&*Hjp@Kh&FShUE?eRGHI~Tng%4DZ|kijMvB{dp(q`A zS&)oRj6&6wZq{bBVg>lsdSSw>V)*e1ng5aYP^*kE*0pl)RLdEx2Gt(2_CR~&wq1-5dWQ-q?P`&Kbl zw8K-0dTZ$z(+ar3Rcm5rtlMr(&}w5NLk&DRh(oUORm3Pb!KfGqJzs&#E7Ht<_ev|7 zi6*3w>WKQ0Z_ul4&Y3`6wnJ(U!aCkfs-lQVWJ#0=MIy)Wv6``b-7(d%*5LjwG4&k7 z=Uy5x9y=8}D8G7&IHE_XBHClvjjTp8IHsbU6438hJC*SyKr7KOVFl+M7)!20k*X@v z?gBFpZcV^e>)=S5abjIRIg7^_tWqrtLVl8_WWkh>Uqd(X52q~D*b!JX0b2*Ya|E`1 zA-5C~ivY?!+E#PzXK)JPpI8ubN&DNDq~?{aFFXLHR;i|uX2%T_d{&!+v5|%PXV%in z8-$mSom(B6gm$*bK6GxLPafZ<$jqXgx}tTullFsJS+aS@GyU@ae?i%VWxs`M<*@K2Sd)A>!$)- zwVnE#pYqexP(Rl_H-xh_-+N_ed#}BluZ(CUWL%WOT~8@ooy6Wg&*o^nPx|@$xh{&T zl6g(U z{J6Ukzmv1|T>LQyhnkD8+mtJFar|;K_mC8^;fl1`E_AXpmx%^j)2I=X@1b$-f}(+UMq%lKtE-#Fyid3kwcl6E`Rh(>EiAZI`bCgRv1FbN0I1H^-Z z%+oTvr!_tp>gKUW4n06vHZQCy#u@2xNwlE4F2*_>{DSi8x$f!AW>c2HcJncF)u!vK zPG-1NUy*?X>!ntOmPHeZ8XPNYRvD2&%_>zb555s3R67>{o9R?-Fx};vF4nq2iev4r zE_dPbtz<+9%0dt70`s~YlRNRab&z^>XosBQ{*;c3T(^x|j1rvDd^sruYq4uy{J zS&iE*RQezirC$y3z`*fC2Iv(ufyEsrv_#hQTl)`cXI_5ubs<;!^T zJU2rRLb2@jJ&@H-8x&bb^+ORe7)XUkCO5BS9J`Mj>0}}rt&OWx11vId1F)|zST2eVZd z*R9E}BKDQrfGZnT3Qn=@)0WQNV6bgV;%}N{}{H(+aGr{a1Tc%C{PZ zFv}PuBZksUmay}!dx|fSQQe_uJOxAiB`nky*VlrnB^eb*j zs|q|H~;NLa1g0gfB>Zs4a532bI!#-od}{e!{!&1QEm!A7b@L~&W7N;#{OHcL6U z02uZcj2yg&m86F~toyjN9`=B@oN!?vN{{fToBU)1D@&X*ikMu15NSVZ?1pmv+imFJ z{VsNtA6*+wN30Atdh84Zd}S|K+NZw|%3 zM?l2PApI97r<-8eIZRrt=gV^V?7lRV101T2CBxQP)kQwn@t$;b#0=BleULK;J{X8s zr1+5BXy+b8+^*+<$SR!gawY8xwd-?6#){nFE%7m3f|E9%y8LNGH=+~GqDX&?l(xC+Q*GQW(Kuur9^kz%NO#^21cFSR^mfuDhkx0~-q;2tN z1wo!nLYcIq8Px)#tf=iE(xZ2%Z}ffVB|33Zt2CI4oLp}Qk`&RYuAL?1vt4E){J*oo66~$#kGG1X-3m)9elq5QP39j9Z zhKOEx*HYg2_5U=98RIXAau}{yGbkFxDWDEoe$J8j}$XytkllX3R3k%<3IBAajMYy_KOcVUB#b#DJ~AA z@EawiGgFBYM1;ZH3>WoyRs*$vHx+W)U*6TQC#f!^WzDWo}l66GptwByFYp z&P28~vVBN(g?rjH_F-vi8?=y$CNLGw#I#%ecY%YeUTBUk@**C-*#|}kNNfVT?g}U)ahXJ!ge}c@7|UD zdh`zU3H9yMuUC(%KzW}YJ$m=)*C$Zb%jjEP-m7P2_db34_3hoer_sMMSQ!lU?BB1l zk5S&eM^)b*fxf-35aR-AkE%)1z-5lX3Vg* zuxuP(=}+3?Ts~THa>ju>d1xyV5345$uy(JF8Kk|~n_XS5{Fj?64$_D@d4sx7nPt}DDMXoBf~wIr{JdOfBAWKleyPl8;=1E zX`9@3@t8~M)IJJnlf8WC>Gvc~UY20@;=b%XH6M>+w`&K$bJU7w#48lEtUBxLG(xN* zkg6f=xx@LM+~*I^9LwAA9v@VguDUi2Sx~p0&ULdc5DE<+*JuU<(dd{& z1!+@?oZjXT6f%MtP&3&YJ-Hn;7O`qM6P6bAjw)>d&3XGYN za7x_=LE0v`J4V7hx1@0yLwrI#*>P1oaaEWvzHn$(2WkxzL)yPwrL^BJF}}s)xqf`J z`}}^Hb0b_FAf80`pQaT+v>;OXSfbLyr=dUPvuW-T^*dD?XAimAr)7>!+!}bo)JVCm zXnWqRN|kjbWfqs!2WsTV_)4Feop>}qY*7vAi>i${t>>Fo^4TH2g?~GhHw_W8Ar%)u zZAYRw0)Y#=s;^cm`OvT@Wcnm zlzkJwoUCAG6|2L)XC2ErfWDnh3mf@c+p3L;*C(WZ2Nm0 zN8k@WE{T>zGkce5q=zd=tkKY(JhNlSOq|?*Fkl*~vf&9?(WqO!+TN6y2|SLUTIcgbZ9y%e zPS&PL`)9kAxqIu4BnuvjiQx-P%w=f zsE3Kl~`8< zs0W=scTWFvBV@8Zlu!!Qd4y>CDctV&ostQ7)wo78jT_mwyQg(mo!yW&FvT8%g_E`; zBW+Z|#CYIJ-|PKZB4sEfc=eFFc7RZNULX8l+5ests}k4{pD5Hf!es50jsvaNQUbzO?pk8 zti_}k*E}!ke^?LqIGHggVcq&;hXC+E55NCvMk2Fk$9nnyhdFS@st!|NwY#BlO8x(Dn|7a4EI2r2MFL+C&hmEa8v)y~#-T?3mNd#`fy z3{bpM#BPAPU{1SLW5A&qb66&5Tx}(3cdKNS2Nx2At;^+WqN@Yu*g7Lsg=^O}+~ZP7 zbXAwCMlr#J{V>f_iIm{owKuDFC@Z$%6q)@n!9)@qVqz*Ohj8i<=j z4akg!RXKjOu9THm4#Ql_C8Ist%Myr2FEvsL!g;DnWlG8q1cNjw_Es+CH&6aFmQq<*}Z5rnUp+T;*ja!bpR0PlF;AvW^`>Dz-(cW?s z_U@wMD=hy5*@n1i4dmp1i$-l170b>^Yn(@|{XfA8GzYQ%gGkkEKdiM3NciRA*?(Do zMffRQREQ!o)W-|!{+E<1JgpfpWBvar8uo&)7xcgb8*Z&Zw7dm4rJ9(Dwfk0O#zlrJ zu-qiVvz3nc6_vD~lb1;90z|m3waJGCVXGzh(;ZpqXWrO^CG0pOSewGLR|aYB+Yn8T zKRaHBlUDiF0h0@5U!Onm)H_0dJ~6{}%6^d4Or+9yAeL<%K|0u5nj!U&=(yTYVmN8v zs~Iw06U}%~bD#bJUbJhZfAbHAIl(~OeRt(!xGgsnN~wD(^XuqwPirM?8nN-L3RPb9{aERE>?358sG^*zuHx;<1S4AaeB#dcSaapRIPn!ja25il2a|gr- zoBZt4A%VKH$>NY8J=lDGNRXbS9m8{qgHzLsv}1nPgz4e-BSD-tIdzp5CP8{~n$` zq!s)gOkdKv{vJ#}(oXt4nEs@7`yH4b<-A9E$a?kYPFmsb;pstI(eJ_ZWSi%q%j(7M z;fIFlP1;|6udF_Vwe+F!^d+tI_we*1t?c(;`jd9*p<$va(#j7FVkWI)UkB~y$spkt z?n70o3ift_wee6;rjfo1<(+Ahgvl9Zn~s`vDMWyy250^E;3+U}z<&>qs-1BN@UW3K zBVp2B#U<0SS}+6jT2^j`8;2PQkBpmX+(DQa5r~J_yoJ*|$jlS?{4_nMJ-^MZ*Ev%p zJu)0mq+mn7MeUbkq{)xatuo{!%u{*eKMMGGAbM5Ag2y8=reKfI-2!jT6zT)<^dPe_ zj*ApwDIvn7tIdy0&fRm$27Cq*;x{Et8wUgdvR{xVqm_yZt>fW zupv1_n2~VEh%)jIeh0Z336Dvn4a@!SdQHo(q#OA1IR_uV%!(~WKGP93;u!Fqj0 z{D;@Eq88X$^Eq(|=gK$3^e#U_pP!`nII2w*0`W-DB<*=OvW%)YIl_sga{9O`M%rx! zPU&tCK5vKpAJTH=TJ4Qs2rftmKMF8eZtXY0JctEw8$y7zmmo4}d=ovl3kyeO=yHlt z%eSF{bih7T@}lGMn2%@;Hi^46{uGT)C0q8U1J$8Fy*0_N>bh+NqW}< zqMf|KaAI6ztTMrG0=PC^cKBTab}-}a@Mxkkfah1%xU9PE8*__FuEN3}mzs>^V=*Dp z_Iov4QZe}yzl$In5a2lfLVRyf6)_yMVmlvM;>$aCc;HnaE(SZ*2OsX+!!S-glRpgur!s1&AaRS#H>>!84 zj0FcQHfZJ7(d>>qK$z@sCm(U^Me^^7O+5sVS)57u=tL+| z6@kFvh!VW8u~F!{DT`g%AMSY4KiwMBrYmk3A(Qv6;k+)CaxR`)L8cLru$cs#haD)V|nFn$tI zw|$u%je&XN=X}FV)TV;iq=DNZB)Spzk>JWAe*P{+)c6DL8O9w(P~f~fiR4#UFF74i=fkJE*ukoKwQB3~Z_mm?jT`ZM*_)(2+w) zi}JpErspUFK+cszC%~rQBqCc+Q0rF3VyDDNsb|~ybzf`itbiW~$b62Hk;AZv zJmdI5?AcKC_|;ws!>x07oB;&kXe4X6R^U*VW>Wc7Xm7YYi8Ga4egnOklQ~-nh7-2^ z%QXUQa*qJ)D>x_>R@a{QAnmCRntG%K+N!|3TDdD1n9)RJyXQI} zmh2@#CerStJ(7tu0~fy&`1m0)^d#*~>@izQ4(2$ldOess-AQ}dho`=Y_qGenbRwyK zIwZB#wpEd+F)9)_NV{FV!pC~nI<{GWWEo_m428L4K7RJ>L3gmXv&%ZHhG})YVMuM- zI-_8+EaE6c#ukJP znIEAiIj((-0q3p0Pcx9JJ_B~QpHYz_&vB9VraLokxIAc?qC3^D?^cbS8@lboVJ(XX zt`-$pJH&oRn}doe`&1$}o75JMbF8EOkg3)tSQlFYS`nZ&X&iJ`1`N;tbtWTGbx7Er zJ)8onLn*XG+XF3_LUp<93V5XSaS@abgiS>AjlR|~6$f1kLq`S+c(c+0uqrgTwhCw> zc!u9e#capU4VO7S@RGTkartkM;sEbv+^pyQf35j{S!-tOl%X-(^#AteG~!MQ=Q})L z^~`@UtB~QH-JMfxYqiFAr$pA37vc0=O5JT!&3jR|{OX=+F_2*6r6HSoEMx^{O(dxn zYz|&gs_oSTeqrN`NyJw%w3(PWC^Ach7YI*_oMKxK3Wq2YhMjaEU2F18A*61YJVe>< zs|JRcq}}1s3aaFIK|wM&7@Cx?q-b;Nl6Cb$=G6o*dJcLu0aud;A2k?a6;0avZgr2T zAO=yF-VRho%8E)bXDlvL^RR{L#k|Z(TPod{G|Fn@!9*;Uh_h`!P4w;%ck5P0(nD%f zW+GK4Z<}h(%I9F!QF&AljLTJ1PQ#>(s;Cj<8)+aEq|MCmPI6K&h+~8%Z8p+vjStet z1Y$;+s_817-GQr@-9U9>)LQvNMPAF}1NF;RJ58 zAWV<*<1~k_D$S22=}|4yJ1%aHPmIG6+g)C*pzX=pbRr#y+8*H;1;+&936t=$Yd$B^ zJRA2jg4^buHMqtCNwaK>QO_5Cjt(RVFPr4YXqo4Vj71}CbTK5K#e1(;e3pxg#UKHw zARe$5c-6*ue9Q>Nj94sDXAoZ=Z7&=YgJ7&FFjKG!FLtj^V@5g^35xiXxR@^qmgMwu zCBen2A`&O<<1;n5kA89{jK$#Tr)OsJ6p$ChaqLUmpkOyLtgHW+jqzZau;;2!0&bY3 z-E-(G_}X|~Va?KxD@5jS4POWtP~dz}|HuF3aP2Cg*zpp{EC6mIRsp|o0a7m|?p4u5 z0FM%)4`w7-GZDgV7Oi?w%!u)QxZ3CT)_vysk$X}Met@5}K@2HkOK%XOl<&d~)$gRd z=h5H?Gz#l$majxKG$bHH37qDT|9zh4=7$osl~G2xlp`E^BW6XS-bjtD8eg9v?Zzw( zdk>k!!bavBSike&X8zw=t+wmJ)j9I9?V^#FDkR0{2y&&5L!%=TQM} z*QX~3`iZij(N8NgCfUz`of@nT1giu6;GXT5J>)`vrpRVed=@d{73q+$>RNMIWX5g*TN&`eyzHoI|O%eb!v?y0XUeq0OO3vAr{3EY2uc2zF@l7TxYxGkJ}GjQLkB7 z!hS0o&MthzB3*rhFSlvOZ_TB~H{8SryZ7kF)LWPGXJ=(Obxpd2$ zUMfrnD(POW*RouCnm=i#hFR9fZ+a>FpT`qjD^-~+A;fbI5f%S*Jkeed=?F;7h#9#0 zsNAM*at4*tJlu}cc$?#>MXHyouRe<`DyMnaZrc`XKO5Ve46M@?*6ED(62YpzuC=jF zKN!~TeQb2i8E9uHv@;m(Y(cBO-m%fnI2hWVJ#2I>8E9uJv@;p)7D21Nvj1-JVCKQl z_UUD#qit>hcu1jrh|&J#?~1kRYmANdp@X4?CbICAXW)HA;eCYhP7=K8>s}l0BdzgP z_3XidU?Z!@K>Dac`Y0oPO^~XuRW{N`+aT@JOChz8B{PuDRY>PD(*Foj_0?su#n`#6 zkp?ZK7P6)cq)#fOPcqUWixpedSCx(Q$u>y4_voicwXn_4!1}bp`ZQy`O|Yu3XKk!c zx5jFwQwphttkZV41U#pZKF3Hu6{PCxI~(b9t&y@~QD`l6$qcmf725fX_L#R62h>-; zw=A~KZ;dt{iMlXb7?)>Yennw^g)v_ynAO*HHs)7aV>YS+LBp72<7(RBX2KgDtu#&a zh6g(AQQ&OA7gYNad^K8M+w2nFP$d)^lW4JL(_6XJu~}5#J|TC%giw8hHBo(~&6yJb zA!hl5*g356gmA_?D8Hk2?-eXbMX_uSITeCF(-aZ~GjL?iMp0mE~CCW*y(5Xvb z4W=?hFF#rI9~0~6yD z!`MB{%G~s|MIw-V<<&|}qOZKz8(R3bmkO~ZWzzM&y4|_tdB;oHvsdW!vXtB`A+pwF z5tV!h(z92r&}oq*SsXpD!bWDh`yvVUBm(-Hw%Dk1PBvxth1T&YvHcf3T|e_W~4 zUP&(6t!M?`4@c2zDY(@*t+(>Oj%#hYVxHRv0wIzx!)drd2iB@tqc`@uV2Duo}?_dIbq>L2)I zk0n50R&d2Xyi^!2r+s`ZbMimDl$R9#ES0dR5_>Tw{mtXqlS^*muyW%KYKw-Jy%sI~ zi0E9_9Ow2c^wVpqVm<>=VY-}FvW@8G_aPfey1{eF_qntMU$I6rOgDPq$A9)x zFBPT?zBb@~4^AV@UFs!&MPCmUnqk_a0qFguULt=J;wB|&4->W%aXF={bXwlm!<_sV zI5}XIPI(`o5l5`jX{oU63kkvS)#z1MuF`3lL|Tat5>ZD;CUNK8F3Ec(MBa5M0-d+! z1F!18yCv0vLhnQWEk^(S5QI=pdX-MpPZ7C=h`K(wN~huvz0~#bRXUCO&`a4btkUV( z?^L7gl@J9^gSPs5l}?Edy;Sle5Q$Fc{JnxcNkTyX2Z_)LCrG4~2&$A$$o8z(X}ly) zkq~)1VrVK~t<&@m!M*;gb!wL6eG&rfFo0FB)=B@!tB@^`R0|3$oB}8ytNaL~an7XG zI+Y6tjzHS|K>v}Kx<0yEr#~;laQ4hal2zj;6{1oy#RxDKLY!!OOgqgNjmrPbNJEilXe^Yjm3bZ^ghx5~6l{(7xxa(dpJ@ zUMfC+jZRyad5JsvLIHDw-vMxR^qh}TP1e;11TI^;7|`yw+Jz+2N_4dg^!%NYJX1pC z-C*Tyl1MAj$=kN0%UES~^YR~6RW(+q+cm9QkhXJg&pzd2ucBKQ7{7!ldpqj)+De_q z0t$kaEvXiiMMUxHl{%%E3l23Mc7PVMW$yk@l#=<1mCPrfh-|(r{F zDFhvAVxZ7ANvwi~f|3uV z0+G0lZuw7cmTy%o-^wgs`l+R_x5{ib5O1Wt{QI=!UdqArWr9>KfvQ34&0H>8?*zfz zMM8*yt@VCf&Yb~$>~cx3mJp0Jfen4^!hcF1Tl!DwV<7=^gWG*<*FPC8`pHazn=2vE zUXAvGh|ZEoD-pCQ-RCe2k~uVzf`!y#QpL}1X5E^_vyNM{Fqb&&Gm+0*vml>m^Cz|! zZq0&#R)6NDLhQ^;<pLnRM_4tq>r)_`Scn z+3`pgch^U<-tOk7IiGu}C>)KLX?i)!cZ459bAL5lFJ!`Yf{<{M{=pKQ{{;r`PNyk| zPZ(n87pl8{$q?5t#AHB(leCfJ9zh(B!(Dz+loUw_@}GbVK_cJ&LL~AeNwuKR>b6Ag z2Qtce9a{f1)m=XThBa$+I`v;(%HFz0rwM`~Bq6%utUnPI``7Ap{=Xo6Jd(EvhJ~7p zjHP!!7T$n2%2yBNN7_S!!$QlU&`dO>`ZkM!u5(4H6tG>G>(n@sd%Zj61 zr1}gAk#`;PLUHtxNGlN{q!dRs6#WQEu9gsaH-m3b`oq8UQb{AvNwSYwtJ^?P7w`sd z*3;JNl$O|gB?PLCVBqCzb(;7kh9#)Exsp6zLV(S)sGTQ~R-!|#qh9Q8-D=#iI(PYR zs%6?6ckcSKy>Vwgh_lrfjwChOSek8YAau7<8M|3!eEy|1-0aT68p2Iubs!Ek*xkY+^Iv01sPS6=uRC< z{B3;7I_FLuH1=HKr83ii(*b-pR3t%V8<9F!3|2bt>hObpe1+0#|IsmWcV3AVC{{YZ z@4p`~jf*7+l7&J_io?^G?r&oF@Z+{0xbuXbF3!znvW8+Hb(#fZLsBlt} zwld`%RzY4cjX6LnU$&+~G{bO@Ok+qQtwdMPz*v8RB%dT9@}A=%Vl6v%m1x;6l4?Og z=xiuo zxI-n2|Og!pV%f#c0)n3ZMDt>~1*prPPS3}=(FYM(}+#Dz& ziZ-pH`$?pg=oHO)CrENsLga0*@`fbRN_6FgE}SFD%OphJn@|Pl{y(fiqhs;YB}@=37nu8Cm2167 z>vU$@MHZ!4$_I^P8e?Whi!ODTL6?X2fM zpS>OBK((d?=2{8CwW~l4Ry(=tyvnrJAgLCV)k3`5DO(2-ag-sHm|Zq*vV^)R?S7CxQTP6w=Za@HdU`@1dXz5Rjfm7>|$ z0g9%6Jq#viDxI*Lu{^&XjRoPKbEXph<)|ElfB$+B{!?2-_?HWaE&S)Vpk^$D^L?t- zzK{@n+5l7#!dk$w<|)cnV7(*+*fz*H<{+m?q?PDs9$rAvS&G6PK#Q?-W{WJKJ_Y_v z2Dci>EH<}D5$z3R-?g+ikR7w3^}vw~q-x-?7D?OaDsoh{R4eZ8pq1k2?QZ6JzYSh0 z#el=!XtbO8e%S_loMUxPY}F{28vX!n2z$nL8@$9OjR|Kty`>l?jYmP2(j7-j;awyI zPY-}c5YZ(YL_|AFss)uHqT3jo!$CEg#VfJ=u`4%6FsMp4d0AAOyl;2&Q^$?=aNFdC zbe3++7!#Yk7%1B*K=lPdyjMb1z+A5;mdd$|vI1oUiRN;NW@Z6%RRQQ*h8lDw#;FsU z04{e{DRVly+w4VEofjGTeL&7$8ZUZbKBk=TTAiAtb^2t%pk&W&c+aG}jjq{7TSG*7 z8Cxd7WUbtXs9xf-U_1FOQldmOk@wgqUQ+C*7SuT%2v^|7;fbBT%%$Ttp^K+$h$h0r zbIc|$`J2!3P*HU#MYa8l{g~}PxK^iqLdO9KfjJJsdEwD1SCu$HLZsq9532WXvIo^{ zR6Ha=NeKZs38-?=Yda0_aLm<+$)3Mfr!NGqMMA*;6&9i`Yjt{K6Lf~ny`t7k8Iy<) zPuLu($WwG`%$eJjlf>;Bc(c~##O)ebL7TnAH9tkT*-Jtce-DVu!IEpBkb5^1v`VMo zX0NLD&6}NSqZ;K>b}tEmXEx_rtJ71P4_ytuVbms{4tH1MNmY#}xf;thdsX8-rE%-| zlLL4I?`b~klC{N4l-Kz%mAzU*ZtNEod{H!ErpKjg=p{|Qv=V_qP3U;88kOX0B}CqfGHFOkk_7?` zZW?kh379{?!+9^Z%AG8^?vxO@q+Gy%A<>&9M7d+EyelNqN_6r%Z4ojmYs383KucD(?Ab_b_{ov zbHqBG21*Q?xHU7u0led9lYy-RHSq$Hs8$+^n8`poSWOf~)kbXdQjxuT6-N}o>Xg7t zs%9IOY)N{Xw>=*ND16BcbbE@O{#GtgPM>u;C557TBFZ^$olbKGde}nJ@mQj+v2{AV zx6MmAu%j%OlugHoC27Yt%-kE+>2$4dZnA_hv1|s|BkOeXZ$}d-95hI=#7Q4C}=WfMo*Z@75H*>RZ-|nSuHpeFOc3-D=5NWga z29E5!1(7E^9J>Y=cyu{&hnEWDMtxWiUV!gFDs0mA+U&l5x)a~AB;CeLe_@9(u3Rd8 zFKPo5+-Ey1#w|Zq5&MONsPqhgq0%`!0S4A@mgMac0*pQ6fbA539fUXCZFbDbV9V#4 z22uHf`4ZgeB~^`oaWx*ox6P3+nInr42i8U6;oXr;jdcF;R^?LYU|K2ej-wo?iJx~_BNTs9A@X&c)Xz^h?e>ztX^@8sO*#yq zOLk)o^yE67mJjj}FdYsVd25|c-mkq>{Qf$f-u^m++3IY|Ak#Eb=}01eb}E%f6_;^f z6IFhKMI1IDPti&ZqEkNZ=ciG7EYeOv@l?o;R#b|Q8}~q1OzMu0yl=cz7&NIjK3@99 zOL=GfLE)_UqY4XSW|;aS`I5aL(7Sw{ZWr8S6HvToole{LTE$+53`E`^*XcIUUI+Mu z^*R-PYXMgYaJTik4MZoZ;lEK_u9xxwO!ypNbW4r10^i1qqWT32RQk_Nh;S# z4)XO{^M+hn&dEb1S5(5nI@+LZJk3wpzXEx+*s-s`HBNFAU5OUW@SXf8@VNp3n^SQ?d7Q{fjF<5J+6R~t-(y*nl`7cT!G}Zz) zdbH9A(T%M8t35s{1h6_RM>lz(Tb}gzD7*XpMB_z{FP0FU(}zu#iu;L9%xX&j9#@RR zKyQ^f=M;C@FM8N7@kK6WV3v>k1BbD4c$wV>K52y!-KstnNZMGsoa)B5#G@4@gY+&B zuv8eK4;f3T?jtpoeUHnz3*X6_Fnz+GbsbcdX%&B-@AbhmEnQ8k`TGc;Pie4ZIz>Nr zaP%EC+K+7SQ#0eq!<1qfhWTywqV+nhkh=Oy6_wx?Qj1ru*D37tQP(Z&b$S9n_N>?G z0Dk!eIQCuifmpQz`bs;>z zXpp8rsVq^0-%un}7^l}5!TM|;6{eCwdewvVy(h;cVfKR)tKv7Ueyx+SKx7q9|n4jj4@B>3H0u zK>qnk9{zw;?q@kZ^6y0i7A=3oGUbGhKAyV76Y6_FM<4mihkMlesT)9U?+6N9Dq4;a z4HKtJn}nub7AaUb9K-gYV_;U>-qA;;reTDLj=_rLPr3M(kE?Tiq*jEShGQbf0_fpf zAI41n?f3i0KLJ!l=@cYZBcf>pWaI>HE%5uO5Z^uMQ~>?#2jT49G+DSbMM4mW zb5-o!RG#M}%IVRfQ~5{_ClzCC8`PrHxI7@vUSqV|($tV(pEwTukC#h~I z6v2e8w+QnbX0zgD$*@pDpgjp_;po*3a5;@FIyDQqU6FKai%ugB^HKKQExMg?kqf~% zKs^t+JFDznyWMWtn_6y(v36bGMphflnaJY{YnBG&#a<+s3`;!HBCWGy0 z(P^k8S4ar3fdKodMW+de`>0skpwo?q`>1Qq2A!V3k0Uqe^ns1R%6WNv7=i;PFmPp?B*L!j5<#$3LU(1+Lz^W7kTV7vrD|_u zuyQUt2!?XWfTNn+c3&PLZMR!;?3EC81zPUcKY@j4yAzH?zh+`4xKJ@Mb0h+}xb5}{ zs{InWtAn;%E*Y2@wB3LsrR^$@l(xGGKhSpbP%xKEqg9=IVEBQe8z>QWx!E!<_$6#z zZc;J;1DE@eRqh@XhmLc^Q9j~wdmUwo{T#{F(<-+`BJ6Tckn*}n*t*A*kiFZ2DeCVjmP34JQfeZSnNF-V=s@zLoi4dA8if4{E6XrDB?#Q z?NhpUj?h08G(h(zj+TL7mXt9^LQrb!-swlnK=8`Z5K-2a&kEo|36TMIWM=+~qlNju zASamrr(>ki2OR@N0TH!we$fg;3B*+?+YoXg`z`o^wtE`c+c2$9FPmu=v&wrbrLUMa zf|)h~O#Ap4n`tA!oV~|bOyf`05ta+1MuJh$!$%!!^<|-UuY{;Kq?G$|pJQe6aLKVY zqYemQwh)92=Kw9LdF`>nsM*Mgn!bY{sOc7AFAK05r5XZdOUaXu=n zru^gHANoLqLN`2IUOhpwmE!!Cay1QE7FI=&1Brewx?W zN8PxBN27k(>pT0X5H^eB0qZ&5wd(mfyK8f<%Z~GNHfG~$)svJ8V-BMB1LnA69Eyud zq&;wVCSN~fQ`)J&o8s|WX*JRKVFUcs|9Bsj;?6Ci@mLNdkN1&(`B)EDE0=+W#2+(S zJfjTk5ZkCbo#rm0mW!xG5wHKzM}_s3bUhnP*5E5X2zMji$F_jq{^%ormkM5!-ohXS zC-^8YEfmj`u&|1sow$u5&OQM|9i-+}(OR>56mHeCNZLEVO&ZP(RpXcth^s!quKygi zz0E!WOW9~e!lY-ptP@Ve`X?TumHasXpSaR(HG@~4=%b=&ByJ3@t)l0($phdFT!Gri ztv(Am*B!`(d+Zjn?dACseMJ6d7uX^O`zOc4ugl8*0mtt{_DcGIGoSS*ANiZk^AIEa zgyU}glaC4mW-t<=&p6_lKY>I33FmukRGS#pzd1Le`F_f6+y-a;*++%J0M>jv7%K2* zWY4bJpwoD1*J~w2Z+w}Ex;AalX$s(S9^9bQ^7BnRIfh!l80Q}!A_!SoXFAR^OLjpHNLcqUmk@2T|u&K4m7#&FAeor$UNTqGr zYaXcR)L1Lt;L%EBL^pV#CkEyFsK`tPf`&;qdgefc@=g%&GbAjG&=fWv+>u#i5#Nq<%*TSdHdte% z2S?IW2P@(}Cr(8iSAaEiZRKEwqnp^)cv=ArOXECL7^QlsjVlT;Ama$?QmMfK2|))u z4ob$TV}XDx(jLg+L{8{R0LXt8AS5I1Y! zf>BL$y9VYwR^X#jzWn8O4cEV{LY=GO?|+AO${;@-(bY%(B&r*sChjmJx9~B?7QK!Fk^-5`e#3q7#i*z|%1HWFzKip{lclsOH^3 z^{01rirE>u#xtlfFtHB|9D&%(o5*X!u zC-bC+FPJGg88)i%Jkm`<;%G!PAtFhs#e@BHBcNkc2P{vV4C1rPH|lh&5H?dnke7r^ zjNGWx2PgZe_=1f(?M4P(AiOMa3nc_Rw*Ppse0U-7+Bypi859cHl}`?{qyxF8;21Q~ z6uyR~M^woL;DLLr;v7D1re?nR(hUeUPMeYVv_WNhIZPH46z{Q{ifj-p&ALP;ke8kiAC3mU=_?k#7_@mb;uwB>2%V`y#NnV8Sh8TSIzS5eO=F)eF zh#BeXM98F-+I2_zDOKttKV9gd!Wvq`SdJ<4k-r5o5n8KVGsI6XmieeKf?X0?#{})* z2=d=bpjsKz!8w=F(aj#|-HZN(5j?J4(>f6m32H`RnI z0_ken$vIE%4uke3Xz;2yeZ%#5r#qSy3-n9wCrVzT#F?ik*WORGrMv9bWDi%Z;I~2| z8UhPFc5BLe013O-cezwy_{wVDYkK&o7zg9u15nN<8+ED>7(R2lW1~(x0Q2oeoqF}e zV0vJqPA82}<&{eak>MlRf8M0iMGOX1{Umv)gaBKQqJiyd1_QPwlDtepfb9U-ahr5{ zxTlX8?RJI-+MN@|Cgvs6cc8>nzh=iL???5NLrn!_}K~n<>pUbBbqd(rF`jK4+6o$M^P8@iUurs_5-= zahJFALWwjw(nvdVa+I6Hxb~?!P-hU$WDecZ+efAK0o*=0lW%u<8WDVp3_vj6ZRpM2 z>H+@f+{Z`RpmDd*HbT&&f+wOcfYQ1?K4MCr!w*opxet1WJq|Ic@kmstwTav-*pxW@ z&7%ozf8)ac*wmg%KBMlsBe_9ol}f{?kb?Al>3d1d(K#QJwDIk z{oK#9?i}i;S^a#3TZKN)LbH5sm+^TPW)SKMwJ-ShiT!<4h>IR+d)A3(`{^!x$Bp_S z^eumX)!#=s3peRBU$oFBNbskdbjm-?M_pHL(rGAuY}%w#_%w|30B#ZBofh!zfC2Dx z_yOSM47_KPPMt1OI`9V^XFO&z+`az1S*Pz$^HFi}W}VJFJwqTWYa`JRW@(BYL+}KP z&5OzBxq0$SmR8DjqhGi(x10{PG$!#4#yrj6fl7RXG5b!p8biUu*zp@5D?jTDtp1Jq zF?1U%m$ZD3i(GBcT{WjH|17W=kWB*F6$c*xdC^%uV&F&565w3` zc6t&I>$vo3Tzv#?pBAqsk8|^EfzEQYK+hTOr^5z-VMGgbuLBGxz%|S3l(Chk>@8#0sL0|(UjV@QhGu%VOV!2_Eh^0Q*M=WWDgDv240j{us z&jHNJO*+N!17g|4z!1v`0(=FU!V$}t28&q!2apbTSu985-C~EuN;WP2HeKN6^A_Eb z%`MD=;X}Y`$mSMi_C$OKnWeOaIq))Kz*E}7O#O9;)f@@~q;&^Fju@(hYz%$R-`B}E zOY4s;txF|p2<_+Mcj4P5ty}f?NBZfovlSvsT7Tx)*x5eHxq7osO~N9asw;VtrSZFa zxkyVSZHgvb21uDdaskr2}8$7S4jKK2o3K;aFA^Hob*)Rg6 zqbjGOCi+Nx8VzYGM{40nG|O+Bb($}C8M^(HR1bkP`hfRkNlmk z^q`uip|(wqI0th_uy>~9oGT#+I~Rn3)x7}b6kb$N!3tOpXCR$MrndPYcfRQC>gEJxanth!VqG445xK zN>1$-od#a%Ar!#uduWSJKc3^G;wQG~^oMiN5P(Sv3=Ui{%$es3%mlz3vPy}hU6bS` z?ix+7YBT{gy762~h$f&;RG1{bR`-c>?aD0xE-;fUm53P<}?#c=2qtv1;r`kc{HvDj$HB3kWb;ZC!J zfQNaK8PR0HzhuKN7q~SN0{#XI|Cbj0Pk?urf@eqS12sl%(ysnqn+-P+(Qz7%DIIso zxqdof40e*JGv*@$#<=H?z+y%XBWY(Ic)7((ASq)cWk6C1B;h2Tii01|w!xBSG*|I# zA|8m^c$%+t<2#q}oeO+FjDcP?iOz*B`P#8QD#c;ZbAf&jzCuPIgsl>RIJv?S)N^5< zpuFZQ6y^pgp|DPFJHDal4P zDzDUXUf7~j`BffXWM0MW@!|pm3$De6=c^C8o$php+7?NCvxLBg#V}v=Hsbt@!4I_b zurbXs+!Q>dX{Ax3hqNtU=Tgo2KIY;quHG7a)%Bytc@OL4aX!K=nki++dY+jxk|UFm zI4#hy{GX2RdSe`7US)QDfS5pfd>|5)n6cwwgbmagc*r4TO6)s`t)ti2#@G7-SPHIE z6H!L|Eyqm2PAEpy?-;D|LLd3(2dv0N9C_1)*v+F#Se8;oT|auC$)AU4qU=3ebXs1i zIM5;?c=shV!Vz0_`Urr0!t;P6X9pEHz}BFJvM~ntlh~mWB6S53m0&z3%E1j7$x79( zI}w2%GpEwSW?0-KK$P=`tsq9hd;Mx~N$kZETB*fvZPjVg#XeQgy<8A^A6u#0QrIFnv{`(R{56z^ zGnb1bzF9)B7dDt;_y(+EMD}z?-A~ZN#?_@-@j~N1c87;0{0JfxcM2&~Y5+^W;mOMTQeYnx6BF7;8)k=t}i3ds4e)s$@0 z=?kP_(KRRJp}9oV4VhsJ_z{`0j{3u8vgkVVG89EQJ-6w!To9cJL>F$;X#$`E+jP1S zP<&59Ls&6tiiBWtA*^7K)oUfvO2qbolG`?%$IHhy*E`iHq>B41757&%_n)}TM{M45 z*h?#!zuzLo<}a;e1|NMn53H-0jeYTIS`S-#$ePi>HaLV{asfu zr^%m05{em3k7Z#7F#ie|;iG}DNpm^kb40|F^aSJk0biI|#OO(mIQ~kR577q<{`QqV z@>hgKebIad+Kk9Kh>S!1y~vTJS1}DNHp|0GvaZxfS&lkYx005Mt7M68UM13UALnO{ zc(3%Bxe`JUsvtPfh_3^Vb=AOXRmyZEL09P$L|2W)59q4Y1k4Vi+jJ_AC^_gQA#ifW zo44sS17M6ZDL9|7a4xh^t!5Y~>^TAhA7duzS587H%O#1k1M|p$MZnOBihvOkf`AuM z3c#WOBX83--4@uSEx5(cZPRI5z#8};$?=KN?dBbnGRDS@ia^+)&p7KJD}9tV zUntloVPS+m=h~cAiEc&_9;LtMs5D2FBMJ|(fJK7?ra=pJ^o;u}eNjZ0J{<%MFZy4xizjL-^B+gFKgBNULNm5lW-K};ti@$@Ko1pKN$aIT+f ztvI@&!<*;&>Di#_DX(wSsU_lJU(jJNihQz7r%#cXy?&ccyQQ=P5~2-1_YjpFxgAGY zoCYc?iB%hcWO13vikuPXqk3JBh*S$FKG@l`ofJjB?U*1zL^V4Zn(5+E;k@TQ1y=52s z>76Q^m7#CEcVFzM;b9;7n~}~0{mghk}tP`=Q)4gu2XqT0sH~C51+AJr-zX{`-1H{O-?F) zPmvH!b^*X*+jVMYu)6I!t&rsX5&~=jZa%*YE!%a*cAYj=`>5-~+ja6rpg@3}Ood#R z5V^Oie-mcTAh>wcj+pbfB;ej@pR%p`_0{%?EgXSk~uqH{q zS3-adu)y{K%n?Yl(LCQUjZ~d6C>jaJjgT#fO?9d;C6Kdyd=hiE56kuACc;Y=wse~9 zgHW7?hyV~+F_REe7YmHRc_^CggRIP(=z|TqoHT_7AKmc*6jYd`XZY8)iP*W1LFO!) z-sOajHR8heK7S69&rwF4mU2u(4H|&^s`RFP5`qQqf(7We57qdn7*evB;n5PhRMsyc z;NJs08sOhHSQkkyk`Q1Y0t^k-IVufydK4mRD`mUS8}Wd$iU#1RIifbqD$}$%J)z#s z@wGly0c(9{jq=l#QOpm)lL&p|!-8o>)JJ(INRD0-7Dju}ULQK#LZ%@b3&KT0>{1Cq z^cGyYk8AszCDKa7P>0nZshdSQ#Z?E|1$fM^w+sAk2?3AUwcPb|K%#Xa2xV=v@{&Ya ziLSh#ZrANf^b-KsKqtR2DMUa7B`8^%noeIr4ccuA7^I2wj&q%k+hlFc=>?J`1sxMA>&h#aM(y8o{bxY zRVl+zzDW;d<9zM(n2-1+hoS~m>?sn0`ciC;;U$Nqu?&e&!*n<}J`!)V>8-d{AyEBo zYmQc`df3(+bdOyzYrkP@4mNj2#;v}zH3wZ%`jP@B%o?}nV4fR|^FGWy9Z~0UdjLXx^#Nw0Cz=p8yu zmZZNRDY!$YRg-+wHM&EmBT^`|euqx;1*SV-?%ttOuau9vKCnZli}2&g9Xef&AFu4t z>EV>f$fBziQ<^1&jO17{(hS_h1hfe9Y9I#zn}7-g{K0Hjkj6ox#w5&OAa2W1#s8?n zlsw&#Ysu3MxoG<{O-r6`$b~#Ti0`;zgswJH38EWvAqIapEg`!h7mY1KrZBLOP059j z`O*-wY9r|CG1D2td7MJ`K+JXkIQ!ilI?WSFepW&>-hIfpc85;G>M`;FtXYzmN(iu-9iUHV zufVKB5%Ul-zOh25NWIVfXiWBBRek&tcBafnW0dR(k|%)#vUg{_$leS10onTuKOlSm zt{2&xB^c*O2)b<9>)ZgbQnDw=R{%L=@AL){{+k%cR>2UtL@=ym56`@gNvM^CEqP5h zsLCh_yvJ`z;5~jw;JgM)0`Kud0?QgLVY|og5;g_Q!ZyPXVQXk?D{N2rA+GOm3O(tE zu=Thaf)u%0gl(Q5!gei3CWG{pAHw#Y`V7SBX+MN*2fpj*89#(=*fl;VwHKI}Y1jBD zuZz@YsDy=S+UAF_y?>1@Y?D;`9^)lqVcW||ENqfg=oOuI)U_gPSKtSPZSu926wMKN zo+lw%s|Xn(Y-<6g#7S@#BMIVE_*Wkl;|-!ofAy(nh?WV4&Bu@OojUz|osYVnzEh{}{{xmn8+S=5mWGfW0y+MFL?9ml%pn9)#L*JNy*DX( zl{oIP#c>ad~5XXY+ zGsJO99>g)qk;x$4kq2@7oB9mI>CQZe1|Q{xq=vUjSeT|Ic@WV4H-ekch84oiq$Qx0oCE=FlB65JXlTQmZWICi6h9!K z-vOA_+-~Uu`y@o;-;9h9(6Li2Iqr0$;(2EY0d_0yfno7&mJ;tkMu=}}iiq!wDI&h( zCF8{swky7S1yLale06!ktsnaijW?$f!r>Ocak`PGmL&kk8bvpZIv;t(hNpRwI zGnVOTH;YWK1Wa3*9u-K14O@aI+^R}ZlKfGgCR4?a@}O7`+-ynmM|n`J!)}53z#iH^ zV&VMjEtYtH#1bXqtuU~7f1C$x`|>RiZ(6}ZybB*ZEnLk4T5v1NWD8@sWX|aq00y*E86UD|DJG$@3%xSPQ@~M_&jqZXHUi7SHvOYMpOT zBBoy_-;P>5y;G-hf$0XA^*ePMdApB_ckI+@^6ftAx^JgWuQ4#H+$6v_Ov6>)gCwSW zi7;xZgdhoLP?+)~?mz{>nPrl^Swetu(csL8JA7(#T!SCzr&E!U>(VSmmx4}c&n19C zUA6(nk+fvWQ0BmJaHvsP8@7dP;BBf=l&~#0%o4T*he6mn-f0Qjg2NzerFU9#wcs#l zD$1K8!rCHXp|TUa#MvwEL_f3V6v>2{&ZNRBz3Fy06`vla;d#SPxrm!Vf)O=h810EZ zWyH%7nTCJXr%cx$?(*^FG_fQt;`{l?zc=lnLVl2jzC7&FN*0((?9$+J<)f$i)aA-ewXH5!?mLxffhK8|Tsg>vX1$ctvM2A->2fJ=f`lj+ zCd-ohcfmsDusvjiYJ=%PrWrBQLsCX4lD7Fxcewe!%WnN$%;mpM<@1WjH=Mpc3_D|U zP`=%uF(Vd?rg&zUln3BVeuWWN#0cs+5-I_^{Rm6IZa)G7_K&GPe#-LpBOp#&QD7b2 z!AUvOu*<`(K9wU*nTBg`YO$}DsH&oA92rDpmpW|D&95DwLTtvx2S+S@dGO0g}b4u zwxJRNyr1i3H%(Q*OdAbJnA>dvm*3?k^zI|I(wGs56Wx6zOiupkR<-Xw@_#U!dCol5Ydc(+cY@uSynos#%5bhl3T;K%sgI=zY?SMJtn z)jdASp1515UQ)k)5~hjhNVK-Q1&tKs*@&lABU(J^GS$tSkB_v(^y4EhTAWM2-s2<7 z0;*#2?}f3c(Hxf!q=`O0vg3F^4ZYXm;KxTooT(^DpBxEO_rVD$Q?I^T#V0e!`1DBJ zQ%j#7d1=s3+2MPA)QzQQITO`sHX@Y1RWY64L5xM!awe+FmV4b!FRkj|-7YbCx0?Yg zZ3e95va)A53{WwRh_Q;Im0Z>l_gM^Bsj3Z2)!rk!?#ZPa?!%V4&3yXy$ZLas`j}(s z&ZEGj0rz8y(WJDM79>sHNkSC*R{%SO>XKxEv@X<9I7KD!8VjU&->bMx z^)5@Ony0I#P=mmdquBFe$x)95{Z#(|Iuh?TEa5hO8xcyXC1x{XY*AZs6#AO)K^zXT zb|97tvy>mLkCBPXYGXZk-+m zs#UvnYQ~R^yLI~UAs_L1>!SsJyo7fW@!dCkmbz?~kBV{d`n*|a&hK{Xbg{r$H-jG_?HVtuT#F)EPF0{6RyM`&FDryAN!9X+`LlGA0=6fyvf!Lp{B7Wi`x@Hh8} zpV)P6J0em>*d+Q%`{Kh~>M$$_D zsY&?`K3mm!j*m)l4vThp&?uF2d}>v+Lb$U=LXbESbb!QX=b+!&^O&NN7?z~PT)Z{M z99w0VJm03EjEL&AQa}^c!NT$b2;d7es1Bn?>7%x);qTC+mcFPP9P*R6xYq;pp+|j` zed*Ua^%MLTONdhH!OjM->QNsR-}tpoIe!zg!x;iLP(pxTgsNc;I`D63q-kI4c5+2) zu;;(lss3+1Dt`5Aot_8Qx4zbCsDQb_Kg7<$)~|Ki0`NUw>vY0nKI;0z*E$V&3@mgt z-9eamK|Bbnz7evyIOjgAJwf;_n#ImrH1My~7lb%4*pglT0g{16r_ULqykX~ zG#ZIPA%D;G-}sb8zk(1ml1ik!FN8at1!*BWHBDt_yEmS)+O0(@=hv`}T)Ri7C6aHc zgs2lzi*MPZ(^_QYs$0p+T7#ig>1nBY*Qdd%nR|4)T*ydD2(&+dqi^ofX&B&_?9r*) zGZ|8l!7418h9y$S^QJ* zIO~}T8)B|g6s^4j~L<+K(IMrsnmLzgs6Ql zXdajY-nPNMkmMB-0<4b(wi#e7k)5tqo$V|%S`Gwppeu%3!1sc*K*XIFL$3j8sL+xF{7#8+HpVe^~KJ zW92m^OH(s~DeOu2>I=~Q7{}H3>GSc;gRF{ZMg;T~P07NP;_L-5>C+uq(0tYSNGIvJ zEHwKa3veFtY7Z5{(<4dKvfe56)4vy>u`U)Oo|RBQW-v(S7vXS4*LbLq)x;}I+UYO) z_+j1#dOZsdl3a=JB)yS!dB{&QI7@|{)n>r;#tAeLmTv)0}HmtKNYI-?&Go=u1B8x^s_C-{QxwdvrSbWuNk(S|QT5Mnah8 z7kh}<8>#Qh=tO+5{)C4V*UKeDGfsoK3J2>K0WLe|8=Zzqa)pEdo23!u^!i4p<=1!^ z|6`CF<#2Jiz)g_Qg6DyBJ);eOqtm^TJX1n|J&xL4|BX(bS5WagzR`)URTY01ggyU_ zPTe?Z;Ws)>kfb+|wE7#JMj(l+HcHyiYEvMe0Wegp24II2%yFsU*m$(jmdk;&6{nPZ zuCnBF6}RZrS0Im&PxXD&tN3R5R1q@~!ScC^<@3bX;Hg(zMH{kS3HfOtK26%l;u^$9 zI!QZOTyK8ONB-t(Eus6C%X=9hEHVFPiTMiON&10>?y!Ym93-aGUlrr<>?=!5_k|)c zui^(JX5~VW82u4dzwAde(uk-t(f^OJFOQF+O#ZH!bSK@(1cpP!_1L%plR<*PWnFZ2 zjevqKXcSb|Wi>;l69&R$oSAU_P@|$EqDDnUM2(7qiW-$wK!k7!Lc}Pb90^3uh#cX* z0=%DZJ=L8_bk{e3q^7Itsi&%*dY*pnZi<*jh$JqvT;Uri5N^dsXSvKCnv1MaFgcPn zPNMtP?AK`e+$7=>S`MBj*E_u%?2P5TrPy0*dpoUMX{Ef5&$LGIRaVsM-E))3Q+_|r zHqx1dNvBijl6lyO1x%36^48T+^APE&lcnsY3(Ocok7!6S6?E>#j2F@bfiTOB3WIJ; z1GDYeuhA?a&lU)_M6jhaXw>ctSbtuFMrjWy$0vc5)1c9nUnG%vQ-emi54ib==mNaj zLJb;?_(D3Ll_GMfz{`o~Cvc(jIUS;nQ4N|yu4>8ldV@xr!1kMc$Dx*A?Z&k|TGq_7 zFKc#Z@nsFpJ@bYA?t7UXrK)wdr0FgY*85;0RJ>$9tbe~jqf)V+NnbT+)O&ss`L;J` zR5(A0(hoIgG;w|snZ5&9T~?mD0%H0e(C7p3797y18b3-7Xmn_P60!4pOSb(4BKvER z4$eRKOBA)zGaw8@1%mN1Fhb9(z{q;mN#0C?o`b(kBHy|OjUL1gDE#J^qVO{OXmdcL z`Y%Ou?}IHgPg#&ezPk=+)D=Hq!fp5g6JiU*gwdi=sX%DvnDEj9HsLqwhFyBRm*~dg z2;c2!mtfu-@pa^DrzF4TlAO1|>GprEp`%l~z%E+#Oc_Jb^W!buE1829CQ(L^*6~9+ z9){5m{BR>4z)e5$a}dv?iX(w+TCd^lH)&xKWdhNSHZTHL*!u&tiP=9|m_(`D#q>;3 zo8@KGX6D!dfykg6ZDE8_i4Gzb2q#OrX#(Lo{&+A7^)hBdK$Q!5jzEVE!F5Ze6w!V7 zfg+lWA1I>FE0f4X70;1!b&6#pWE1y&t3+tEKnNe?wtL3w1zRH!)PI8d8zsv3k$gJJkA3kNh{iZ3p3)ID(}60cXZT?`9)eWH54-U&jGpC( z)FpVYp#^`ApR$m%N)GGB5duC{N`sGneS1%f)op-vI(G=ZS<8NuI(x`?!E zX>TcGRLm_m_~Me~yx;bETj7bMH(j=uJ5ZU+Dhki36YWzu1{=w>DxPmZ{jNjZ7s^C$7{1eX|sW~>HL-5dTn4mC?kq( zzxYDSXxhlA*jEVV5U)DZCPt+!$6BZ|d7FWY8}0yRNKo!F+`)GNKM7pKik1|`=m@7< zv^~&Bw77uofGWPMUagzl7)FA;XkM70&O4w{-b1RgG@{&oI-n7KjSUkAG)l*h;|Dal@oNd=@`qFh zW(T?^c=2{zi8_bAO|_7Af`qh1+*K+nX6iE5s(AQUV=eDHN~aD2Q31PRgD9G|i>sQ|ySM>(&tPnX zKkQgkIa5hd4ZwsH8GcXT4as*@VK{@~je4HM&pfzsIR$P1Ng(sayRjtnN(u(nSFFZc zq#P(72EE37cdX`t^Xn-XIKK`S8MFkX+=dE-kDh{$Fwk5KMvj=#LLMg&Y!ktTsjVFj zTe*;@3j|x4&1QUy!ke>SBYITD7xokL$mcR9VMLoNq}Q?U86(A z#3`Zj_u@dTBFtiZ!_PN;56QHe3*-6k z5r_O{u6sZ=2OB%t z`?$(zwe92^L27bxI8t0Vuvn^ie}8ni|9^*pz2(; z)-GA~Jb100=_|ZPmM5XqI^Z-;aNat*EA~1wT#e@tmuopA-UT8|Z!-7af53!)am-(Y zxT7_^Te0#7^xhsv$%mpb`jC@t180aT_#yE}-U73hA4>3mVB5(e-^U~N`pjive^`&Y zJ^{jL4)b5T9+f!l30${SP(WXMa2a+KYD z#pj)fcETTvS&D2VtA zX+EQ00NUeylFeAks5wB9(dD2+plyWD0wPrDG+S`u-UG1*wzYpy+E-2XtND=*o=>qG><^AU2U^H z=&;#jZyeh-HrtyHTX!M%5D014+ic6g*3>NU&I@&Z&S?W6?XJR)FU5T{>fd95NB-iH%1Pk?Lv zdW}{H)#gGU{uVb;=Y8vWn=CsfJllnoR3Q+eQZYU-=N;52caneKqTbrbkTJ_3bxZA6>4xnRIs?zZ3r0R0KD)srG7_&@IyB^klkA<*f|10vrOrnTTaUDvuq%bYU7{6!1+`5DOSN6-dsG z2E8%FLtpM}xs5*afk@n@`pjq%g(?g1^_&KTNbzub04m$~qyWm|(vg+E(hi_QB31wCCL#PXgwY~DYoUN-N0pK02>-+A*Ujfl7K z-vRpJO&aAq(_#z%=z7@0J4C~h*4e`UilX@rX;l7<(l!D^ZaJjUg8C%#-FZkO<7bQk z3l3>iCp;tZN}O;Ad6ym1DCg%SG7!hx=P8FY2+Ss;7xD5kKH8|!aU1)E;sO;HHu-jJ z)F}VwB=YUusL^BiacrYT?;=~}$+R-1+o!fC4~vT$1-9^H7s+XyKzQ;q*ac5k{Vbl$ zEo<_m?}tqqd3GnoyY%wisGgfNDlKc_(%W}CF0BwDj!R1v?KQYd)W#QCe9I1LG-Y=Z z`Mx=%(N-{-wTCoHdsdnDG}Ln)*60tvB$3ZNtWn?>H0)^eOXn+xO%e!mUIl;6L5-$? zA1{KP;OD>#i_EudcU4KkwE|nXt5MMXq^W>>-m(ODUA#x!RsL)Xca7cC%w4a75$>vf zHqKpXH5yg!Ng`injYiw{IDVt&lr+b0iF-ja&OfZtAkiZr5c*7qy?;5Zk&pR)v+*kl zH8p>Bvsbo2`Od}7-#4mv@Qn)%;G=udJ0SXo-}^K7s+Q|}{#yB}y-AeXPtpz+m>G@I z9@TUZ*O~0+RN);vk!F!g_kEhN5F#u>%I@mhX*yf3p~GVNnxaTl%smu z!gv!-DHBey31j!!ZTKlR!s)y;a_8mMeXxm6Gp%x8l4`zYi8&Pl;oe4QhvqAJKZ?O| zFMpD%*H9~eI6g}kb#$RhTPGZ9xYU)IUu5|sag3cO7J7(Mdx-861ZMbybTw}7$wEG< zHhO&=`C5>3L2gpR3I*b1+nLujQ{qOKdEH@+%ARxcL5n^(2!HosjVhmW)44>%XaB|> z)+k$&T_X_ltxs!2onJnTUKqDzIYR3v5IoO1R2w^}CDnZUutp8fsX67zn2DW#Sfl9v zBuZa;Sff|isyMr%R+DobbLtky`!?5=h3Rq1#XEVy{v^s8;*VPLY&IlO7G@C?c_1Fu zq&VebQvCG>J3eypZY1uzhw0w*$`m^HfIWM8FHQ(wbs$MiB&R*E9C#ZVZgcfvjdBHZ zFM?#(VT}eKz?VA?YcvBtPQ&H{NtA9J(dg2HNtE8^h(6LX!zf`4AhaHTu189cp z(V&OYdYwqJ8=~dB_xKGUc*=e`--Ejp&%tQ4#EQ}?4#Hh0l2lXFAdXpXH$@knl&0wa z@B>ZJ(v#8@ox~3`MHih)B5sOuPDxW#x>EUPf-+&(hFB9EmbLg?s8?JxuVaxy$UuaQKx8_ z=5WCollYP5ajAMgYD^-t^oT}jFRHlZ-Q2Gn(dhfeB=Ws|M580k)hZ~C+~Ch2Iv{N2 z2mM7()(zjptMH!Nl{JRHs!H#QI%noX7lB?bs%`BxbfYoSP=IAP_>Zh{8*uoj=;e;6bVw0^6ViUS3LjlZV_8{cNOe{Ikx zPsl|A!Nzwt<3m*mZi5V5$QBT)&4tNE%w+QQ+(X<3I#3lyG-?oixR+adM59G+gM9Tz zH2PzLLFsfvDl52o?RiwACE&UFs79A)2BrV&s7Af%pGT97soR;wvi zHAEJ*G2_OEDC?mlj=T5Kkv-fyM?|a;2>EUCjUu>L`F6gLY!>7K-*%SofSI^6vRv|N zhnGa}dX0wZ2Br6_*XRw-3)97#1Z8`H$O|tOw#5*nO++rrmZnh8?2N8?s3d$_iB)3x zx>}+8@WY9ATBBU<;;8>7!IgC%(KiW^A`h+C4Z_slHwj-{=%Ln$1|jBGCm`nE!BaR) z-zJ!*hidUOgsKy6y}(1~BpKwXo{D4eIBmCqkyDZo7&tAP_KFIO)!4m^Te7|Y$-oZk z% z(X;r`^_WJpAGje2~qJaLUch`a?NpFO5gE0dM!k4x%A zLOp2tKWo&@G>F4>D42|?$21xwGA0XztR3KAcub=wO@lfwKcvEh)npA(wb)c~^n!*sI)NunYX#^VO&%j7Ec@LYl&;tQ3PX@`uo?8e(p2ib0;7X;>=_(`y=H@>Y*Q9`CDe?2mqj znIawoZ5o2G`c)N#=iq}+2*PU6%<5wrd0$hcZrE#g@R&x2JO=qr9@8i{)u43uag9Dq zMKGhB@`RZ;{Gx0Qfn>BguF=}H$_48M!tFgEv)gfva$7+HN~cUD^aKed5^ZIW4<+(m zD?=5^_;pHBg+NF`q443`Z@`Yy(Oa{1h>ALq*%vYq6`8FiD((dnSBfeTRwEEX@vSU1 z%;AZg?AKLQ!nB(`wE6D=fD6N&a?e?>R95A`MwR~>F8_yH8ov?0&&9JfIu`K@ z;v6p+iX+2mgNDs*J@D+0@;6F0GUi`i1K&`O#;8$CYJ(}@yw|z88C6KTS?B{^_!pl? zE_+=SCvROfF-ut~n7et?AsX}v60MU3!WRSKJ2dETc;$ULNvJj#T3fW~KD6oUks+B^ zAJ?cs<&M~`O>A)D{j^i4o{iyttD-byrA;$^FHEP`k%3ocpQJy4x0AV_~R--FG zVhMvK<#>URFvgZ}2eaY*F-6GJ1%mB9H>vl>G(p-#Sft(`=G+>M%HwkP)zoM-x{X1` z&Kiv>CE-$mnK+q!F_F(xtI;dSoH%z)!oVvKxtH3x8-lcnO}RG(nR9uy9Z=Ph$Te*e z5q0BFuXN+7kv0Z39n9~RJkUc&fEsC^%kPFEV~4f|4i09Ms~e61Q%0D&y+$eRPzRkhGCC%DW5lu? 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

#eVvo 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 p8__yTjBkM)kRCL@Us?|u6 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%0AcBNT@3yn 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 literal 0 HcmV?d00001 diff --git a/service/frontend/admin_handler.go b/service/frontend/admin_handler.go index d8f957b5eda..9376025be5a 100644 --- a/service/frontend/admin_handler.go +++ b/service/frontend/admin_handler.go @@ -27,11 +27,13 @@ import ( clusterspb "go.temporal.io/server/api/cluster/v1" commonspb "go.temporal.io/server/api/common/v1" enumsspb "go.temporal.io/server/api/enums/v1" + healthspb "go.temporal.io/server/api/health/v1" "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/api/matchingservice/v1" persistencespb "go.temporal.io/server/api/persistence/v1" replicationspb "go.temporal.io/server/api/replication/v1" "go.temporal.io/server/chasm" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" serverClient "go.temporal.io/server/client" "go.temporal.io/server/client/admin" "go.temporal.io/server/client/frontend" @@ -66,8 +68,9 @@ import ( "go.temporal.io/server/service/worker/addsearchattributes" "go.temporal.io/server/service/worker/batcher" "go.temporal.io/server/service/worker/dlq" + "go.temporal.io/server/service/worker/scheduler" "google.golang.org/grpc/health" - healthpb "google.golang.org/grpc/health/grpc_health_v1" + grpchealthspb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -112,6 +115,7 @@ type ( healthServer *health.Server historyHealthChecker HealthChecker chasmRegistry *chasm.Registry + schedulerClient schedulerpb.SchedulerServiceClient // DEPRECATED: only history service on server side is supposed to // use the following components. @@ -147,6 +151,8 @@ type ( EventSerializer serialization.Serializer TimeSource clock.TimeSource ChasmRegistry *chasm.Registry + NamespaceDataMerger nsreplication.NamespaceDataMerger + SchedulerClient schedulerpb.SchedulerServiceClient // DEPRECATED: only history service on server side is supposed to // use the following components. @@ -162,38 +168,25 @@ var ( // NewAdminHandler creates a gRPC handler for the adminservice func NewAdminHandler( args NewAdminHandlerArgs, + namespaceDLQHandler nsreplication.DLQMessageHandler, ) *AdminHandler { - namespaceReplicationTaskExecutor := nsreplication.NewTaskExecutor( - args.ClusterMetadata.GetCurrentClusterName(), - args.PersistenceMetadataManager, - args.Logger, - ) - historyHealthChecker := NewHealthChecker( primitives.HistoryService, args.MembershipMonitor, args.Config.HistoryHostErrorPercentage, args.Config.HistoryHostSelfErrorProportion, - func(ctx context.Context, hostAddress string) (enumsspb.HealthState, error) { - resp, err := args.HistoryClient.DeepHealthCheck(ctx, &historyservice.DeepHealthCheckRequest{HostAddress: hostAddress}) - if err != nil { - return enumsspb.HEALTH_STATE_NOT_SERVING, err - } - return resp.GetState(), nil + func(ctx context.Context, hostAddress string) (*historyservice.DeepHealthCheckResponse, error) { + return args.HistoryClient.DeepHealthCheck(ctx, &historyservice.DeepHealthCheckRequest{HostAddress: hostAddress}) }, args.Logger, ) return &AdminHandler{ - logger: args.Logger, - status: common.DaemonStatusInitialized, - numberOfHistoryShards: args.PersistenceConfig.NumHistoryShards, - config: args.Config, - namespaceDLQHandler: nsreplication.NewDLQMessageHandler( - namespaceReplicationTaskExecutor, - args.NamespaceReplicationQueue, - args.Logger, - ), + logger: args.Logger, + status: common.DaemonStatusInitialized, + numberOfHistoryShards: args.PersistenceConfig.NumHistoryShards, + config: args.Config, + namespaceDLQHandler: namespaceDLQHandler, eventSerializer: args.EventSerializer, visibilityMgr: args.visibilityMgr, persistenceExecutionName: args.PersistenceExecutionManager.GetName(), @@ -232,6 +225,7 @@ func NewAdminHandler( taskCategoryRegistry: args.CategoryRegistry, matchingClient: args.matchingClient, chasmRegistry: args.ChasmRegistry, + schedulerClient: args.SchedulerClient, } } @@ -242,7 +236,7 @@ func (adh *AdminHandler) Start() { common.DaemonStatusInitialized, common.DaemonStatusStarted, ) { - adh.healthServer.SetServingStatus(AdminServiceName, healthpb.HealthCheckResponse_SERVING) + adh.healthServer.SetServingStatus(AdminServiceName, grpchealthspb.HealthCheckResponse_SERVING) } } @@ -253,7 +247,7 @@ func (adh *AdminHandler) Stop() { common.DaemonStatusStarted, common.DaemonStatusStopped, ) { - adh.healthServer.SetServingStatus(AdminServiceName, healthpb.HealthCheckResponse_NOT_SERVING) + adh.healthServer.SetServingStatus(AdminServiceName, grpchealthspb.HealthCheckResponse_NOT_SERVING) } } @@ -263,11 +257,20 @@ func (adh *AdminHandler) DeepHealthCheck( ) (_ *adminservice.DeepHealthCheckResponse, retError error) { defer log.CapturePanic(adh.logger, &retError) - healthStatus, err := adh.historyHealthChecker.Check(ctx) + result, err := adh.historyHealthChecker.Check(ctx) if err != nil { return nil, err } - return &adminservice.DeepHealthCheckResponse{State: healthStatus}, nil + + var services []*healthspb.ServiceHealthDetail + if result.ServiceDetail != nil { + services = append(services, result.ServiceDetail) + } + + return &adminservice.DeepHealthCheckResponse{ + State: result.State, + Services: services, + }, nil } // AddSearchAttributes add search attribute to the cluster. @@ -780,7 +783,7 @@ func (adh *AdminHandler) DescribeMutableState(ctx context.Context, request *admi return nil, err } - archetypeID, err := adh.archetypeNameToID(request.GetArchetype()) + archetypeID, err := adh.validateAndResolveArchetypeID(request.GetArchetype(), request.GetArchetypeId()) if err != nil { return nil, err } @@ -1557,7 +1560,7 @@ func (adh *AdminHandler) RefreshWorkflowTasks( return nil, err } - archetypeID, err := adh.archetypeNameToID(request.GetArchetype()) + archetypeID, err := adh.validateAndResolveArchetypeID(request.GetArchetype(), request.GetArchetypeId()) if err != nil { return nil, err } @@ -1845,7 +1848,7 @@ func (adh *AdminHandler) DeleteWorkflowExecution( return nil, err } - archetypeID, err := adh.archetypeNameToID(request.GetArchetype()) + archetypeID, err := adh.validateAndResolveArchetypeID(request.GetArchetype(), request.GetArchetypeId()) if err != nil { return nil, err } @@ -2310,7 +2313,7 @@ func (adh *AdminHandler) GenerateLastHistoryReplicationTasks( return nil, err } - archetypeID, err := adh.archetypeNameToID(request.GetArchetype()) + archetypeID, err := adh.validateAndResolveArchetypeID(request.GetArchetype(), request.GetArchetypeId()) if err != nil { return nil, err } @@ -2346,7 +2349,35 @@ func (adh *AdminHandler) getDLQWorkflowID( ) } -func (adh *AdminHandler) archetypeNameToID(archetype chasm.Archetype) (chasm.ArchetypeID, error) { +// validateAndResolveArchetypeID validates the archetype and archetypeID fields and returns the resolved archetype ID. +// It performs the following checks: +// 1. If archetypeID is specified (non-zero), validates it's registered in the chasm registry +// 2. If both archetypeID and archetype are specified, validates they match each other +// 3. If only archetype is specified, converts it to an ID +func (adh *AdminHandler) validateAndResolveArchetypeID( + archetype chasm.Archetype, + archetypeID uint32, +) (chasm.ArchetypeID, error) { + // If archetypeID is specified, use it + if archetypeID != chasm.UnspecifiedArchetypeID { + // Validate that the archetypeID is registered in the chasm registry + archetypeFqn, ok := adh.chasmRegistry.ComponentFqnByID(archetypeID) + if !ok { + return chasm.UnspecifiedArchetypeID, serviceerror.NewInvalidArgumentf( + "unknown archetype ID: %d", archetypeID) + } + + // If both archetype and archetypeID are specified, validate they match + if len(archetype) > 0 && archetype != archetypeFqn { + return chasm.UnspecifiedArchetypeID, serviceerror.NewInvalidArgumentf( + "archetype mismatch: archetypeID (%d) does not match archetype name (%s), registered name %s", + archetypeID, archetype, archetypeFqn) + } + + return chasm.ArchetypeID(archetypeID), nil + } + + // If only archetype is specified, convert it to an ID if len(archetype) == 0 { // For backwards compatibility, default to Workflow return chasm.WorkflowArchetypeID, nil @@ -2399,3 +2430,80 @@ func convertFailoverHistoryToReplicationProto( return replicationProto } + +func (adh *AdminHandler) MigrateSchedule(ctx context.Context, request *adminservice.MigrateScheduleRequest) (_ *adminservice.MigrateScheduleResponse, retErr error) { + defer log.CapturePanic(adh.logger, &retErr) + if request == nil { + return nil, errRequestNotSet + } + if request.GetNamespace() == "" { + return nil, errNamespaceNotSet + } + if request.GetScheduleId() == "" { + return nil, errScheduleIDNotSet + } + if request.GetTarget() == adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_UNSPECIFIED { + return nil, errMigrationTargetNotSet + } + if request.GetIdentity() == "" { + return nil, errIdentityNotSet + } + + namespaceID, err := adh.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace())) + if err != nil { + return nil, err + } + + switch request.GetTarget() { + case adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_CHASM: + return adh.migrateScheduleToChasm(ctx, request, namespaceID.String()) + case adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW: + return adh.migrateScheduleToWorkflow(ctx, request, namespaceID.String()) + default: + return nil, serviceerror.NewInvalidArgumentf("unknown migration target: %v", request.GetTarget()) + } +} + +func (adh *AdminHandler) migrateScheduleToChasm( + ctx context.Context, + request *adminservice.MigrateScheduleRequest, + namespaceID string, +) (*adminservice.MigrateScheduleResponse, error) { + workflowID := scheduler.WorkflowIDPrefix + request.GetScheduleId() + + _, err := adh.historyClient.SignalWorkflowExecution(ctx, + &historyservice.SignalWorkflowExecutionRequest{ + NamespaceId: namespaceID, + SignalRequest: &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: request.Namespace, + WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, + SignalName: scheduler.SignalNameMigrateToChasm, + Identity: request.Identity, + RequestId: request.RequestId, + }, + }, + ) + if err != nil { + return nil, err + } + return &adminservice.MigrateScheduleResponse{}, nil +} + +func (adh *AdminHandler) migrateScheduleToWorkflow( + ctx context.Context, + request *adminservice.MigrateScheduleRequest, + namespaceID string, +) (*adminservice.MigrateScheduleResponse, error) { + _, err := adh.schedulerClient.MigrateToWorkflow(ctx, + &schedulerpb.MigrateToWorkflowRequest{ + NamespaceId: namespaceID, + ScheduleId: request.GetScheduleId(), + Identity: request.GetIdentity(), + RequestId: request.GetRequestId(), + }, + ) + if err != nil { + return nil, err + } + return &adminservice.MigrateScheduleResponse{}, nil +} diff --git a/service/frontend/admin_handler_test.go b/service/frontend/admin_handler_test.go index 3b34d73afca..568b8dec1c8 100644 --- a/service/frontend/admin_handler_test.go +++ b/service/frontend/admin_handler_test.go @@ -32,6 +32,7 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/chasm" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" clientmocks "go.temporal.io/server/client" historyclient "go.temporal.io/server/client/history" @@ -41,6 +42,7 @@ import ( "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/membership" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/namespace/nsreplication" "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/persistence/serialization" "go.temporal.io/server/common/persistence/visibility/manager" @@ -180,13 +182,23 @@ func (s *adminHandlerSuite) SetupTest() { serialization.NewSerializer(), clock.NewRealTimeSource(), chasmRegistry, + nsreplication.NewNoopDataMerger(), + nil, // schedulerClient - not needed for most admin handler tests tasks.NewDefaultTaskCategoryRegistry(), s.mockResource.GetMatchingClient(), } s.mockMetadata.EXPECT().GetCurrentClusterName().Return(uuid.NewString()).AnyTimes() s.mockExecutionMgr.EXPECT().GetName().Return("mock-execution-manager").AnyTimes() s.mockVisibilityMgr.EXPECT().GetStoreNames().Return([]string{"mock-vis-store"}) - s.handler = NewAdminHandler(args) + + namespaceDLQHandler := NamespaceDLQHandlerProvider( + s.mockMetadata, + s.mockResource.GetMetadataManager(), + nsreplication.NewNoopDataMerger(), + s.mockResource.GetNamespaceReplicationQueue(), + s.mockResource.GetLogger(), + ) + s.handler = NewAdminHandler(args, namespaceDLQHandler) s.handler.Start() } @@ -1462,7 +1474,7 @@ func (s *adminHandlerSuite) TestDescribeDLQJob() { dlq.QueryTypeProgress, ) mockValue := mocksdk.NewMockEncodedValue(s.controller) - mockValue.EXPECT().Get(gomock.Any()).Do(func(result interface{}) { + mockValue.EXPECT().Get(gomock.Any()).Do(func(result any) { *(result.(*dlq.ProgressQueryResponse)) = tc.progressQueryResponse }) queryExpectation.Return(mockValue, nil) @@ -2094,3 +2106,86 @@ func (s *adminHandlerSuite) validatePhysicalTaskQueueInfo(expectedPhysicalTaskQu s.Equal(expectedPhysicalTaskQueueInfo.GetTaskQueueStats(), responsePhysicalTaskQueueInfo.GetTaskQueueStats()) s.Equal(expectedPhysicalTaskQueueInfo.GetInternalTaskQueueStatus(), responsePhysicalTaskQueueInfo.GetInternalTaskQueueStatus()) } + +// fakeSchedulerClient is a minimal test double for SchedulerServiceClient. +// Only MigrateToWorkflow is implemented; other methods panic if called. +type fakeSchedulerClient struct { + migrateToWorkflowFn func(context.Context, *schedulerpb.MigrateToWorkflowRequest) (*schedulerpb.MigrateToWorkflowResponse, error) +} + +func (f *fakeSchedulerClient) CreateSchedule(context.Context, *schedulerpb.CreateScheduleRequest, ...grpc.CallOption) (*schedulerpb.CreateScheduleResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) UpdateSchedule(context.Context, *schedulerpb.UpdateScheduleRequest, ...grpc.CallOption) (*schedulerpb.UpdateScheduleResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) PatchSchedule(context.Context, *schedulerpb.PatchScheduleRequest, ...grpc.CallOption) (*schedulerpb.PatchScheduleResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) DeleteSchedule(context.Context, *schedulerpb.DeleteScheduleRequest, ...grpc.CallOption) (*schedulerpb.DeleteScheduleResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) DescribeSchedule(context.Context, *schedulerpb.DescribeScheduleRequest, ...grpc.CallOption) (*schedulerpb.DescribeScheduleResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) ListScheduleMatchingTimes(context.Context, *schedulerpb.ListScheduleMatchingTimesRequest, ...grpc.CallOption) (*schedulerpb.ListScheduleMatchingTimesResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) CreateFromMigrationState(_ context.Context, _ *schedulerpb.CreateFromMigrationStateRequest, _ ...grpc.CallOption) (*schedulerpb.CreateFromMigrationStateResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) CreateSentinel(context.Context, *schedulerpb.CreateSentinelRequest, ...grpc.CallOption) (*schedulerpb.CreateSentinelResponse, error) { + panic("not implemented") +} +func (f *fakeSchedulerClient) MigrateToWorkflow(ctx context.Context, req *schedulerpb.MigrateToWorkflowRequest, _ ...grpc.CallOption) (*schedulerpb.MigrateToWorkflowResponse, error) { + return f.migrateToWorkflowFn(ctx, req) +} + +func (s *adminHandlerSuite) TestMigrateScheduleToWorkflow() { + s.mockNamespaceCache.EXPECT().GetNamespaceID(s.namespace).Return(s.namespaceID, nil) + + var capturedReq *schedulerpb.MigrateToWorkflowRequest + fake := &fakeSchedulerClient{ + migrateToWorkflowFn: func(_ context.Context, req *schedulerpb.MigrateToWorkflowRequest) (*schedulerpb.MigrateToWorkflowResponse, error) { + capturedReq = req + return &schedulerpb.MigrateToWorkflowResponse{}, nil + }, + } + s.handler.schedulerClient = fake + + resp, err := s.handler.MigrateSchedule(context.Background(), &adminservice.MigrateScheduleRequest{ + Namespace: s.namespace.String(), + ScheduleId: "test-schedule", + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW, + Identity: "test-identity", + RequestId: "test-request-id", + }) + s.NoError(err) + s.NotNil(resp) + s.Equal(s.namespaceID.String(), capturedReq.NamespaceId) + s.Equal("test-schedule", capturedReq.ScheduleId) + s.Equal("test-identity", capturedReq.Identity) + s.Equal("test-request-id", capturedReq.RequestId) +} + +func (s *adminHandlerSuite) TestMigrateScheduleToWorkflowError() { + s.mockNamespaceCache.EXPECT().GetNamespaceID(s.namespace).Return(s.namespaceID, nil) + + fake := &fakeSchedulerClient{ + migrateToWorkflowFn: func(_ context.Context, _ *schedulerpb.MigrateToWorkflowRequest) (*schedulerpb.MigrateToWorkflowResponse, error) { + return nil, serviceerror.NewNotFound("schedule not found") + }, + } + s.handler.schedulerClient = fake + + _, err := s.handler.MigrateSchedule(context.Background(), &adminservice.MigrateScheduleRequest{ + Namespace: s.namespace.String(), + ScheduleId: "nonexistent", + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW, + Identity: "test-identity", + RequestId: "test-request-id", + }) + s.Error(err) + var notFoundErr *serviceerror.NotFound + s.ErrorAs(err, ¬FoundErr) +} diff --git a/service/frontend/configs/quotas.go b/service/frontend/configs/quotas.go index af667883e44..bef6821cbcc 100644 --- a/service/frontend/configs/quotas.go +++ b/service/frontend/configs/quotas.go @@ -62,6 +62,15 @@ var ( DispatchNexusTaskByEndpointAPIName: 1, } + // PollTaskAPISet is the set of API methods for which NamespaceRateLimitInterceptor will + // block waiting for a token (rather than rejecting immediately) when + // FrontendPollWaitForNamespaceRateLimitToken is enabled. + PollTaskAPISet = map[string]struct{}{ + "/temporal.api.workflowservice.v1.WorkflowService/PollActivityTaskQueue": {}, + "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowTaskQueue": {}, + "/temporal.api.workflowservice.v1.WorkflowService/PollNexusTaskQueue": {}, + } + // APIToPriority determines common API priorities. // If APIs rely on visibility, they should be added to VisibilityAPIToPriority. // If APIs result in replication in namespace replication queue, they belong to NamespaceReplicationInducingAPIToPriority @@ -98,55 +107,59 @@ var ( CompleteNexusOperation: 1, // P2: Change State APIs - "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelWorkflowExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/TerminateWorkflowExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/ResetWorkflowExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkflowExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/GetWorkflowExecutionHistory": 2, // relatively high priority because it is required for replay - "/temporal.api.workflowservice.v1.WorkflowService/UpdateSchedule": 2, - "/temporal.api.workflowservice.v1.WorkflowService/PatchSchedule": 2, - "/temporal.api.workflowservice.v1.WorkflowService/DeleteSchedule": 2, - "/temporal.api.workflowservice.v1.WorkflowService/StopBatchOperation": 2, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateActivityOptions": 2, - "/temporal.api.workflowservice.v1.WorkflowService/PauseActivity": 2, - "/temporal.api.workflowservice.v1.WorkflowService/UnpauseActivity": 2, - "/temporal.api.workflowservice.v1.WorkflowService/ResetActivity": 2, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkflowExecutionOptions": 2, - "/temporal.api.workflowservice.v1.WorkflowService/SetCurrentDeployment": 2, // [cleanup-wv-pre-release] - "/temporal.api.workflowservice.v1.WorkflowService/SetCurrentDeploymentVersion": 2, // [cleanup-wv-pre-release] - "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentCurrentVersion": 2, - "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentRampingVersion": 2, - "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentManager": 2, - "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkerDeployment": 2, - "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkerDeploymentVersion": 2, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerDeploymentVersionMetadata": 2, - "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkflowRule": 2, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkflowRule": 2, - "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkflowRule": 2, - "/temporal.api.workflowservice.v1.WorkflowService/ListWorkflowRules": 2, - "/temporal.api.workflowservice.v1.WorkflowService/TriggerWorkflowRule": 2, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateTaskQueueConfig": 2, - "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelActivityExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/TerminateActivityExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/DeleteActivityExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/PauseWorkflowExecution": 2, - "/temporal.api.workflowservice.v1.WorkflowService/UnpauseWorkflowExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelWorkflowExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/TerminateWorkflowExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/ResetWorkflowExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkflowExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/GetWorkflowExecutionHistory": 2, // relatively high priority because it is required for replay + "/temporal.api.workflowservice.v1.WorkflowService/UpdateSchedule": 2, + "/temporal.api.workflowservice.v1.WorkflowService/PatchSchedule": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteSchedule": 2, + "/temporal.api.workflowservice.v1.WorkflowService/StopBatchOperation": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateActivityOptions": 2, + "/temporal.api.workflowservice.v1.WorkflowService/PauseActivity": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UnpauseActivity": 2, + "/temporal.api.workflowservice.v1.WorkflowService/ResetActivity": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkflowExecutionOptions": 2, + "/temporal.api.workflowservice.v1.WorkflowService/SetCurrentDeployment": 2, // [cleanup-wv-pre-release] + "/temporal.api.workflowservice.v1.WorkflowService/SetCurrentDeploymentVersion": 2, // [cleanup-wv-pre-release] + "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkerDeployment": 2, + "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkerDeploymentVersion": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerDeploymentVersionComputeConfig": 2, + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentCurrentVersion": 2, + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentRampingVersion": 2, + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentManager": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkerDeployment": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkerDeploymentVersion": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerDeploymentVersionMetadata": 2, + "/temporal.api.workflowservice.v1.WorkflowService/CreateWorkflowRule": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkflowRule": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkflowRule": 2, + "/temporal.api.workflowservice.v1.WorkflowService/ListWorkflowRules": 2, + "/temporal.api.workflowservice.v1.WorkflowService/TriggerWorkflowRule": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateTaskQueueConfig": 2, + "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelActivityExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/TerminateActivityExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteActivityExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/PauseWorkflowExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UnpauseWorkflowExecution": 2, // P3: Status Querying APIs - "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkflowExecution": 3, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeActivityExecution": 3, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeTaskQueue": 3, - "/temporal.api.workflowservice.v1.WorkflowService/GetWorkerBuildIdCompatibility": 3, - "/temporal.api.workflowservice.v1.WorkflowService/GetWorkerVersioningRules": 3, - "/temporal.api.workflowservice.v1.WorkflowService/ListTaskQueuePartitions": 3, - "/temporal.api.workflowservice.v1.WorkflowService/QueryWorkflow": 3, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeSchedule": 3, - "/temporal.api.workflowservice.v1.WorkflowService/ListScheduleMatchingTimes": 3, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeBatchOperation": 3, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeDeployment": 3, // [cleanup-wv-pre-release] - "/temporal.api.workflowservice.v1.WorkflowService/GetCurrentDeployment": 3, // [cleanup-wv-pre-release] - "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkerDeploymentVersion": 3, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkerDeployment": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkflowExecution": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeActivityExecution": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeTaskQueue": 3, + "/temporal.api.workflowservice.v1.WorkflowService/GetWorkerBuildIdCompatibility": 3, + "/temporal.api.workflowservice.v1.WorkflowService/GetWorkerVersioningRules": 3, + "/temporal.api.workflowservice.v1.WorkflowService/ListTaskQueuePartitions": 3, + "/temporal.api.workflowservice.v1.WorkflowService/QueryWorkflow": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeSchedule": 3, + "/temporal.api.workflowservice.v1.WorkflowService/ListScheduleMatchingTimes": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeBatchOperation": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeDeployment": 3, // [cleanup-wv-pre-release] + "/temporal.api.workflowservice.v1.WorkflowService/GetCurrentDeployment": 3, // [cleanup-wv-pre-release] + "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkerDeploymentVersion": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkerDeployment": 3, + "/temporal.api.workflowservice.v1.WorkflowService/ValidateWorkerDeploymentVersionComputeConfig": 3, // P3: Progress APIs for reporting cancellations and failures. // They are relatively low priority as the tasks need to be retried anyway. @@ -216,6 +229,12 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/UpdateNamespace": 1, "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerBuildIdCompatibility": 2, "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerVersioningRules": 2, + + // Anything that changes task queue user data also creates replication tasks. + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentCurrentVersion": 2, + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentRampingVersion": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkerDeploymentVersion": 2, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateTaskQueueConfig": 2, } NamespaceReplicationInducingAPIPrioritiesOrdered = []int{0, 1, 2} diff --git a/service/frontend/configs/quotas_test.go b/service/frontend/configs/quotas_test.go index 8488ddbbff2..dcd5107d237 100644 --- a/service/frontend/configs/quotas_test.go +++ b/service/frontend/configs/quotas_test.go @@ -125,10 +125,14 @@ func (s *quotasSuite) TestVisibilityAPIs() { func (s *quotasSuite) TestNamespaceReplicationInducingAPIs() { apis := map[string]struct{}{ - "/temporal.api.workflowservice.v1.WorkflowService/RegisterNamespace": {}, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateNamespace": {}, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerBuildIdCompatibility": {}, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerVersioningRules": {}, + "/temporal.api.workflowservice.v1.WorkflowService/RegisterNamespace": {}, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateNamespace": {}, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerBuildIdCompatibility": {}, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkerVersioningRules": {}, + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentCurrentVersion": {}, + "/temporal.api.workflowservice.v1.WorkflowService/SetWorkerDeploymentRampingVersion": {}, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteWorkerDeploymentVersion": {}, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateTaskQueueConfig": {}, } var service workflowservice.WorkflowServiceServer @@ -202,7 +206,7 @@ func (s *quotasSuite) testOperatorPrioritized(limiter quotas.RequestRateLimiter, requestTime := time.Now() limitCount := 0 - for i := 0; i < 12; i++ { + for range 12 { if !limiter.Allow(requestTime, apiRequest) { limitCount++ s.True(limiter.Allow(requestTime, operatorRequest)) diff --git a/service/frontend/errors.go b/service/frontend/errors.go index 60a41f37cf0..c6bbbb6c118 100644 --- a/service/frontend/errors.go +++ b/service/frontend/errors.go @@ -54,6 +54,9 @@ var ( errCronNotAllowed = serviceerror.NewInvalidArgument("Scheduled workflow must not contain CronSchedule") errIDReusePolicyNotAllowed = serviceerror.NewInvalidArgument("Scheduled workflow must not contain WorkflowIDReusePolicy") errBatchJobIDNotSet = serviceerror.NewInvalidArgument("JobId is not set on request.") + errScheduleIDNotSet = serviceerror.NewInvalidArgument("ScheduleId is not set on request.") + errIdentityNotSet = serviceerror.NewInvalidArgument("Identity is not set on request.") + errMigrationTargetNotSet = serviceerror.NewInvalidArgument("Target is not set on request.") errNamespaceNotSet = serviceerror.NewInvalidArgument("Namespace is not set on request.") errReasonNotSet = serviceerror.NewInvalidArgument("Reason is not set on request.") errBatchOperationNotSet = serviceerror.NewInvalidArgument("Batch operation is not set on request.") diff --git a/service/frontend/fx.go b/service/frontend/fx.go index 41887e35719..e18524006f1 100644 --- a/service/frontend/fx.go +++ b/service/frontend/fx.go @@ -23,6 +23,7 @@ import ( "go.temporal.io/server/common/membership" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/namespace/nsreplication" "go.temporal.io/server/common/nexus" "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/persistence/serialization" @@ -96,11 +97,13 @@ var Module = fx.Options( fx.Provide(PersistenceRateLimitingParamsProvider), service.PersistenceLazyLoadedServiceResolverModule, fx.Provide(FEReplicatorNamespaceReplicationQueueProvider), + fx.Provide(nsreplication.NewNoopDataMerger), fx.Provide(AuthorizationInterceptorProvider), fx.Provide(NamespaceCheckerProvider), fx.Provide(func(so GrpcServerOptions) *grpc.Server { return grpc.NewServer(so.Options...) }), fx.Provide(HandlerProvider), fx.Provide(AdminHandlerProvider), + fx.Provide(NamespaceDLQHandlerProvider), fx.Provide(OperatorHandlerProvider), fx.Provide(NewVersionChecker), fx.Provide(ServiceResolverProvider), @@ -180,6 +183,7 @@ func AuthorizationInterceptorProvider( cfg.Global.Authorization.AuthExtraHeaderName, serviceConfig.ExposeAuthorizerErrors, dynamicconfig.EnableCrossNamespaceCommands.Get(dc), + dynamicconfig.EnablePrincipalPropagation.Get(dc), ) } @@ -256,6 +260,8 @@ func GrpcServerOptionsProvider( maskInternalErrorDetailsInterceptor.Intercept, interceptor.ServiceErrorInterceptor, interceptor.NewFrontendServiceErrorInterceptor(logger), + // BusinessID interceptor extracts business ID and adds it to context for use, must be before any interceptor that touches namespaces (namespaceValidator, handoverInterceptor) + businessIDInterceptor.Intercept, namespaceValidatorInterceptor.NamespaceValidateIntercept, namespaceLogInterceptor.Intercept, // TODO: Deprecate this with a outer custom interceptor metrics.NewServerMetricsContextInjectorInterceptor(), @@ -263,8 +269,6 @@ func GrpcServerOptionsProvider( // Handover interceptor has to above redirection because the request will route to the correct cluster after handover completed. // And retry cannot be performed before customInterceptors. namespaceHandoverInterceptor.Intercept, - // BusinessID interceptor extracts business ID and adds it to context for use by redirection policy - businessIDInterceptor.Intercept, redirectionInterceptor.Intercept, // Telemetry interceptor must be after redirection to ensure metrics are recorded in the correct cluster telemetryInterceptor.UnaryIntercept, @@ -479,6 +483,7 @@ func NamespaceRateLimitInterceptorProvider( serviceConfig *Config, namespaceRegistry namespace.Registry, frontendServiceResolver membership.ServiceResolver, + metricsHandler metrics.Handler, logger log.SnTaggedLogger, ) interceptor.NamespaceRateLimitInterceptor { var globalNamespaceRPS, globalNamespaceVisibilityRPS, globalNamespaceNamespaceReplicationInducingAPIsRPS dynamicconfig.IntPropertyFnWithNamespaceFilter @@ -531,7 +536,7 @@ func NamespaceRateLimitInterceptorProvider( ) }, ) - return interceptor.NewNamespaceRateLimitInterceptor(namespaceRegistry, namespaceRateLimiter, map[string]int{}) + return interceptor.NewNamespaceRateLimitInterceptor(namespaceRegistry, namespaceRateLimiter, map[string]int{}, configs.PollTaskAPISet, serviceConfig.PollWaitForNamespaceRateLimitToken, metricsHandler) } func NamespaceCountLimitInterceptorProvider( @@ -686,6 +691,9 @@ func AdminHandlerProvider( taskCategoryRegistry tasks.TaskCategoryRegistry, matchingClient resource.MatchingClient, chasmRegistry *chasm.Registry, + namespaceDataMerger nsreplication.NamespaceDataMerger, + schedulerClient schedulerpb.SchedulerServiceClient, + namespaceDLQHandler nsreplication.DLQMessageHandler, ) *AdminHandler { args := NewAdminHandlerArgs{ persistenceConfig, @@ -715,10 +723,33 @@ func AdminHandlerProvider( eventSerializer, timeSource, chasmRegistry, + namespaceDataMerger, + schedulerClient, taskCategoryRegistry, matchingClient, } - return NewAdminHandler(args) + return NewAdminHandler(args, namespaceDLQHandler) +} + +// NamespaceDLQHandlerProvider provides the default namespace DLQ message handler. +func NamespaceDLQHandlerProvider( + clusterMetadata cluster.Metadata, + persistenceMetadataManager persistence.MetadataManager, + namespaceDataMerger nsreplication.NamespaceDataMerger, + namespaceReplicationQueue persistence.NamespaceReplicationQueue, + logger log.SnTaggedLogger, +) nsreplication.DLQMessageHandler { + taskExecutor := nsreplication.NewTaskExecutor( + clusterMetadata.GetCurrentClusterName(), + persistenceMetadataManager, + namespaceDataMerger, + logger, + ) + return nsreplication.NewDLQMessageHandler( + taskExecutor, + namespaceReplicationQueue, + logger, + ) } func OperatorHandlerProvider( diff --git a/service/frontend/fx_test.go b/service/frontend/fx_test.go index 2c8d48d841c..566565350b7 100644 --- a/service/frontend/fx_test.go +++ b/service/frontend/fx_test.go @@ -575,6 +575,7 @@ func TestNamespaceRateLimitInterceptorProvider(t *testing.T) { &config, mockRegistry, serviceResolver, + metrics.NoopMetricsHandler, log.NewTestLogger(), ) @@ -707,6 +708,7 @@ func getTestConfig(tc namespaceRateLimitInterceptorTestCase) Config { MaxNamespaceNamespaceReplicationInducingAPIsBurstRatioPerInstance: func(namespace string) float64 { return getOrDefaultLimit(tc.maxNamespaceNamespaceReplicationInducingAPIsBurstRatioPerInstance) }, + PollWaitForNamespaceRateLimitToken: func(_ string) bool { return false }, } } diff --git a/service/frontend/health_check.go b/service/frontend/health_check.go index 2610555dd54..eb2652109f6 100644 --- a/service/frontend/health_check.go +++ b/service/frontend/health_check.go @@ -2,10 +2,14 @@ package frontend import ( "context" + "fmt" "math" enumsspb "go.temporal.io/server/api/enums/v1" + healthspb "go.temporal.io/server/api/health/v1" + "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/health" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/membership" @@ -13,8 +17,13 @@ import ( ) type ( + HealthCheckResult struct { + State enumsspb.HealthState + ServiceDetail *healthspb.ServiceHealthDetail + } + HealthChecker interface { - Check(ctx context.Context) (enumsspb.HealthState, error) + Check(ctx context.Context) (HealthCheckResult, error) } healthCheckerImpl struct { @@ -22,9 +31,14 @@ type ( membershipMonitor membership.Monitor hostFailurePercentage dynamicconfig.FloatPropertyFn hostDeclinedServingProportion dynamicconfig.FloatPropertyFn - healthCheckFn func(ctx context.Context, hostAddress string) (enumsspb.HealthState, error) + healthCheckFn func(ctx context.Context, hostAddress string) (*historyservice.DeepHealthCheckResponse, error) logger log.Logger } + + hostResult struct { + address string + response *historyservice.DeepHealthCheckResponse + } ) func NewHealthChecker( @@ -32,7 +46,7 @@ func NewHealthChecker( membershipMonitor membership.Monitor, hostFailurePercentage dynamicconfig.FloatPropertyFn, hostDeclinedServingProportion dynamicconfig.FloatPropertyFn, - healthCheckFn func(ctx context.Context, hostAddress string) (enumsspb.HealthState, error), + healthCheckFn func(ctx context.Context, hostAddress string) (*historyservice.DeepHealthCheckResponse, error), logger log.Logger, ) HealthChecker { return &healthCheckerImpl{ @@ -45,42 +59,77 @@ func NewHealthChecker( } } -func (h *healthCheckerImpl) Check(ctx context.Context) (enumsspb.HealthState, error) { +func (h *healthCheckerImpl) Check(ctx context.Context) (HealthCheckResult, error) { resolver, err := h.membershipMonitor.GetResolver(h.serviceName) if err != nil { - return enumsspb.HEALTH_STATE_UNSPECIFIED, err + return HealthCheckResult{ + State: enumsspb.HEALTH_STATE_INTERNAL_ERROR, + ServiceDetail: &healthspb.ServiceHealthDetail{ + Service: string(h.serviceName), + State: enumsspb.HEALTH_STATE_INTERNAL_ERROR, + Message: fmt.Sprintf("failed to get membership resolver: %v", err), + }, + }, err } hosts := resolver.AvailableMembers() if len(hosts) == 0 { - return enumsspb.HEALTH_STATE_NOT_SERVING, nil + return HealthCheckResult{ + State: enumsspb.HEALTH_STATE_NOT_SERVING, + ServiceDetail: &healthspb.ServiceHealthDetail{ + Service: string(h.serviceName), + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Message: "no available hosts in membership", + }, + }, nil } - receiveCh := make(chan enumsspb.HealthState, len(hosts)) + receiveCh := make(chan hostResult, len(hosts)) for _, host := range hosts { go func(hostAddress string) { - resp, err := h.healthCheckFn( - ctx, - hostAddress, - ) + resp, err := h.checkHost(ctx, hostAddress) if err != nil { - h.logger.Warn("failed to ping deep health check", tag.Error(err), tag.ServerName(string(h.serviceName))) + resp = &historyservice.DeepHealthCheckResponse{ + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Checks: []*healthspb.HealthCheck{ + { + CheckType: health.CheckTypeHostAvailability, + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Message: fmt.Sprintf("failed to reach host for health check: %v", err), + }, + }, + } } - receiveCh <- resp + receiveCh <- hostResult{address: hostAddress, response: resp} }(host.GetAddress()) } var failedHostCount float64 var hostDeclinedServingCount float64 - for i := 0; i < len(hosts); i++ { - healthState := <-receiveCh - switch healthState { - case enumsspb.HEALTH_STATE_NOT_SERVING, enumsspb.HEALTH_STATE_UNSPECIFIED: - failedHostCount++ - case enumsspb.HEALTH_STATE_DECLINED_SERVING: - hostDeclinedServingCount++ + var hostDetails []*healthspb.HostHealthDetail + var exampleFailedHost *healthspb.HostHealthDetail + for range hosts { + result := <-receiveCh + state := result.response.GetState() + + detail := &healthspb.HostHealthDetail{ + Address: result.address, + State: state, + Checks: result.response.GetChecks(), + } + hostDetails = append(hostDetails, detail) + + switch state { case enumsspb.HEALTH_STATE_SERVING: // Do nothing. + case enumsspb.HEALTH_STATE_DECLINED_SERVING: + hostDeclinedServingCount++ + default: + // NOT_SERVING, UNSPECIFIED, INTERNAL_ERROR, or any unknown state. + failedHostCount++ + if exampleFailedHost == nil { + exampleFailedHost = detail + } } } close(receiveCh) @@ -88,19 +137,69 @@ func (h *healthCheckerImpl) Check(ctx context.Context) (enumsspb.HealthState, er // Make sure that at lease 2 hosts must be not ready to trigger this check. proportionOfDeclinedServiceHosts := ensureMinimumProportionOfHosts(h.hostDeclinedServingProportion(), len(hosts)) + var overallState enumsspb.HealthState hostDeclinedServingProportion := hostDeclinedServingCount / float64(len(hosts)) if hostDeclinedServingProportion > proportionOfDeclinedServiceHosts { h.logger.Warn("health check exceeded host declined serving proportion threshold", tag.Float64("host declined serving proportion threshold", proportionOfDeclinedServiceHosts)) - return enumsspb.HEALTH_STATE_DECLINED_SERVING, nil + overallState = enumsspb.HEALTH_STATE_DECLINED_SERVING + } else { + failedHostCountProportion := failedHostCount / float64(len(hosts)) + if failedHostCountProportion+hostDeclinedServingProportion > h.hostFailurePercentage() { + h.logger.Warn("health check exceeded host failure percentage threshold", + tag.Float64("host failure percentage threshold", h.hostFailurePercentage()), + tag.Float64("host failure percentage", failedHostCountProportion), + tag.Float64("host declined serving percentage", hostDeclinedServingProportion), + tag.NewStringTag("example_failed_host", failedHostSummary(exampleFailedHost)), + ) + overallState = enumsspb.HEALTH_STATE_NOT_SERVING + } else { + overallState = enumsspb.HEALTH_STATE_SERVING + } } - failedHostCountProportion := failedHostCount / float64(len(hosts)) - if failedHostCountProportion+hostDeclinedServingProportion > h.hostFailurePercentage() { - h.logger.Warn("health check exceeded host failure percentage threshold", tag.Float64("host failure percentage threshold", h.hostFailurePercentage()), tag.Float64("host failure percentage", failedHostCountProportion), tag.Float64("host declined serving percentage", hostDeclinedServingProportion)) - return enumsspb.HEALTH_STATE_NOT_SERVING, nil + return HealthCheckResult{ + State: overallState, + ServiceDetail: &healthspb.ServiceHealthDetail{ + Service: string(h.serviceName), + State: overallState, + Hosts: hostDetails, + }, + }, nil +} + +func (h *healthCheckerImpl) checkHost(ctx context.Context, hostAddress string) (resp *historyservice.DeepHealthCheckResponse, retErr error) { + defer log.CapturePanic(h.logger, &retErr) + + resp, err := h.healthCheckFn(ctx, hostAddress) + if err != nil { + h.logger.Warn("failed to ping deep health check", tag.Error(err), tag.ServerName(string(h.serviceName))) + return nil, err } + if resp == nil { + resp = &historyservice.DeepHealthCheckResponse{ + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Checks: []*healthspb.HealthCheck{ + { + CheckType: health.CheckTypeHostAvailability, + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Message: "no response received from health check", + }, + }, + } + } + return resp, nil +} - return enumsspb.HEALTH_STATE_SERVING, nil +func failedHostSummary(host *healthspb.HostHealthDetail) string { + if host == nil { + return "unknown" + } + for _, check := range host.GetChecks() { + if check.GetState() != enumsspb.HEALTH_STATE_SERVING && check.GetMessage() != "" { + return fmt.Sprintf("%s: %s", host.GetAddress(), check.GetMessage()) + } + } + return fmt.Sprintf("%s: %s", host.GetAddress(), host.GetState().String()) } func ensureMinimumProportionOfHosts(proportionOfDeclinedServingHosts float64, totalHosts int) float64 { diff --git a/service/frontend/health_check_test.go b/service/frontend/health_check_test.go index 0d8ed38ef05..f52fd3d43e6 100644 --- a/service/frontend/health_check_test.go +++ b/service/frontend/health_check_test.go @@ -7,6 +7,9 @@ import ( "github.com/stretchr/testify/suite" enumsspb "go.temporal.io/server/api/enums/v1" + healthspb "go.temporal.io/server/api/health/v1" + "go.temporal.io/server/api/historyservice/v1" + "go.temporal.io/server/common/health" "go.temporal.io/server/common/log" "go.temporal.io/server/common/membership" "go.temporal.io/server/common/primitives" @@ -45,16 +48,16 @@ func (s *healthCheckerSuite) SetupTest() { func() float64 { return 0.15 }, - func(ctx context.Context, hostAddress string) (enumsspb.HealthState, error) { + func(ctx context.Context, hostAddress string) (*historyservice.DeepHealthCheckResponse, error) { switch hostAddress { case "1", "3": - return enumsspb.HEALTH_STATE_SERVING, nil + return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_SERVING}, nil case "2": - return enumsspb.HEALTH_STATE_UNSPECIFIED, errors.New("test") + return nil, errors.New("test") case "4": - return enumsspb.HEALTH_STATE_DECLINED_SERVING, nil + return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_DECLINED_SERVING}, nil default: - return enumsspb.HEALTH_STATE_NOT_SERVING, nil + return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_NOT_SERVING}, nil } }, log.NewNoopLogger(), @@ -78,9 +81,9 @@ func (s *healthCheckerSuite) Test_Check_Serving() { membership.NewHostInfoFromAddress("1"), }) - state, err := s.checker.Check(context.Background()) + result, err := s.checker.Check(context.Background()) s.NoError(err) - s.Equal(enumsspb.HEALTH_STATE_SERVING, state) + s.Equal(enumsspb.HEALTH_STATE_SERVING, result.State) } func (s *healthCheckerSuite) Test_Check_Not_Serving() { @@ -92,9 +95,9 @@ func (s *healthCheckerSuite) Test_Check_Not_Serving() { membership.NewHostInfoFromAddress("5"), }) - state, err := s.checker.Check(context.Background()) + result, err := s.checker.Check(context.Background()) s.NoError(err) - s.Equal(enumsspb.HEALTH_STATE_NOT_SERVING, state) + s.Equal(enumsspb.HEALTH_STATE_NOT_SERVING, result.State) } func (s *healthCheckerSuite) Test_Check_Declined_Serving() { @@ -108,17 +111,19 @@ func (s *healthCheckerSuite) Test_Check_Declined_Serving() { membership.NewHostInfoFromAddress("7"), }) - state, err := s.checker.Check(context.Background()) + result, err := s.checker.Check(context.Background()) s.NoError(err) - s.Equal(enumsspb.HEALTH_STATE_DECLINED_SERVING, state) + s.Equal(enumsspb.HEALTH_STATE_DECLINED_SERVING, result.State) } func (s *healthCheckerSuite) Test_Check_No_Available_Hosts() { s.resolver.EXPECT().AvailableMembers().Return([]membership.HostInfo{}) - state, err := s.checker.Check(context.Background()) + result, err := s.checker.Check(context.Background()) s.NoError(err) - s.Equal(enumsspb.HEALTH_STATE_NOT_SERVING, state) + s.Equal(enumsspb.HEALTH_STATE_NOT_SERVING, result.State) + s.NotNil(result.ServiceDetail) + s.Equal("no available hosts in membership", result.ServiceDetail.Message) } func (s *healthCheckerSuite) Test_Check_GetResolver_Error() { @@ -131,16 +136,19 @@ func (s *healthCheckerSuite) Test_Check_GetResolver_Error() { membershipMonitor, func() float64 { return 0.25 }, func() float64 { return 0.15 }, - func(ctx context.Context, hostAddress string) (enumsspb.HealthState, error) { - return enumsspb.HEALTH_STATE_SERVING, nil + func(ctx context.Context, hostAddress string) (*historyservice.DeepHealthCheckResponse, error) { + return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_SERVING}, nil }, log.NewNoopLogger(), ) - state, err := checker.Check(context.Background()) + result, err := checker.Check(context.Background()) s.Error(err) - s.Equal(enumsspb.HEALTH_STATE_UNSPECIFIED, state) + s.Equal(enumsspb.HEALTH_STATE_INTERNAL_ERROR, result.State) s.Contains(err.Error(), "resolver error") + s.NotNil(result.ServiceDetail) + s.Equal(enumsspb.HEALTH_STATE_INTERNAL_ERROR, result.ServiceDetail.State) + s.Contains(result.ServiceDetail.Message, "failed to get membership resolver") } func (s *healthCheckerSuite) Test_Check_Boundary_Failure_Percentage_Equals_Threshold() { @@ -148,14 +156,14 @@ func (s *healthCheckerSuite) Test_Check_Boundary_Failure_Percentage_Equals_Thres // With 4 hosts, 1 failed = 0.25 (25%), should return SERVING since it's not > threshold s.resolver.EXPECT().AvailableMembers().Return([]membership.HostInfo{ membership.NewHostInfoFromAddress("1"), // SERVING - membership.NewHostInfoFromAddress("2"), // UNSPECIFIED (failed) + membership.NewHostInfoFromAddress("2"), // NOT_SERVING (failed) membership.NewHostInfoFromAddress("3"), // SERVING membership.NewHostInfoFromAddress("1"), // SERVING }) - state, err := s.checker.Check(context.Background()) + result, err := s.checker.Check(context.Background()) s.NoError(err) - s.Equal(enumsspb.HEALTH_STATE_SERVING, state) + s.Equal(enumsspb.HEALTH_STATE_SERVING, result.State) } func (s *healthCheckerSuite) Test_Check_Single_Host_Scenarios() { @@ -171,7 +179,7 @@ func (s *healthCheckerSuite) Test_Check_Single_Host_Scenarios() { }, { name: "single host failed", - hostAddress: "2", // UNSPECIFIED (failed) + hostAddress: "2", // NOT_SERVING (failed) expectedState: enumsspb.HEALTH_STATE_NOT_SERVING, }, { @@ -192,9 +200,9 @@ func (s *healthCheckerSuite) Test_Check_Single_Host_Scenarios() { membership.NewHostInfoFromAddress(tc.hostAddress), }) - state, err := s.checker.Check(context.Background()) + result, err := s.checker.Check(context.Background()) s.NoError(err) - s.Equal(tc.expectedState, state) + s.Equal(tc.expectedState, result.State) }) } } @@ -215,20 +223,20 @@ func (s *healthCheckerSuite) Test_Check_Context_Cancellation() { s.membershipMonitor, func() float64 { return 0.25 }, func() float64 { return 0.15 }, - func(ctx context.Context, hostAddress string) (enumsspb.HealthState, error) { + func(ctx context.Context, hostAddress string) (*historyservice.DeepHealthCheckResponse, error) { select { case <-ctx.Done(): - return enumsspb.HEALTH_STATE_UNSPECIFIED, ctx.Err() + return nil, ctx.Err() default: - return enumsspb.HEALTH_STATE_SERVING, nil + return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_SERVING}, nil } }, log.NewNoopLogger(), ) - state, err := checker.Check(ctx) - s.NoError(err) // Context cancellation in individual health checks should not fail the overall check - s.Equal(enumsspb.HEALTH_STATE_NOT_SERVING, state) // All hosts will return UNSPECIFIED due to cancellation + result, err := checker.Check(ctx) + s.Require().NoError(err) // Context cancellation in individual health checks should not fail the overall check + s.Equal(enumsspb.HEALTH_STATE_NOT_SERVING, result.State) // All hosts will return NOT_SERVING due to cancellation } func (s *healthCheckerSuite) Test_Check_Mixed_Host_States_Edge_Cases() { @@ -278,13 +286,72 @@ func (s *healthCheckerSuite) Test_Check_Mixed_Host_States_Edge_Cases() { } s.resolver.EXPECT().AvailableMembers().Return(hostInfos) - state, err := s.checker.Check(context.Background()) + result, err := s.checker.Check(context.Background()) s.NoError(err, tc.description) - s.Equal(tc.expectedState, state, tc.description) + s.Equal(tc.expectedState, result.State, tc.description) }) } } +func (s *healthCheckerSuite) Test_Check_ServiceDetail_Populated() { + s.resolver.EXPECT().AvailableMembers().Return([]membership.HostInfo{ + membership.NewHostInfoFromAddress("1"), + membership.NewHostInfoFromAddress("2"), + }) + + result, err := s.checker.Check(context.Background()) + s.Require().NoError(err) + s.NotNil(result.ServiceDetail) + s.Equal("history", result.ServiceDetail.Service) + s.Len(result.ServiceDetail.Hosts, 2) +} + +func (s *healthCheckerSuite) Test_Check_HostChecks_Propagated() { + // Create a checker that returns checks in the response + membershipMonitor := membership.NewMockMonitor(s.controller) + resolver := membership.NewMockServiceResolver(s.controller) + membershipMonitor.EXPECT().GetResolver(gomock.Any()).Return(resolver, nil) + resolver.EXPECT().AvailableMembers().Return([]membership.HostInfo{ + membership.NewHostInfoFromAddress("host1"), + }) + + checks := []*healthspb.HealthCheck{ + { + CheckType: health.CheckTypeRPCLatency, + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Value: 850.0, + Threshold: 500.0, + Message: "RPC latency 850.00ms exceeded 500.00ms threshold", + }, + } + + checker := NewHealthChecker( + primitives.HistoryService, + membershipMonitor, + func() float64 { return 0.25 }, + func() float64 { return 0.15 }, + func(ctx context.Context, hostAddress string) (*historyservice.DeepHealthCheckResponse, error) { + return &historyservice.DeepHealthCheckResponse{ + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Checks: checks, + }, nil + }, + log.NewNoopLogger(), + ) + + result, err := checker.Check(context.Background()) + s.Require().NoError(err) + s.NotNil(result.ServiceDetail) + s.Require().Len(result.ServiceDetail.Hosts, 1) + host := result.ServiceDetail.Hosts[0] + s.Equal("host1", host.Address) + s.Equal(enumsspb.HEALTH_STATE_NOT_SERVING, host.State) + s.Require().Len(host.Checks, 1) + s.Equal(health.CheckTypeRPCLatency, host.Checks[0].CheckType) + s.InDelta(850.0, host.Checks[0].Value, 0.01) + s.InDelta(500.0, host.Checks[0].Threshold, 0.01) +} + func (s *healthCheckerSuite) Test_GetProportionOfNotReadyHosts() { testCases := []struct { name string diff --git a/service/frontend/http_api_server.go b/service/frontend/http_api_server.go index ae4ba70799c..88483f7c6a5 100644 --- a/service/frontend/http_api_server.go +++ b/service/frontend/http_api_server.go @@ -386,7 +386,7 @@ func newInlineClientConn( fullMethod := "/" + qualifiedServerName + "/" + reflectMethod.Name methods[fullMethod] = &serviceMethod{ info: grpc.UnaryServerInfo{Server: server, FullMethod: fullMethod}, - handler: func(ctx context.Context, req interface{}) (interface{}, error) { + handler: func(ctx context.Context, req any) (any, error) { ret := methodVal.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(req)}) err, _ := ret[1].Interface().(error) return ret[0].Interface(), err @@ -487,7 +487,7 @@ func chainUnaryServerInterceptors(interceptors []grpc.UnaryServerInterceptor) gr } func chainUnaryInterceptors(interceptors []grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return interceptors[0](ctx, req, info, getChainUnaryHandler(interceptors, 0, info, handler)) } } @@ -501,7 +501,7 @@ func getChainUnaryHandler( if curr == len(interceptors)-1 { return finalHandler } - return func(ctx context.Context, req interface{}) (interface{}, error) { + return func(ctx context.Context, req any) (any, error) { return interceptors[curr+1](ctx, req, info, getChainUnaryHandler(interceptors, curr+1, info, finalHandler)) } } diff --git a/service/frontend/namespace_handler.go b/service/frontend/namespace_handler.go index 604c69eaa71..b5a929ff149 100644 --- a/service/frontend/namespace_handler.go +++ b/service/frontend/namespace_handler.go @@ -254,6 +254,7 @@ func (d *namespaceHandler) RegisterNamespace( namespaceRequest.Namespace.FailoverVersion, namespaceRequest.IsGlobalNamespace, nil, + false, // forceReplicate ) if err != nil { return nil, err @@ -594,6 +595,7 @@ func (d *namespaceHandler) UpdateNamespace( failoverVersion, isGlobalNamespace, failoverHistory, + false, // forceReplicate ) if err != nil { return nil, err @@ -865,6 +867,7 @@ func (d *namespaceHandler) createResponse( WorkflowPause: d.config.WorkflowPauseEnabled(info.Name), StandaloneActivities: d.config.Activity.Enabled(info.Name), WorkerPollCompleteOnShutdown: d.config.EnableCancelWorkerPollsOnShutdown(info.Name), + PollerAutoscaling: true, }, Limits: &namespacepb.NamespaceInfo_Limits{ BlobSizeLimitError: int64(d.config.BlobSizeLimitError(info.Name)), diff --git a/service/frontend/namespace_handler_test.go b/service/frontend/namespace_handler_test.go index d586a1a95ab..590449dadde 100644 --- a/service/frontend/namespace_handler_test.go +++ b/service/frontend/namespace_handler_test.go @@ -387,6 +387,7 @@ func (s *namespaceHandlerCommonSuite) TestCapabilitiesAndLimits() { s.False(resp.NamespaceInfo.Capabilities.WorkflowPause) s.False(resp.NamespaceInfo.Capabilities.StandaloneActivities) s.False(resp.NamespaceInfo.Capabilities.WorkerPollCompleteOnShutdown) + s.True(resp.NamespaceInfo.Capabilities.PollerAutoscaling) s.Equal(int64(2*1024*1024), resp.NamespaceInfo.Limits.BlobSizeLimitError) s.Equal(int64(2*1024*1024), resp.NamespaceInfo.Limits.MemoSizeLimitError) diff --git a/service/frontend/nexus_handler.go b/service/frontend/nexus_handler.go index 48432e45fbe..d5062c077b5 100644 --- a/service/frontend/nexus_handler.go +++ b/service/frontend/nexus_handler.go @@ -158,7 +158,7 @@ func (c *operationContext) interceptRequest( request *matchingservice.DispatchNexusTaskRequest, header nexus.Header, ) error { - err := c.auth.Authorize(ctx, c.claims, &authorization.CallTarget{ + _, err := c.auth.Authorize(ctx, c.claims, &authorization.CallTarget{ APIName: c.apiName, Namespace: c.namespaceName, NexusEndpointName: c.endpointName, @@ -178,7 +178,7 @@ func (c *operationContext) interceptRequest( return commonnexus.ConvertGRPCError(err, false) } - if err := c.namespaceValidationInterceptor.ValidateState(c.namespace, c.apiName); err != nil { + if err := c.namespaceValidationInterceptor.ValidateState(c.namespace, c.apiName, ""); err != nil { c.metricsHandler = c.metricsHandler.WithTags(metrics.OutcomeTag("invalid_namespace_state")) return commonnexus.ConvertGRPCError(err, false) } diff --git a/service/frontend/nexus_handler_test.go b/service/frontend/nexus_handler_test.go index c26ca624b2c..7d0a703eebe 100644 --- a/service/frontend/nexus_handler_test.go +++ b/service/frontend/nexus_handler_test.go @@ -22,6 +22,7 @@ import ( "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/headers" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/metrics/metricstest" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/primitives/timestamp" @@ -128,6 +129,7 @@ func newOperationContext(options contextOptions) *operationContext { "", dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), // enablePrincipalPropagation ) oc.namespaceConcurrencyLimitInterceptor = interceptor.NewConcurrentRequestLimitInterceptor( nil, @@ -143,6 +145,9 @@ func newOperationContext(options contextOptions) *operationContext { nil, mockRateLimiter{options.namespaceRateLimitAllow}, make(map[string]int), + map[string]struct{}{}, + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), + metrics.NoopMetricsHandler, ) oc.rateLimitInterceptor = interceptor.NewRateLimitInterceptor( mockRateLimiter{options.rateLimitAllow}, diff --git a/service/frontend/nexus_http_handler.go b/service/frontend/nexus_http_handler.go index b3da5446a3e..265458e2928 100644 --- a/service/frontend/nexus_http_handler.go +++ b/service/frontend/nexus_http_handler.go @@ -15,7 +15,6 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/authorization" "go.temporal.io/server/common/cluster" - "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" @@ -44,7 +43,6 @@ type NexusHTTPHandler struct { namespaceRateLimitInterceptor interceptor.NamespaceRateLimitInterceptor namespaceConcurrencyLimitInterceptor *interceptor.ConcurrentRequestLimitInterceptor rateLimitInterceptor *interceptor.RateLimitInterceptor - enabled dynamicconfig.BoolPropertyFn } func NewNexusHTTPHandler( @@ -79,7 +77,6 @@ func NewNexusHTTPHandler( namespaceRateLimitInterceptor: namespaceRateLimitInterceptor, namespaceConcurrencyLimitInterceptor: namespaceConcurrencyLimitIntercptor, rateLimitInterceptor: rateLimitInterceptor, - enabled: serviceConfig.EnableNexusAPIs, preprocessErrorCounter: metricsHandler.Counter(metrics.NexusRequestPreProcessErrors.Name()).Record, nexusHandler: nexusrpc.NewHTTPHandler(nexusrpc.HandlerOptions{ Handler: &nexusHandler{ @@ -121,11 +118,6 @@ func (h *NexusHTTPHandler) writeFailure(writer http.ResponseWriter, r *http.Requ // Handler for [nexushttp.RouteSet.DispatchNexusTaskByNamespaceAndTaskQueue]. func (h *NexusHTTPHandler) dispatchNexusTaskByNamespaceAndTaskQueue(w http.ResponseWriter, r *http.Request) { - if !h.enabled() { - h.writeFailure(w, r, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "nexus endpoints disabled")) - return - } - var err error nc := h.baseNexusContext(configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName, r.Header) params := prepareRequest(commonnexus.RouteDispatchNexusTaskByNamespaceAndTaskQueue, w, r) @@ -166,11 +158,6 @@ func (h *NexusHTTPHandler) dispatchNexusTaskByNamespaceAndTaskQueue(w http.Respo // Handler for [nexushttp.RouteSet.DispatchNexusTaskByEndpoint]. func (h *NexusHTTPHandler) dispatchNexusTaskByEndpoint(w http.ResponseWriter, r *http.Request) { - if !h.enabled() { - h.writeFailure(w, r, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "nexus endpoints disabled")) - return - } - endpointIDEscaped := prepareRequest(commonnexus.RouteDispatchNexusTaskByEndpoint, w, r) endpointID, err := url.PathUnescape(endpointIDEscaped) @@ -188,14 +175,15 @@ func (h *NexusHTTPHandler) dispatchNexusTaskByEndpoint(w http.ResponseWriter, r } switch s.Code() { case codes.NotFound: - if r, ok := (err.(interface{ Retryable() bool })); ok { - if r.Retryable() { - w.Header().Set("nexus-request-retryable", "true") - } else { - w.Header().Set("nexus-request-retryable", "false") - } + retryBehavior := nexus.HandlerErrorRetryBehaviorNonRetryable + if r, ok := (err.(interface{ Retryable() bool })); ok && r.Retryable() { + retryBehavior = nexus.HandlerErrorRetryBehaviorRetryable } - h.writeFailure(w, r, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "nexus endpoint not found")) + h.writeFailure(w, r, &nexus.HandlerError{ + Type: nexus.HandlerErrorTypeNotFound, + Message: "nexus endpoint not found", + RetryBehavior: retryBehavior, + }) case codes.DeadlineExceeded: h.writeFailure(w, r, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeRequestTimeout, "request timed out")) default: @@ -253,8 +241,11 @@ func (h *NexusHTTPHandler) nexusContextFromEndpoint(entry *persistencespb.NexusE h.logger.Error("failed to get namespace name by ID", tag.Error(err)) var notFoundErr *serviceerror.NamespaceNotFound if errors.As(err, ¬FoundErr) { - w.Header().Set("nexus-request-retryable", "true") - h.writeFailure(w, r, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "invalid endpoint target")) + h.writeFailure(w, r, &nexus.HandlerError{ + Type: nexus.HandlerErrorTypeNotFound, + Message: "invalid endpoint target", + RetryBehavior: nexus.HandlerErrorRetryBehaviorRetryable, + }) } else { h.writeFailure(w, r, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeInternal, "internal error")) } diff --git a/service/frontend/nexus_http_handler_test.go b/service/frontend/nexus_http_handler_test.go new file mode 100644 index 00000000000..3c61a38b694 --- /dev/null +++ b/service/frontend/nexus_http_handler_test.go @@ -0,0 +1,151 @@ +package frontend + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/require" + "go.temporal.io/api/serviceerror" + persistencespb "go.temporal.io/server/api/persistence/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" + "go.temporal.io/server/common/nexus/nexusrpc" + "go.temporal.io/server/common/nexus/nexustest" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// retryableNotFoundError is a gRPC NotFound error that also implements Retryable() bool. +type retryableNotFoundError struct { + msg string +} + +func (e *retryableNotFoundError) Error() string { return e.msg } +func (e *retryableNotFoundError) Retryable() bool { return true } +func (e *retryableNotFoundError) GRPCStatus() *status.Status { + return status.New(codes.NotFound, e.msg) +} + +// fakeNamespaceRegistry implements namespace.Registry with just GetNamespaceName. +// All other methods panic. +type fakeNamespaceRegistry struct { + namespace.Registry + getNamespaceName func(id namespace.ID) (namespace.Name, error) +} + +func (f *fakeNamespaceRegistry) GetNamespaceName(id namespace.ID) (namespace.Name, error) { + return f.getNamespaceName(id) +} + +func newTestNexusHTTPHandler( + endpointRegistry commonnexus.EndpointRegistry, + namespaceRegistry namespace.Registry, +) (*NexusHTTPHandler, *mux.Router) { + logger := log.NewTestLogger() + h := &NexusHTTPHandler{ + base: nexusrpc.BaseHTTPHandler{ + Logger: log.NewSlogLogger(logger), + FailureConverter: nexusrpc.DefaultFailureConverter(), + }, + logger: logger, + enpointRegistry: endpointRegistry, + namespaceRegistry: namespaceRegistry, + preprocessErrorCounter: metrics.CounterFunc(func(int64, ...metrics.Tag) {}), + } + router := mux.NewRouter() + h.RegisterRoutes(router) + return h, router +} + +func doNexusHTTPRequest(t *testing.T, router *mux.Router, endpointID string) *httptest.ResponseRecorder { + t.Helper() + path := "/" + commonnexus.RouteDispatchNexusTaskByEndpoint.Path(endpointID) + "/test-service/test-operation" + req := httptest.NewRequest(http.MethodPost, path, nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + return rec +} + +func TestDispatchNexusTaskByEndpoint_NotFound_NonRetryable(t *testing.T) { + reg := nexustest.FakeEndpointRegistry{ + OnGetByID: func(_ context.Context, _ string) (*persistencespb.NexusEndpointEntry, error) { + return nil, serviceerror.NewNotFound("endpoint not found") + }, + } + _, router := newTestNexusHTTPHandler(reg, nil) + + rec := doNexusHTTPRequest(t, router, "test-endpoint-id") + + require.Equal(t, http.StatusNotFound, rec.Code) + require.Equal(t, "false", rec.Header().Get("nexus-request-retryable")) + + var failure nexus.Failure + require.NoError(t, json.NewDecoder(rec.Body).Decode(&failure)) + require.Equal(t, "nexus endpoint not found", failure.Message) +} + +func TestDispatchNexusTaskByEndpoint_NotFound_Retryable(t *testing.T) { + reg := nexustest.FakeEndpointRegistry{ + OnGetByID: func(_ context.Context, _ string) (*persistencespb.NexusEndpointEntry, error) { + return nil, &retryableNotFoundError{msg: "endpoint temporarily unavailable"} + }, + } + _, router := newTestNexusHTTPHandler(reg, nil) + + rec := doNexusHTTPRequest(t, router, "test-endpoint-id") + + require.Equal(t, http.StatusNotFound, rec.Code) + require.Equal(t, "true", rec.Header().Get("nexus-request-retryable")) + + var failure nexus.Failure + require.NoError(t, json.NewDecoder(rec.Body).Decode(&failure)) + require.Equal(t, "nexus endpoint not found", failure.Message) +} + +func TestDispatchNexusTaskByEndpoint_NamespaceNotFound_Retryable(t *testing.T) { + endpointEntry := &persistencespb.NexusEndpointEntry{ + Id: "test-endpoint-id", + Endpoint: &persistencespb.NexusEndpoint{ + Spec: &persistencespb.NexusEndpointSpec{ + Name: "test-endpoint", + Target: &persistencespb.NexusEndpointTarget{ + Variant: &persistencespb.NexusEndpointTarget_Worker_{ + Worker: &persistencespb.NexusEndpointTarget_Worker{ + NamespaceId: "test-ns-id", + TaskQueue: "test-task-queue", + }, + }, + }, + }, + }, + } + + reg := nexustest.FakeEndpointRegistry{ + OnGetByID: func(_ context.Context, _ string) (*persistencespb.NexusEndpointEntry, error) { + return endpointEntry, nil + }, + } + nsReg := &fakeNamespaceRegistry{ + getNamespaceName: func(id namespace.ID) (namespace.Name, error) { + return "", serviceerror.NewNamespaceNotFound("test-ns-id") + }, + } + + _, router := newTestNexusHTTPHandler(reg, nsReg) + + rec := doNexusHTTPRequest(t, router, "test-endpoint-id") + + require.Equal(t, http.StatusNotFound, rec.Code) + require.Equal(t, "true", rec.Header().Get("nexus-request-retryable")) + + var failure nexus.Failure + require.NoError(t, json.NewDecoder(rec.Body).Decode(&failure)) + require.Equal(t, "invalid endpoint target", failure.Message) +} diff --git a/service/frontend/operator_handler.go b/service/frontend/operator_handler.go index 45d72aa73fa..6df2a8348f3 100644 --- a/service/frontend/operator_handler.go +++ b/service/frontend/operator_handler.go @@ -37,10 +37,8 @@ import ( "go.temporal.io/server/service/worker/deletenamespace" "go.temporal.io/server/service/worker/deletenamespace/deleteexecutions" delnserrors "go.temporal.io/server/service/worker/deletenamespace/errors" - "google.golang.org/grpc/codes" "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/status" ) var _ OperatorHandler = (*OperatorHandlerImpl)(nil) @@ -775,9 +773,6 @@ func (h *OperatorHandlerImpl) CreateNexusEndpoint( request *operatorservice.CreateNexusEndpointRequest, ) (_ *operatorservice.CreateNexusEndpointResponse, retErr error) { defer log.CapturePanic(h.logger, &retErr) - if !h.config.EnableNexusAPIs() { - return nil, status.Error(codes.NotFound, "Nexus APIs are disabled") - } return h.nexusEndpointClient.Create(ctx, request) } @@ -786,9 +781,6 @@ func (h *OperatorHandlerImpl) UpdateNexusEndpoint( request *operatorservice.UpdateNexusEndpointRequest, ) (_ *operatorservice.UpdateNexusEndpointResponse, retErr error) { defer log.CapturePanic(h.logger, &retErr) - if !h.config.EnableNexusAPIs() { - return nil, status.Error(codes.NotFound, "Nexus APIs are disabled") - } return h.nexusEndpointClient.Update(ctx, request) } @@ -797,9 +789,6 @@ func (h *OperatorHandlerImpl) DeleteNexusEndpoint( request *operatorservice.DeleteNexusEndpointRequest, ) (_ *operatorservice.DeleteNexusEndpointResponse, retErr error) { defer log.CapturePanic(h.logger, &retErr) - if !h.config.EnableNexusAPIs() { - return nil, status.Error(codes.NotFound, "Nexus APIs are disabled") - } return h.nexusEndpointClient.Delete(ctx, request) } @@ -808,9 +797,6 @@ func (h *OperatorHandlerImpl) GetNexusEndpoint( request *operatorservice.GetNexusEndpointRequest, ) (_ *operatorservice.GetNexusEndpointResponse, retErr error) { defer log.CapturePanic(h.logger, &retErr) - if !h.config.EnableNexusAPIs() { - return nil, status.Error(codes.NotFound, "Nexus APIs are disabled") - } return h.nexusEndpointClient.Get(ctx, request) } @@ -819,8 +805,5 @@ func (h *OperatorHandlerImpl) ListNexusEndpoints( request *operatorservice.ListNexusEndpointsRequest, ) (_ *operatorservice.ListNexusEndpointsResponse, retErr error) { defer log.CapturePanic(h.logger, &retErr) - if !h.config.EnableNexusAPIs() { - return nil, status.Error(codes.NotFound, "Nexus APIs are disabled") - } return h.nexusEndpointClient.List(ctx, request) } diff --git a/service/frontend/operator_handler_test.go b/service/frontend/operator_handler_test.go index bc716c18e16..b2ddade3e78 100644 --- a/service/frontend/operator_handler_test.go +++ b/service/frontend/operator_handler_test.go @@ -1106,7 +1106,7 @@ func (s *operatorHandlerSuite) Test_DeleteNamespace() { s.Nil(resp) // Success case. - mockRun.EXPECT().Get(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, valuePtr interface{}) error { + mockRun.EXPECT().Get(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, valuePtr any) error { wfResult := valuePtr.(*deletenamespace.DeleteNamespaceWorkflowResult) wfResult.DeletedNamespace = "test-namespace-deleted-ka2te" wfResult.DeletedNamespaceID = "c13c01a7-3887-4eda-ba4b-9a07a6359e7e" @@ -1122,7 +1122,7 @@ func (s *operatorHandlerSuite) Test_DeleteNamespace() { s.Equal("test-namespace-deleted-ka2te", resp.DeletedNamespace) // Success case with id. - mockRun.EXPECT().Get(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, valuePtr interface{}) error { + mockRun.EXPECT().Get(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, valuePtr any) error { wfResult := valuePtr.(*deletenamespace.DeleteNamespaceWorkflowResult) wfResult.DeletedNamespace = "test-namespace-deleted-ka2te" wfResult.DeletedNamespaceID = "c13c01a7-3887-4eda-ba4b-9a07a6359e7e" diff --git a/service/frontend/protojson_marshaler.go b/service/frontend/protojson_marshaler.go index f4687341c8d..0a9503096db 100644 --- a/service/frontend/protojson_marshaler.go +++ b/service/frontend/protojson_marshaler.go @@ -32,7 +32,7 @@ type temporalProtoDecoder struct { } func newTemporalProtoMarshaler(indent string, enablePayloadShorthand bool) (string, temporalProtoMarshaler) { - metadata := map[string]interface{}{} + metadata := map[string]any{} if enablePayloadShorthand { metadata[commonpb.EnablePayloadShorthandMetadataKey] = true } @@ -73,7 +73,7 @@ func (p temporalProtoMarshaler) Marshal(v any) ([]byte, error) { return json.Marshal(v) } -func (p temporalProtoMarshaler) Unmarshal(data []byte, v interface{}) error { +func (p temporalProtoMarshaler) Unmarshal(data []byte, v any) error { if m, ok := v.(proto.Message); ok { return p.uOpts.Unmarshal(data, m) } diff --git a/service/frontend/service.go b/service/frontend/service.go index 3e79063b3bf..53ac70a8a69 100644 --- a/service/frontend/service.go +++ b/service/frontend/service.go @@ -1,9 +1,7 @@ package frontend import ( - "fmt" "net" - "os" "regexp" "sync" "time" @@ -61,6 +59,7 @@ type Config struct { MaxNamespaceBurstRatioPerInstance dynamicconfig.FloatPropertyFnWithNamespaceFilter MaxConcurrentLongRunningRequestsPerInstance dynamicconfig.IntPropertyFnWithNamespaceFilter MaxGlobalConcurrentLongRunningRequests dynamicconfig.IntPropertyFnWithNamespaceFilter + PollWaitForNamespaceRateLimitToken dynamicconfig.BoolPropertyFnWithNamespaceFilter MaxNamespaceVisibilityRPSPerInstance dynamicconfig.IntPropertyFnWithNamespaceFilter MaxNamespaceVisibilityBurstRatioPerInstance dynamicconfig.FloatPropertyFnWithNamespaceFilter MaxNamespaceNamespaceReplicationInducingAPIsRPSPerInstance dynamicconfig.IntPropertyFnWithNamespaceFilter @@ -166,8 +165,12 @@ type Config struct { // Enable schedule-related RPCs EnableSchedules dynamicconfig.BoolPropertyFnWithNamespaceFilter + // Enable CHASM tree infrastructure + EnableChasm dynamicconfig.BoolPropertyFnWithNamespaceFilter // Enable creation of new schedules on CHASM (V2) engine EnableCHASMSchedulerCreation dynamicconfig.BoolPropertyFnWithNamespaceFilter + // Enable CHASM-first routing for schedule RPCs other than CreateSchedule + EnableCHASMSchedulerRouting dynamicconfig.BoolPropertyFnWithNamespaceFilter // Enable deployment RPCs EnableDeployments dynamicconfig.BoolPropertyFnWithNamespaceFilter @@ -191,9 +194,6 @@ type Config struct { EnableWorkerVersioningWorkflow dynamicconfig.BoolPropertyFnWithNamespaceFilter EnableWorkerVersioningRules dynamicconfig.BoolPropertyFnWithNamespaceFilter - // EnableNexusAPIs controls whether to allow invoking Nexus related APIs. - EnableNexusAPIs dynamicconfig.BoolPropertyFn - CallbackURLMaxLength dynamicconfig.IntPropertyFnWithNamespaceFilter CallbackHeaderMaxSize dynamicconfig.IntPropertyFnWithNamespaceFilter MaxCallbacksPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter @@ -225,7 +225,6 @@ type Config struct { WorkerHeartbeatsEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter EnableCancelWorkerPollsOnShutdown dynamicconfig.BoolPropertyFnWithNamespaceFilter NumTaskQueueReadPartitions dynamicconfig.IntPropertyFnWithTaskQueueFilter - ListWorkersEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter WorkerCommandsEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter WorkflowPauseEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter @@ -286,6 +285,7 @@ func NewConfig( MaxNamespaceBurstRatioPerInstance: dynamicconfig.FrontendMaxNamespaceBurstRatioPerInstance.Get(dc), MaxConcurrentLongRunningRequestsPerInstance: dynamicconfig.FrontendMaxConcurrentLongRunningRequestsPerInstance.Get(dc), MaxGlobalConcurrentLongRunningRequests: dynamicconfig.FrontendGlobalMaxConcurrentLongRunningRequests.Get(dc), + PollWaitForNamespaceRateLimitToken: dynamicconfig.FrontendPollWaitForNamespaceRateLimitToken.Get(dc), MaxNamespaceVisibilityRPSPerInstance: dynamicconfig.FrontendMaxNamespaceVisibilityRPSPerInstance.Get(dc), MaxNamespaceVisibilityBurstRatioPerInstance: dynamicconfig.FrontendMaxNamespaceVisibilityBurstRatioPerInstance.Get(dc), MaxNamespaceNamespaceReplicationInducingAPIsRPSPerInstance: dynamicconfig.FrontendMaxNamespaceNamespaceReplicationInducingAPIsRPSPerInstance.Get(dc), @@ -346,7 +346,9 @@ func NewConfig( MaxFairnessWeightOverrideConfigLimit: dynamicconfig.MatchingMaxFairnessKeyWeightOverrides.Get(dc), EnableSchedules: dynamicconfig.FrontendEnableSchedules.Get(dc), + EnableChasm: dynamicconfig.EnableChasm.Get(dc), EnableCHASMSchedulerCreation: dynamicconfig.EnableCHASMSchedulerCreation.Get(dc), + EnableCHASMSchedulerRouting: dynamicconfig.EnableCHASMSchedulerRouting.Get(dc), // [cleanup-wv-pre-release] EnableDeployments: dynamicconfig.EnableDeployments.Get(dc), @@ -365,7 +367,6 @@ func NewConfig( EnableWorkerVersioningWorkflow: dynamicconfig.FrontendEnableWorkerVersioningWorkflowAPIs.Get(dc), EnableWorkerVersioningRules: dynamicconfig.FrontendEnableWorkerVersioningRuleAPIs.Get(dc), - EnableNexusAPIs: dynamicconfig.EnableNexus.Get(dc), CallbackURLMaxLength: dynamicconfig.FrontendCallbackURLMaxLength.Get(dc), CallbackHeaderMaxSize: dynamicconfig.FrontendCallbackHeaderMaxSize.Get(dc), MaxCallbacksPerWorkflow: dynamicconfig.MaxCallbacksPerWorkflow.Get(dc), @@ -391,7 +392,6 @@ func NewConfig( WorkerHeartbeatsEnabled: dynamicconfig.WorkerHeartbeatsEnabled.Get(dc), EnableCancelWorkerPollsOnShutdown: dynamicconfig.EnableCancelWorkerPollsOnShutdown.Get(dc), NumTaskQueueReadPartitions: dynamicconfig.MatchingNumTaskqueueReadPartitions.Get(dc), - ListWorkersEnabled: dynamicconfig.ListWorkersEnabled.Get(dc), WorkerCommandsEnabled: dynamicconfig.WorkerCommandsEnabled.Get(dc), WorkflowPauseEnabled: dynamicconfig.WorkflowPauseEnabled.Get(dc), @@ -485,15 +485,9 @@ func (s *Service) Start() { s.logger.Fatal("Failed to serve HTTP API server", tag.Error(err)) } }() - } else if s.config.EnableNexusAPIs() { - var action string - if os.Args[0] == "temporal" { - action = "To enable Nexus, start the server with: `temporal server start-dev --http-port 7243 --dynamic-config-value system.enableNexus=true`." - } else { - action = "To enable Nexus, follow these instructions: https://github.com/temporalio/temporal/blob/main/docs/architecture/nexus.md#enabling-nexus." - } - - s.logger.Warn(fmt.Sprintf("system.enableNexus dynamic config is enabled but the HTTP API port has not been set. Starting with Nexus disabled. %s", action)) + } else { + s.logger.Warn("HTTP API port has not been set. Nexus HTTP endpoints will not be available. " + + "To enable Nexus, follow these instructions: https://github.com/temporalio/temporal/blob/main/docs/architecture/nexus.md#enabling-nexus.") } go s.membershipMonitor.Start() diff --git a/service/frontend/workflow_handler.go b/service/frontend/workflow_handler.go index c6afbbcafae..51df553ffbf 100644 --- a/service/frontend/workflow_handler.go +++ b/service/frontend/workflow_handler.go @@ -78,6 +78,7 @@ import ( "go.temporal.io/server/common/worker_versioning" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/worker/batcher" + "go.temporal.io/server/service/worker/dummy" "go.temporal.io/server/service/worker/scheduler" "go.temporal.io/server/service/worker/workerdeployment" "google.golang.org/grpc/codes" @@ -150,6 +151,148 @@ type ( } ) +func (wh *WorkflowHandler) CreateWorkerDeploymentVersion( + ctx context.Context, + request *workflowservice.CreateWorkerDeploymentVersionRequest, +) (_ *workflowservice.CreateWorkerDeploymentVersionResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + + if len(request.Namespace) == 0 { + return nil, errNamespaceNotSet + } + + if request.GetDeploymentVersion().GetDeploymentName() == "" { + return nil, serviceerror.NewInvalidArgument("deployment name cannot be empty") + } + + if request.GetDeploymentVersion().GetBuildId() == "" { + return nil, serviceerror.NewInvalidArgument("build ID cannot be empty") + } + + if !wh.config.EnableDeploymentVersions(request.Namespace) { + return nil, errDeploymentVersionsNotAllowed + } + + namespaceEntry, err := wh.namespaceRegistry.GetNamespace(namespace.Name(request.GetNamespace())) + if err != nil { + return nil, err + } + + requestID := request.RequestId + if requestID == "" { + requestID = uuid.NewString() + } + + err = wh.workerDeploymentClient.CreateWorkerDeploymentVersion( + ctx, + namespaceEntry, + request.GetDeploymentVersion().GetDeploymentName(), + request.GetDeploymentVersion().GetBuildId(), + request.Identity, + requestID, + request.GetComputeConfig(), + ) + if err != nil { + return nil, err + } + + return &workflowservice.CreateWorkerDeploymentVersionResponse{}, nil +} + +func (wh *WorkflowHandler) UpdateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest, +) (_ *workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + + if len(request.Namespace) == 0 { + return nil, errNamespaceNotSet + } + + if request.GetDeploymentVersion().GetDeploymentName() == "" { + return nil, serviceerror.NewInvalidArgument("deployment name cannot be empty") + } + + if request.GetDeploymentVersion().GetBuildId() == "" { + return nil, serviceerror.NewInvalidArgument("build ID cannot be empty") + } + + if !wh.config.EnableDeploymentVersions(request.Namespace) { + return nil, errDeploymentVersionsNotAllowed + } + + namespaceEntry, err := wh.namespaceRegistry.GetNamespace(namespace.Name(request.GetNamespace())) + if err != nil { + return nil, err + } + + requestID := request.RequestId + if requestID == "" { + requestID = uuid.NewString() + } + + err = wh.workerDeploymentClient.UpdateVersionComputeConfig( + ctx, + namespaceEntry, + request.GetDeploymentVersion(), + request.GetComputeConfigScalingGroups(), + request.GetRemoveComputeConfigScalingGroups(), + request.Identity, + requestID, + ) + if err != nil { + return nil, err + } + + return &workflowservice.UpdateWorkerDeploymentVersionComputeConfigResponse{}, nil +} + +func (wh *WorkflowHandler) ValidateWorkerDeploymentVersionComputeConfig( + ctx context.Context, + request *workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest, +) (_ *workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + + if len(request.Namespace) == 0 { + return nil, errNamespaceNotSet + } + + if !wh.config.EnableDeploymentVersions(request.Namespace) { + return nil, errDeploymentVersionsNotAllowed + } + + namespaceEntry, err := wh.namespaceRegistry.GetNamespace(namespace.Name(request.GetNamespace())) + if err != nil { + return nil, err + } + + err = wh.workerDeploymentClient.ValidateComputeConfig( + ctx, + namespaceEntry, + request.GetDeploymentVersion(), + request.GetComputeConfigScalingGroups(), + request.GetRemoveComputeConfigScalingGroups(), + request.Identity, + ) + if err != nil { + return nil, err + } + + return &workflowservice.ValidateWorkerDeploymentVersionComputeConfigResponse{}, nil +} + // NewWorkflowHandler creates a gRPC handler for workflowservice func NewWorkflowHandler( config *Config, @@ -3235,7 +3378,8 @@ func (wh *WorkflowHandler) GetSystemInfo(ctx context.Context, request *workflows SdkMetadata: true, BuildIdBasedVersioning: true, CountGroupByExecutionStatus: true, - Nexus: wh.httpEnabled && wh.config.EnableNexusAPIs(), + Nexus: wh.httpEnabled, + ServerScaledDeployments: true, }, }, nil } @@ -3316,12 +3460,47 @@ func (wh *WorkflowHandler) createScheduleCHASM( return nil, serviceerror.NewInvalidArgument("Only StartWorkflow action is supported for schedules") } + // Phase 1: Write sentinel to V1 key space (dummy workflow) to prevent a + // concurrent V1 CreateSchedule from succeeding for the same schedule ID. + if err := wh.writeSchedulerWorkflowSentinel(ctx, namespaceID.String(), request); err != nil { + var alreadyStartedErr *serviceerror.WorkflowExecutionAlreadyStarted + if !errors.As(err, &alreadyStartedErr) { + return nil, err + } + // V1 key is occupied. Check if it's a sentinel (proceed) or real scheduler (fail). + isReal, checkErr := wh.isRealSchedulerInV1KeySpace(ctx, namespaceID.String(), request.Namespace, request.ScheduleId) + if checkErr != nil { + return nil, checkErr + } + if isReal { + return nil, serviceerror.NewWorkflowExecutionAlreadyStarted( + fmt.Sprintf("schedule %q is already registered", request.ScheduleId), "", "") + } + } + + // Phase 2: Write real CHASM scheduler. res, err := wh.schedulerClient.CreateSchedule(ctx, &schedulerpb.CreateScheduleRequest{ NamespaceId: namespaceID.String(), FrontendRequest: request, }) - return res.GetFrontendResponse(), err - + if err != nil { + // The CHASM handler returns NotFound when the key is occupied by a + // sentinel (written by a V1-path node that raced us). Translate to + // WorkflowExecutionAlreadyStarted so the SDK can map it to + // ErrScheduleAlreadyRunning. + if isSchedulerErrorLegacyRoutable(err) { + wh.logger.Warn("CreateSchedule race detected: sentinel found at CHASM key", + tag.ScheduleID(request.ScheduleId)) + return nil, serviceerror.NewWorkflowExecutionAlreadyStarted( + fmt.Sprintf("schedule %q: concurrent creation detected", request.ScheduleId), "", "") + } + var alreadyExistsErr *serviceerror.AlreadyExists + if errors.As(err, &alreadyExistsErr) { + return nil, serviceerror.NewWorkflowExecutionAlreadyStarted(alreadyExistsErr.Message, "", "") + } + return nil, err + } + return res.GetFrontendResponse(), nil } func (wh *WorkflowHandler) createScheduleWorkflow( @@ -3336,6 +3515,34 @@ func (wh *WorkflowHandler) createScheduleWorkflow( return nil, err } + // Phase 1: Write sentinel to CHASM key space to prevent a concurrent CHASM + // CreateSchedule from succeeding for the same schedule ID. + // + // We gate on EnableCHASM because the point of the sentinel is to account for a + // case where the fleet has an inconsistent view of dynamic config. EnableCHASM + // will be true well in advance of us enabling scheduler creation across the entire + // fleet, so it is stable for usage here. + if wh.config.EnableChasm(namespaceName.String()) { + if err := wh.writeSchedulerCHASMSentinel(ctx, namespaceID.String(), namespaceName.String(), request.ScheduleId); err != nil { + // Translate AlreadyExists (from CHASM handler) to + // WorkflowExecutionAlreadyStarted for SDK compatibility. + var alreadyExistsErr *serviceerror.AlreadyExists + if errors.As(err, &alreadyExistsErr) { + return nil, serviceerror.NewWorkflowExecutionAlreadyStarted(alreadyExistsErr.Message, "", "") + } + // Ignore unimplemented to avoid issues with mixed brain testing. + // + // We wouldn't hit this condition in prod, as we wouldn't migrate with the fleet + // halfway deployed to the target version. + var unimplErr *serviceerror.Unimplemented + if !errors.As(err, &unimplErr) { + return nil, err + } + } + } + + // Phase 2: Write real V1 scheduler workflow. + // Add namespace division before unaliasing search attributes. searchattribute.AddSearchAttribute(&request.SearchAttributes, sadefs.TemporalNamespaceDivision, payload.EncodeString(scheduler.NamespaceDivision)) @@ -3397,6 +3604,23 @@ func (wh *WorkflowHandler) createScheduleWorkflow( ) if err != nil { + var alreadyStartedErr *serviceerror.WorkflowExecutionAlreadyStarted + if errors.As(err, &alreadyStartedErr) { + // V1 key is occupied. Check if it's a sentinel (race) or real scheduler. + isReal, checkErr := wh.isRealSchedulerInV1KeySpace(ctx, namespaceID.String(), request.Namespace, request.ScheduleId) + if checkErr != nil { + return nil, checkErr + } + if !isReal { + // A dummy workflow exists — a CHASM-path node raced us. + wh.logger.Warn("CreateSchedule race detected: sentinel found at V1 key", + tag.ScheduleID(request.ScheduleId)) + return nil, serviceerror.NewWorkflowExecutionAlreadyStarted( + fmt.Sprintf("schedule %q: concurrent creation detected", request.ScheduleId), "", "") + } + return nil, serviceerror.NewWorkflowExecutionAlreadyStarted( + fmt.Sprintf("schedule %q is already registered", request.ScheduleId), "", "") + } return nil, err } token := make([]byte, 8) @@ -3406,6 +3630,83 @@ func (wh *WorkflowHandler) createScheduleWorkflow( }, nil } +// writeSchedulerWorkflowSentinel starts a dummy workflow to reserve the V1 +// workflow ID space for the given schedule ID. The dummy workflow sleeps for a +// fixed duration and then completes, releasing the reservation. +func (wh *WorkflowHandler) writeSchedulerWorkflowSentinel( + ctx context.Context, + namespaceID string, + request *workflowservice.CreateScheduleRequest, +) error { + startReq := &workflowservice.StartWorkflowExecutionRequest{ + Namespace: request.Namespace, + WorkflowId: scheduler.WorkflowIDPrefix + request.ScheduleId, + WorkflowType: &commonpb.WorkflowType{Name: dummy.DummyWFTypeName}, + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + Identity: request.Identity, + RequestId: request.RequestId, + WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + // Set TemporalNamespaceDivision so the dummy is hidden from + // ListWorkflowExecutions. Use a distinct value so it doesn't match the + // ListSchedules query (which filters on scheduler.NamespaceDivision). + SearchAttributes: &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + sadefs.TemporalNamespaceDivision: payload.EncodeString("TemporalSchedulerSentinel"), + }, + }, + } + _, err := wh.historyClient.StartWorkflowExecution( + ctx, + common.CreateHistoryStartWorkflowRequest(namespaceID, startReq, nil, nil, time.Now().UTC()), + ) + return err +} + +// writeSchedulerCHASMSentinel creates a CHASM sentinel to reserve the CHASM key +// space for the given schedule ID. The sentinel auto-closes after a fixed +// duration via the idle task mechanism. +func (wh *WorkflowHandler) writeSchedulerCHASMSentinel( + ctx context.Context, + namespaceID, namespaceName, scheduleID string, +) error { + _, err := wh.schedulerClient.CreateSentinel(ctx, &schedulerpb.CreateSentinelRequest{ + NamespaceId: namespaceID, + Namespace: namespaceName, + ScheduleId: scheduleID, + }) + return err +} + +// isRealSchedulerInV1KeySpace checks if a running V1 scheduler workflow (not a +// dummy sentinel) exists for the given schedule ID. +func (wh *WorkflowHandler) isRealSchedulerInV1KeySpace( + ctx context.Context, + namespaceID, namespaceName, scheduleID string, +) (bool, error) { + workflowID := scheduler.WorkflowIDPrefix + scheduleID + descResp, err := wh.historyClient.DescribeWorkflowExecution(ctx, &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: namespaceID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: namespaceName, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, + }, + }) + if err != nil { + var notFoundErr *serviceerror.NotFound + if errors.As(err, ¬FoundErr) { + return false, nil + } + return false, err + } + + info := descResp.GetWorkflowExecutionInfo() + if info.GetStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING { + return false, nil + } + return info.GetType().GetName() != dummy.DummyWFTypeName, nil +} + func (wh *WorkflowHandler) CreateSchedule( ctx context.Context, request *workflowservice.CreateScheduleRequest, @@ -3438,7 +3739,7 @@ func (wh *WorkflowHandler) CreateSchedule( namespaceName := namespace.Name(request.Namespace) - useChasmScheduler := wh.chasmSchedulerEnabled(ctx, namespaceName.String()) + useChasmScheduler := wh.chasmSchedulerCreationEnabled(ctx, namespaceName.String()) wh.logger.Debug("Received CreateSchedule", tag.ScheduleID(request.ScheduleId), tag.WorkflowNamespace(namespaceName.String()), @@ -3462,24 +3763,40 @@ func (wh *WorkflowHandler) CreateSchedule( return wh.createScheduleWorkflow(ctx, request) } -// chasmSchedulerEnabled returns true when CHASM codepaths should be enabled for -// the request. All handlers must be capable of falling back to V1 codepaths for -// schedules that haven't been migrated to CHASM. -func (wh *WorkflowHandler) chasmSchedulerEnabled(ctx context.Context, namespaceName string) bool { +// chasmSchedulerCreationEnabled returns true when CreateSchedule should create on +// the CHASM scheduler. +func (wh *WorkflowHandler) chasmSchedulerCreationEnabled(ctx context.Context, namespaceName string) bool { return (headers.IsExperimentRequested(ctx, ChasmSchedulerExperiment) && wh.config.IsExperimentAllowed(ChasmSchedulerExperiment, namespaceName)) || wh.config.EnableCHASMSchedulerCreation(namespaceName) } +// chasmSchedulerEnabled returns true when schedule RPCs should route to CHASM +// first. Handlers must be capable of falling back to V1 codepaths for schedules +// that haven't been migrated to CHASM. +func (wh *WorkflowHandler) chasmSchedulerEnabled(ctx context.Context, namespaceName string) bool { + return wh.chasmSchedulerCreationEnabled(ctx, namespaceName) || + wh.config.EnableCHASMSchedulerRouting(namespaceName) +} + // isSchedulerErrorLegacyRoutable returns true if the error from the CHASM scheduler // indicates that the request should be routed to the legacy (V1) scheduler stack. -// This accounts for two situations: +// This accounts for three situations: // - NotFound: the CHASM stack doesn't have a schedule for that ID // - NotFound (sentinel): the key at that ID is a sentinel value (reserving the ID // for the V1 stack) +// - ErrClosed: the CHASM schedule was migrated to V1 and marked closed; the +// request should be retried against the workflow-backed stack. func isSchedulerErrorLegacyRoutable(err error) bool { var notFoundErr *serviceerror.NotFound - return errors.As(err, ¬FoundErr) + if errors.As(err, ¬FoundErr) { + return true + } + var failedPreconditionErr *serviceerror.FailedPrecondition + if errors.As(err, &failedPreconditionErr) { + return failedPreconditionErr.Message == chasmscheduler.ErrClosed.(*serviceerror.FailedPrecondition).Message + } + return false } // Validates inner start workflow request. Note that this can mutate search attributes if present. @@ -3815,6 +4132,52 @@ func (wh *WorkflowHandler) DescribeWorkerDeployment(ctx context.Context, request }, nil } +func (wh *WorkflowHandler) CreateWorkerDeployment(ctx context.Context, request *workflowservice.CreateWorkerDeploymentRequest) (_ *workflowservice.CreateWorkerDeploymentResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + + if len(request.Namespace) == 0 { + return nil, errNamespaceNotSet + } + + if request.DeploymentName == "" { + return nil, serviceerror.NewInvalidArgument("deployment name cannot be empty") + } + + if !wh.config.EnableDeploymentVersions(request.Namespace) { + return nil, errDeploymentVersionsNotAllowed + } + + namespaceEntry, err := wh.namespaceRegistry.GetNamespace(namespace.Name(request.GetNamespace())) + if err != nil { + return nil, err + } + + // Generate request ID if not provided (for idempotency) + requestID := request.RequestId + if requestID == "" { + requestID = uuid.NewString() + } + + conflictToken, err := wh.workerDeploymentClient.CreateWorkerDeployment( + ctx, + namespaceEntry, + request.DeploymentName, + request.Identity, + requestID, + ) + if err != nil { + return nil, err + } + + return &workflowservice.CreateWorkerDeploymentResponse{ + ConflictToken: conflictToken, + }, nil +} + func (wh *WorkflowHandler) SetWorkerDeploymentManager(ctx context.Context, request *workflowservice.SetWorkerDeploymentManagerRequest) (_ *workflowservice.SetWorkerDeploymentManagerResponse, retError error) { defer log.CapturePanic(wh.logger, &retError) @@ -3902,8 +4265,7 @@ func (wh *WorkflowHandler) UpdateWorkerDeploymentVersionMetadata(ctx context.Con version = worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(request.GetVersion()) } - identity := uuid.NewString() - updatedMetadata, err := wh.workerDeploymentClient.UpdateVersionMetadata(ctx, namespaceEntry, version, request.UpsertEntries, request.RemoveEntries, identity) + updatedMetadata, err := wh.workerDeploymentClient.UpdateVersionMetadata(ctx, namespaceEntry, version, request.UpsertEntries, request.RemoveEntries, request.Identity) if err != nil { return nil, err } @@ -3978,6 +4340,10 @@ func (wh *WorkflowHandler) describeScheduleWorkflow(ctx context.Context, request // only treat running schedules as existing return nil, serviceerror.NewNotFound("schedule not found") } + if executionInfo.GetType().GetName() == dummy.DummyWFTypeName { + // This is a sentinel workflow, not a real scheduler. + return nil, serviceerror.NewNotFound("schedule not found") + } // map search attributes if sas := executionInfo.GetSearchAttributes(); sas != nil { @@ -4182,6 +4548,11 @@ func (wh *WorkflowHandler) UpdateSchedule( } } + // Reject memo updates for V1 schedules. + if request.GetMemo() != nil { + return nil, serviceerror.NewFailedPrecondition("memo updates are not supported on workflow-backed schedules") + } + return wh.updateScheduleWorkflow(ctx, request) } @@ -4312,8 +4683,6 @@ func (wh *WorkflowHandler) PatchSchedule( return nil, err } - // TODO - when V2 supports updating the scheduler memo, make sure to add that to - // the size validation here (like in CreateSchedule). sizeLimitError := wh.config.BlobSizeLimitError(request.GetNamespace()) sizeLimitWarn := wh.config.BlobSizeLimitWarn(request.GetNamespace()) if err := common.CheckEventBlobSizeLimit( @@ -4639,11 +5008,10 @@ func (wh *WorkflowHandler) listSchedulesChasm( query string, ) (_ *workflowservice.ListSchedulesResponse, retError error) { resp, err := chasm.ListExecutions[*chasmscheduler.Scheduler, *schedulepb.ScheduleListInfo](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: namespaceID.String(), + NamespaceName: namespaceName.String(), PageSize: int(request.GetMaximumPageSize()), NextPageToken: request.NextPageToken, Query: query, - NamespaceName: namespaceName.String(), }) if err != nil { return nil, err @@ -4770,7 +5138,6 @@ func (wh *WorkflowHandler) countSchedulesChasm( query string, ) (*workflowservice.CountSchedulesResponse, error) { resp, err := chasm.CountExecutions[*chasmscheduler.Scheduler](ctx, &chasm.CountExecutionsRequest{ - NamespaceID: namespaceID.String(), NamespaceName: namespaceName.String(), Query: query, }) @@ -5182,6 +5549,15 @@ func (wh *WorkflowHandler) StartBatchOperation( return nil, err } + // Validate visibility query syntax before starting the batch workflow. + // Malformed queries (e.g. "()") would otherwise cause the batch activity + // to retry indefinitely since the error is not marked non-retryable. + if q := request.GetVisibilityQuery(); len(q) > 0 { + if _, err := sqlparser.Parse("select * from dummy where " + q); err != nil { + return nil, serviceerror.NewInvalidArgumentf("invalid visibility query: %v", err) + } + } + // Validate concurrent batch operation maxConcurrentBatchOperation := wh.config.MaxConcurrentBatchOperation(request.GetNamespace()) countResp, err := wh.CountWorkflowExecutions(ctx, &workflowservice.CountWorkflowExecutionsRequest{ @@ -5915,13 +6291,6 @@ func (wh *WorkflowHandler) validateWorkflowCompletionCallbacks( ns namespace.Name, callbacks []*commonpb.Callback, ) error { - if len(callbacks) > 0 && !wh.config.EnableNexusAPIs() { - return status.Error( - codes.InvalidArgument, - "attaching workflow callbacks is disabled for this namespace", - ) - } - if len(callbacks) > wh.config.MaxCallbacksPerWorkflow(ns.String()) { return status.Error( codes.InvalidArgument, @@ -6739,9 +7108,6 @@ func (wh *WorkflowHandler) RecordWorkerHeartbeat( func (wh *WorkflowHandler) ListWorkers( ctx context.Context, request *workflowservice.ListWorkersRequest, ) (*workflowservice.ListWorkersResponse, error) { - if !wh.config.ListWorkersEnabled(request.GetNamespace()) { - return nil, serviceerror.NewUnimplemented("method ListWorkers not supported") - } namespaceName := namespace.Name(request.GetNamespace()) namespaceID, err := wh.namespaceRegistry.GetNamespaceID(namespaceName) if err != nil { @@ -6759,6 +7125,7 @@ func (wh *WorkflowHandler) ListWorkers( return &workflowservice.ListWorkersResponse{ WorkersInfo: resp.GetWorkersInfo(), + Workers: resp.GetWorkers(), NextPageToken: resp.GetNextPageToken(), }, nil } @@ -6847,9 +7214,6 @@ func (wh *WorkflowHandler) UpdateWorkerConfig(_ context.Context, request *workfl func (wh *WorkflowHandler) DescribeWorker(ctx context.Context, request *workflowservice.DescribeWorkerRequest, ) (*workflowservice.DescribeWorkerResponse, error) { - if !wh.config.ListWorkersEnabled(request.GetNamespace()) { - return nil, serviceerror.NewUnimplemented("DescribeWorker command is not enabled.") - } namespaceName := namespace.Name(request.GetNamespace()) namespaceID, err := wh.namespaceRegistry.GetNamespaceID(namespaceName) if err != nil { diff --git a/service/frontend/workflow_handler_second_test.go b/service/frontend/workflow_handler_second_test.go index 70fbfb31659..4efdbbbd593 100644 --- a/service/frontend/workflow_handler_second_test.go +++ b/service/frontend/workflow_handler_second_test.go @@ -1,19 +1,84 @@ package frontend import ( + "context" "testing" + "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" schedulepb "go.temporal.io/api/schedule/v1" workflowpb "go.temporal.io/api/workflow/v1" schedulespb "go.temporal.io/server/api/schedule/v1" + dc "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/headers" "go.temporal.io/server/common/payload" "go.temporal.io/server/common/persistence/visibility/manager" "go.temporal.io/server/common/searchattribute" "go.uber.org/mock/gomock" + "google.golang.org/grpc/metadata" ) +func TestCHASMSchedulerRoutingAndCreationGates(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + enableCreation bool + enableRouting bool + allowedExp []string + requestExperiment bool + expectCreation bool + expectRouting bool + }{ + { + name: "routing-only config routes but does not create", + enableCreation: false, + enableRouting: true, + expectCreation: false, + expectRouting: true, + }, + { + name: "creation config still enables both", + enableCreation: true, + enableRouting: false, + expectCreation: true, + expectRouting: true, + }, + { + name: "experiment enables both", + enableCreation: false, + enableRouting: false, + allowedExp: []string{ChasmSchedulerExperiment}, + requestExperiment: true, + expectCreation: true, + expectRouting: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + config := NewConfig(dc.NewNoopCollection(), 1) + config.EnableCHASMSchedulerCreation = dc.GetBoolPropertyFnFilteredByNamespace(tc.enableCreation) + config.EnableCHASMSchedulerRouting = dc.GetBoolPropertyFnFilteredByNamespace(tc.enableRouting) + config.AllowedExperiments = dc.GetTypedPropertyFnFilteredByNamespace(tc.allowedExp) + + wh := &WorkflowHandler{config: config} + + ctx := context.Background() + if tc.requestExperiment { + ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(headers.ExperimentHeaderName, ChasmSchedulerExperiment)) + } + + require.Equal(t, tc.expectCreation, wh.chasmSchedulerCreationEnabled(ctx, "test-namespace")) + require.Equal(t, tc.expectRouting, wh.chasmSchedulerEnabled(ctx, "test-namespace")) + }) + } +} + func TestDescribeScheduleAnnotatesScheduledWorkflowWithTypes(t *testing.T) { makeWorkflowHandler := func( visibilityManager *manager.MockVisibilityManager, diff --git a/service/frontend/workflow_handler_test.go b/service/frontend/workflow_handler_test.go index 38d68e9238a..8726d6bf071 100644 --- a/service/frontend/workflow_handler_test.go +++ b/service/frontend/workflow_handler_test.go @@ -719,7 +719,7 @@ func (s *WorkflowHandlerSuite) TestStartWorkflowExecution_Failed_InvalidLinks() s.ErrorContains(err, "link exceeds allowed size of 4000") req.Links = []*commonpb.Link{} - for i := 0; i < 11; i++ { + for range 11 { req.Links = append(req.Links, &commonpb.Link{ Variant: &commonpb.Link_WorkflowEvent_{ WorkflowEvent: &commonpb.Link_WorkflowEvent{ @@ -900,7 +900,7 @@ func (s *WorkflowHandlerSuite) TestStartWorkflowExecution_Failed_InvalidAggregat // add 10 links and one of them is duplicated in the callback req.Links = []*commonpb.Link{} - for i := 0; i < 10; i++ { + for i := range 10 { req.Links = append(req.Links, &commonpb.Link{ Variant: &commonpb.Link_WorkflowEvent_{ WorkflowEvent: &commonpb.Link_WorkflowEvent{ @@ -2192,7 +2192,7 @@ func (s *WorkflowHandlerSuite) TestCountWorkflowExecutions() { func (s *WorkflowHandlerSuite) TestVerifyHistoryIsComplete() { logger := log.NewTestLogger() events := make([]*historyspb.StrippedHistoryEvent, 50) - for i := 0; i < len(events); i++ { + for i := range events { events[i] = &historyspb.StrippedHistoryEvent{EventId: int64(i + 1)} } var eventsWithHoles []*historyspb.StrippedHistoryEvent @@ -2272,6 +2272,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Terminate() { testNamespace := namespace.Name("test-namespace") namespaceID := namespace.ID(uuid.NewString()) inputString := "unit test" + visibilityQuery := "WorkflowType='unit-test'" jobId := uuid.NewString() config := s.newConfig() wh := s.getWorkflowHandler(config) @@ -2281,7 +2282,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Terminate() { BatchType: enumspb.BATCH_OPERATION_TYPE_TERMINATE, Request: &workflowservice.StartBatchOperationRequest{ Namespace: testNamespace.String(), - VisibilityQuery: inputString, + VisibilityQuery: visibilityQuery, JobId: jobId, Reason: inputString, Operation: &workflowservice.StartBatchOperationRequest_TerminationOperation{ @@ -2322,7 +2323,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Terminate() { Identity: inputString, }, }, - VisibilityQuery: inputString, + VisibilityQuery: visibilityQuery, } _, err = wh.StartBatchOperation(context.Background(), request) @@ -2333,6 +2334,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Cancellation() { testNamespace := namespace.Name("test-namespace") namespaceID := namespace.ID(uuid.NewString()) inputString := "unit test" + visibilityQuery := "WorkflowType='unit-test'" jobId := uuid.NewString() config := s.newConfig() wh := s.getWorkflowHandler(config) @@ -2342,7 +2344,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Cancellation() { BatchType: enumspb.BATCH_OPERATION_TYPE_CANCEL, Request: &workflowservice.StartBatchOperationRequest{ Namespace: testNamespace.String(), - VisibilityQuery: inputString, + VisibilityQuery: visibilityQuery, JobId: jobId, Reason: inputString, Operation: &workflowservice.StartBatchOperationRequest_CancellationOperation{ @@ -2383,7 +2385,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Cancellation() { Identity: inputString, }, }, - VisibilityQuery: inputString, + VisibilityQuery: visibilityQuery, } _, err = wh.StartBatchOperation(context.Background(), request) @@ -2394,6 +2396,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Signal() { testNamespace := namespace.Name("test-namespace") namespaceID := namespace.ID(uuid.NewString()) inputString := "unit test" + visibilityQuery := "WorkflowType='unit-test'" signalName := "signal name" jobId := uuid.NewString() config := s.newConfig() @@ -2404,7 +2407,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Signal() { BatchType: enumspb.BATCH_OPERATION_TYPE_SIGNAL, Request: &workflowservice.StartBatchOperationRequest{ Namespace: testNamespace.String(), - VisibilityQuery: inputString, + VisibilityQuery: visibilityQuery, JobId: jobId, Reason: inputString, Operation: &workflowservice.StartBatchOperationRequest_SignalOperation{ @@ -2449,7 +2452,7 @@ func (s *WorkflowHandlerSuite) TestStartBatchOperation_Signal() { }, }, Reason: inputString, - VisibilityQuery: inputString, + VisibilityQuery: visibilityQuery, } _, err = wh.StartBatchOperation(context.Background(), request) diff --git a/service/history/api/addtasks/api_test.go b/service/history/api/addtasks/api_test.go index 0a6672c6bde..37f9c60bf72 100644 --- a/service/history/api/addtasks/api_test.go +++ b/service/history/api/addtasks/api_test.go @@ -81,7 +81,7 @@ func TestInvoke(t *testing.T) { return nil }).Times(2) params.req.Tasks = nil - for i := 0; i < numWorkflows; i++ { + for i := range numWorkflows { workflowKey := definition.NewWorkflowKey( string(tests.NamespaceID), strconv.Itoa(i), diff --git a/service/history/api/command_attr_validator.go b/service/history/api/command_attr_validator.go index 00deed07cc8..da9a5e88edc 100644 --- a/service/history/api/command_attr_validator.go +++ b/service/history/api/command_attr_validator.go @@ -76,6 +76,7 @@ func (v *CommandAttrValidator) ValidateActivityScheduleAttributes( namespaceID namespace.ID, attributes *commandpb.ScheduleActivityTaskCommandAttributes, runTimeout *durationpb.Duration, + workflowTaskQueue string, ) (enumspb.WorkflowTaskFailedCause, error) { const failedCause = enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_ACTIVITY_ATTRIBUTES @@ -108,7 +109,7 @@ func (v *CommandAttrValidator) ValidateActivityScheduleAttributes( RetryPolicy: attributes.RetryPolicy, } - err := activity.ValidateAndNormalizeActivityAttributes( + err := activity.ValidateAndNormalizeEmbeddedActivity( activityID, activityType, v.getDefaultActivityRetrySettings, @@ -116,7 +117,8 @@ func (v *CommandAttrValidator) ValidateActivityScheduleAttributes( namespaceID, opts, attributes.GetPriority(), - runTimeout) + runTimeout, + workflowTaskQueue) if err != nil { return failedCause, err diff --git a/service/history/api/command_attr_validator_test.go b/service/history/api/command_attr_validator_test.go index 40e0bd107dc..208e1c6fbae 100644 --- a/service/history/api/command_attr_validator_test.go +++ b/service/history/api/command_attr_validator_test.go @@ -194,7 +194,7 @@ func (s *commandAttrValidatorSuite) TestValidateUpsertWorkflowSearchAttributes() s.EqualError(err, "IndexedFields is not set on UpsertWorkflowSearchAttributesCommand.") s.Equal(enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SEARCH_ATTRIBUTES, fc) - saPayload, err := searchattribute.EncodeValue("bytes", enumspb.INDEXED_VALUE_TYPE_KEYWORD) + saPayload, err := sadefs.EncodeValue("bytes", enumspb.INDEXED_VALUE_TYPE_KEYWORD) s.NoError(err) attributes.SearchAttributes.IndexedFields = map[string]*commonpb.Payload{ "Keyword01": saPayload, @@ -238,7 +238,7 @@ func (s *commandAttrValidatorSuite) TestValidateContinueAsNewWorkflowExecutionAt executionInfo, ) s.Error(err) - s.Contains(err.Error(), "cannot use internal per namespace task queue") + s.Contains(err.Error(), "cannot use internal per-namespace task queue") s.Equal(enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_CONTINUE_AS_NEW_ATTRIBUTES, fc) executionInfo.TaskQueue = primitives.PerNSWorkerTaskQueue @@ -286,7 +286,7 @@ func (s *commandAttrValidatorSuite) TestValidateContinueAsNewWorkflowExecutionAt s.Equal(maxWorkflowTaskStartToCloseTimeout, attributes.GetWorkflowTaskTimeout().AsDuration()) // Predefined Worker-Deployment related SA's should be rejected when they are attempted to be set during CAN - saPayload, _ := searchattribute.EncodeValue([]string{"a"}, enumspb.INDEXED_VALUE_TYPE_KEYWORD) + saPayload, _ := sadefs.EncodeValue([]string{"a"}, enumspb.INDEXED_VALUE_TYPE_KEYWORD) attributes.SearchAttributes = &commonpb.SearchAttributes{} deploymentRestrictedAttributes := []string{ @@ -841,3 +841,60 @@ func (s *commandAttrValidatorSuite) TestValidateStartChildExecutionAttributes_In }) } } + +func (s *commandAttrValidatorSuite) TestValidateActivityScheduleAttributes_WorkflowTaskQueue() { + testCases := []struct { + name string + workflowTaskQueue string + activityTaskQueue string + expectError bool + }{ + { + name: "normal workflow scheduling activity on normal task queue is allowed", + workflowTaskQueue: "user-task-queue", + activityTaskQueue: "user-task-queue", + expectError: false, + }, + { + name: "normal workflow scheduling activity on per-ns-tq is blocked", + workflowTaskQueue: "user-task-queue", + activityTaskQueue: primitives.PerNSWorkerTaskQueue, + expectError: true, + }, + { + name: "per-ns-tq workflow scheduling activity on per-ns-tq is allowed", + workflowTaskQueue: primitives.PerNSWorkerTaskQueue, + activityTaskQueue: primitives.PerNSWorkerTaskQueue, + expectError: false, + }, + } + + for _, tt := range testCases { + s.Run(tt.name, func() { + attributes := &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: "test-activity-id", + ActivityType: &commonpb.ActivityType{Name: "test-activity-type"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tt.activityTaskQueue}, + StartToCloseTimeout: durationpb.New(10 * time.Second), + } + + fc, err := s.validator.ValidateActivityScheduleAttributes( + s.testNamespaceID, + attributes, + durationpb.New(0), + tt.workflowTaskQueue, + ) + + if tt.expectError { + s.Error(err) + var invalidArgument *serviceerror.InvalidArgument + s.ErrorAs(err, &invalidArgument) + s.Contains(err.Error(), "internal per-namespace task queue") + s.Equal(enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_ACTIVITY_ATTRIBUTES, fc) + } else { + s.NoError(err) + s.Equal(enumspb.WORKFLOW_TASK_FAILED_CAUSE_UNSPECIFIED, fc) + } + }) + } +} diff --git a/service/history/api/deletedlqtasks/deletedlqtaskstest/apitest.go b/service/history/api/deletedlqtasks/deletedlqtaskstest/apitest.go index 512a1517694..972e2d54650 100644 --- a/service/history/api/deletedlqtasks/deletedlqtaskstest/apitest.go +++ b/service/history/api/deletedlqtasks/deletedlqtaskstest/apitest.go @@ -26,7 +26,7 @@ func TestInvoke(t *testing.T, manager persistence.HistoryTaskQueueManager) { QueueKey: queueKey, }) require.NoError(t, err) - for i := 0; i < 3; i++ { + for range 3 { _, err := manager.EnqueueTask(ctx, &persistence.EnqueueTaskRequest{ QueueType: queueKey.QueueType, SourceCluster: queueKey.SourceCluster, diff --git a/service/history/api/deleteworkflow/api.go b/service/history/api/deleteworkflow/api.go index 21e6f06535b..b61927d81e4 100644 --- a/service/history/api/deleteworkflow/api.go +++ b/service/history/api/deleteworkflow/api.go @@ -84,7 +84,7 @@ func Invoke( } // If workflow execution is closed or in passive cluster. - if err := workflowDeleteManager.AddDeleteWorkflowExecutionTask( + if err := workflowDeleteManager.AddDeleteExecutionTask( ctx, namespace.ID(request.GetNamespaceId()), &commonpb.WorkflowExecution{ diff --git a/service/history/api/forcedeleteworkflowexecution/api.go b/service/history/api/forcedeleteworkflowexecution/api.go index 2a15a4c808b..c31a6d50847 100644 --- a/service/history/api/forcedeleteworkflowexecution/api.go +++ b/service/history/api/forcedeleteworkflowexecution/api.go @@ -84,19 +84,6 @@ func Invoke( } } - // NOTE: the deletion is best effort, for sql visibility implementation, - // we can't guarantee there's no update or record close request for this workflow since - // visibility queue processing is async. Operator can call this api again to delete visibility - // record again if this happens. - if err := persistenceVisibilityMgr.DeleteWorkflowExecution(ctx, &manager.VisibilityDeleteWorkflowExecutionRequest{ - NamespaceID: namespace.ID(request.GetNamespaceId()), - WorkflowID: execution.GetWorkflowId(), - RunID: execution.GetRunId(), - TaskID: math.MaxInt64, - }); err != nil { - return nil, err - } - if err := persistenceExecutionMgr.DeleteCurrentWorkflowExecution(ctx, &persistence.DeleteCurrentWorkflowExecutionRequest{ ShardID: shardID, NamespaceID: request.NamespaceId, @@ -128,6 +115,19 @@ func Invoke( } } + // NOTE: the deletion is best effort, for sql visibility implementation, + // we can't guarantee there's no update or record close request for this workflow since + // visibility queue processing is async. Operator can call this api again to delete visibility + // record again if this happens. + if err := persistenceVisibilityMgr.DeleteWorkflowExecution(ctx, &manager.VisibilityDeleteWorkflowExecutionRequest{ + NamespaceID: namespace.ID(request.GetNamespaceId()), + WorkflowID: execution.GetWorkflowId(), + RunID: execution.GetRunId(), + TaskID: math.MaxInt64, + }); err != nil { + return nil, err + } + return &historyservice.ForceDeleteWorkflowExecutionResponse{ Response: &adminservice.DeleteWorkflowExecutionResponse{ Warnings: warnings, diff --git a/service/history/api/get_history_util.go b/service/history/api/get_history_util.go index 5bef4300381..ffd5c81058f 100644 --- a/service/history/api/get_history_util.go +++ b/service/history/api/get_history_util.go @@ -115,26 +115,31 @@ func GetRawHistory( metrics.HistorySize.With(metricsHandler).Record(int64(size)) if len(nextToken) == 0 && transientWorkflowTaskInfo != nil { - if err := validateTransientWorkflowTaskEvents(nextEventID, transientWorkflowTaskInfo); err != nil { - logger := shardContext.GetLogger() - metricsHandler := interceptor.GetMetricsHandlerFromContext(ctx, logger).WithTags(metrics.OperationTag(metrics.HistoryGetRawHistoryScope)) - metrics.ServiceErrIncompleteHistoryCounter.With(metricsHandler).Record(1) - logger.Error("getHistory error", - tag.WorkflowNamespaceID(namespaceID.String()), - tag.WorkflowID(execution.GetWorkflowId()), - tag.WorkflowRunID(execution.GetRunId()), - tag.Error(err)) - return nil, nil, err - } - - if len(transientWorkflowTaskInfo.HistorySuffix) > 0 { - blob, err := shardContext.GetPayloadSerializer().SerializeEvents(transientWorkflowTaskInfo.HistorySuffix) - if err != nil { - return nil, nil, err + // Check if we should include transient/speculative events + if shouldIncludeTransientOrSpeculativeTasks(ctx, transientWorkflowTaskInfo) { + if err := ValidateTransientWorkflowTaskEvents(nextEventID, transientWorkflowTaskInfo); err != nil { + logger := shardContext.GetLogger() + metricsHandler := interceptor.GetMetricsHandlerFromContext(ctx, logger).WithTags(metrics.OperationTag(metrics.HistoryGetRawHistoryScope)) + metrics.ServiceErrIncompleteHistoryCounter.With(metricsHandler).Record(1) + } else { + if len(transientWorkflowTaskInfo.HistorySuffix) > 0 { + blob, err := shardContext.GetPayloadSerializer().SerializeEvents(transientWorkflowTaskInfo.HistorySuffix) + if err != nil { + return nil, nil, err + } + rawHistory = append(rawHistory, blob) + } } - rawHistory = append(rawHistory, blob) } } + + // Ensure all raw history is proto3 encoded since data may be stored in other formats during testing. + // In production (proto3 encoding), this returns the input unchanged. + rawHistory, err = serialization.ReencodeEventBlobsAsProto3(shardContext.GetPayloadSerializer(), rawHistory) + if err != nil { + return nil, nil, err + } + return rawHistory, nextToken, nil } @@ -227,16 +232,19 @@ func GetHistory( tag.Error(err)) } if len(nextPageToken) == 0 && transientWorkflowTaskInfo != nil { - if err := validateTransientWorkflowTaskEvents(nextEventID, transientWorkflowTaskInfo); err != nil { - metrics.ServiceErrIncompleteHistoryCounter.With(metricsHandler).Record(1) - logger.Error("getHistory error", - tag.WorkflowNamespaceID(namespaceID.String()), - tag.WorkflowID(execution.GetWorkflowId()), - tag.WorkflowRunID(execution.GetRunId()), - tag.Error(err)) + // Check if we should include transient/speculative events + if shouldIncludeTransientOrSpeculativeTasks(ctx, transientWorkflowTaskInfo) { + if err := ValidateTransientWorkflowTaskEvents(nextEventID, transientWorkflowTaskInfo); err != nil { + metrics.ServiceErrIncompleteHistoryCounter.With(metricsHandler).Record(1) + // Don't append events, but don't fail request + } else { + if len(transientWorkflowTaskInfo.HistorySuffix) > 0 { + // Validation passed, append events + // Append the transient workflow task events once we are done enumerating everything from the events table + historyEvents = append(historyEvents, transientWorkflowTaskInfo.HistorySuffix...) + } + } } - // Append the transient workflow task events once we are done enumerating everything from the events table - historyEvents = append(historyEvents, transientWorkflowTaskInfo.HistorySuffix...) } if err := ProcessOutgoingSearchAttributes( @@ -378,7 +386,58 @@ func ProcessOutgoingSearchAttributes( return nil } -func validateTransientWorkflowTaskEvents( +// shouldIncludeTransientOrSpeculativeTasks determines if transient/speculative events should be included. +// This function is called only when on the last page of history pagination (nextToken is empty). +func shouldIncludeTransientOrSpeculativeTasks( + ctx context.Context, + tranOrSpecEvents *historyspb.TransientWorkflowTaskInfo, +) bool { + return len(tranOrSpecEvents.GetHistorySuffix()) > 0 && + ClientSupportsTranOrSpecEvents(ctx) && + areValidTransientOrSpecEvents(tranOrSpecEvents) +} + +func areValidTransientOrSpecEvents(tranOrSpecEvents *historyspb.TransientWorkflowTaskInfo) bool { + events := tranOrSpecEvents.GetHistorySuffix() + if len(events) == 0 || len(events) > 2 { + return false + } + + // First must be WFT_SCHEDULED + if events[0].GetEventType() != enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED { + return false + } + + // If 2 events, second must be WFT_STARTED immediately after + if len(events) == 2 { + if events[1].GetEventType() != enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + return false + } + if events[1].GetEventId() != events[0].GetEventId()+1 { + return false + } + } + + return true +} + +// ClientSupportsTranOrSpecEvents detects if client supports transient events +// Default to include transient events for clients, only CLI and UI are +// explicitly excluded for backward compatability +func ClientSupportsTranOrSpecEvents(ctx context.Context) bool { + clientName, _ := headers.GetClientNameAndVersion(ctx) + + switch clientName { + case headers.ClientNameCLI, headers.ClientNameUI: + return false + default: + return true + } +} + +// ValidateTransientWorkflowTaskEvents validates that transient workflow task events have sequential event IDs +// starting from the given offset. Returns an error if any event ID doesn't match the expected sequence. +func ValidateTransientWorkflowTaskEvents( eventIDOffset int64, transientWorkflowTaskInfo *historyspb.TransientWorkflowTaskInfo, ) error { diff --git a/service/history/api/get_history_util_test.go b/service/history/api/get_history_util_test.go new file mode 100644 index 00000000000..3e713144e6b --- /dev/null +++ b/service/history/api/get_history_util_test.go @@ -0,0 +1,316 @@ +package api + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + enumspb "go.temporal.io/api/enums/v1" + historypb "go.temporal.io/api/history/v1" + historyspb "go.temporal.io/server/api/history/v1" + "go.temporal.io/server/common/headers" +) + +func TestShouldIncludeTransientOrSpeculativeTasks(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + clientName string + historySuffix []*historypb.HistoryEvent + expected bool + }{ + { + name: "all conditions met", + clientName: headers.ClientNameGoSDK, + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + }, + expected: true, + }, + { + name: "empty history suffix", + clientName: headers.ClientNameGoSDK, + historySuffix: []*historypb.HistoryEvent{}, + expected: false, + }, + { + name: "nil history suffix", + clientName: headers.ClientNameGoSDK, + historySuffix: nil, + expected: false, + }, + { + name: "CLI client", + clientName: headers.ClientNameCLI, + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + }, + expected: false, + }, + { + name: "UI client", + clientName: headers.ClientNameUI, + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + }, + expected: false, + }, + { + name: "invalid events - first event not scheduled", + clientName: headers.ClientNameGoSDK, + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED, + }, + }, + expected: false, + }, + { + name: "two valid events", + clientName: headers.ClientNameGoSDK, + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + { + EventId: 11, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + ctx := headers.SetVersionsForTests(context.Background(), "1.0.0", tt.clientName, "", "") + tranOrSpecEvents := &historyspb.TransientWorkflowTaskInfo{ + HistorySuffix: tt.historySuffix, + } + + result := shouldIncludeTransientOrSpeculativeTasks(ctx, tranOrSpecEvents) + r.Equal(tt.expected, result) + }) + } +} + +func TestClientSupportsTranOrSpecTasks(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + clientName string + expected bool + }{ + { + name: "CLI client returns false", + clientName: headers.ClientNameCLI, + expected: false, + }, + { + name: "UI client returns false", + clientName: headers.ClientNameUI, + expected: false, + }, + { + name: "Go SDK returns true", + clientName: headers.ClientNameGoSDK, + expected: true, + }, + { + name: "Java SDK returns true", + clientName: headers.ClientNameJavaSDK, + expected: true, + }, + { + name: "TypeScript SDK returns true", + clientName: headers.ClientNameTypeScriptSDK, + expected: true, + }, + { + name: "Python SDK returns true", + clientName: headers.ClientNamePythonSDK, + expected: true, + }, + { + name: "PHP SDK returns true", + clientName: headers.ClientNamePHPSDK, + expected: true, + }, + { + name: "no client name returns true", + clientName: "", + expected: true, + }, + { + name: "unknown client returns true", + clientName: "unknown-client", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + ctx := headers.SetVersionsForTests(context.Background(), "1.0.0", tt.clientName, "", "") + result := ClientSupportsTranOrSpecEvents(ctx) + r.Equal(tt.expected, result) + }) + } + + // Test empty context without any headers set + t.Run("empty context without headers returns true", func(t *testing.T) { + t.Parallel() + r := require.New(t) + + ctx := context.Background() + result := ClientSupportsTranOrSpecEvents(ctx) + r.True(result) + }) +} + +func TestAreValidTransientOrSpecTasks(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + historySuffix []*historypb.HistoryEvent + expected bool + }{ + { + name: "single scheduled event", + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + }, + expected: true, + }, + { + name: "two events with consecutive IDs", + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + { + EventId: 11, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, + }, + }, + expected: true, + }, + { + name: "empty events", + historySuffix: []*historypb.HistoryEvent{}, + expected: false, + }, + { + name: "nil events", + historySuffix: nil, + expected: false, + }, + { + name: "more than two events", + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + { + EventId: 11, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, + }, + { + EventId: 12, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, + }, + }, + expected: false, + }, + { + name: "first event not scheduled", + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, + }, + }, + expected: false, + }, + { + name: "second event not started", + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + { + EventId: 11, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, + }, + }, + expected: false, + }, + { + name: "non-consecutive event IDs", + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + { + EventId: 12, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, + }, + }, + expected: false, + }, + { + name: "same event IDs", + historySuffix: []*historypb.HistoryEvent{ + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, + }, + { + EventId: 10, + EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + tranOrSpecEvents := &historyspb.TransientWorkflowTaskInfo{ + HistorySuffix: tt.historySuffix, + } + + result := areValidTransientOrSpecEvents(tranOrSpecEvents) + r.Equal(tt.expected, result) + }) + } +} diff --git a/service/history/api/get_workflow_util.go b/service/history/api/get_workflow_util.go index 1242d5e5db2..f2a8b380120 100644 --- a/service/history/api/get_workflow_util.go +++ b/service/history/api/get_workflow_util.go @@ -9,6 +9,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" + historyspb "go.temporal.io/server/api/history/v1" "go.temporal.io/server/api/historyservice/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common" @@ -373,6 +374,14 @@ func MutableStateToGetResponse( } } + // Get transient/speculative workflow task events if present + var transientOrSpeculativeTasks *historyspb.TransientWorkflowTaskInfo + if workflowTask := mutableState.GetPendingWorkflowTask(); workflowTask != nil { + transientOrSpeculativeTasks = mutableState.GetTransientWorkflowTaskInfo(workflowTask, "") + } else if workflowTask := mutableState.GetStartedWorkflowTask(); workflowTask != nil { + transientOrSpeculativeTasks = mutableState.GetTransientWorkflowTaskInfo(workflowTask, "") + } + return &historyservice.GetMutableStateResponse{ Execution: &commonpb.WorkflowExecution{ WorkflowId: mutableState.GetExecutionInfo().WorkflowId, @@ -405,6 +414,7 @@ func MutableStateToGetResponse( InheritedBuildId: mutableState.GetInheritedBuildId(), MostRecentWorkerVersionStamp: mostRecentWorkerVersionStamp, TransitionHistory: transitionhistory.CopyVersionedTransitions(mutableState.GetExecutionInfo().TransitionHistory), - VersioningInfo: mutableState.GetExecutionInfo().VersioningInfo, + VersioningInfo: common.CloneProto(mutableState.GetExecutionInfo().VersioningInfo), + TransientOrSpeculativeTasks: transientOrSpeculativeTasks, }, nil } diff --git a/service/history/api/getworkflowexecutionhistory/api.go b/service/history/api/getworkflowexecutionhistory/api.go index c6249fb44a0..64ae77b2c59 100644 --- a/service/history/api/getworkflowexecutionhistory/api.go +++ b/service/history/api/getworkflowexecutionhistory/api.go @@ -29,6 +29,93 @@ import ( historyi "go.temporal.io/server/service/history/interfaces" ) +// appendTransientEvents queries mutable state for transient/speculative events and appends them to the response. +// This function attempts to use cached transient tasks from the initial mutable state call to avoid a second call. +// If cached tasks are nil or stale (validation fails), it falls back to querying mutable state. +// Validation of event IDs is handled by validateTransientWorkflowTaskEvents. +func appendTransientTasks( + ctx context.Context, + shardContext historyi.ShardContext, + workflowConsistencyChecker api.WorkflowConsistencyChecker, + eventNotifier events.Notifier, + namespaceID namespace.ID, + namespaceName string, + execution *commonpb.WorkflowExecution, + useRawHistory bool, + history *historypb.History, + historyBlob *[]*commonpb.DataBlob, + cachedTransientTasks *historyspb.TransientWorkflowTaskInfo, + nextEventID int64, +) { + if !shardContext.GetConfig().SendTransientOrSpeculativeWorkflowTaskEvents(namespaceName) { + return + } + + // CLI and UI clients should not receive transient events for backward compatibility + if !api.ClientSupportsTranOrSpecEvents(ctx) { + return + } + + var transientWorkflowTask *historyspb.TransientWorkflowTaskInfo + + // Try cached tasks first + if cachedTransientTasks != nil { + // Validate cached tasks are still valid (not stale) + if err := api.ValidateTransientWorkflowTaskEvents(nextEventID, cachedTransientTasks); err == nil { + transientWorkflowTask = cachedTransientTasks + } + } + + // If no valid cache, fetch fresh from mutable state + if transientWorkflowTask == nil { + msResp, err := api.GetOrPollWorkflowMutableState( + ctx, + shardContext, + &historyservice.GetMutableStateRequest{ + NamespaceId: namespaceID.String(), + Execution: execution, + }, + workflowConsistencyChecker, + eventNotifier, + ) + if err != nil { + // Transient events don't exist or are already committed - this is OK + // Just return without appending (events are in persisted history) + return + } + transientWorkflowTask = msResp.GetTransientOrSpeculativeTasks() + if transientWorkflowTask == nil { + return + } + + if msResp.GetWorkflowStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING { + return + } + + if err := api.ValidateTransientWorkflowTaskEvents(nextEventID, transientWorkflowTask); err != nil { + return + } + } + + // Manually append transient events to the response + if useRawHistory { + transientEventsBlob, err := shardContext.GetPayloadSerializer().SerializeEvents(transientWorkflowTask.GetHistorySuffix()) + if err == nil { + *historyBlob = append(*historyBlob, transientEventsBlob) + } else { + softassert.Fail(shardContext.GetLogger(), "GetWorkflowExecutionHistory could not serialize transient workflow tasks") + shardContext.GetLogger().Error("Failed to serialize transient workflow tasks", + tag.WorkflowNamespaceID(namespaceID.String()), + tag.WorkflowID(execution.GetWorkflowId()), + tag.WorkflowRunID(execution.GetRunId()), + tag.Error(err), + ) + } + } else { + history.Events = append(history.Events, transientWorkflowTask.GetHistorySuffix()...) + } +} + func Invoke( ctx context.Context, shardContext historyi.ShardContext, @@ -47,22 +134,24 @@ func Invoke( isCloseEventOnly := request.Request.GetHistoryEventFilterType() == enumspb.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT - // this function returns the following 7 things, - // 1. the current branch token (to use to retrieve history events) - // 2. the workflow run ID - // 3. the last first event ID (the event ID of the last batch of events in the history) - // 4. the last first event transaction id - // 5. the next event ID - // 6. whether the workflow is running - // 7. error if any - queryHistory := func( + queryMutableState := func( namespaceUUID namespace.ID, execution *commonpb.WorkflowExecution, expectedNextEventID int64, currentBranchToken []byte, versionHistoryItem *historyspb.VersionHistoryItem, versionedTransition *persistencespb.VersionedTransition, - ) ([]byte, string, int64, int64, bool, *historyspb.VersionHistoryItem, *persistencespb.VersionedTransition, error) { + ) ( + []byte, // current branch token (to use to retrieve history events) + string, // workflow run ID + int64, // last first event ID (the event ID of the last batch of events in the history) + int64, // last first event transaction id + bool, // whether the workflow is running + *historyspb.VersionHistoryItem, // version history item for the current branch + *persistencespb.VersionedTransition, // last versioned transition + *historyspb.TransientWorkflowTaskInfo, // transient workflow task info + error, // error if any + ) { response, err := api.GetOrPollWorkflowMutableState( ctx, shardContext, @@ -103,17 +192,17 @@ func Invoke( ) } if err != nil { - return nil, "", 0, 0, false, nil, nil, err + return nil, "", 0, 0, false, nil, nil, nil, err } isWorkflowRunning := response.GetWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING currentVersionHistory, err := versionhistory.GetCurrentVersionHistory(response.GetVersionHistories()) if err != nil { - return nil, "", 0, 0, false, nil, nil, err + return nil, "", 0, 0, false, nil, nil, nil, err } lastVersionHistoryItem, err := versionhistory.GetLastVersionHistoryItem(currentVersionHistory) if err != nil { - return nil, "", 0, 0, false, nil, nil, err + return nil, "", 0, 0, false, nil, nil, nil, err } lastVersionedTransition := transitionhistory.LastVersionedTransition(response.GetTransitionHistory()) @@ -124,6 +213,7 @@ func Invoke( isWorkflowRunning, lastVersionHistoryItem, lastVersionedTransition, + response.GetTransientOrSpeculativeTasks(), nil } @@ -135,6 +225,7 @@ func Invoke( lastFirstEventID := common.FirstEventID var nextEventID int64 var isWorkflowRunning bool + var cachedTransientTasks *historyspb.TransientWorkflowTaskInfo // process the token for paging queryNextEventID := common.EndEventID @@ -149,13 +240,16 @@ func Invoke( execution.RunId = continuationToken.GetRunId() + // Set isWorkflowRunning from continuation token for pagination + isWorkflowRunning = continuationToken.IsWorkflowRunning + // we need to update the current next event ID and whether workflow is running if len(continuationToken.PersistenceToken) == 0 && isLongPoll && continuationToken.IsWorkflowRunning { if !isCloseEventOnly { queryNextEventID = continuationToken.GetNextEventId() } - continuationToken.BranchToken, _, lastFirstEventID, nextEventID, isWorkflowRunning, continuationToken.VersionHistoryItem, continuationToken.VersionedTransition, err = - queryHistory(namespaceID, execution, queryNextEventID, continuationToken.BranchToken, continuationToken.VersionHistoryItem, continuationToken.VersionedTransition) + continuationToken.BranchToken, _, lastFirstEventID, nextEventID, isWorkflowRunning, continuationToken.VersionHistoryItem, continuationToken.VersionedTransition, cachedTransientTasks, err = + queryMutableState(namespaceID, execution, queryNextEventID, continuationToken.BranchToken, continuationToken.VersionHistoryItem, continuationToken.VersionedTransition) if err != nil { return nil, err } @@ -168,8 +262,8 @@ func Invoke( if !isCloseEventOnly { queryNextEventID = common.FirstEventID } - continuationToken.BranchToken, runID, lastFirstEventID, nextEventID, isWorkflowRunning, continuationToken.VersionHistoryItem, continuationToken.VersionedTransition, err = - queryHistory(namespaceID, execution, queryNextEventID, nil, nil, nil) + continuationToken.BranchToken, runID, lastFirstEventID, nextEventID, isWorkflowRunning, continuationToken.VersionHistoryItem, continuationToken.VersionedTransition, cachedTransientTasks, err = + queryMutableState(namespaceID, execution, queryNextEventID, nil, nil, nil) if err != nil { return nil, err } @@ -207,6 +301,29 @@ func Invoke( config := shardContext.GetConfig() sendRawHistoryBetweenInternalServices := config.SendRawHistoryBetweenInternalServices() sendRawWorkflowHistoryForNamespace := config.SendRawWorkflowHistory(request.Request.GetNamespace()) + // fetchGapEvents fetches events in [fromEventID, toEventID) from persistence and appends + // them to the current response (history or historyBlob). Used to close gaps that form + // when events are committed to DB between paginated GetWorkflowExecutionHistory calls. + fetchGapEvents := func(fromEventID, toEventID int64, branchToken []byte) error { + if sendRawWorkflowHistoryForNamespace || sendRawHistoryBetweenInternalServices { + gapBlob, _, err := api.GetRawHistory(ctx, shardContext, namespaceName, namespaceID, execution, + fromEventID, toEventID, request.Request.GetMaximumPageSize(), nil, nil, branchToken) + if err != nil { + return err + } + historyBlob = append(historyBlob, gapBlob...) + } else { + gapHistory, _, err := api.GetHistory(ctx, shardContext, namespaceName, namespaceID, execution, + fromEventID, toEventID, request.Request.GetMaximumPageSize(), nil, nil, branchToken, persistenceVisibilityMgr) + if err != nil { + return err + } + if gapHistory != nil { + history.Events = append(history.Events, gapHistory.Events...) + } + } + return nil + } if isCloseEventOnly { if !isWorkflowRunning { if sendRawWorkflowHistoryForNamespace || sendRawHistoryBetweenInternalServices { @@ -220,7 +337,7 @@ func Invoke( nextEventID, request.Request.GetMaximumPageSize(), nil, - continuationToken.TransientWorkflowTask, + nil, continuationToken.BranchToken, ) if err != nil { @@ -239,7 +356,7 @@ func Invoke( nextEventID, request.Request.GetMaximumPageSize(), nil, - continuationToken.TransientWorkflowTask, + nil, continuationToken.BranchToken, persistenceVisibilityMgr, ) @@ -299,7 +416,7 @@ func Invoke( continuationToken.NextEventId, request.Request.GetMaximumPageSize(), continuationToken.PersistenceToken, - continuationToken.TransientWorkflowTask, + nil, continuationToken.BranchToken, ) } else { @@ -313,7 +430,7 @@ func Invoke( continuationToken.NextEventId, request.Request.GetMaximumPageSize(), continuationToken.PersistenceToken, - continuationToken.TransientWorkflowTask, + nil, continuationToken.BranchToken, persistenceVisibilityMgr, ) @@ -323,6 +440,49 @@ func Invoke( return nil, err } + // Query and append transient/speculative tasks if on last page + if len(continuationToken.PersistenceToken) == 0 { + // Re-query mutable state to detect events committed to DB during pagination (race condition fix). + // When a speculative/transient WFT times out or fails between the first and last DB page fetches, + // those events are committed to DB with IDs < continuationToken.NextEventId but were excluded + // because the DB fetch was capped at the original boundary. Fetch the gap now, then update + // the nextEventID boundary so appendTransientTasks validates against the correct ID. + _, _, _, freshNextEventID, freshIsRunning, freshVersionHistoryItem, freshVersionedTransition, freshTransientTasks, freshErr := + queryMutableState(namespaceID, execution, common.EmptyEventID, + continuationToken.BranchToken, continuationToken.VersionHistoryItem, continuationToken.VersionedTransition) + if freshErr != nil { + return nil, freshErr + } + if freshNextEventID > continuationToken.NextEventId { + // Events were committed to DB during pagination — fetch the gap. + if freshErr = fetchGapEvents(continuationToken.NextEventId, freshNextEventID, continuationToken.BranchToken); freshErr != nil { + return nil, freshErr + } + // Update the event boundary so appendTransientTasks validates against the correct nextEventID. + continuationToken.NextEventId = freshNextEventID + continuationToken.IsWorkflowRunning = freshIsRunning + continuationToken.VersionHistoryItem = freshVersionHistoryItem + continuationToken.VersionedTransition = freshVersionedTransition + // Provide fresh transient tasks to appendTransientTasks to avoid a redundant re-query. + cachedTransientTasks = freshTransientTasks + } + + appendTransientTasks( + ctx, + shardContext, + workflowConsistencyChecker, + eventNotifier, + namespaceID, + namespaceName.String(), + execution, + sendRawWorkflowHistoryForNamespace || sendRawHistoryBetweenInternalServices, + history, + &historyBlob, + cachedTransientTasks, + continuationToken.NextEventId, + ) + } + // here, for long pull on history events, we need to intercept the paging token from cassandra // and do something clever if len(continuationToken.PersistenceToken) == 0 && (!continuationToken.IsWorkflowRunning || !isLongPoll) { diff --git a/service/history/api/getworkflowexecutionrawhistory/api.go b/service/history/api/getworkflowexecutionrawhistory/api.go index 71b1caec0ab..954743d4f60 100644 --- a/service/history/api/getworkflowexecutionrawhistory/api.go +++ b/service/history/api/getworkflowexecutionrawhistory/api.go @@ -13,6 +13,7 @@ import ( "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" + "go.temporal.io/server/common/persistence/serialization" "go.temporal.io/server/common/persistence/versionhistory" "go.temporal.io/server/common/rpc/interceptor" "go.temporal.io/server/service/history/api" @@ -130,9 +131,16 @@ func Invoke( metrics.OperationTag(metrics.AdminGetWorkflowExecutionRawHistoryScope), ) + // Ensure all raw history is proto3 encoded since data may be stored in other formats during testing. + // In production (proto3 encoding), this returns the input unchanged. + historyBlobs, err := serialization.ReencodeEventBlobsAsProto3(shardContext.GetPayloadSerializer(), rawHistoryResponse.HistoryEventBlobs) + if err != nil { + return nil, err + } + result := &adminservice.GetWorkflowExecutionRawHistoryResponse{ - HistoryBatches: rawHistoryResponse.HistoryEventBlobs, + HistoryBatches: historyBlobs, VersionHistory: targetVersionHistory, HistoryNodeIds: rawHistoryResponse.NodeIDs, } diff --git a/service/history/api/getworkflowexecutionrawhistoryv2/api.go b/service/history/api/getworkflowexecutionrawhistoryv2/api.go index 728f4d72b6f..38aa9567350 100644 --- a/service/history/api/getworkflowexecutionrawhistoryv2/api.go +++ b/service/history/api/getworkflowexecutionrawhistoryv2/api.go @@ -13,6 +13,7 @@ import ( "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" + "go.temporal.io/server/common/persistence/serialization" "go.temporal.io/server/common/persistence/versionhistory" "go.temporal.io/server/common/rpc/interceptor" "go.temporal.io/server/service/history/api" @@ -136,9 +137,16 @@ func Invoke( metrics.OperationTag(metrics.AdminGetWorkflowExecutionRawHistoryV2Scope), ) + // Ensure all raw history is proto3 encoded since data may be stored in other formats during testing. + // In production (proto3 encoding), this returns the input unchanged. + historyBlobs, err := serialization.ReencodeEventBlobsAsProto3(shardContext.GetPayloadSerializer(), rawHistoryResponse.HistoryEventBlobs) + if err != nil { + return nil, err + } + result := &adminservice.GetWorkflowExecutionRawHistoryV2Response{ - HistoryBatches: rawHistoryResponse.HistoryEventBlobs, + HistoryBatches: historyBlobs, VersionHistory: targetVersionHistory, HistoryNodeIds: rawHistoryResponse.NodeIDs, } diff --git a/service/history/api/listqueues/listqueuestest/apitest.go b/service/history/api/listqueues/listqueuestest/apitest.go index 174374d44db..f7351e0e23f 100644 --- a/service/history/api/listqueues/listqueuestest/apitest.go +++ b/service/history/api/listqueues/listqueuestest/apitest.go @@ -26,7 +26,7 @@ func TestInvoke(t *testing.T, manager persistence.HistoryTaskQueueManager) { targetCluster := "test-target-cluster-" + t.Name() queueType := persistence.QueueTypeHistoryDLQ var queueKeys []persistence.QueueKey - for i := 0; i < 3; i++ { + for i := range 3 { queueKey := persistence.QueueKey{ QueueType: queueType, Category: inTask.GetCategory(), diff --git a/service/history/api/listtasks/api_test.go b/service/history/api/listtasks/api_test.go index 197f614f3a8..30f859c419f 100644 --- a/service/history/api/listtasks/api_test.go +++ b/service/history/api/listtasks/api_test.go @@ -137,7 +137,7 @@ func (s *apiSuite) TestGetHistoryTasks() { respNextPageToken := []byte("resp-next-page-token") fakeTasks := make([]tasks.Task, 0, batchSize) - for i := 0; i < batchSize; i++ { + for i := range batchSize { fakeTask := tasks.NewFakeTask( tests.WorkflowKey, tasks.CategoryTransfer, diff --git a/service/history/api/multioperation/api.go b/service/history/api/multioperation/api.go index c0d8bd84f68..8941b040dcb 100644 --- a/service/history/api/multioperation/api.go +++ b/service/history/api/multioperation/api.go @@ -142,7 +142,7 @@ func Invoke( return nil, err } - testhooks.Call(uws.testHooks, testhooks.UpdateWithStartOnClosingWorkflowRetry) + testhooks.Call(uws.testHooks, testhooks.UpdateWithStartOnClosingWorkflowRetry, uws.namespaceId) res, err = uws.Invoke(ctx) if err != nil { @@ -229,7 +229,7 @@ func (uws *updateWithStart) Invoke(ctx context.Context) (*historyservice.Execute workflowLease.GetReleaseFn()(nil) } - testhooks.Call(uws.testHooks, testhooks.UpdateWithStartInBetweenLockAndStart) + testhooks.Call(uws.testHooks, testhooks.UpdateWithStartInBetweenLockAndStart, uws.namespaceId) // Workflow does not exist or requires a new run - start and update it! return uws.startAndUpdateWorkflow(ctx) diff --git a/service/history/api/pauseactivity/api.go b/service/history/api/pauseactivity/api.go index 568e8b0492c..3221ed0787d 100644 --- a/service/history/api/pauseactivity/api.go +++ b/service/history/api/pauseactivity/api.go @@ -7,6 +7,8 @@ import ( "go.temporal.io/server/api/historyservice/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/definition" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/consts" historyi "go.temporal.io/server/service/history/interfaces" @@ -20,6 +22,7 @@ func Invoke( shardContext historyi.ShardContext, workflowConsistencyChecker api.WorkflowConsistencyChecker, ) (resp *historyservice.PauseActivityResponse, retError error) { + err := api.GetAndUpdateWorkflowWithNew( ctx, nil, @@ -78,5 +81,16 @@ func Invoke( return nil, err } + targetingMethod := "type" + if _, ok := request.GetFrontendRequest().GetActivity().(*workflowservice.PauseActivityRequest_Id); ok { + targetingMethod = "id" + } + if ns, err := shardContext.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(request.NamespaceId)); err == nil { + metrics.ActivityPauseRequests.With(shardContext.GetMetricsHandler().WithTags( + metrics.NamespaceTag(ns.Name().String()), + metrics.ActivityTargetingMethodTag(targetingMethod), + )).Record(1) + } + return &historyservice.PauseActivityResponse{}, nil } diff --git a/service/history/api/reapplyevents/api.go b/service/history/api/reapplyevents/api.go index 421af46c96b..06d9d796250 100644 --- a/service/history/api/reapplyevents/api.go +++ b/service/history/api/reapplyevents/api.go @@ -131,7 +131,6 @@ func Invoke( baseRebuildLastEventVersion, baseNextEventID, resetRunID.String(), - uuid.New().String(), baseWorkflow, baseWorkflow, ndc.EventsReapplicationResetWorkflowReason, diff --git a/service/history/api/recordactivitytaskstarted/api.go b/service/history/api/recordactivitytaskstarted/api.go index bb8fd4c56c1..1acc162af32 100644 --- a/service/history/api/recordactivitytaskstarted/api.go +++ b/service/history/api/recordactivitytaskstarted/api.go @@ -238,7 +238,7 @@ func recordActivityTaskStarted( } } - versioningStamp := worker_versioning.StampFromCapabilities(request.PollRequest.WorkerVersionCapabilities) + versioningStamp := worker_versioning.StampFromCapabilities(request.PollRequest.WorkerVersionCapabilities, request.PollRequest.DeploymentOptions) //nolint:staticcheck // SA1019: WorkerVersionCapabilities is deprecated but still used for old versioning [cleanup-old-wv] if _, err := mutableState.AddActivityTaskStartedEvent( ai, scheduledEventID, requestID, request.PollRequest.GetIdentity(), versioningStamp, pollerDeployment, request.GetBuildIdRedirectInfo(), diff --git a/service/history/api/recordworkflowtaskstarted/api.go b/service/history/api/recordworkflowtaskstarted/api.go index 7d76267f8ed..e75db5684e0 100644 --- a/service/history/api/recordworkflowtaskstarted/api.go +++ b/service/history/api/recordworkflowtaskstarted/api.go @@ -165,7 +165,7 @@ func Invoke( requestID, pollerTaskQueue, req.PollRequest.Identity, - worker_versioning.StampFromCapabilities(req.PollRequest.WorkerVersionCapabilities), + worker_versioning.StampFromCapabilities(req.PollRequest.WorkerVersionCapabilities, req.PollRequest.DeploymentOptions), //nolint:staticcheck // SA1019: WorkerVersionCapabilities is deprecated but still used for old versioning [cleanup-old-wv] req.GetBuildIdRedirectInfo(), workflowLease.GetContext().UpdateRegistry(ctx), false, @@ -336,12 +336,11 @@ func setHistoryForRecordWfTaskStartedResp( var continuation []byte if len(persistenceToken) != 0 { continuation, err = api.SerializeHistoryToken(&tokenspb.HistoryContinuation{ - RunId: workflowKey.GetRunID(), - FirstEventId: firstEventID, - NextEventId: nextEventID, - PersistenceToken: persistenceToken, - TransientWorkflowTask: response.GetTransientWorkflowTask(), - BranchToken: response.GetBranchToken(), + RunId: workflowKey.GetRunID(), + FirstEventId: firstEventID, + NextEventId: nextEventID, + PersistenceToken: persistenceToken, + BranchToken: response.GetBranchToken(), }) if err != nil { return err diff --git a/service/history/api/resetactivity/api.go b/service/history/api/resetactivity/api.go index d2e02199359..6b46ee16fff 100644 --- a/service/history/api/resetactivity/api.go +++ b/service/history/api/resetactivity/api.go @@ -6,6 +6,8 @@ import ( "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/common/definition" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/consts" historyi "go.temporal.io/server/service/history/interfaces" @@ -19,6 +21,7 @@ func Invoke( workflowConsistencyChecker api.WorkflowConsistencyChecker, ) (resp *historyservice.ResetActivityResponse, retError error) { request := req.GetFrontendRequest() + workflowKey := definition.NewWorkflowKey( req.NamespaceId, request.GetExecution().GetWorkflowId(), @@ -72,5 +75,16 @@ func Invoke( return nil, err } + targetingMethod := "type" + if _, ok := req.GetFrontendRequest().GetActivity().(*workflowservice.ResetActivityRequest_Id); ok { + targetingMethod = "id" + } + if ns, err := shardContext.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(req.NamespaceId)); err == nil { + metrics.ActivityResetRequests.With(shardContext.GetMetricsHandler().WithTags( + metrics.NamespaceTag(ns.Name().String()), + metrics.ActivityTargetingMethodTag(targetingMethod), + )).Record(1) + } + return &historyservice.ResetActivityResponse{}, nil } diff --git a/service/history/api/resetworkflow/api.go b/service/history/api/resetworkflow/api.go index a05ded26017..42ad3579115 100644 --- a/service/history/api/resetworkflow/api.go +++ b/service/history/api/resetworkflow/api.go @@ -163,7 +163,6 @@ func Invoke( baseRebuildLastEventVersion, baseNextEventID, resetRunID, - request.GetRequestId(), baseWorkflow, ndc.NewWorkflow( shardContext.GetClusterMetadata(), diff --git a/service/history/api/respondactivitytaskcanceled/api.go b/service/history/api/respondactivitytaskcanceled/api.go index 66d1af600e8..3d0fabf8e0f 100644 --- a/service/history/api/respondactivitytaskcanceled/api.go +++ b/service/history/api/respondactivitytaskcanceled/api.go @@ -10,6 +10,7 @@ import ( "go.temporal.io/server/common/definition" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/tasktoken" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/consts" @@ -96,8 +97,8 @@ func Invoke( return nil, err } - attemptStartedTime = ai.StartedTime.AsTime() - firstScheduledTime = ai.FirstScheduledTime.AsTime() + attemptStartedTime = timestamp.TimeValue(ai.StartedTime) + firstScheduledTime = timestamp.TimeValue(ai.FirstScheduledTime) taskQueue = ai.TaskQueue versioningBehavior = mutableState.GetEffectiveVersioningBehavior() return &api.UpdateWorkflowAction{ diff --git a/service/history/api/respondactivitytaskcompleted/api.go b/service/history/api/respondactivitytaskcompleted/api.go index 99675b340a2..f8af8bea732 100644 --- a/service/history/api/respondactivitytaskcompleted/api.go +++ b/service/history/api/respondactivitytaskcompleted/api.go @@ -10,6 +10,7 @@ import ( "go.temporal.io/server/common/definition" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/tasktoken" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/consts" @@ -112,9 +113,9 @@ func Invoke( } if !fabricateStartedEvent { // leave it zero if the event is fabricated so the latency metrics are not emitted - attemptStartedTime = ai.StartedTime.AsTime() + attemptStartedTime = timestamp.TimeValue(ai.StartedTime) } - firstScheduledTime = ai.FirstScheduledTime.AsTime() + firstScheduledTime = timestamp.TimeValue(ai.FirstScheduledTime) taskQueue = ai.TaskQueue versioningBehavior = mutableState.GetEffectiveVersioningBehavior() return &api.UpdateWorkflowAction{ diff --git a/service/history/api/respondactivitytaskfailed/api.go b/service/history/api/respondactivitytaskfailed/api.go index 0f70117d93e..2108eed3dd9 100644 --- a/service/history/api/respondactivitytaskfailed/api.go +++ b/service/history/api/respondactivitytaskfailed/api.go @@ -11,6 +11,7 @@ import ( "go.temporal.io/server/common/definition" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/tasktoken" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/consts" @@ -95,6 +96,11 @@ func Invoke( postActions := &api.UpdateWorkflowAction{} failure := request.GetFailure() + // RetryActivity mutates ai in place and clears per-attempt timing fields when it resets the activity for retry. + // Capture the metric inputs before that mutation so we record the attempt that just finished. + attemptStartedTime = timestamp.TimeValue(ai.GetStartedTime()) + firstScheduledTime = timestamp.TimeValue(ai.FirstScheduledTime) + taskQueue = ai.TaskQueue mutableState.RecordLastActivityCompleteTime(ai) retryState, err := mutableState.RetryActivity(ai, failure) if err != nil { @@ -114,9 +120,6 @@ func Invoke( closed = false } - attemptStartedTime = ai.StartedTime.AsTime() - firstScheduledTime = ai.FirstScheduledTime.AsTime() - taskQueue = ai.TaskQueue versioningBehavior = mutableState.GetEffectiveVersioningBehavior() return postActions, nil }, diff --git a/service/history/api/respondactivitytaskfailed/api_test.go b/service/history/api/respondactivitytaskfailed/api_test.go index 62ddaaf0d98..6f5dea10047 100644 --- a/service/history/api/respondactivitytaskfailed/api_test.go +++ b/service/history/api/respondactivitytaskfailed/api_test.go @@ -4,11 +4,13 @@ import ( "context" "fmt" "testing" + "time" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" "go.temporal.io/api/workflowservice/v1" enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/api/historyservice/v1" @@ -83,6 +85,7 @@ type UsecaseConfig struct { isActivityActive bool isExecutionRunning bool expectRetryActivity bool + customRetryActivity bool retryActivityError error retryActivityState enumspb.RetryState namespaceId namespace.ID @@ -116,6 +119,37 @@ func (s *workflowSuite) Test_NormalFlowShouldRescheduleActivity_UpdatesWorkflowE s.NoError(err) } +func (s *workflowSuite) Test_NormalFlowShouldRescheduleActivity_UsesOriginalStartedTimeForMetrics() { + ctx := context.Background() + uc := newUseCase(UsecaseConfig{ + attempt: int32(1), + startedEventId: int64(40), + scheduledEventId: int64(42), + taskQueueId: "some-task-queue", + expectRetryActivity: true, + customRetryActivity: true, + isCacheStale: false, + retryActivityState: enumspb.RETRY_STATE_IN_PROGRESS, + }) + request := s.newRespondActivityTaskFailedRequest(uc) + s.setupStubs(uc) + + originalStartedTime := s.activityInfo.GetStartedTime().AsTime() + s.currentMutableState.EXPECT().RecordLastActivityCompleteTime(gomock.Any()) + s.currentMutableState.EXPECT().RetryActivity(s.activityInfo, gomock.Any()).DoAndReturn( + func(ai *persistencespb.ActivityInfo, _ *failurepb.Failure) (enumspb.RetryState, error) { + ai.StartedTime = nil + return enumspb.RETRY_STATE_IN_PROGRESS, nil + }, + ) + s.expectTransientFailureMetricsRecordedWithStartedTime(uc, s.shardContext, originalStartedTime) + s.workflowContext.EXPECT().UpdateWorkflowExecutionAsActive(ctx, s.shardContext).Return(nil) + s.currentMutableState.EXPECT().GetEffectiveVersioningBehavior().Return(enumspb.VERSIONING_BEHAVIOR_UNSPECIFIED) + + _, err := Invoke(ctx, request, s.shardContext, s.workflowConsistencyChecker) + s.NoError(err) +} + func (s *workflowSuite) Test_WorkflowExecutionIsNotRunning_ReturnWorkflowNotRunningError() { uc := newUseCase(UsecaseConfig{ attempt: int32(1), @@ -465,6 +499,46 @@ func (s *workflowSuite) expectTransientFailureMetricsRecorded(uc UsecaseConfig, shardContext.EXPECT().GetMetricsHandler().Return(metricsHandler).AnyTimes() } +func (s *workflowSuite) expectTransientFailureMetricsRecordedWithStartedTime( + uc UsecaseConfig, + shardContext *historyi.MockShardContext, + startedTime time.Time, +) { + startToCloseTimer := metrics.NewMockTimerIface(s.controller) + e2eTimer := metrics.NewMockTimerIface(s.controller) + counter := metrics.NewMockCounterIface(s.controller) + tags := []metrics.Tag{ + metrics.OperationTag(metrics.HistoryRespondActivityTaskFailedScope), + metrics.WorkflowTypeTag(uc.wfType.Name), + metrics.ActivityTypeTag(uc.activityType), + metrics.VersioningBehaviorTag(enumspb.VERSIONING_BEHAVIOR_UNSPECIFIED), + metrics.NamespaceTag(uc.namespaceName.String()), + metrics.UnsafeTaskQueueTag(uc.taskQueueId), + } + + metricsHandler := metrics.NewMockHandler(s.controller) + metricsHandler.EXPECT().WithTags(tags).Return(metricsHandler) + + assertReasonableAttemptDuration := func(d time.Duration) { + s.Greater(d, time.Second) + s.Less(d, time.Minute) + s.InDelta(time.Since(startedTime), d, float64(5*time.Second)) + } + e2eTimer.EXPECT().Record(gomock.AssignableToTypeOf(time.Duration(0))).Do(func(d time.Duration, _ ...metrics.Tag) { + assertReasonableAttemptDuration(d) + }).Times(1) + startToCloseTimer.EXPECT().Record(gomock.AssignableToTypeOf(time.Duration(0))).Do(func(d time.Duration, _ ...metrics.Tag) { + assertReasonableAttemptDuration(d) + }).Times(1) + metricsHandler.EXPECT().Timer(metrics.ActivityE2ELatency.Name()).Return(e2eTimer) + metricsHandler.EXPECT().Timer(metrics.ActivityStartToCloseLatency.Name()).Return(startToCloseTimer) + + counter.EXPECT().Record(int64(1)) + metricsHandler.EXPECT().Counter(metrics.ActivityTaskFail.Name()).Return(counter) + + shardContext.EXPECT().GetMetricsHandler().Return(metricsHandler).AnyTimes() +} + func (s *workflowSuite) expectTerminalFailureMetricsRecorded(uc UsecaseConfig, shardContext *historyi.MockShardContext) { timer := metrics.NewMockTimerIface(s.controller) counter := metrics.NewMockCounterIface(s.controller) @@ -549,8 +623,10 @@ func (s *workflowSuite) setupMutableState(uc UsecaseConfig, ai *persistencespb.A currentMutableState.EXPECT().GetWorkflowType().Return(uc.wfType).AnyTimes() if uc.expectRetryActivity { - currentMutableState.EXPECT().RecordLastActivityCompleteTime(gomock.Any()) - currentMutableState.EXPECT().RetryActivity(ai, gomock.Any()).Return(uc.retryActivityState, uc.retryActivityError) + if !uc.customRetryActivity { + currentMutableState.EXPECT().RecordLastActivityCompleteTime(gomock.Any()) + currentMutableState.EXPECT().RetryActivity(ai, gomock.Any()).Return(uc.retryActivityState, uc.retryActivityError) + } currentMutableState.EXPECT().HasPendingWorkflowTask().Return(false).AnyTimes() } return currentMutableState @@ -558,9 +634,11 @@ func (s *workflowSuite) setupMutableState(uc UsecaseConfig, ai *persistencespb.A func (s *workflowSuite) setupActivityInfo(uc UsecaseConfig) *persistencespb.ActivityInfo { return &persistencespb.ActivityInfo{ - ScheduledEventId: uc.scheduledEventId, - Attempt: uc.attempt, - StartedEventId: uc.startedEventId, - TaskQueue: uc.taskQueueId, + ScheduledEventId: uc.scheduledEventId, + Attempt: uc.attempt, + FirstScheduledTime: timestamp.TimeNowPtrUtcAddSeconds(-10), + StartedEventId: uc.startedEventId, + StartedTime: timestamp.TimeNowPtrUtcAddSeconds(-5), + TaskQueue: uc.taskQueueId, } } diff --git a/service/history/api/respondworkflowtaskcompleted/api.go b/service/history/api/respondworkflowtaskcompleted/api.go index 39e79877459..8c5ca2afb64 100644 --- a/service/history/api/respondworkflowtaskcompleted/api.go +++ b/service/history/api/respondworkflowtaskcompleted/api.go @@ -860,12 +860,11 @@ func (handler *WorkflowTaskCompletedHandler) createPollWorkflowTaskQueueResponse if len(persistenceToken) != 0 { continuation, err = api.SerializeHistoryToken(&tokenspb.HistoryContinuation{ - RunId: matchingResp.WorkflowExecution.GetRunId(), - FirstEventId: firstEventID, - NextEventId: nextEventID, - PersistenceToken: persistenceToken, - TransientWorkflowTask: matchingResp.GetTransientWorkflowTask(), - BranchToken: branchToken, + RunId: matchingResp.WorkflowExecution.GetRunId(), + FirstEventId: firstEventID, + NextEventId: nextEventID, + PersistenceToken: persistenceToken, + BranchToken: branchToken, }) if err != nil { return nil, err diff --git a/service/history/api/respondworkflowtaskcompleted/api_test.go b/service/history/api/respondworkflowtaskcompleted/api_test.go index 9120b363cc3..bbbdcb9a8da 100644 --- a/service/history/api/respondworkflowtaskcompleted/api_test.go +++ b/service/history/api/respondworkflowtaskcompleted/api_test.go @@ -487,7 +487,7 @@ func (s *WorkflowTaskCompletedHandlerSuite) TestUpdateWorkflow() { ms, err := wfContext.LoadMutableState(context.Background(), s.workflowTaskCompletedHandler.shardContext) s.NoError(err) - for i := 0; i < 11; i++ { + for i := range 11 { _, _, err = ms.AddTimerStartedEvent( 1, &commandpb.StartTimerCommandAttributes{ @@ -605,7 +605,7 @@ func (s *WorkflowTaskCompletedHandlerSuite) TestHandleBufferedQueries() { constructQueryRegistry := func(numQueries int) historyi.QueryRegistry { queryRegistry := workflow.NewQueryRegistry() - for i := 0; i < numQueries; i++ { + for range numQueries { queryRegistry.BufferQuery(&querypb.WorkflowQuery{}) } return queryRegistry diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_size_checker_test.go b/service/history/api/respondworkflowtaskcompleted/workflow_size_checker_test.go index 7ce59aae142..9360bcc3596 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_size_checker_test.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_size_checker_test.go @@ -135,7 +135,7 @@ func TestWorkflowSizeChecker_NumChildWorkflows(t *testing.T) { } { if len(msg) > 0 { logger.EXPECT().Error(msg, gomock.Any()).Do(func(msg string, tags ...tag.Tag) { - var namespaceID, workflowID, runID interface{} + var namespaceID, workflowID, runID any for _, t := range tags { if t.Key() == "wf-namespace-id" { namespaceID = t.Value() diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 980eff9e9ac..bae4884e613 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -461,6 +461,7 @@ func (handler *workflowTaskCompletedHandler) handleCommandScheduleActivity( namespaceID, attr, executionInfo.WorkflowRunTimeout, + executionInfo.TaskQueue, ) }, ); err != nil || handler.stopProcessing { @@ -620,6 +621,7 @@ func (handler *workflowTaskCompletedHandler) handlePostCommandEagerExecuteActivi HeartbeatDetails: ai.LastHeartbeatDetails, WorkflowType: handler.mutableState.GetWorkflowType(), WorkflowNamespace: handler.mutableState.GetNamespaceEntry().Name().String(), + Priority: ai.Priority, } metrics.ActivityEagerExecutionCounter.With( workflow.GetPerTaskQueueFamilyScope(handler.metricsHandler, handler.mutableState.GetNamespaceEntry().Name(), ai.TaskQueue, handler.config), diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go index 7e575fdcde7..1ed8808cde5 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler_test.go @@ -77,6 +77,7 @@ func TestCommandProtocolMessage(t *testing.T) { nsReg := nsregistry.NewRegistry( mockMeta, true, + "active", func() time.Duration { return 1 * time.Hour }, dynamicconfig.GetBoolPropertyFn(false), metricsHandler, diff --git a/service/history/api/respondworkflowtaskfailed/api.go b/service/history/api/respondworkflowtaskfailed/api.go index 43fc6ad280f..410d8e1758f 100644 --- a/service/history/api/respondworkflowtaskfailed/api.go +++ b/service/history/api/respondworkflowtaskfailed/api.go @@ -8,6 +8,7 @@ import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/definition" + "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/tasktoken" @@ -98,14 +99,21 @@ func Invoke( return api.UpdateWorkflowTerminate, nil } - + //nolint:staticcheck // SA1019 + versioningStamp := request.GetWorkerVersion() + if versioningStamp.GetUseVersioning() && mutableState.GetAssignedBuildId() == "" { + // WV2 is not used. making sure the versioning stamp does not go through otherwise the + // workflow will start using WV2 which can cause issues. + // TODO: remove this block after deleting old wv [cleanup-old-wv] + versioningStamp = nil + } if _, err := mutableState.AddWorkflowTaskFailedEvent( workflowTask, request.GetCause(), request.GetFailure(), request.GetIdentity(), //nolint:staticcheck - request.GetWorkerVersion(), + versioningStamp, //nolint:staticcheck request.GetBinaryChecksum(), "", @@ -114,6 +122,15 @@ func Invoke( return nil, err } + shardContext.GetLogger().Warn("workflow task failed", + tag.WorkflowNamespaceID(token.NamespaceId), + tag.WorkflowID(token.WorkflowId), + tag.WorkflowRunID(token.RunId), + tag.WorkflowTaskFailedCause(request.GetCause()), + tag.NewStringTag("failure-message", request.GetFailure().GetMessage()), + tag.NewStringTag("failure-source", request.GetFailure().GetSource()), + ) + // TODO (alex-update): if it was speculative WT that failed, and there is nothing but pending updates, // new WT also should be create as speculative (or not?). Currently, it will be recreated as normal WT. return &api.UpdateWorkflowAction{ diff --git a/service/history/api/unpauseactivity/api.go b/service/history/api/unpauseactivity/api.go index d6407e34b77..d1c2865b239 100644 --- a/service/history/api/unpauseactivity/api.go +++ b/service/history/api/unpauseactivity/api.go @@ -6,6 +6,9 @@ import ( "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/common/definition" + "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/consts" historyi "go.temporal.io/server/service/history/interfaces" @@ -49,6 +52,27 @@ func Invoke( return nil, err } + frontendReq := request.GetFrontendRequest() + targetingMethod := "type" + if _, ok := frontendReq.GetActivity().(*workflowservice.UnpauseActivityRequest_Id); ok { + targetingMethod = "id" + } + if ns, err := shardContext.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(request.NamespaceId)); err == nil { + metrics.ActivityUnpauseRequests.With(shardContext.GetMetricsHandler().WithTags( + metrics.NamespaceTag(ns.Name().String()), + metrics.ActivityTargetingMethodTag(targetingMethod), + )).Record(1) + } + + shardContext.GetLogger().Info("unpauseactivity: activity unpaused", + tag.WorkflowNamespaceID(request.GetNamespaceId()), + tag.WorkflowID(frontendReq.GetExecution().GetWorkflowId()), + tag.WorkflowRunID(frontendReq.GetExecution().GetRunId()), + tag.NewBoolTag("reset_attempts", frontendReq.GetResetAttempts()), + tag.NewBoolTag("reset_heartbeat", frontendReq.GetResetHeartbeat()), + tag.NewDurationTag("jitter", frontendReq.GetJitter().AsDuration()), + ) + return response, err } diff --git a/service/history/api/updateactivityoptions/api.go b/service/history/api/updateactivityoptions/api.go index 1d92d64934c..8d7406206c8 100644 --- a/service/history/api/updateactivityoptions/api.go +++ b/service/history/api/updateactivityoptions/api.go @@ -2,6 +2,7 @@ package updateactivityoptions import ( "context" + "strings" activitypb "go.temporal.io/api/activity/v1" commandpb "go.temporal.io/api/command/v1" @@ -15,6 +16,8 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/definition" + "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/util" "go.temporal.io/server/service/history/api" @@ -81,6 +84,38 @@ func Invoke( return nil, err } + targetingMethod := "type" + if _, ok := updateRequest.GetActivity().(*workflowservice.UpdateActivityOptionsRequest_Id); ok { + targetingMethod = "id" + } + if ns, err := shardContext.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(request.NamespaceId)); err == nil { + metrics.ActivityUpdateOptionsRequests.With(shardContext.GetMetricsHandler().WithTags( + metrics.NamespaceTag(ns.Name().String()), + metrics.ActivityTargetingMethodTag(targetingMethod), + )).Record(1) + } + + logger := shardContext.GetLogger() + if updateRequest.RestoreOriginal { + logger.Info("updateactivityoptions: activity options restored to original", + tag.WorkflowNamespaceID(request.GetNamespaceId()), + tag.WorkflowID(updateRequest.GetExecution().GetWorkflowId()), + tag.WorkflowRunID(updateRequest.GetExecution().GetRunId()), + ) + } else if mask := updateRequest.GetUpdateMask(); mask != nil { + updatedFields := util.ParseFieldMask(mask) + fields := make([]string, 0, len(updatedFields)) + for f := range updatedFields { + fields = append(fields, f) + } + logger.Info("updateactivityoptions: activity options updated", + tag.WorkflowNamespaceID(request.GetNamespaceId()), + tag.WorkflowID(updateRequest.GetExecution().GetWorkflowId()), + tag.WorkflowRunID(updateRequest.GetExecution().GetRunId()), + tag.NewStringTag("updated_fields", strings.Join(fields, ",")), + ) + } + return response, err } @@ -164,7 +199,7 @@ func processActivityOptionsUpdate( } // validate the updated options - adjustedOptions, err := adjustActivityOptions(validator, namespaceID, ai.ActivityId, ai.ActivityType, mergeInto) + adjustedOptions, err := adjustActivityOptions(validator, namespaceID, mutableState.GetExecutionInfo().TaskQueue, ai.ActivityId, ai.ActivityType, mergeInto) if err != nil { return nil, err } @@ -279,6 +314,7 @@ func mergeActivityOptions( func adjustActivityOptions( validator *api.CommandAttrValidator, namespaceID string, + workflowTaskQueue string, activityID string, activityType *commonpb.ActivityType, ao *activitypb.ActivityOptions, @@ -293,7 +329,7 @@ func adjustActivityOptions( ActivityType: activityType, } - _, err := validator.ValidateActivityScheduleAttributes(namespace.ID(namespaceID), attributes, nil) + _, err := validator.ValidateActivityScheduleAttributes(namespace.ID(namespaceID), attributes, nil, workflowTaskQueue) if err != nil { return nil, err } diff --git a/service/history/api/updateactivityoptions/api_test.go b/service/history/api/updateactivityoptions/api_test.go index d0b7ceb64c3..8dd3ebf4ae7 100644 --- a/service/history/api/updateactivityoptions/api_test.go +++ b/service/history/api/updateactivityoptions/api_test.go @@ -22,6 +22,7 @@ import ( "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence/versionhistory" + "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/util" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/consts" @@ -579,3 +580,70 @@ func (s *activityOptionsSuite) Test_updateActivityOptions_RestoreDefaultSuccess( s.NotNil(response) s.NoError(err) } + +// Test_updateActivityOptions_PerNSTQ_Blocked verifies that a workflow running on a normal task queue +// cannot update an activity's task queue to the internal per-namespace task queue. +func (s *activityOptionsSuite) Test_updateActivityOptions_PerNSTQ_Blocked() { + s.executionInfo.TaskQueue = "normal-task-queue" + + activityInfo := &persistencespb.ActivityInfo{ + ActivityId: "activity_id", + ActivityType: &commonpb.ActivityType{Name: "activity_type"}, + // TaskQueue: "normal-task-queue", + StartToCloseTimeout: durationpb.New(time.Second), + } + + request := &historyservice.UpdateActivityOptionsRequest{ + UpdateRequest: &workflowservice.UpdateActivityOptionsRequest{ + Activity: &workflowservice.UpdateActivityOptionsRequest_Id{Id: "activity_id"}, + ActivityOptions: &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"task_queue.name"}, + }, + }, + } + + s.mockMutableState.EXPECT().IsWorkflowExecutionRunning().Return(true) + s.mockMutableState.EXPECT().GetActivityByActivityID("activity_id").Return(activityInfo, true) + + _, err := processActivityOptionsRequest(s.validator, s.mockMutableState, request.GetUpdateRequest(), request.GetNamespaceId()) + s.Error(err) + s.Contains(err.Error(), "internal per-namespace task queue") +} + +// Test_updateActivityOptions_PerNSTQ_Allowed verifies that a workflow running on the internal +// per-namespace task queue can update an activity's task queue to the same internal task queue. +func (s *activityOptionsSuite) Test_updateActivityOptions_PerNSTQ_Allowed() { + s.executionInfo.TaskQueue = primitives.PerNSWorkerTaskQueue + + activityInfo := &persistencespb.ActivityInfo{ + ActivityId: "activity_id", + ActivityType: &commonpb.ActivityType{Name: "activity_type"}, + TaskQueue: primitives.PerNSWorkerTaskQueue, + StartToCloseTimeout: durationpb.New(time.Second), + } + + request := &historyservice.UpdateActivityOptionsRequest{ + UpdateRequest: &workflowservice.UpdateActivityOptionsRequest{ + Activity: &workflowservice.UpdateActivityOptionsRequest_Id{Id: "activity_id"}, + ActivityOptions: &activitypb.ActivityOptions{ + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + StartToCloseTimeout: durationpb.New(2 * time.Second), + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"task_queue.name", "start_to_close_timeout"}, + }, + }, + } + + s.mockMutableState.EXPECT().IsWorkflowExecutionRunning().Return(true) + s.mockMutableState.EXPECT().GetActivityByActivityID("activity_id").Return(activityInfo, true) + s.mockMutableState.EXPECT().RegenerateActivityRetryTask(gomock.Any(), gomock.Any()).Return(nil) + s.mockMutableState.EXPECT().UpdateActivity(gomock.Any(), gomock.Any()).Return(nil) + + resp, err := processActivityOptionsRequest(s.validator, s.mockMutableState, request.GetUpdateRequest(), request.GetNamespaceId()) + s.NoError(err) + s.NotNil(resp) +} diff --git a/service/history/archival_queue_factory.go b/service/history/archival_queue_factory.go index d722a83ef36..06fc2d73ef0 100644 --- a/service/history/archival_queue_factory.go +++ b/service/history/archival_queue_factory.go @@ -73,9 +73,17 @@ func newHostScheduler(params ArchivalQueueFactoryParams) queues.Scheduler { ActiveNamespaceWeights: dynamicconfig.GetMapPropertyFnFilteredByNamespace(ArchivalTaskPriorities), StandbyNamespaceWeights: dynamicconfig.GetMapPropertyFnFilteredByNamespace(ArchivalTaskPriorities), InactiveNamespaceDeletionDelay: params.Config.TaskSchedulerInactiveChannelDeletionDelay, + ExecutionAwareSchedulerOptions: ctasks.ExecutionAwareSchedulerOptions{ + Enabled: params.Config.TaskSchedulerEnableExecutionQueueScheduler, + MaxQueues: params.Config.TaskSchedulerExecutionQueueSchedulerMaxQueues, + QueueTTL: params.Config.TaskSchedulerExecutionQueueSchedulerQueueTTL, + QueueConcurrency: params.Config.TaskSchedulerExecutionQueueSchedulerQueueConcurrency, + }, }, params.NamespaceRegistry, params.Logger, + params.MetricsHandler, + params.TimeSource, ) } diff --git a/service/history/archival_queue_factory_test.go b/service/history/archival_queue_factory_test.go index 63072271091..3fd6bf6ab27 100644 --- a/service/history/archival_queue_factory_test.go +++ b/service/history/archival_queue_factory_test.go @@ -31,7 +31,6 @@ func TestArchivalQueueFactory(t *testing.T) { return metricsHandler }, ).Times(1) - metricsHandler.EXPECT().WithTags(gomock.Any()).Return(metricsHandler).Times(1) mockShard := shard.NewTestContext( ctrl, diff --git a/service/history/chasm_engine.go b/service/history/chasm_engine.go index ce94daeb1e2..c1c41932220 100644 --- a/service/history/chasm_engine.go +++ b/service/history/chasm_engine.go @@ -5,23 +5,29 @@ import ( "errors" "fmt" + commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common" + "go.temporal.io/server/common/contextutil" + "go.temporal.io/server/common/convert" "go.temporal.io/server/common/definition" "go.temporal.io/server/common/headers" "go.temporal.io/server/common/locks" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/membership" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/primitives" + serviceerrors "go.temporal.io/server/common/serviceerror" "go.temporal.io/server/common/softassert" "go.temporal.io/server/service/history/api" "go.temporal.io/server/service/history/configs" "go.temporal.io/server/service/history/consts" + "go.temporal.io/server/service/history/deletemanager" historyi "go.temporal.io/server/service/history/interfaces" "go.temporal.io/server/service/history/shard" "go.temporal.io/server/service/history/workflow" @@ -31,11 +37,14 @@ import ( type ( ChasmEngine struct { - executionCache cache.Cache - shardController shard.Controller - registry *chasm.Registry - config *configs.Config - notifier *ChasmNotifier + executionCache cache.Cache + shardController shard.Controller + registry *chasm.Registry + config *configs.Config + notifier *ChasmNotifier + logger log.Logger + historyServiceResolver membership.ServiceResolver + hostInfoProvider membership.HostInfoProvider } newExecutionParams struct { @@ -73,12 +82,18 @@ func newChasmEngine( registry *chasm.Registry, config *configs.Config, notifier *ChasmNotifier, + logger log.Logger, + historyServiceResolver membership.ServiceResolver, + hostInfoProvider membership.HostInfoProvider, ) *ChasmEngine { return &ChasmEngine{ - executionCache: executionCache, - registry: registry, - config: config, - notifier: notifier, + executionCache: executionCache, + registry: registry, + config: config, + notifier: notifier, + logger: logger, + historyServiceResolver: historyServiceResolver, + hostInfoProvider: hostInfoProvider, } } @@ -94,14 +109,86 @@ func (e *ChasmEngine) NotifyExecution(key chasm.ExecutionKey) { e.notifier.Notify(key) } +func (e *ChasmEngine) setContextMetadata( + ctx context.Context, + chasmTree *chasm.Node, +) chasm.Context { + chasmContext := chasm.NewContext(ctx, chasmTree) + + rootComponent, err := chasmTree.Component(chasmContext, chasm.ComponentRef{}) + if err != nil { + executionKey := chasmContext.ExecutionKey() + e.logger.Error( + "Failed to resolve CHASM root component for context metadata", + tag.WorkflowNamespaceID(executionKey.NamespaceID), + tag.WorkflowID(executionKey.BusinessID), + tag.WorkflowRunID(executionKey.RunID), + tag.Error(err), + ) + return chasmContext + } + + root, ok := rootComponent.(chasm.RootComponent) + if !ok { + softassert.Fail( + e.logger, + "root node must implement RootComponent interface", + tag.NewStringTag("component_type", fmt.Sprintf("%T", rootComponent)), + ) + return chasmContext + } + + for key, value := range root.ContextMetadata(chasmContext) { + contextutil.ContextMetadataSet(ctx, key, value) + } + + return chasmContext +} + +func chasmTreeFromMutableState( + logger log.Logger, + mutableState historyi.MutableState, +) (*chasm.Node, error) { + chasmTree, ok := mutableState.ChasmTree().(*chasm.Node) + if !ok { + return nil, softassert.UnexpectedInternalErr( + logger, + "CHASM tree implementation not properly wired up", + fmt.Errorf("encountered type: %T, expected type: %T", mutableState.ChasmTree(), &chasm.Node{}), + ) + } + return chasmTree, nil +} + +func (e *ChasmEngine) setContextMetadataFromMutableState( + ctx context.Context, + mutableState historyi.MutableState, +) { + chasmTree, err := chasmTreeFromMutableState(e.logger, mutableState) + if err != nil { + e.logger.Error("Failed to resolve CHASM tree for context metadata", tag.Error(err)) + return + } + e.setContextMetadata(ctx, chasmTree) +} + func (e *ChasmEngine) StartExecution( ctx context.Context, executionRef chasm.ComponentRef, - startFn func(chasm.MutableContext) (chasm.Component, error), + startFn func(chasm.MutableContext) (chasm.RootComponent, error), opts ...chasm.TransitionOption, -) (result chasm.StartExecutionResult, retErr error) { +) (chasm.StartExecutionResult, error) { options := e.constructTransitionOptions(opts...) + result, err := e.startExecution(ctx, executionRef, startFn, options) + return result, e.convertError(err, executionRef, options.RequestID) +} +func (e *ChasmEngine) startExecution( + ctx context.Context, + executionRef chasm.ComponentRef, + startFn func(chasm.MutableContext) (chasm.RootComponent, error), + options chasm.TransitionOptions, +) (result chasm.StartExecutionResult, retErr error) { shardContext, err := e.getShardContext(executionRef) if err != nil { return chasm.StartExecutionResult{}, err @@ -154,6 +241,7 @@ func (e *ChasmEngine) StartExecution( return chasm.StartExecutionResult{}, err } if !hasCurrentRun { + e.setContextMetadataFromMutableState(ctx, newExecutionParams.mutableState) serializedRef, err := newExecutionParams.executionRef.Serialize(e.registry) if err != nil { // Created is true here because persistAsBrandNew succeeded, but we failed to serialize the ref. @@ -181,12 +269,22 @@ func (e *ChasmEngine) StartExecution( func (e *ChasmEngine) UpdateWithStartExecution( ctx context.Context, executionRef chasm.ComponentRef, - startFn func(chasm.MutableContext) (chasm.Component, error), + startFn func(chasm.MutableContext) (chasm.RootComponent, error), updateFn func(chasm.MutableContext, chasm.Component) error, opts ...chasm.TransitionOption, -) (result chasm.EngineUpdateWithStartExecutionResult, retError error) { +) (chasm.EngineUpdateWithStartExecutionResult, error) { options := e.constructTransitionOptions(opts...) + result, err := e.updateWithStartExecution(ctx, executionRef, startFn, updateFn, options) + return result, e.convertError(err, executionRef, options.RequestID) +} +func (e *ChasmEngine) updateWithStartExecution( + ctx context.Context, + executionRef chasm.ComponentRef, + startFn func(chasm.MutableContext) (chasm.RootComponent, error), + updateFn func(chasm.MutableContext, chasm.Component) error, + options chasm.TransitionOptions, +) (result chasm.EngineUpdateWithStartExecutionResult, retError error) { shardContext, err := e.getShardContext(executionRef) if err != nil { return chasm.EngineUpdateWithStartExecutionResult{}, err @@ -209,18 +307,18 @@ func (e *ChasmEngine) UpdateWithStartExecution( }() if executionLease.GetMutableState().IsWorkflowExecutionRunning() { - executionKey, executionRef, err := e.updateExecution(ctx, shardContext, executionLease, executionRef, updateFn) + executionKey, newExecutionRef, err := e.updateExecution(ctx, shardContext, executionLease, executionRef, updateFn) if err != nil { return chasm.EngineUpdateWithStartExecutionResult{}, err } return chasm.EngineUpdateWithStartExecutionResult{ ExecutionKey: executionKey, - ExecutionRef: executionRef, + ExecutionRef: newExecutionRef, Created: false, }, nil } - executionKey, executionRef, created, err := e.startNewForClosedExecution( + executionKey, newExecutionRef, created, err := e.startNewForClosedExecution( ctx, shardContext, executionLease, @@ -235,11 +333,11 @@ func (e *ChasmEngine) UpdateWithStartExecution( } return chasm.EngineUpdateWithStartExecutionResult{ ExecutionKey: executionKey, - ExecutionRef: executionRef, + ExecutionRef: newExecutionRef, Created: created, }, nil case *serviceerror.NotFound: - executionKey, executionRef, created, err := e.startAndUpdateExecution( + executionKey, newExecutionRef, created, err := e.startAndUpdateExecution( ctx, shardContext, executionRef, @@ -253,7 +351,7 @@ func (e *ChasmEngine) UpdateWithStartExecution( } return chasm.EngineUpdateWithStartExecutionResult{ ExecutionKey: executionKey, - ExecutionRef: executionRef, + ExecutionRef: newExecutionRef, Created: created, }, nil default: @@ -288,7 +386,7 @@ func (e *ChasmEngine) startNewForClosedExecution( executionLease api.WorkflowLease, executionRef chasm.ComponentRef, archetypeID chasm.ArchetypeID, - startFn func(chasm.MutableContext) (chasm.Component, error), + startFn func(chasm.MutableContext) (chasm.RootComponent, error), updateFn func(chasm.MutableContext, chasm.Component) error, options chasm.TransitionOptions, ) (chasm.ExecutionKey, []byte, bool, error) { @@ -321,13 +419,9 @@ func (e *ChasmEngine) applyUpdateWithLease( updateFn func(chasm.MutableContext, chasm.Component) error, ) ([]byte, error) { mutableState := executionLease.GetMutableState() - chasmTree, ok := mutableState.ChasmTree().(*chasm.Node) - if !ok { - return nil, serviceerror.NewInternalf( - "CHASM tree implementation not properly wired up, encountered type: %T, expected type: %T", - mutableState.ChasmTree(), - &chasm.Node{}, - ) + chasmTree, err := chasmTreeFromMutableState(shardContext.GetLogger(), mutableState) + if err != nil { + return nil, err } mutableContext := chasm.NewMutableContext(ctx, chasmTree) @@ -342,6 +436,8 @@ func (e *ChasmEngine) applyUpdateWithLease( // TODO: Support WithSpeculative() TransitionOption. + e.setContextMetadata(ctx, chasmTree) + if err := executionLease.GetContext().UpdateWorkflowExecutionAsActive( ctx, shardContext, @@ -362,7 +458,7 @@ func (e *ChasmEngine) startAndUpdateExecution( shardContext historyi.ShardContext, executionRef chasm.ComponentRef, archetypeID chasm.ArchetypeID, - startFn func(chasm.MutableContext) (chasm.Component, error), + startFn func(chasm.MutableContext) (chasm.RootComponent, error), updateFn func(chasm.MutableContext, chasm.Component) error, options chasm.TransitionOptions, ) (retKey chasm.ExecutionKey, retRef []byte, created bool, retErr error) { @@ -405,6 +501,8 @@ func (e *ChasmEngine) startAndUpdateExecution( return chasm.ExecutionKey{}, nil, false, currentRunInfo.CurrentWorkflowConditionFailedError } + e.setContextMetadataFromMutableState(ctx, newExecutionParams.mutableState) + serializedRef, err := newExecutionParams.executionRef.Serialize(e.registry) return newExecutionParams.executionRef.ExecutionKey, serializedRef, true, err @@ -419,11 +517,22 @@ func (e *ChasmEngine) UpdateComponent( ref chasm.ComponentRef, updateFn func(chasm.MutableContext, chasm.Component) error, opts ...chasm.TransitionOption, +) ([]byte, error) { + options := e.constructTransitionOptions(opts...) + result, err := e.updateComponent(ctx, ref, updateFn) + return result, e.convertError(err, ref, options.RequestID) +} + +func (e *ChasmEngine) updateComponent( + ctx context.Context, + ref chasm.ComponentRef, + updateFn func(chasm.MutableContext, chasm.Component) error, ) (updatedRef []byte, retError error) { shardContext, executionLease, err := e.getExecutionLease(ctx, ref) if err != nil { return nil, err } + defer func() { executionLease.GetReleaseFn()(retError) }() @@ -431,6 +540,85 @@ func (e *ChasmEngine) UpdateComponent( return e.applyUpdateWithLease(ctx, shardContext, executionLease, ref, updateFn) } +// DeleteExecution deletes a CHASM execution. If the execution is still running on the active +// cluster, it is terminated first. A DeleteExecutionTask is then queued to remove all execution +// data from persistence. On standby clusters, the execution is deleted regardless of state. +func (e *ChasmEngine) DeleteExecution( + ctx context.Context, + ref chasm.ComponentRef, + request chasm.DeleteExecutionRequest, +) error { + return e.convertError(e.deleteExecution(ctx, ref, request), ref, request.RequestID) +} + +func (e *ChasmEngine) deleteExecution( + ctx context.Context, + ref chasm.ComponentRef, + request chasm.DeleteExecutionRequest, +) (retError error) { + shardContext, executionLease, err := e.getExecutionLease(ctx, ref) + if err != nil { + return err + } + defer func() { + executionLease.GetReleaseFn()(retError) + }() + + mutableState := executionLease.GetMutableState() + we := mutableState.GetWorkflowKey() + + e.setContextMetadataFromMutableState(ctx, mutableState) + + log.With(shardContext.GetLogger(), + tag.WorkflowNamespaceID(ref.NamespaceID), + tag.WorkflowID(we.WorkflowID), + tag.WorkflowRunID(we.RunID), + ).Info("Deleting CHASM execution") + + if mutableState.IsWorkflowExecutionRunning() { + ns, err := shardContext.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(ref.NamespaceID)) + if err != nil { + return err + } + if ns.ActiveInCluster(shardContext.GetClusterMetadata().GetCurrentClusterName()) { + chasmTree, ok := mutableState.ChasmTree().(*chasm.Node) + if !ok { + return serviceerror.NewInternalf( + "CHASM tree implementation not properly wired up, encountered type: %T, expected type: %T", + mutableState.ChasmTree(), + &chasm.Node{}, + ) + } + + chasmTree.SetDeleteAfterClose(true) + if err := chasmTree.Terminate(request.TerminateComponentRequest); err != nil { + return err + } + + taskGenerator := workflow.GetTaskGeneratorProvider().NewTaskGenerator(shardContext, mutableState) + deleteTask, err := taskGenerator.GenerateDeleteExecutionTask() + if err != nil { + return err + } + mutableState.AddTasks(deleteTask) + + return executionLease.GetContext().UpdateWorkflowExecutionAsActive(ctx, shardContext) + } + } + + // Execution is closed or namespace is standby: add delete task directly. + return deletemanager.AddDeleteExecutionTask( + ctx, + shardContext, + namespace.ID(ref.NamespaceID), + &commonpb.WorkflowExecution{ + WorkflowId: we.WorkflowID, + RunId: we.RunID, + }, + mutableState, + ) +} + // ReadComponent evaluates readFn against the current state of the component identified by the // supplied component reference. An error is returned if the state transition specified by the // component reference is inconsistent with execution transition history. opts are currently ignored. @@ -439,7 +627,16 @@ func (e *ChasmEngine) ReadComponent( ref chasm.ComponentRef, readFn func(chasm.Context, chasm.Component) error, opts ...chasm.TransitionOption, -) (retError error) { +) error { + options := e.constructTransitionOptions(opts...) + return e.convertError(e.readComponent(ctx, ref, readFn), ref, options.RequestID) +} + +func (e *ChasmEngine) readComponent( + ctx context.Context, + ref chasm.ComponentRef, + readFn func(chasm.Context, chasm.Component) error, +) error { _, executionLease, err := e.getExecutionLease(ctx, ref) if err != nil { return err @@ -450,16 +647,12 @@ func (e *ChasmEngine) ReadComponent( executionLease.GetReleaseFn()(nil) }() - chasmTree, ok := executionLease.GetMutableState().ChasmTree().(*chasm.Node) - if !ok { - return serviceerror.NewInternalf( - "CHASM tree implementation not properly wired up, encountered type: %T, expected type: %T", - executionLease.GetMutableState().ChasmTree(), - &chasm.Node{}, - ) + chasmTree, err := chasmTreeFromMutableState(e.logger, executionLease.GetMutableState()) + if err != nil { + return err } - chasmContext := chasm.NewContext(ctx, chasmTree) + chasmContext := e.setContextMetadata(ctx, chasmTree) component, err := chasmTree.Component(chasmContext, ref) if err != nil { return err @@ -486,6 +679,16 @@ func (e *ChasmEngine) PollComponent( requestRef chasm.ComponentRef, monotonicPredicate func(chasm.Context, chasm.Component) (bool, error), opts ...chasm.TransitionOption, +) ([]byte, error) { + options := e.constructTransitionOptions(opts...) + result, err := e.pollComponent(ctx, requestRef, monotonicPredicate) + return result, e.convertError(err, requestRef, options.RequestID) +} + +func (e *ChasmEngine) pollComponent( + ctx context.Context, + requestRef chasm.ComponentRef, + monotonicPredicate func(chasm.Context, chasm.Component) (bool, error), ) (retRef []byte, retError error) { var ch <-chan struct{} @@ -546,16 +749,13 @@ func (e *ChasmEngine) predicateSatisfied( ref chasm.ComponentRef, executionLease api.WorkflowLease, ) ([]byte, error) { - chasmTree, ok := executionLease.GetMutableState().ChasmTree().(*chasm.Node) - if !ok { - return nil, serviceerror.NewInternalf( - "CHASM tree implementation not properly wired up, encountered type: %T, expected type: %T", - executionLease.GetMutableState().ChasmTree(), - &chasm.Node{}, - ) + chasmTree, err := chasmTreeFromMutableState(e.logger, executionLease.GetMutableState()) + if err != nil { + return nil, err } - chasmContext := chasm.NewContext(ctx, chasmTree) + chasmContext := e.setContextMetadata(ctx, chasmTree) + component, err := chasmTree.Component(chasmContext, ref) if err != nil { return nil, err @@ -610,7 +810,7 @@ func (e *ChasmEngine) createNewExecution( shardContext historyi.ShardContext, executionRef chasm.ComponentRef, archetypeID chasm.ArchetypeID, - newFn func(chasm.MutableContext) (chasm.Component, error), + startFn func(chasm.MutableContext) (chasm.RootComponent, error), options chasm.TransitionOptions, ) (newExecutionParams, error) { return e.createNewExecutionWithUpdate( @@ -618,7 +818,7 @@ func (e *ChasmEngine) createNewExecution( shardContext, executionRef, archetypeID, - newFn, + startFn, nil, options, ) @@ -629,7 +829,7 @@ func (e *ChasmEngine) createNewExecutionWithUpdate( shardContext historyi.ShardContext, executionRef chasm.ComponentRef, archetypeID chasm.ArchetypeID, - newFn func(chasm.MutableContext) (chasm.Component, error), + startFn func(chasm.MutableContext) (chasm.RootComponent, error), updateFn func(chasm.MutableContext, chasm.Component) error, options chasm.TransitionOptions, ) (newExecutionParams, error) { @@ -664,7 +864,7 @@ func (e *ChasmEngine) createNewExecutionWithUpdate( chasmContext := chasm.NewMutableContext(ctx, chasmTree) - rootComponent, err := newFn(chasmContext) + rootComponent, err := startFn(chasmContext) if err != nil { return newExecutionParams{}, err } @@ -727,9 +927,8 @@ func (e *ChasmEngine) persistAsBrandNew( return currentExecutionInfo{}, false, nil } - var currentRunConditionFailedError *persistence.CurrentWorkflowConditionFailedError - if !errors.As(err, ¤tRunConditionFailedError) || - len(currentRunConditionFailedError.RunID) == 0 { + currentRunConditionFailedError, ok := errors.AsType[*persistence.CurrentWorkflowConditionFailedError](err) + if !ok || len(currentRunConditionFailedError.RunID) == 0 { return currentExecutionInfo{}, false, err } @@ -913,6 +1112,8 @@ func (e *ChasmEngine) handleReusePolicy( return chasm.StartExecutionResult{}, err } + e.setContextMetadataFromMutableState(ctx, newExecutionParams.mutableState) + serializedRef, err := newExecutionParams.executionRef.Serialize(e.registry) if err != nil { return chasm.StartExecutionResult{ExecutionKey: newExecutionParams.executionRef.ExecutionKey, Created: true}, err @@ -996,7 +1197,7 @@ func (e *ChasmEngine) getExecutionLease( lockPriority, ) if err != nil { - return nil, nil, e.convertError(err, archetypeID, ref.BusinessID) + return nil, nil, err } if predicateErr != nil { @@ -1050,17 +1251,73 @@ func (e *ChasmEngine) getExecutionLease( return shardContext, executionLease, nil } -// convertError is a hook containing error conversion logic that creates more appropriate and/or -// helpful errors. -func (e *ChasmEngine) convertError(err error, archetypeID chasm.ArchetypeID, businessID string) error { - switch { - case errors.As(err, new(*serviceerror.NotFound)): - displayName, ok := e.registry.ArchetypeDisplayName(archetypeID) +// convertError converts non-serviceerror errors to appropriate serviceerror types. +// Known persistence errors are converted to service errors with a request ID for correlation. +// All other errors (service errors, context errors, chasm errors, unknown errors) pass through unchanged. +// When the component ref has a known archetype and businessID, NotFound errors get enriched messages. +// NOTE: Keep in sync with Handler.convertError in handler.go. The CHASM engine is a superset that additionally handles +// ConditionFailedError and includes a request ID in error messages for debugging correlation. +func (e *ChasmEngine) convertError( + err error, + ref chasm.ComponentRef, + requestID string, +) error { + if err == nil { + return nil + } + + if solErr, ok := errors.AsType[*persistence.ShardOwnershipLostError](err); ok { + hostInfo := e.hostInfoProvider.HostInfo() + e.logger.Error("chasm ShardOwnershipLostError", tag.Error(err), tag.RequestID(requestID)) + if ownerInfo, lookupErr := e.historyServiceResolver.Lookup(convert.Int32ToString(solErr.ShardID)); lookupErr == nil { + return serviceerrors.NewShardOwnershipLost(ownerInfo.GetAddress(), hostInfo.GetAddress()) + } + return serviceerrors.NewShardOwnershipLost("", hostInfo.GetAddress()) + } + if _, ok := errors.AsType[*persistence.AppendHistoryTimeoutError](err); ok { + e.logger.Error("chasm AppendHistoryTimeoutError", tag.Error(err), tag.RequestID(requestID)) + return serviceerror.NewUnavailablef("append history timed out (request ID: %s)", requestID) + } + if _, ok := errors.AsType[*persistence.WorkflowConditionFailedError](err); ok { + e.logger.Error("chasm WorkflowConditionFailedError", tag.Error(err), tag.RequestID(requestID)) + return serviceerror.NewUnavailablef("workflow condition failed (request ID: %s)", requestID) + } + if cwcfe, ok := errors.AsType[*persistence.CurrentWorkflowConditionFailedError](err); ok { + e.logger.Error("chasm CurrentWorkflowConditionFailedError", tag.Error(err), tag.RequestID(requestID)) + return serviceerror.NewUnavailablef("current workflow condition failed for RunID %s (request ID: %s)", cwcfe.RunID, requestID) + } + if _, ok := errors.AsType[*persistence.ConditionFailedError](err); ok { + e.logger.Error("chasm ConditionFailedError", tag.Error(err), tag.RequestID(requestID)) + return serviceerror.NewUnavailablef("condition failed (request ID: %s)", requestID) + } + if _, ok := errors.AsType[*persistence.TransactionSizeLimitError](err); ok { + e.logger.Error("chasm TransactionSizeLimitError", tag.Error(err), tag.RequestID(requestID)) + return serviceerror.NewInvalidArgumentf("transaction size limit exceeded (request ID: %s)", requestID) + } + if _, ok := errors.AsType[*persistence.TimeoutError](err); ok { + e.logger.Error("chasm TimeoutError", tag.Error(err), tag.RequestID(requestID)) + return serviceerror.NewDeadlineExceededf("persistence operation timed out (request ID: %s)", requestID) + } + + if _, ok := errors.AsType[*serviceerror.NotFound](err); !ok { + return err + } + + return e.convertNotFoundError(err, ref) +} + +func (e *ChasmEngine) convertNotFoundError(err error, ref chasm.ComponentRef) error { + archID, archErr := ref.ArchetypeID(e.registry) + if archErr != nil { + return err + } + + if archID != chasm.UnspecifiedArchetypeID && ref.BusinessID != "" { + displayName, ok := e.registry.ArchetypeDisplayName(archID) if !ok { displayName = "execution" } - return serviceerror.NewNotFoundf("%s not found for ID: %s", displayName, businessID) - default: - return err + return serviceerror.NewNotFoundf("%s not found for ID: %s", displayName, ref.BusinessID) } + return err } diff --git a/service/history/chasm_engine_test.go b/service/history/chasm_engine_test.go index bd828f59e82..da5d745f896 100644 --- a/service/history/chasm_engine_test.go +++ b/service/history/chasm_engine_test.go @@ -2,6 +2,8 @@ package history import ( "context" + "errors" + "fmt" "testing" "time" @@ -15,17 +17,21 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/cluster" + "go.temporal.io/server/common/contextutil" "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/membership" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/persistence/serialization" + serviceerrors "go.temporal.io/server/common/serviceerror" "go.temporal.io/server/common/testing/protorequire" "go.temporal.io/server/common/testing/testvars" "go.temporal.io/server/service/history/configs" "go.temporal.io/server/service/history/hsm" historyi "go.temporal.io/server/service/history/interfaces" "go.temporal.io/server/service/history/shard" + "go.temporal.io/server/service/history/tasks" "go.temporal.io/server/service/history/tests" "go.temporal.io/server/service/history/workflow" wcache "go.temporal.io/server/service/history/workflow/cache" @@ -121,6 +127,9 @@ func (s *chasmEngineSuite) SetupTest() { s.registry, s.config, NewChasmNotifier(), + s.mockShard.GetLogger(), + s.mockShard.Resource.HistoryServiceResolver, + s.mockShard.Resource.HostInfoProvider, ) s.engine.SetShardController(s.mockShardController) } @@ -179,6 +188,35 @@ func (s *chasmEngineSuite) TestNewExecution_BrandNew() { s.True(result.Created) } +func (s *chasmEngineSuite) TestStartExecution_SetsContextMetadata() { + tv := testvars.New(s.T()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: "", + }, + ) + newActivityID := tv.ActivityID() + + s.mockExecutionManager.EXPECT().CreateWorkflowExecution(gomock.Any(), gomock.Any()).Return( + tests.CreateWorkflowExecutionResponse, + nil, + ).Times(1) + s.mockEngine.EXPECT().NotifyChasmExecution(gomock.Any(), gomock.Any()).Return().Times(1) + + requestCtx := newTestMetadataContext("start-request") + + _, err := s.engine.StartExecution( + requestCtx, + ref, + s.newTestExecutionFn(newActivityID), + ) + s.NoError(err) + s.assertTestContextMetadata(requestCtx, newActivityID, "start-request") +} + func (s *chasmEngineSuite) TestNewExecution_RequestIDDedup() { tv := testvars.New(s.T()) tv = tv.WithRunID(tv.Any().RunID()) @@ -485,8 +523,8 @@ func (s *chasmEngineSuite) TestNewExecution_ConflictPolicy_TerminateExisting() { func (s *chasmEngineSuite) newTestExecutionFn( activityID string, -) func(ctx chasm.MutableContext) (chasm.Component, error) { - return func(ctx chasm.MutableContext) (chasm.Component, error) { +) func(chasm.MutableContext) (chasm.RootComponent, error) { + return func(ctx chasm.MutableContext) (chasm.RootComponent, error) { return &testComponent{ ActivityInfo: &persistencespb.ActivityInfo{ ActivityId: activityID, @@ -495,6 +533,58 @@ func (s *chasmEngineSuite) newTestExecutionFn( } } +func (s *chasmEngineSuite) TestSetContextMetadata_StateAndRequestScopedValues() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + activityID := tv.ActivityID() + mutableState := s.newTestMutableState( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + &testComponent{ + ActivityInfo: &persistencespb.ActivityInfo{ + ActivityId: activityID, + }, + }, + ) + + chasmTree, err := chasmTreeFromMutableState(s.mockShard.GetLogger(), mutableState) + s.NoError(err) + + requestCtx := newTestMetadataContext("helper-request") + chasmContext := s.engine.setContextMetadata(requestCtx, chasmTree) + s.Equal("helper-request", chasmContext.Value(testRequestContextKey{})) + s.assertTestContextMetadata(requestCtx, activityID, "helper-request") +} + +func (s *chasmEngineSuite) TestSetContextMetadata_NoProvider() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + mutableState := s.newTestMutableState( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + &testComponentNoMetadata{ + ActivityInfo: &persistencespb.ActivityInfo{ + ActivityId: tv.ActivityID(), + }, + }, + ) + + chasmTree, err := chasmTreeFromMutableState(s.mockShard.GetLogger(), mutableState) + s.NoError(err) + + requestCtx := contextutil.WithMetadataContext(context.Background()) + s.engine.setContextMetadata(requestCtx, chasmTree) + + _, ok := contextutil.ContextMetadataGet(requestCtx, testContextMetadataActivityKey) + s.False(ok) +} + func (s *chasmEngineSuite) validateCreateRequest( request *persistence.CreateWorkflowExecutionRequest, expectedArchetypeID chasm.ArchetypeID, @@ -556,6 +646,189 @@ func (s *chasmEngineSuite) currentRunConditionFailedErr( } } +func (s *chasmEngineSuite) TestDeleteExecution_RunningExecution() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + ) + + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState(ref.ExecutionKey, &persistencespb.ActivityInfo{ + ActivityId: tv.ActivityID(), + }, enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, nil), + }, nil).Times(1) + s.mockExecutionManager.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()).DoAndReturn( + func( + _ context.Context, + request *persistence.UpdateWorkflowExecutionRequest, + ) (*persistence.UpdateWorkflowExecutionResponse, error) { + s.Equal( + enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED, + request.UpdateWorkflowMutation.ExecutionState.State, + ) + s.Equal( + enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, + request.UpdateWorkflowMutation.ExecutionState.Status, + ) + + transferTasks := request.UpdateWorkflowMutation.Tasks[tasks.CategoryTransfer] + var foundDeleteTask bool + for _, t := range transferTasks { + if _, ok := t.(*tasks.DeleteExecutionTask); ok { + foundDeleteTask = true + break + } + } + s.True(foundDeleteTask, "expected DeleteExecutionTask in transfer tasks") + + return tests.UpdateWorkflowExecutionResponse, nil + }, + ).Times(1) + s.mockEngine.EXPECT().NotifyChasmExecution(ref.ExecutionKey, gomock.Any()).Return().Times(1) + + err := s.engine.DeleteExecution( + context.Background(), + ref, + chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: "test deletion", + Identity: "test-identity", + RequestID: tv.Any().String(), + }, + }, + ) + s.NoError(err) +} + +func (s *chasmEngineSuite) TestDeleteExecution_ClosedExecution() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + ) + + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState(ref.ExecutionKey, &persistencespb.ActivityInfo{ + ActivityId: tv.ActivityID(), + }, enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED, enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, nil), + }, nil).Times(1) + s.mockExecutionManager.EXPECT().AddHistoryTasks(gomock.Any(), gomock.Any()).DoAndReturn( + func( + _ context.Context, + request *persistence.AddHistoryTasksRequest, + ) error { + transferTasks := request.Tasks[tasks.CategoryTransfer] + s.Len(transferTasks, 1) + deleteTask, ok := transferTasks[0].(*tasks.DeleteExecutionTask) + s.True(ok) + s.Equal(s.archetypeID, deleteTask.ArchetypeID) + return nil + }, + ).Times(1) + + err := s.engine.DeleteExecution( + context.Background(), + ref, + chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: "test deletion", + Identity: "test-identity", + RequestID: tv.Any().String(), + }, + }, + ) + s.NoError(err) +} + +func (s *chasmEngineSuite) TestDeleteExecution_RunningExecution_SetsContextMetadata() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + ) + + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState(ref.ExecutionKey, &persistencespb.ActivityInfo{ + ActivityId: tv.ActivityID(), + }, enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, nil), + }, nil).Times(1) + s.mockExecutionManager.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()). + Return(tests.UpdateWorkflowExecutionResponse, nil).Times(1) + s.mockEngine.EXPECT().NotifyChasmExecution(ref.ExecutionKey, gomock.Any()).Return().Times(1) + + requestCtx := newTestMetadataContext("delete-running-request") + + err := s.engine.DeleteExecution( + requestCtx, + ref, + chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: "test deletion", + Identity: "test-identity", + RequestID: tv.Any().String(), + }, + }, + ) + s.NoError(err) + s.assertTestContextMetadata(requestCtx, tv.ActivityID(), "delete-running-request") +} + +func (s *chasmEngineSuite) TestDeleteExecution_ClosedExecution_SetsContextMetadata() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + ) + + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState(ref.ExecutionKey, &persistencespb.ActivityInfo{ + ActivityId: tv.ActivityID(), + }, enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED, enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, nil), + }, nil).Times(1) + s.mockExecutionManager.EXPECT().AddHistoryTasks(gomock.Any(), gomock.Any()).Return(nil).Times(1) + + requestCtx := newTestMetadataContext("delete-closed-request") + + err := s.engine.DeleteExecution( + requestCtx, + ref, + chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: "test deletion", + Identity: "test-identity", + RequestID: tv.Any().String(), + }, + }, + ) + s.NoError(err) + s.assertTestContextMetadata(requestCtx, tv.ActivityID(), "delete-closed-request") +} + func (s *chasmEngineSuite) TestUpdateComponent_Success() { tv := testvars.New(s.T()) tv = tv.WithRunID(tv.Any().RunID()) @@ -610,6 +883,48 @@ func (s *chasmEngineSuite) TestUpdateComponent_Success() { s.NoError(err) } +func (s *chasmEngineSuite) TestUpdateComponent_SetsContextMetadata() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + ) + newActivityID := tv.ActivityID() + + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState(ref.ExecutionKey, &persistencespb.ActivityInfo{ + ActivityId: "", + }, enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, nil), + }, nil).Times(1) + s.mockExecutionManager.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()). + Return(tests.UpdateWorkflowExecutionResponse, nil).Times(1) + s.mockEngine.EXPECT().NotifyChasmExecution(ref.ExecutionKey, gomock.Any()).Return().Times(1) + + requestCtx := newTestMetadataContext("update-request") + + _, err := s.engine.UpdateComponent( + requestCtx, + ref, + func( + ctx chasm.MutableContext, + component chasm.Component, + ) error { + tc, ok := component.(*testComponent) + s.True(ok) + tc.ActivityInfo.ActivityId = newActivityID + return nil + }, + ) + s.NoError(err) + s.assertTestContextMetadata(requestCtx, newActivityID, "update-request") +} + func (s *chasmEngineSuite) TestReadComponent_Success() { tv := testvars.New(s.T()) tv = tv.WithRunID(tv.Any().RunID()) @@ -641,7 +956,7 @@ func (s *chasmEngineSuite) TestReadComponent_Success() { s.True(ok) s.Equal(expectedActivityID, tc.ActivityInfo.ActivityId) - closeTime := ctx.ExecutionCloseTime() + closeTime := ctx.ExecutionInfo().CloseTime s.True(closeTime.IsZero(), "CloseTime should be zero when component is still running") return nil }, @@ -649,6 +964,42 @@ func (s *chasmEngineSuite) TestReadComponent_Success() { s.NoError(err) } +func (s *chasmEngineSuite) TestReadComponent_SetsContextMetadata() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + ) + expectedActivityID := tv.ActivityID() + + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState(ref.ExecutionKey, &persistencespb.ActivityInfo{ + ActivityId: expectedActivityID, + }, enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, nil), + }, nil).Times(1) + + requestCtx := newTestMetadataContext("read-request") + + err := s.engine.ReadComponent( + requestCtx, + ref, + func( + ctx chasm.Context, + component chasm.Component, + ) error { + s.assertTestContextMetadata(requestCtx, expectedActivityID, "read-request") + return nil + }, + ) + s.NoError(err) +} + // TestPollComponent_Success_NoWait tests the behavior of PollComponent when the predicate is // satisfied at the outset. func (s *chasmEngineSuite) TestPollComponent_Success_NoWait() { @@ -685,6 +1036,39 @@ func (s *chasmEngineSuite) TestPollComponent_Success_NoWait() { s.Equal(ref.BusinessID, newRef.BusinessID) } +func (s *chasmEngineSuite) TestPollComponent_SetsContextMetadata() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + RunID: tv.RunID(), + }, + ) + expectedActivityID := tv.ActivityID() + + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState(ref.ExecutionKey, &persistencespb.ActivityInfo{ + ActivityId: expectedActivityID, + }, enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, nil), + }, nil).Times(1) + + requestCtx := newTestMetadataContext("poll-request") + + _, err := s.engine.PollComponent( + requestCtx, + ref, + func(ctx chasm.Context, component chasm.Component) (bool, error) { + s.assertTestContextMetadata(requestCtx, expectedActivityID, "poll-request") + return true, nil + }, + ) + s.NoError(err) +} + // TestPollComponent_Success_Wait tests the waiting behavior of PollComponent. func (s *chasmEngineSuite) TestPollComponent_Success_Wait() { testCases := []struct { @@ -934,7 +1318,7 @@ func (s *chasmEngineSuite) TestCloseTime_ReturnsNonZeroWhenCompleted() { component chasm.Component, ) error { // Verify CloseTime returns non-zero time when component is completed - closeTime := ctx.ExecutionCloseTime() + closeTime := ctx.ExecutionInfo().CloseTime s.False(closeTime.IsZero(), "CloseTime should be non-zero when component is completed") s.Equal(expectedCloseTime.Unix(), closeTime.Unix(), "CloseTime should match the expected close time") return nil @@ -987,7 +1371,7 @@ func (s *chasmEngineSuite) TestStateTransitionCount() { context.Background(), ref, func(ctx chasm.Context, component chasm.Component) error { - s.Equal(initialCount+1, ctx.StateTransitionCount()) + s.Equal(initialCount+1, ctx.ExecutionInfo().StateTransitionCount) return nil }, ) @@ -1055,7 +1439,7 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_ExistingRunning() { result, err := s.engine.UpdateWithStartExecution( context.Background(), chasm.NewComponentRef[*testComponent](executionKey), - func(ctx chasm.MutableContext) (chasm.Component, error) { + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { s.Fail("newFn should not be called when execution exists and is running") return nil, nil }, @@ -1078,6 +1462,61 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_ExistingRunning() { s.Equal(result.ExecutionKey, deserializedRef.ExecutionKey) } +func (s *chasmEngineSuite) TestUpdateWithStartExecution_ExistingRunning_SetsContextMetadata() { + tv := testvars.New(s.T()) + tv = tv.WithRunID(tv.Any().RunID()) + + executionKey := chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + } + existingActivityID := tv.ActivityID() + updatedActivityID := "updated-" + existingActivityID + + s.mockExecutionManager.EXPECT().GetCurrentExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetCurrentExecutionResponse{ + RunID: tv.RunID(), + }, nil).Times(1) + s.mockExecutionManager.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()). + Return(&persistence.GetWorkflowExecutionResponse{ + State: s.buildPersistenceMutableState( + chasm.ExecutionKey{ + NamespaceID: executionKey.NamespaceID, + BusinessID: executionKey.BusinessID, + RunID: tv.RunID(), + }, + &persistencespb.ActivityInfo{ + ActivityId: existingActivityID, + }, + enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, + enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, + nil, + ), + }, nil).Times(1) + s.mockExecutionManager.EXPECT().UpdateWorkflowExecution(gomock.Any(), gomock.Any()). + Return(tests.UpdateWorkflowExecutionResponse, nil).Times(1) + s.mockEngine.EXPECT().NotifyChasmExecution(gomock.Any(), gomock.Any()).Return().Times(1) + + requestCtx := newTestMetadataContext("update-with-start-existing") + + _, err := s.engine.UpdateWithStartExecution( + requestCtx, + chasm.NewComponentRef[*testComponent](executionKey), + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { + s.Fail("newFn should not be called when execution exists and is running") + return nil, nil + }, + func(ctx chasm.MutableContext, component chasm.Component) error { + tc, ok := component.(*testComponent) + s.True(ok) + tc.ActivityInfo.ActivityId = updatedActivityID + return nil + }, + ) + s.NoError(err) + s.assertTestContextMetadata(requestCtx, updatedActivityID, "update-with-start-existing") +} + func (s *chasmEngineSuite) TestUpdateWithStartExecution_NotFound() { tv := testvars.New(s.T()) @@ -1122,7 +1561,7 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_NotFound() { result, err := s.engine.UpdateWithStartExecution( context.Background(), chasm.NewComponentRef[*testComponent](executionKey), - func(ctx chasm.MutableContext) (chasm.Component, error) { + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { newFnCalled = true return &testComponent{ ActivityInfo: &persistencespb.ActivityInfo{ @@ -1152,6 +1591,44 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_NotFound() { s.Equal(result.ExecutionKey, deserializedRef.ExecutionKey) } +func (s *chasmEngineSuite) TestUpdateWithStartExecution_NotFound_SetsContextMetadata() { + tv := testvars.New(s.T()) + + executionKey := chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: tv.WorkflowID(), + } + newActivityID := "updated-" + tv.Any().String() + + s.mockExecutionManager.EXPECT().GetCurrentExecution(gomock.Any(), gomock.Any()). + Return(nil, serviceerror.NewNotFound("execution not found")).Times(1) + s.mockExecutionManager.EXPECT().CreateWorkflowExecution(gomock.Any(), gomock.Any()). + Return(tests.CreateWorkflowExecutionResponse, nil).Times(1) + s.mockEngine.EXPECT().NotifyChasmExecution(gomock.Any(), gomock.Any()).Return().Times(1) + + requestCtx := newTestMetadataContext("update-with-start-create") + + _, err := s.engine.UpdateWithStartExecution( + requestCtx, + chasm.NewComponentRef[*testComponent](executionKey), + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { + return &testComponent{ + ActivityInfo: &persistencespb.ActivityInfo{ + ActivityId: tv.Any().String(), + }, + }, nil + }, + func(ctx chasm.MutableContext, component chasm.Component) error { + tc, ok := component.(*testComponent) + s.True(ok) + tc.ActivityInfo.ActivityId = newActivityID + return nil + }, + ) + s.NoError(err) + s.assertTestContextMetadata(requestCtx, newActivityID, "update-with-start-create") +} + func (s *chasmEngineSuite) TestUpdateWithStartExecution_ExistingClosed() { tv := testvars.New(s.T()) tv = tv.WithRunID(tv.Any().RunID()) @@ -1212,7 +1689,7 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_ExistingClosed() { result, err := s.engine.UpdateWithStartExecution( context.Background(), chasm.NewComponentRef[*testComponent](executionKey), - func(ctx chasm.MutableContext) (chasm.Component, error) { + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { newFnCalled = true return &testComponent{ ActivityInfo: &persistencespb.ActivityInfo{ @@ -1280,7 +1757,7 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_UpdateFnError() { _, err := s.engine.UpdateWithStartExecution( context.Background(), chasm.NewComponentRef[*testComponent](executionKey), - func(ctx chasm.MutableContext) (chasm.Component, error) { + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { s.Fail("newFn should not be called") return nil, nil }, @@ -1307,7 +1784,7 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_NewFnError() { _, err := s.engine.UpdateWithStartExecution( context.Background(), chasm.NewComponentRef[*testComponent](executionKey), - func(ctx chasm.MutableContext) (chasm.Component, error) { + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { return nil, expectedErr }, func(ctx chasm.MutableContext, component chasm.Component) error { @@ -1335,7 +1812,7 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_UpdateFnErrorOnCreate() _, err := s.engine.UpdateWithStartExecution( context.Background(), chasm.NewComponentRef[*testComponent](executionKey), - func(ctx chasm.MutableContext) (chasm.Component, error) { + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { newFnCalled = true return &testComponent{ ActivityInfo: &persistencespb.ActivityInfo{ @@ -1401,7 +1878,7 @@ func (s *chasmEngineSuite) TestUpdateWithStartExecution_UpdatePathVersionConflic _, err := s.engine.UpdateWithStartExecution( context.Background(), chasm.NewComponentRef[*testComponent](executionKey), - func(ctx chasm.MutableContext) (chasm.Component, error) { + func(ctx chasm.MutableContext) (chasm.RootComponent, error) { s.Fail("newFn should not be called when execution exists") return nil, nil }, @@ -1511,9 +1988,54 @@ func (s *chasmEngineSuite) serializeComponentState( return blob } +func (s *chasmEngineSuite) newTestMutableState( + key chasm.ExecutionKey, + rootComponent chasm.RootComponent, +) historyi.MutableState { + mutableState := workflow.NewMutableState( + s.mockShard, + s.mockShard.GetEventsCache(), + s.mockShard.GetLogger(), + s.namespaceEntry, + key.BusinessID, + key.RunID, + s.mockShard.GetTimeSource().Now(), + ) + + chasmTree, err := chasmTreeFromMutableState(s.mockShard.GetLogger(), mutableState) + s.NoError(err) + s.NoError(chasmTree.SetRootComponent(rootComponent)) + + return mutableState +} + +func newTestMetadataContext( + requestValue string, +) context.Context { + return contextutil.WithMetadataContext( + context.WithValue(context.Background(), testRequestContextKey{}, requestValue), + ) +} + +func (s *chasmEngineSuite) assertTestContextMetadata( + ctx context.Context, + expectedActivityID string, + expectedRequestValue string, +) { + activityID, ok := contextutil.ContextMetadataGet(ctx, testContextMetadataActivityKey) + s.True(ok) + s.Equal(expectedActivityID, activityID) + + requestValue, ok := contextutil.ContextMetadataGet(ctx, testContextMetadataRequestKey) + s.True(ok) + s.Equal(expectedRequestValue, requestValue) +} + const ( - testComponentPausedSAName = "PausedSA" - testTransitionCount = 10 + testComponentPausedSAName = "PausedSA" + testTransitionCount = 10 + testContextMetadataActivityKey = "test.activity-id" + testContextMetadataRequestKey = "test.request-value" ) var ( @@ -1521,8 +2043,12 @@ var ( _ chasm.VisibilitySearchAttributesProvider = (*testComponent)(nil) _ chasm.VisibilityMemoProvider = (*testComponent)(nil) + _ chasm.RootComponent = (*testComponent)(nil) + _ chasm.RootComponent = (*testComponentNoMetadata)(nil) ) +type testRequestContextKey struct{} + type testComponent struct { chasm.UnimplementedComponent @@ -1533,6 +2059,13 @@ func (l *testComponent) LifecycleState(_ chasm.Context) chasm.LifecycleState { return chasm.LifecycleStateRunning } +func (l *testComponent) Terminate( + _ chasm.MutableContext, + _ chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + return chasm.TerminateComponentResponse{}, nil +} + func (l *testComponent) SearchAttributes(_ chasm.Context) []chasm.SearchAttributeKeyValue { return []chasm.SearchAttributeKeyValue{ testComponentPausedSearchAttribute.Value(l.ActivityInfo.Paused), @@ -1545,6 +2078,40 @@ func (l *testComponent) Memo(_ chasm.Context) proto.Message { } } +func (l *testComponent) ContextMetadata(ctx chasm.Context) map[string]string { + metadata := map[string]string{ + testContextMetadataActivityKey: l.ActivityInfo.GetActivityId(), + } + + if requestValue, ok := ctx.Value(testRequestContextKey{}).(string); ok && requestValue != "" { + metadata[testContextMetadataRequestKey] = requestValue + } + + return metadata +} + +type testComponentNoMetadata struct { + chasm.UnimplementedComponent + + ActivityInfo *persistencespb.ActivityInfo +} + +func (l *testComponentNoMetadata) LifecycleState(_ chasm.Context) chasm.LifecycleState { + return chasm.LifecycleStateRunning +} + +func (l *testComponentNoMetadata) ContextMetadata(_ chasm.Context) map[string]string { + // TODO: Export context metadata from this root. + return nil +} + +func (l *testComponentNoMetadata) Terminate( + _ chasm.MutableContext, + _ chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + return chasm.TerminateComponentResponse{}, nil +} + func newTestComponentStateBlob(info *persistencespb.ActivityInfo) *commonpb.DataBlob { data, _ := info.Marshal() return &commonpb.DataBlob{ @@ -1565,5 +2132,274 @@ func (l *testChasmLibrary) Components() []*chasm.RegistrableComponent { return []*chasm.RegistrableComponent{ chasm.NewRegistrableComponent[*testComponent]("test_component", chasm.WithSearchAttributes(testComponentPausedSearchAttribute)), + chasm.NewRegistrableComponent[*testComponentNoMetadata]("test_component_no_metadata"), } } + +func (s *chasmEngineSuite) TestConvertError() { + t := s.T() + tv := testvars.New(t) + tv = tv.WithRunID(tv.Any().RunID()) + businessID := tv.WorkflowID() + + ref := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: businessID, + RunID: tv.RunID(), + }, + ) + + t.Run("NotFound", func(t *testing.T) { + err := serviceerror.NewNotFound("original not found") + convertedErr := s.engine.convertError(err, ref, tv.RequestID()) + require.Error(t, convertedErr) + var notFoundErr *serviceerror.NotFound + require.ErrorAs(t, convertedErr, ¬FoundErr) + require.Equal(t, fmt.Sprintf("%s not found for ID: %s", "test_component", businessID), convertedErr.Error()) + }) + + t.Run("NotFound_WithoutBusinessID", func(t *testing.T) { + refWithoutBusinessID := chasm.NewComponentRef[*testComponent]( + chasm.ExecutionKey{ + NamespaceID: string(tests.NamespaceID), + BusinessID: "", + RunID: tv.RunID(), + }, + ) + err := serviceerror.NewNotFound("original not found") + convertedErr := s.engine.convertError(err, refWithoutBusinessID, tv.RequestID()) + require.Error(t, convertedErr) + var notFoundErr *serviceerror.NotFound + require.ErrorAs(t, convertedErr, ¬FoundErr) + require.Equal(t, err, convertedErr) + }) + + t.Run("UnconvertedServiceErrors", func(t *testing.T) { + testErrors := []error{ + chasm.NewExecutionAlreadyStartedErr("already started", "request-123", "run-456"), + serviceerror.NewInvalidArgument("invalid argument"), + serviceerror.NewAlreadyExists("already exists"), + serviceerror.NewFailedPrecondition("failed precondition"), + serviceerror.NewResourceExhausted(enumspb.RESOURCE_EXHAUSTED_CAUSE_APS_LIMIT, "resource exhausted"), + serviceerror.NewCanceled("canceled"), + serviceerror.NewDeadlineExceeded("deadline exceeded"), + serviceerror.NewInternal("internal error"), + serviceerror.NewUnavailable("unavailable"), + serviceerror.NewDataLoss("data loss"), + serviceerror.NewPermissionDenied("permission denied", ""), + serviceerror.NewUnimplemented("unimplemented"), + serviceerror.NewNamespaceNotActive("test-namespace", "cluster1", "cluster2"), + } + + for _, err := range testErrors { + convertedErr := s.engine.convertError(err, ref, tv.RequestID()) + require.Equal(t, err, convertedErr) + } + }) + + t.Run("PersistenceErrors", func(t *testing.T) { + persistenceErrorCases := []struct { + name string + err error + setupMocks func() + assertErrType func(t *testing.T, err error) + expectedErrMsg []string + }{ + { + name: "ShardOwnershipLostError", + err: &persistence.ShardOwnershipLostError{ + ShardID: 123, + Msg: "shard ownership lost", + }, + setupMocks: func() { + ownerHost := membership.NewHostInfoFromAddress("owner-host:1234") + currentHost := membership.NewHostInfoFromAddress("current-host:5678") + s.mockShard.Resource.HistoryServiceResolver.EXPECT(). + Lookup("123"). + Return(ownerHost, nil). + Times(1) + s.mockShard.Resource.HostInfoProvider.EXPECT(). + HostInfo(). + Return(currentHost). + Times(1) + }, + assertErrType: func(t *testing.T, err error) { + var solErr *serviceerrors.ShardOwnershipLost + require.ErrorAs(t, err, &solErr) + require.Equal(t, "owner-host:1234", solErr.OwnerHost) + require.Equal(t, "current-host:5678", solErr.CurrentHost) + }, + expectedErrMsg: []string{"Shard is owned by:owner-host:1234 but not by current-host:5678"}, + }, + { + name: "ShardOwnershipLostError_LookupFails", + err: &persistence.ShardOwnershipLostError{ + ShardID: 456, + Msg: "shard ownership lost", + }, + setupMocks: func() { + currentHost := membership.NewHostInfoFromAddress("current-host:5678") + s.mockShard.Resource.HistoryServiceResolver.EXPECT(). + Lookup("456"). + Return(nil, errors.New("lookup failed")). + Times(1) + s.mockShard.Resource.HostInfoProvider.EXPECT(). + HostInfo(). + Return(currentHost). + Times(1) + }, + assertErrType: func(t *testing.T, err error) { + var solErr *serviceerrors.ShardOwnershipLost + require.ErrorAs(t, err, &solErr) + require.Empty(t, solErr.OwnerHost) + require.Equal(t, "current-host:5678", solErr.CurrentHost) + }, + expectedErrMsg: []string{"Shard is owned by: but not by current-host:5678"}, + }, + { + name: "AppendHistoryTimeoutError", + err: &persistence.AppendHistoryTimeoutError{ + Msg: "append history timeout", + }, + assertErrType: func(t *testing.T, err error) { + var unavailable *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailable) + }, + expectedErrMsg: []string{"append history timed out"}, + }, + { + name: "WorkflowConditionFailedError", + err: &persistence.WorkflowConditionFailedError{ + Msg: "workflow condition failed", + DBRecordVersion: 10, + NextEventID: 20, + }, + assertErrType: func(t *testing.T, err error) { + var unavailable *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailable) + }, + expectedErrMsg: []string{"workflow condition failed"}, + }, + { + name: "CurrentWorkflowConditionFailedError", + err: &persistence.CurrentWorkflowConditionFailedError{ + Msg: "current workflow condition failed", + RunID: tv.RunID(), + Status: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, + State: enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, + }, + assertErrType: func(t *testing.T, err error) { + var unavailable *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailable) + }, + expectedErrMsg: []string{"current workflow condition failed", tv.RunID()}, + }, + { + name: "ConditionFailedError", + err: &persistence.ConditionFailedError{ + Msg: "condition failed", + }, + assertErrType: func(t *testing.T, err error) { + var unavailable *serviceerror.Unavailable + require.ErrorAs(t, err, &unavailable) + }, + expectedErrMsg: []string{"condition failed"}, + }, + { + name: "TransactionSizeLimitError", + err: &persistence.TransactionSizeLimitError{ + Msg: "transaction too large", + }, + assertErrType: func(t *testing.T, err error) { + var invalidArgument *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgument) + }, + expectedErrMsg: []string{"transaction size limit exceeded"}, + }, + { + name: "TimeoutError", + err: &persistence.TimeoutError{ + Msg: "persistence timeout", + }, + assertErrType: func(t *testing.T, err error) { + var deadlineExceeded *serviceerror.DeadlineExceeded + require.ErrorAs(t, err, &deadlineExceeded) + }, + expectedErrMsg: []string{"persistence operation timed out"}, + }, + } + + for _, tc := range persistenceErrorCases { + t.Run(tc.name, func(t *testing.T) { + if tc.setupMocks != nil { + tc.setupMocks() + } + convertedErr := s.engine.convertError(tc.err, ref, tv.RequestID()) + require.Error(t, convertedErr) + tc.assertErrType(t, convertedErr) + for _, msg := range tc.expectedErrMsg { + require.Contains(t, convertedErr.Error(), msg) + } + }) + } + }) + + t.Run("UncategorizedError", func(t *testing.T) { + err := errors.New("some unknown error") + convertedErr := s.engine.convertError(err, ref, tv.RequestID()) + require.Error(t, convertedErr) + // Should pass through + require.Equal(t, err, convertedErr) + }) + + t.Run("WrappedErrors", func(t *testing.T) { + // Test that wrapped errors are properly detected using errors.AsType() + t.Run("WrappedServiceError", func(t *testing.T) { + baseErr := serviceerror.NewInvalidArgument("invalid input") + wrappedErr := fmt.Errorf("context: %w", baseErr) + convertedErr := s.engine.convertError(wrappedErr, ref, tv.RequestID()) + require.ErrorAs(t, convertedErr, new(*serviceerror.InvalidArgument)) + }) + + t.Run("WrappedPersistenceError", func(t *testing.T) { + baseErr := &persistence.TimeoutError{Msg: "timeout"} + wrappedErr := fmt.Errorf("operation failed: %w", baseErr) + convertedErr := s.engine.convertError(wrappedErr, ref, tv.RequestID()) + require.ErrorAs(t, convertedErr, new(*serviceerror.DeadlineExceeded)) + require.Contains(t, convertedErr.Error(), "persistence operation timed out") + }) + + t.Run("WrappedChasmError", func(t *testing.T) { + baseErr := chasm.NewExecutionAlreadyStartedErr("already started", tv.RequestID(), tv.RunID()) + wrappedErr := fmt.Errorf("wrapped: %w", baseErr) + convertedErr := s.engine.convertError(wrappedErr, ref, tv.RequestID()) + var chasmErr *chasm.ExecutionAlreadyStartedError + require.ErrorAs(t, convertedErr, &chasmErr) + }) + }) + + t.Run("ContextErrors", func(t *testing.T) { + t.Run("ContextCanceled", func(t *testing.T) { + convertedErr := s.engine.convertError(context.Canceled, ref, tv.RequestID()) + require.ErrorIs(t, convertedErr, context.Canceled) + }) + + t.Run("ContextDeadlineExceeded", func(t *testing.T) { + convertedErr := s.engine.convertError(context.DeadlineExceeded, ref, tv.RequestID()) + require.ErrorIs(t, convertedErr, context.DeadlineExceeded) + }) + + t.Run("WrappedContextCanceled", func(t *testing.T) { + wrappedErr := fmt.Errorf("operation canceled: %w", context.Canceled) + convertedErr := s.engine.convertError(wrappedErr, ref, tv.RequestID()) + require.ErrorIs(t, convertedErr, context.Canceled) + }) + + t.Run("WrappedContextDeadlineExceeded", func(t *testing.T) { + wrappedErr := fmt.Errorf("operation timed out: %w", context.DeadlineExceeded) + convertedErr := s.engine.convertError(wrappedErr, ref, tv.RequestID()) + require.ErrorIs(t, convertedErr, context.DeadlineExceeded) + }) + }) +} diff --git a/service/history/chasm_task_util.go b/service/history/chasm_task_util.go index 654b2da633c..4513b10d832 100644 --- a/service/history/chasm_task_util.go +++ b/service/history/chasm_task_util.go @@ -2,9 +2,14 @@ package history import ( "context" + "errors" enumsspb "go.temporal.io/server/api/enums/v1" "go.temporal.io/server/chasm" + "go.temporal.io/server/client" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/namespace" "go.temporal.io/server/service/history/consts" historyi "go.temporal.io/server/service/history/interfaces" "go.temporal.io/server/service/history/tasks" @@ -16,26 +21,20 @@ func validateChasmSideEffectTask( ctx context.Context, ms historyi.MutableState, task *tasks.ChasmTask, -) (any, error) { +) (bool, error) { // Because CHASM timers can target closed workflows, we need to specifically // exclude zombie workflows, instead of merely checking that the workflow is // running. if ms.GetExecutionState().State == enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE { - return nil, consts.ErrWorkflowZombie + return false, consts.ErrWorkflowZombie } tree := ms.ChasmTree() if tree == nil { - return nil, errNoChasmTree + return false, errNoChasmTree } - isValid, err := tree.ValidateSideEffectTask(ctx, task) - if err == nil && isValid { - // If the task is still valid, keep it around by returning a non-nil value. - return &struct{}{}, nil - } - - return nil, err + return tree.ValidateSideEffectTask(ctx, task) } // executeChasmSideEffectTask completes execution of a CHASM side effect task @@ -46,7 +45,6 @@ func validateChasmSideEffectTask( func executeChasmSideEffectTask( ctx context.Context, engine chasm.Engine, - registry *chasm.Registry, tree historyi.ChasmTree, task *tasks.ChasmTask, ) error { @@ -78,9 +76,70 @@ func executeChasmSideEffectTask( engineCtx := chasm.NewEngineContext(ctx, engine) return tree.ExecuteSideEffectTask( engineCtx, + executionKey, + task, + validate, + ) +} + +// discardChasmSideEffectTask handles discard of a CHASM side effect task on standby. It first checks if the execution +// still exists on the source (active) cluster — if gone, it silently drops the task by returning nil. If the execution +// still exists, it calls the handler's Discard method. If Discard returns ErrTaskDiscarded (the default from +// SideEffectTaskHandlerBase), it logs a warning and returns consts.ErrTaskDiscarded. +func discardChasmSideEffectTask( + ctx context.Context, + engine chasm.Engine, + registry *chasm.Registry, + tree historyi.ChasmTree, + task *tasks.ChasmTask, + logger log.Logger, + clusterName string, + clientBean client.Bean, + namespaceRegistry namespace.Registry, +) error { + if !executionExistsOnSource( + ctx, + taskWorkflowKey(task), + getTaskArchetypeID(task), + logger, + clusterName, + clientBean, + namespaceRegistry, registry, + ) { + return nil + } + + executionKey := chasm.ExecutionKey{ + NamespaceID: task.NamespaceID, + BusinessID: task.WorkflowID, + RunID: task.RunID, + } + + validate := func(backend chasm.NodeBackend, _ chasm.Context, _ chasm.Component) error { + if backend.GetExecutionState().State == enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE { + return consts.ErrWorkflowZombie + } + + taskID := task.TaskID + tgClock := backend.GetExecutionInfo().TaskGenerationShardClockTimestamp + if tgClock != 0 && taskID != 0 && taskID < tgClock { + return consts.ErrStaleReference + } + + return nil + } + + engineCtx := chasm.NewEngineContext(ctx, engine) + err := tree.ExecuteSideEffectDiscardTask( + engineCtx, executionKey, task, validate, ) + if errors.Is(err, chasm.ErrTaskDiscarded) { + logger.Warn("Discarding standby CHASM task due to task being pending for too long.", tag.Task(task)) + return consts.ErrTaskDiscarded + } + return err } diff --git a/service/history/chasm_task_util_test.go b/service/history/chasm_task_util_test.go new file mode 100644 index 00000000000..30d39e898c0 --- /dev/null +++ b/service/history/chasm_task_util_test.go @@ -0,0 +1,73 @@ +package history + +import ( + "context" + + "go.temporal.io/server/chasm" +) + +// discardableTaskTestLibrary is a minimal CHASM library that registers a side-effect task whose handler has a custom +// Discard implementation, used for testing discard paths in standby task executors. +type discardableTaskTestLibrary struct { + chasm.UnimplementedLibrary +} + +func (l *discardableTaskTestLibrary) Name() string { return "DiscardableTestLib" } + +func (l *discardableTaskTestLibrary) Tasks() []*chasm.RegistrableTask { + return []*chasm.RegistrableTask{ + chasm.NewRegistrableSideEffectTask( + "discard_task", + &discardableTestTaskHandler{}, + ), + } +} + +type discardableTestTask struct{} + +type discardableTestTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*discardableTestTask] +} + +func (e *discardableTestTaskHandler) Validate(_ chasm.Context, _ any, _ chasm.TaskAttributes, _ *discardableTestTask) (bool, error) { + return true, nil +} + +func (e *discardableTestTaskHandler) Execute(_ context.Context, _ chasm.ComponentRef, _ chasm.TaskAttributes, _ *discardableTestTask) error { + return nil +} + +func (e *discardableTestTaskHandler) Discard(_ context.Context, _ chasm.ComponentRef, _ chasm.TaskAttributes, _ *discardableTestTask) error { + return nil +} + +// nonDiscardableTaskTestLibrary is a minimal CHASM library that registers a side-effect task whose handler uses the +// default Discard from SideEffectTaskHandlerBase (returns ErrTaskDiscarded). +type nonDiscardableTaskTestLibrary struct { + chasm.UnimplementedLibrary +} + +func (l *nonDiscardableTaskTestLibrary) Name() string { return "NonDiscardableTestLib" } + +func (l *nonDiscardableTaskTestLibrary) Tasks() []*chasm.RegistrableTask { + return []*chasm.RegistrableTask{ + chasm.NewRegistrableSideEffectTask( + "non_discard_task", + &nonDiscardableTestTaskHandler{}, + ), + } +} + +type nonDiscardableTestTask struct{} + +type nonDiscardableTestTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*nonDiscardableTestTask] +} + +func (e *nonDiscardableTestTaskHandler) Validate(_ chasm.Context, _ any, _ chasm.TaskAttributes, _ *nonDiscardableTestTask) (bool, error) { + return true, nil +} + +func (e *nonDiscardableTestTaskHandler) Execute(_ context.Context, _ chasm.ComponentRef, _ chasm.TaskAttributes, _ *nonDiscardableTestTask) error { + return nil +} diff --git a/service/history/configs/config.go b/service/history/configs/config.go index e5cbb50d6b9..0e949b34e40 100644 --- a/service/history/configs/config.go +++ b/service/history/configs/config.go @@ -59,7 +59,6 @@ type Config struct { HistoryCacheTTL dynamicconfig.DurationPropertyFn HistoryCacheNonUserContextLockTimeout dynamicconfig.DurationPropertyFn HistoryCacheBackgroundEvict dynamicconfig.TypedPropertyFn[dynamicconfig.CacheBackgroundEvictSettings] - EnableNexus dynamicconfig.BoolPropertyFn EnableWorkflowExecutionTimeoutTimer dynamicconfig.BoolPropertyFn EnableUpdateWorkflowModeIgnoreCurrent dynamicconfig.BoolPropertyFn EnableTransitionHistory dynamicconfig.BoolPropertyFnWithNamespaceFilter @@ -96,6 +95,7 @@ type Config struct { StandbyClusterDelay dynamicconfig.DurationPropertyFn StandbyTaskMissingEventsResendDelay dynamicconfig.DurationPropertyFnWithTaskTypeFilter StandbyTaskMissingEventsDiscardDelay dynamicconfig.DurationPropertyFnWithTaskTypeFilter + ChasmStandbyTaskDiscardDelay dynamicconfig.DurationPropertyFnWithChasmTaskTypeFilter QueuePendingTaskCriticalCount dynamicconfig.IntPropertyFn QueueReaderStuckCriticalAttempts dynamicconfig.IntPropertyFn @@ -119,6 +119,14 @@ type Config struct { TaskSchedulerNamespaceMaxQPS dynamicconfig.IntPropertyFnWithNamespaceFilter TaskSchedulerInactiveChannelDeletionDelay dynamicconfig.DurationPropertyFn + // ExecutionQueueScheduler settings for sequential per-workflow task processing + TaskSchedulerEnableExecutionQueueScheduler dynamicconfig.BoolPropertyFn + // TaskSchedulerExecutionQueueSchedulerMaxQueues requires restart to take effect + TaskSchedulerExecutionQueueSchedulerMaxQueues dynamicconfig.IntPropertyFn + // TaskSchedulerExecutionQueueSchedulerQueueTTL requires restart to take effect + TaskSchedulerExecutionQueueSchedulerQueueTTL dynamicconfig.DurationPropertyFn + TaskSchedulerExecutionQueueSchedulerQueueConcurrency dynamicconfig.IntPropertyFn + // TimerQueueProcessor settings TimerTaskBatchSize dynamicconfig.IntPropertyFn TimerProcessorSchedulerWorkerCount dynamicconfig.TypedSubscribable[int] @@ -200,8 +208,6 @@ type Config struct { // right now only used by GetMutableState LongPollExpirationInterval dynamicconfig.DurationPropertyFnWithNamespaceFilter - // encoding the history events - EventEncodingType dynamicconfig.StringPropertyFnWithNamespaceFilter // whether or not using ParentClosePolicy EnableParentClosePolicy dynamicconfig.BoolPropertyFnWithNamespaceFilter // whether or not enable system workers for processing parent close policy task @@ -253,6 +259,8 @@ type Config struct { EnableWorkflowTaskStampIncrementOnFailure dynamicconfig.BoolPropertyFn DiscardSpeculativeWorkflowTaskMaximumEventsCount dynamicconfig.IntPropertyFn EnableDropRepeatedWorkflowTaskFailures dynamicconfig.BoolPropertyFnWithNamespaceFilter + // TODO seankane: Added on 3/10/2026, if this is never used we should remove it at a later date + SendTransientOrSpeculativeWorkflowTaskEvents dynamicconfig.BoolPropertyFnWithNamespaceFilter // The following is used by the new RPC replication stack ReplicationTaskApplyTimeout dynamicconfig.DurationPropertyFn @@ -308,6 +316,7 @@ type Config struct { ReplicationStreamReceiverLivenessMultiplier dynamicconfig.IntPropertyFn ReplicationStreamSenderLivenessMultiplier dynamicconfig.IntPropertyFn EnableHistoryReplicationRateLimiter dynamicconfig.BoolPropertyFnWithNamespaceFilter + EnableDeleteWorkflowExecutionReplication dynamicconfig.BoolPropertyFn // The following are used by consistent query MaxBufferedQueryCount dynamicconfig.IntPropertyFn @@ -466,7 +475,6 @@ func NewConfig( HistoryCacheTTL: dynamicconfig.HistoryCacheTTL.Get(dc), HistoryCacheNonUserContextLockTimeout: dynamicconfig.HistoryCacheNonUserContextLockTimeout.Get(dc), HistoryCacheBackgroundEvict: dynamicconfig.HistoryCacheBackgroundEvict.Get(dc), - EnableNexus: dynamicconfig.EnableNexus.Get(dc), EnableWorkflowExecutionTimeoutTimer: dynamicconfig.EnableWorkflowExecutionTimeoutTimer.Get(dc), EnableUpdateWorkflowModeIgnoreCurrent: dynamicconfig.EnableUpdateWorkflowModeIgnoreCurrent.Get(dc), EnableTransitionHistory: dynamicconfig.EnableTransitionHistory.Get(dc), @@ -501,6 +509,7 @@ func NewConfig( StandbyClusterDelay: dynamicconfig.StandbyClusterDelay.Get(dc), StandbyTaskMissingEventsResendDelay: dynamicconfig.StandbyTaskMissingEventsResendDelay.Get(dc), StandbyTaskMissingEventsDiscardDelay: dynamicconfig.StandbyTaskMissingEventsDiscardDelay.Get(dc), + ChasmStandbyTaskDiscardDelay: dynamicconfig.ChasmStandbyTaskDiscardDelay.Get(dc), QueuePendingTaskCriticalCount: dynamicconfig.QueuePendingTaskCriticalCount.Get(dc), QueueReaderStuckCriticalAttempts: dynamicconfig.QueueReaderStuckCriticalAttempts.Get(dc), @@ -515,14 +524,18 @@ func NewConfig( TaskDLQInternalErrors: dynamicconfig.HistoryTaskDLQInternalErrors.Get(dc), TaskDLQErrorPattern: dynamicconfig.HistoryTaskDLQErrorPattern.Get(dc), - TaskSchedulerEnableRateLimiter: dynamicconfig.TaskSchedulerEnableRateLimiter.Get(dc), - TaskSchedulerEnableRateLimiterShadowMode: dynamicconfig.TaskSchedulerEnableRateLimiterShadowMode.Get(dc), - TaskSchedulerRateLimiterStartupDelay: dynamicconfig.TaskSchedulerRateLimiterStartupDelay.Get(dc), - TaskSchedulerGlobalMaxQPS: dynamicconfig.TaskSchedulerGlobalMaxQPS.Get(dc), - TaskSchedulerMaxQPS: dynamicconfig.TaskSchedulerMaxQPS.Get(dc), - TaskSchedulerNamespaceMaxQPS: dynamicconfig.TaskSchedulerNamespaceMaxQPS.Get(dc), - TaskSchedulerGlobalNamespaceMaxQPS: dynamicconfig.TaskSchedulerGlobalNamespaceMaxQPS.Get(dc), - TaskSchedulerInactiveChannelDeletionDelay: dynamicconfig.TaskSchedulerInactiveChannelDeletionDelay.Get(dc), + TaskSchedulerEnableRateLimiter: dynamicconfig.TaskSchedulerEnableRateLimiter.Get(dc), + TaskSchedulerEnableRateLimiterShadowMode: dynamicconfig.TaskSchedulerEnableRateLimiterShadowMode.Get(dc), + TaskSchedulerRateLimiterStartupDelay: dynamicconfig.TaskSchedulerRateLimiterStartupDelay.Get(dc), + TaskSchedulerGlobalMaxQPS: dynamicconfig.TaskSchedulerGlobalMaxQPS.Get(dc), + TaskSchedulerMaxQPS: dynamicconfig.TaskSchedulerMaxQPS.Get(dc), + TaskSchedulerNamespaceMaxQPS: dynamicconfig.TaskSchedulerNamespaceMaxQPS.Get(dc), + TaskSchedulerGlobalNamespaceMaxQPS: dynamicconfig.TaskSchedulerGlobalNamespaceMaxQPS.Get(dc), + TaskSchedulerInactiveChannelDeletionDelay: dynamicconfig.TaskSchedulerInactiveChannelDeletionDelay.Get(dc), + TaskSchedulerEnableExecutionQueueScheduler: dynamicconfig.TaskSchedulerEnableExecutionQueueScheduler.Get(dc), + TaskSchedulerExecutionQueueSchedulerMaxQueues: dynamicconfig.TaskSchedulerExecutionQueueSchedulerMaxQueues.Get(dc), + TaskSchedulerExecutionQueueSchedulerQueueTTL: dynamicconfig.TaskSchedulerExecutionQueueSchedulerQueueTTL.Get(dc), + TaskSchedulerExecutionQueueSchedulerQueueConcurrency: dynamicconfig.TaskSchedulerExecutionQueueSchedulerQueueConcurrency.Get(dc), TimerTaskBatchSize: dynamicconfig.TimerTaskBatchSize.Get(dc), TimerProcessorSchedulerWorkerCount: dynamicconfig.TimerProcessorSchedulerWorkerCount.Subscribe(dc), @@ -605,6 +618,7 @@ func NewConfig( ReplicationStreamReceiverLivenessMultiplier: dynamicconfig.ReplicationStreamReceiverLivenessMultiplier.Get(dc), ReplicationStreamSenderLivenessMultiplier: dynamicconfig.ReplicationStreamSenderLivenessMultiplier.Get(dc), EnableHistoryReplicationRateLimiter: dynamicconfig.EnableHistoryReplicationRateLimiter.Get(dc), + EnableDeleteWorkflowExecutionReplication: dynamicconfig.EnableDeleteWorkflowExecutionReplication.Get(dc), MaximumBufferedEventsBatch: dynamicconfig.MaximumBufferedEventsBatch.Get(dc), MaximumBufferedEventsSizeInBytes: dynamicconfig.MaximumBufferedEventsSizeInBytes.Get(dc), @@ -618,7 +632,6 @@ func NewConfig( // history client: client/history/client.go set the client timeout 30s // TODO: Return this value to the client: go.temporal.io/server/issues/294 LongPollExpirationInterval: dynamicconfig.HistoryLongPollExpirationInterval.Get(dc), - EventEncodingType: dynamicconfig.DefaultEventEncoding.Get(dc), EnableParentClosePolicy: dynamicconfig.EnableParentClosePolicy.Get(dc), NumParentClosePolicySystemWorkflows: dynamicconfig.NumParentClosePolicySystemWorkflows.Get(dc), EnableParentClosePolicyWorker: dynamicconfig.EnableParentClosePolicyWorker.Get(dc), @@ -656,6 +669,7 @@ func NewConfig( EnableWorkflowTaskStampIncrementOnFailure: dynamicconfig.EnableWorkflowTaskStampIncrementOnFailure.Get(dc), DiscardSpeculativeWorkflowTaskMaximumEventsCount: dynamicconfig.DiscardSpeculativeWorkflowTaskMaximumEventsCount.Get(dc), EnableDropRepeatedWorkflowTaskFailures: dynamicconfig.EnableDropRepeatedWorkflowTaskFailures.Get(dc), + SendTransientOrSpeculativeWorkflowTaskEvents: dynamicconfig.SendTransientOrSpeculativeWorkflowTaskEvents.Get(dc), ReplicationTaskApplyTimeout: dynamicconfig.ReplicationTaskApplyTimeout.Get(dc), ReplicationTaskFetcherParallelism: dynamicconfig.ReplicationTaskFetcherParallelism.Get(dc), diff --git a/service/history/configs/quotas_test.go b/service/history/configs/quotas_test.go index 657cc997cc6..fec8d229af4 100644 --- a/service/history/configs/quotas_test.go +++ b/service/history/configs/quotas_test.go @@ -73,7 +73,7 @@ func (s *quotasSuite) TestOperatorPrioritized() { requestTime := time.Now() limitCount := 0 - for i := 0; i < 12; i++ { + for range 12 { if !limiter.Allow(requestTime, apiRequest) { limitCount++ s.True(limiter.Allow(requestTime, operatorRequest)) diff --git a/service/history/configs/task.go b/service/history/configs/task.go index a890ba71091..640f1868a07 100644 --- a/service/history/configs/task.go +++ b/service/history/configs/task.go @@ -25,8 +25,8 @@ var ( func ConvertWeightsToDynamicConfigValue( weights map[tasks.Priority]int, -) map[string]interface{} { - weightsForDC := make(map[string]interface{}) +) map[string]any { + weightsForDC := make(map[string]any) for priority, weight := range weights { weightsForDC[priority.String()] = weight } @@ -34,7 +34,7 @@ func ConvertWeightsToDynamicConfigValue( } func ConvertDynamicConfigValueToWeights( - weightsFromDC map[string]interface{}, + weightsFromDC map[string]any, logger log.Logger, ) map[tasks.Priority]int { weights := make(map[tasks.Priority]int) diff --git a/service/history/deletemanager/delete_manager.go b/service/history/deletemanager/delete_manager.go index 8765f606ed3..9ca47c9afec 100644 --- a/service/history/deletemanager/delete_manager.go +++ b/service/history/deletemanager/delete_manager.go @@ -6,7 +6,6 @@ import ( "context" commonpb "go.temporal.io/api/common/v1" - "go.temporal.io/server/chasm" "go.temporal.io/server/common/clock" "go.temporal.io/server/common/definition" "go.temporal.io/server/common/metrics" @@ -22,7 +21,7 @@ import ( type ( DeleteManager interface { - AddDeleteWorkflowExecutionTask( + AddDeleteExecutionTask( ctx context.Context, nsID namespace.ID, we *commonpb.WorkflowExecution, @@ -77,28 +76,38 @@ func NewDeleteManager( return deleteManager } -func (m *DeleteManagerImpl) AddDeleteWorkflowExecutionTask( +func (m *DeleteManagerImpl) AddDeleteExecutionTask( ctx context.Context, nsID namespace.ID, we *commonpb.WorkflowExecution, ms historyi.MutableState, ) error { + return AddDeleteExecutionTask(ctx, m.shardContext, nsID, we, ms) +} - taskGenerator := workflow.GetTaskGeneratorProvider().NewTaskGenerator(m.shardContext, ms) +// AddDeleteExecutionTask creates a DeleteExecutionTask and adds it to the shard's task queue. +// This is a package-level function so it can be used by callers that don't have a DeleteManager instance. +func AddDeleteExecutionTask( + ctx context.Context, + shardContext historyi.ShardContext, + nsID namespace.ID, + we *commonpb.WorkflowExecution, + ms historyi.MutableState, +) error { + taskGenerator := workflow.GetTaskGeneratorProvider().NewTaskGenerator(shardContext, ms) - // We can make this task immediately because the task itself will keep rescheduling itself until the workflow is - // closed before actually deleting the workflow. + // We can make this task immediately because the task itself will keep rescheduling itself until the + // execution is closed before actually deleting it. deleteTask, err := taskGenerator.GenerateDeleteExecutionTask() if err != nil { return err } - return m.shardContext.AddTasks(ctx, &persistence.AddHistoryTasksRequest{ - ShardID: m.shardContext.GetShardID(), - // RangeID is set by shardContext + return shardContext.AddTasks(ctx, &persistence.AddHistoryTasksRequest{ + ShardID: shardContext.GetShardID(), NamespaceID: nsID.String(), WorkflowID: we.GetWorkflowId(), - ArchetypeID: chasm.WorkflowArchetypeID, // this method is specific to workflow executions + ArchetypeID: ms.ChasmTree().ArchetypeID(), Tasks: map[tasks.Category][]tasks.Task{ tasks.CategoryTransfer: {deleteTask}, }, @@ -125,6 +134,9 @@ func (m *DeleteManagerImpl) DeleteWorkflowExecutionByRetention( ms historyi.MutableState, stage *tasks.DeleteWorkflowExecutionStage, ) error { + // Skip replication for retention-based deletion. Both clusters have independent retention + // timers and will delete on their own schedule. + stage.MarkProcessed(tasks.DeleteWorkflowExecutionStageReplication) return m.deleteWorkflowExecutionInternal(ctx, nsID, we, weCtx, ms, stage, m.metricsHandler.WithTags(metrics.OperationTag(metrics.HistoryProcessDeleteHistoryEventScope))) } diff --git a/service/history/deletemanager/delete_manager_mock.go b/service/history/deletemanager/delete_manager_mock.go index e9cec7f8e5a..f1f72488c84 100644 --- a/service/history/deletemanager/delete_manager_mock.go +++ b/service/history/deletemanager/delete_manager_mock.go @@ -44,18 +44,18 @@ func (m *MockDeleteManager) EXPECT() *MockDeleteManagerMockRecorder { return m.recorder } -// AddDeleteWorkflowExecutionTask mocks base method. -func (m *MockDeleteManager) AddDeleteWorkflowExecutionTask(ctx context.Context, nsID namespace.ID, we *common.WorkflowExecution, ms interfaces.MutableState) error { +// AddDeleteExecutionTask mocks base method. +func (m *MockDeleteManager) AddDeleteExecutionTask(ctx context.Context, nsID namespace.ID, we *common.WorkflowExecution, ms interfaces.MutableState) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddDeleteWorkflowExecutionTask", ctx, nsID, we, ms) + ret := m.ctrl.Call(m, "AddDeleteExecutionTask", ctx, nsID, we, ms) ret0, _ := ret[0].(error) return ret0 } -// AddDeleteWorkflowExecutionTask indicates an expected call of AddDeleteWorkflowExecutionTask. -func (mr *MockDeleteManagerMockRecorder) AddDeleteWorkflowExecutionTask(ctx, nsID, we, ms any) *gomock.Call { +// AddDeleteExecutionTask indicates an expected call of AddDeleteExecutionTask. +func (mr *MockDeleteManagerMockRecorder) AddDeleteExecutionTask(ctx, nsID, we, ms any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDeleteWorkflowExecutionTask", reflect.TypeOf((*MockDeleteManager)(nil).AddDeleteWorkflowExecutionTask), ctx, nsID, we, ms) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDeleteExecutionTask", reflect.TypeOf((*MockDeleteManager)(nil).AddDeleteExecutionTask), ctx, nsID, we, ms) } // DeleteWorkflowExecution mocks base method. diff --git a/service/history/deletemanager/delete_manager_test.go b/service/history/deletemanager/delete_manager_test.go index 78cdb094f5b..34f253a531a 100644 --- a/service/history/deletemanager/delete_manager_test.go +++ b/service/history/deletemanager/delete_manager_test.go @@ -164,6 +164,61 @@ func (s *deleteManagerWorkflowSuite) TestDeleteDeletedWorkflowExecution_Error() s.Error(err) } +func (s *deleteManagerWorkflowSuite) TestDeleteWorkflowExecutionByRetention_SkipsReplication() { + we := commonpb.WorkflowExecution{ + WorkflowId: tests.WorkflowID, + RunId: tests.RunID, + } + + mockWeCtx := historyi.NewMockWorkflowContext(s.controller) + mockMutableState := historyi.NewMockMutableState(s.controller) + mockMutableState.EXPECT().GetCurrentBranchToken().Return([]byte{22, 8, 78}, nil) + closeExecutionVisibilityTaskID := int64(39) + mockMutableState.EXPECT().GetExecutionInfo().Return(&persistencespb.WorkflowExecutionInfo{ + CloseVisibilityTaskId: closeExecutionVisibilityTaskID, + }) + mockMutableState.EXPECT().ChasmTree().Return(workflow.NoopChasmTree).AnyTimes() + stage := tasks.DeleteWorkflowExecutionStageNone + + s.mockShardContext.EXPECT().DeleteWorkflowExecution( + gomock.Any(), + definition.WorkflowKey{ + NamespaceID: tests.NamespaceID.String(), + WorkflowID: tests.WorkflowID, + RunID: tests.RunID, + }, + workflow.NoopChasmTree.ArchetypeID(), + []byte{22, 8, 78}, + closeExecutionVisibilityTaskID, + time.Unix(0, 0).UTC(), + gomock.Any(), + ).DoAndReturn(func( + _ context.Context, + _ definition.WorkflowKey, + _ chasm.ArchetypeID, + _ []byte, + _ int64, + _ time.Time, + stagePtr *tasks.DeleteWorkflowExecutionStage, + ) error { + // Verify that replication stage is already marked as processed before DeleteWorkflowExecution is called. + s.True(stagePtr.IsProcessed(tasks.DeleteWorkflowExecutionStageReplication), + "Replication stage should be pre-marked as processed for retention-based deletion") + return nil + }) + mockWeCtx.EXPECT().Clear() + + err := s.deleteManager.DeleteWorkflowExecutionByRetention( + context.Background(), + tests.NamespaceID, + &we, + mockWeCtx, + mockMutableState, + &stage, + ) + s.NoError(err) +} + func (s *deleteManagerWorkflowSuite) TestDeleteWorkflowExecution_OpenWorkflow() { we := commonpb.WorkflowExecution{ WorkflowId: tests.WorkflowID, diff --git a/service/history/events/cache.go b/service/history/events/cache.go index e55c835ce72..a4362f21009 100644 --- a/service/history/events/cache.go +++ b/service/history/events/cache.go @@ -212,7 +212,7 @@ func (e *CacheImpl) getHistoryEventFromStore( return nil, errEventNotFoundInBatch } -func (e *CacheImpl) put(key EventKey, event *historypb.HistoryEvent) interface{} { +func (e *CacheImpl) put(key EventKey, event *historypb.HistoryEvent) any { return e.Put(key, newHistoryEventCacheItem(event)) } diff --git a/service/history/events/notifier.go b/service/history/events/notifier.go index 1e48d997908..885e0eb4a83 100644 --- a/service/history/events/notifier.go +++ b/service/history/events/notifier.go @@ -105,7 +105,7 @@ func NewNotifier( workflowIDToShardID func(namespace.ID, string) int32, ) *NotifierImpl { - hashFn := func(key interface{}) uint32 { + hashFn := func(key any) uint32 { notification, ok := key.(Notification) if !ok { return 0 @@ -134,7 +134,7 @@ func (notifier *NotifierImpl) WatchHistoryEvent( subscriberID: channel, } - _, _, err := notifier.eventsPubsubs.PutOrDo(identifier, subscribers, func(key interface{}, value interface{}) error { + _, _, err := notifier.eventsPubsubs.PutOrDo(identifier, subscribers, func(key any, value any) error { subscribers := value.(map[string]chan *Notification) if _, ok := subscribers[subscriberID]; ok { @@ -156,7 +156,7 @@ func (notifier *NotifierImpl) UnwatchHistoryEvent( identifier definition.WorkflowKey, subscriberID string) error { success := true - notifier.eventsPubsubs.RemoveIf(identifier, func(key interface{}, value interface{}) bool { + notifier.eventsPubsubs.RemoveIf(identifier, func(key any, value any) bool { subscribers := value.(map[string]chan *Notification) if _, ok := subscribers[subscriberID]; !ok { @@ -184,7 +184,7 @@ func (notifier *NotifierImpl) dispatchHistoryEventNotification(event *Notificati defer func() { metrics.HistoryEventNotificationFanoutLatency.With(notifier.metricsHandler).Record(time.Since(startTime)) }() - _, _, _ = notifier.eventsPubsubs.GetAndDo(identifier, func(key interface{}, value interface{}) error { + _, _, _ = notifier.eventsPubsubs.GetAndDo(identifier, func(key any, value any) error { subscribers := value.(map[string]chan *Notification) for _, channel := range subscribers { diff --git a/service/history/events/notifier_test.go b/service/history/events/notifier_test.go index a572d4c4466..61ae3d2471d 100644 --- a/service/history/events/notifier_test.go +++ b/service/history/events/notifier_test.go @@ -142,7 +142,7 @@ func (s *notifierSuite) TestMultipleSubscriberWatchingEvents() { waitGroup.Done() } - for count := 0; count < subscriberCount; count++ { + for range subscriberCount { go watchFunc() } diff --git a/service/history/fx.go b/service/history/fx.go index e37adf8907f..7f57e2ab90f 100644 --- a/service/history/fx.go +++ b/service/history/fx.go @@ -88,7 +88,7 @@ var Module = fx.Options( fx.Provide(ReplicationProgressCacheProvider), fx.Provide(VersionMembershipCacheProvider), fx.Provide(ReactivationSignalCacheProvider), - fx.Provide(workerdeployment.ClientProvider), + workerdeployment.ClientModule, fx.Provide(RoutingInfoCacheProvider), fx.Invoke(ServiceLifetimeHooks), @@ -366,10 +366,12 @@ func VisibilityManagerProvider( func ChasmVisibilityManagerProvider( chasmRegistry *chasm.Registry, + nsRegistry namespace.Registry, visibilityManager manager.VisibilityManager, ) chasm.VisibilityManager { return visibility.NewChasmVisibilityManager( chasmRegistry, + nsRegistry, visibilityManager, ) } diff --git a/service/history/handler.go b/service/history/handler.go index 73518d4fef5..d3850db8332 100644 --- a/service/history/handler.go +++ b/service/history/handler.go @@ -19,6 +19,7 @@ import ( nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/serviceerror" enumsspb "go.temporal.io/server/api/enums/v1" + healthspb "go.temporal.io/server/api/health/v1" "go.temporal.io/server/api/historyservice/v1" namespacespb "go.temporal.io/server/api/namespace/v1" persistencespb "go.temporal.io/server/api/persistence/v1" @@ -34,6 +35,7 @@ import ( "go.temporal.io/server/common/convert" "go.temporal.io/server/common/definition" "go.temporal.io/server/common/headers" + healthcheck "go.temporal.io/server/common/health" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/membership" @@ -65,7 +67,7 @@ import ( "go.temporal.io/server/service/history/tasks" "go.uber.org/fx" "google.golang.org/grpc/health" - healthpb "google.golang.org/grpc/health/grpc_health_v1" + grpchealthspb "google.golang.org/grpc/health/grpc_health_v1" ) type ( @@ -206,45 +208,123 @@ func (h *Handler) DeepHealthCheck( _ *historyservice.DeepHealthCheckRequest, ) (*historyservice.DeepHealthCheckResponse, error) { - status, err := h.healthServer.Check(ctx, &healthpb.HealthCheckRequest{Service: serviceName}) + var checks []*healthspb.HealthCheck + overallState := enumsspb.HEALTH_STATE_SERVING + + // Check 1: gRPC health (graceful shutdown / hysteresis). + // If this fails, return early with only this check — no point running + // metric checks if we can't even reach the gRPC health server. + status, err := h.healthServer.Check(ctx, &grpchealthspb.HealthCheckRequest{Service: serviceName}) if err != nil { - return nil, err - } - if status.Status != healthpb.HealthCheckResponse_SERVING { - metrics.HistoryHostHealthGauge.With(h.metricsHandler).Record(float64(enumsspb.HEALTH_STATE_DECLINED_SERVING)) - return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_DECLINED_SERVING}, nil - } + checks = append(checks, &healthspb.HealthCheck{ + CheckType: healthcheck.CheckTypeGRPCHealth, + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Message: fmt.Sprintf("gRPC health check failed: %v", err), + }) + metrics.HistoryHostHealthGauge.With(h.metricsHandler).Record(float64(enumsspb.HEALTH_STATE_NOT_SERVING)) + return &historyservice.DeepHealthCheckResponse{ + State: enumsspb.HEALTH_STATE_NOT_SERVING, + Checks: checks, + }, nil + } + grpcState := enumsspb.HEALTH_STATE_SERVING + grpcMsg := "" + if status.Status != grpchealthspb.HealthCheckResponse_SERVING { + grpcState = enumsspb.HEALTH_STATE_DECLINED_SERVING + overallState = enumsspb.HEALTH_STATE_DECLINED_SERVING + grpcMsg = "gRPC health server not serving" + } + checks = append(checks, &healthspb.HealthCheck{ + CheckType: healthcheck.CheckTypeGRPCHealth, + State: grpcState, + Message: grpcMsg, + }) - rsp := h.checkHistoryHealthSignals() - if rsp != nil { - return rsp, nil + // Check 2: RPC latency + rpcLatency := h.historyHealthSignal.AverageLatency() + rpcLatencyThreshold := h.config.HealthRPCLatencyFailure() + rpcLatencyState := enumsspb.HEALTH_STATE_SERVING + rpcLatencyMsg := "" + if rpcLatency > rpcLatencyThreshold { + rpcLatencyState = enumsspb.HEALTH_STATE_NOT_SERVING + rpcLatencyMsg = fmt.Sprintf("RPC latency %.2fms exceeded %.2fms threshold", rpcLatency, rpcLatencyThreshold) + if overallState != enumsspb.HEALTH_STATE_DECLINED_SERVING { + overallState = enumsspb.HEALTH_STATE_NOT_SERVING + } } + checks = append(checks, &healthspb.HealthCheck{ + CheckType: healthcheck.CheckTypeRPCLatency, + State: rpcLatencyState, + Value: rpcLatency, + Threshold: rpcLatencyThreshold, + Message: rpcLatencyMsg, + }) - latency := h.persistenceHealthSignal.AverageLatency() - errRatio := h.persistenceHealthSignal.ErrorRatio() - - if latency > h.config.HealthPersistenceLatencyFailure() || errRatio > h.config.HealthPersistenceErrorRatio() { - metrics.HistoryHostHealthGauge.With(h.metricsHandler).Record(float64(enumsspb.HEALTH_STATE_NOT_SERVING)) - return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_NOT_SERVING}, nil + // Check 3: RPC error ratio + rpcErrRatio := h.historyHealthSignal.ErrorRatio() + rpcErrThreshold := h.config.HealthRPCErrorRatio() + rpcErrState := enumsspb.HEALTH_STATE_SERVING + rpcErrMsg := "" + if rpcErrRatio > rpcErrThreshold { + rpcErrState = enumsspb.HEALTH_STATE_NOT_SERVING + rpcErrMsg = fmt.Sprintf("RPC error ratio %.4f exceeded %.4f threshold", rpcErrRatio, rpcErrThreshold) + if overallState != enumsspb.HEALTH_STATE_DECLINED_SERVING { + overallState = enumsspb.HEALTH_STATE_NOT_SERVING + } } - metrics.HistoryHostHealthGauge.With(h.metricsHandler).Record(float64(enumsspb.HEALTH_STATE_SERVING)) - return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_SERVING}, nil -} + checks = append(checks, &healthspb.HealthCheck{ + CheckType: healthcheck.CheckTypeRPCErrorRatio, + State: rpcErrState, + Value: rpcErrRatio, + Threshold: rpcErrThreshold, + Message: rpcErrMsg, + }) -// checkHistoryHealthSignals checks the history health signal that is captured by the interceptor. -func (h *Handler) checkHistoryHealthSignals() *historyservice.DeepHealthCheckResponse { - // Check that the RPC latency doesn't exceed the threshold. - if h.historyHealthSignal.AverageLatency() > h.config.HealthRPCLatencyFailure() { - metrics.HistoryHostHealthGauge.With(h.metricsHandler).Record(float64(enumsspb.HEALTH_STATE_NOT_SERVING)) - return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_NOT_SERVING} + // Check 4: Persistence latency + persLatency := h.persistenceHealthSignal.AverageLatency() + persLatencyThreshold := h.config.HealthPersistenceLatencyFailure() + persLatencyState := enumsspb.HEALTH_STATE_SERVING + persLatencyMsg := "" + if persLatency > persLatencyThreshold { + persLatencyState = enumsspb.HEALTH_STATE_NOT_SERVING + persLatencyMsg = fmt.Sprintf("Persistence latency %.2fms exceeded %.2fms threshold", persLatency, persLatencyThreshold) + if overallState != enumsspb.HEALTH_STATE_DECLINED_SERVING { + overallState = enumsspb.HEALTH_STATE_NOT_SERVING + } } + checks = append(checks, &healthspb.HealthCheck{ + CheckType: healthcheck.CheckTypePersistenceLatency, + State: persLatencyState, + Value: persLatency, + Threshold: persLatencyThreshold, + Message: persLatencyMsg, + }) - // Check if the RPC error ratio exceeds the threshold - if h.historyHealthSignal.ErrorRatio() > h.config.HealthRPCErrorRatio() { - metrics.HistoryHostHealthGauge.With(h.metricsHandler).Record(float64(enumsspb.HEALTH_STATE_NOT_SERVING)) - return &historyservice.DeepHealthCheckResponse{State: enumsspb.HEALTH_STATE_NOT_SERVING} + // Check 5: Persistence error ratio + persErrRatio := h.persistenceHealthSignal.ErrorRatio() + persErrThreshold := h.config.HealthPersistenceErrorRatio() + persErrState := enumsspb.HEALTH_STATE_SERVING + persErrMsg := "" + if persErrRatio > persErrThreshold { + persErrState = enumsspb.HEALTH_STATE_NOT_SERVING + persErrMsg = fmt.Sprintf("Persistence error ratio %.4f exceeded %.4f threshold", persErrRatio, persErrThreshold) + if overallState != enumsspb.HEALTH_STATE_DECLINED_SERVING { + overallState = enumsspb.HEALTH_STATE_NOT_SERVING + } } - return nil + checks = append(checks, &healthspb.HealthCheck{ + CheckType: healthcheck.CheckTypePersistenceErrRatio, + State: persErrState, + Value: persErrRatio, + Threshold: persErrThreshold, + Message: persErrMsg, + }) + + metrics.HistoryHostHealthGauge.With(h.metricsHandler).Record(float64(overallState)) + return &historyservice.DeepHealthCheckResponse{ + State: overallState, + Checks: checks, + }, nil } // IsWorkflowTaskValid - whether workflow task is still valid @@ -438,11 +518,6 @@ func (h *Handler) RespondActivityTaskCompleted(ctx context.Context, request *his // Handle standalone activity if component ref is present in the token. if componentRef := taskToken.GetComponentRef(); len(componentRef) > 0 { - namespaceName, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(request.GetNamespaceId())) - if err != nil { - return nil, err - } - response, _, err := chasm.UpdateComponent( ctx, componentRef, @@ -450,11 +525,6 @@ func (h *Handler) RespondActivityTaskCompleted(ctx context.Context, request *his activity.RespondCompletedEvent{ Request: request, Token: taskToken, - MetricsHandlerBuilderParams: activity.MetricsHandlerBuilderParams{ - Handler: h.metricsHandler, - NamespaceName: namespaceName.String(), - BreakdownMetricsByTaskQueue: h.config.BreakdownMetricsByTaskQueue, - }, }, ) if err != nil { @@ -499,11 +569,6 @@ func (h *Handler) RespondActivityTaskFailed(ctx context.Context, request *histor // Handle standalone activity if component ref is present in the token. if componentRef := taskToken.GetComponentRef(); len(componentRef) > 0 { - namespaceName, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(request.GetNamespaceId())) - if err != nil { - return nil, err - } - response, _, err := chasm.UpdateComponent( ctx, componentRef, @@ -511,11 +576,6 @@ func (h *Handler) RespondActivityTaskFailed(ctx context.Context, request *histor activity.RespondFailedEvent{ Request: request, Token: taskToken, - MetricsHandlerBuilderParams: activity.MetricsHandlerBuilderParams{ - Handler: h.metricsHandler, - NamespaceName: namespaceName.String(), - BreakdownMetricsByTaskQueue: h.config.BreakdownMetricsByTaskQueue, - }, }, ) if err != nil { @@ -560,11 +620,6 @@ func (h *Handler) RespondActivityTaskCanceled(ctx context.Context, request *hist // Handle standalone activity if component ref is present in the token. if componentRef := taskToken.GetComponentRef(); len(componentRef) > 0 { - namespaceName, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(request.GetNamespaceId())) - if err != nil { - return nil, err - } - response, _, err := chasm.UpdateComponent( ctx, componentRef, @@ -572,11 +627,6 @@ func (h *Handler) RespondActivityTaskCanceled(ctx context.Context, request *hist activity.RespondCancelledEvent{ Request: request, Token: taskToken, - MetricsHandlerBuilderParams: activity.MetricsHandlerBuilderParams{ - Handler: h.metricsHandler, - NamespaceName: namespaceName.String(), - BreakdownMetricsByTaskQueue: h.config.BreakdownMetricsByTaskQueue, - }, }, ) if err != nil { @@ -1588,7 +1638,7 @@ func (h *Handler) GetReplicationMessages(ctx context.Context, request *historyse wg.Wait() messagesByShard := make(map[int32]*replicationspb.ReplicationMessages) - result.Range(func(key, value interface{}) bool { + result.Range(func(key, value any) bool { shardID := key.(int32) messagesByShard[shardID] = value.(*replicationspb.ReplicationMessages) return true @@ -2241,6 +2291,7 @@ func (h *Handler) CompleteNexusOperationChasm( // convertError is a helper method to convert ShardOwnershipLostError from persistence layer returned by various // HistoryEngine API calls to ShardOwnershipLost error return by HistoryService for client to be redirected to the // correct shard. +// NOTE: Keep in sync with ChasmEngine.convertError in chasm_engine.go, which is a superset of this function. func (h *Handler) convertError(err error) error { switch err := err.(type) { case *persistence.ShardOwnershipLostError: diff --git a/service/history/history_engine2_test.go b/service/history/history_engine2_test.go index 92d1211aad5..25bdcc2c144 100644 --- a/service/history/history_engine2_test.go +++ b/service/history/history_engine2_test.go @@ -1322,7 +1322,7 @@ func (s *engine2Suite) TestRespondWorkflowTaskCompleted_StartChildWorkflow_Excee s.mockNamespaceCache.EXPECT().GetNamespace(tests.Namespace).Return(tests.LocalNamespaceEntry, nil).AnyTimes() var commands []*commandpb.Command - for i := 0; i < 6; i++ { + for range 6 { commands = append( commands, &commandpb.Command{ @@ -2851,7 +2851,7 @@ func newCreateWorkflowExecutionRequestMatcher(f func(request *persistence.Create } } -func (m *createWorkflowExecutionRequestMatcher) Matches(x interface{}) bool { +func (m *createWorkflowExecutionRequestMatcher) Matches(x any) bool { request, ok := x.(*persistence.CreateWorkflowExecutionRequest) if !ok { return false diff --git a/service/history/history_engine_test.go b/service/history/history_engine_test.go index 165782b3ef7..028bed90a27 100644 --- a/service/history/history_engine_test.go +++ b/service/history/history_engine_test.go @@ -673,9 +673,9 @@ func (s *engineSuite) TestQueryWorkflow_WorkflowTaskDispatch_Timeout() { wg := &sync.WaitGroup{} wg.Add(1) - var capturedWorkflowType interface{} + var capturedWorkflowType any var capturedWorkflowTypeOk bool - var capturedTaskQueue interface{} + var capturedTaskQueue any var capturedTaskQueueOk bool go func() { metadataCtx := contextutil.WithMetadataContext(context.Background()) @@ -5409,7 +5409,7 @@ func (s *engineSuite) TestReapplyEvents_ResetWorkflow() { s.mockEventsReapplier.EXPECT().ReapplyEvents(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) s.mockWorkflowResetter.EXPECT().ResetWorkflow( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), ).Return(nil) @@ -5438,7 +5438,7 @@ func (s *engineSuite) TestEagerWorkflowStart_DoesNotCreateTransferTask() { s.mockShard.Resource.Logger, s.config.LogAllReqErrors, s.mockErrorHandler) - response, err := i.UnaryIntercept(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "StartWorkflowExecution"}, func(ctx context.Context, req interface{}) (interface{}, error) { + response, err := i.UnaryIntercept(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "StartWorkflowExecution"}, func(ctx context.Context, req any) (any, error) { response, err := s.historyEngine.StartWorkflowExecution(ctx, &historyservice.StartWorkflowExecutionRequest{ NamespaceId: tests.NamespaceID.String(), Attempt: 1, @@ -5477,7 +5477,7 @@ func (s *engineSuite) TestEagerWorkflowStart_FromCron_SkipsEager() { s.mockShard.Resource.Logger, s.config.LogAllReqErrors, s.mockErrorHandler) - response, err := i.UnaryIntercept(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "StartWorkflowExecution"}, func(ctx context.Context, req interface{}) (interface{}, error) { + response, err := i.UnaryIntercept(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "StartWorkflowExecution"}, func(ctx context.Context, req any) (any, error) { firstWorkflowTaskBackoff := time.Second response, err := s.historyEngine.StartWorkflowExecution(ctx, &historyservice.StartWorkflowExecutionRequest{ NamespaceId: tests.NamespaceID.String(), @@ -5521,7 +5521,7 @@ func (s *engineSuite) TestEagerWorkflowStart_WithSearchAttributes() { s.mockShard.Resource.Logger, s.config.LogAllReqErrors, s.mockErrorHandler) - response, err := i.UnaryIntercept(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "StartWorkflowExecution"}, func(ctx context.Context, req interface{}) (interface{}, error) { + response, err := i.UnaryIntercept(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "StartWorkflowExecution"}, func(ctx context.Context, req any) (any, error) { response, err := s.historyEngine.StartWorkflowExecution(ctx, &historyservice.StartWorkflowExecutionRequest{ NamespaceId: tests.NamespaceID.String(), Attempt: 1, @@ -5855,23 +5855,12 @@ func (s *engineSuite) TestGetWorkflowExecutionHistory_RawHistoryWithTransientDec branchToken := []byte{1, 2, 3} persistenceToken := []byte("some random persistence token") nextPageToken, err := api.SerializeHistoryToken(&tokenspb.HistoryContinuation{ - RunId: we.GetRunId(), - FirstEventId: common.FirstEventID, - NextEventId: 5, - PersistenceToken: persistenceToken, - TransientWorkflowTask: &historyspb.TransientWorkflowTaskInfo{ - HistorySuffix: []*historypb.HistoryEvent{ - { - EventId: 5, - EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, - }, - { - EventId: 6, - EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, - }, - }, - }, - BranchToken: branchToken, + RunId: we.GetRunId(), + FirstEventId: common.FirstEventID, + NextEventId: 5, + PersistenceToken: persistenceToken, + BranchToken: branchToken, + IsWorkflowRunning: true, // Workflow is running, so we'll query MS for transient events }) s.NoError(err) s.config.SendRawWorkflowHistory = func(string) bool { return true } @@ -5888,6 +5877,40 @@ func (s *engineSuite) TestGetWorkflowExecutionHistory_RawHistoryWithTransientDec } s.mockNamespaceCache.EXPECT().GetNamespaceID(tests.Namespace).Return(tests.NamespaceID, nil).AnyTimes() + + // Mock GetMutableState to return transient workflow task events + s.mockExecutionMgr.EXPECT().GetWorkflowExecution(gomock.Any(), gomock.Any()).Return(&persistence.GetWorkflowExecutionResponse{ + State: &persistencespb.WorkflowMutableState{ + ExecutionState: &persistencespb.WorkflowExecutionState{ + State: enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, + Status: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, + RunId: we.RunId, + }, + NextEventId: 5, + ExecutionInfo: &persistencespb.WorkflowExecutionInfo{ + NamespaceId: tests.NamespaceID.String(), + WorkflowId: we.WorkflowId, + WorkflowTaskScheduledEventId: 5, + WorkflowTaskStartedEventId: 6, + WorkflowTaskAttempt: 2, // Attempt > 1 makes it transient + TaskQueue: "test-task-queue", + WorkflowTypeName: "test-workflow-type", + VersionHistories: &historyspb.VersionHistories{ + CurrentVersionHistoryIndex: 0, + Histories: []*historyspb.VersionHistory{ + { + BranchToken: branchToken, + Items: []*historyspb.VersionHistoryItem{ + {EventId: 4, Version: 0}, + }, + }, + }, + }, + }, + }, + MutableStateStats: persistence.MutableStateStatistics{}, + }, nil).Times(1) + historyBlob1, err := s.mockShard.GetPayloadSerializer().SerializeEvents( []*historypb.HistoryEvent{ { diff --git a/service/history/historybuilder/event_factory.go b/service/history/historybuilder/event_factory.go index 80d4b445e42..2ae2f824f10 100644 --- a/service/history/historybuilder/event_factory.go +++ b/service/history/historybuilder/event_factory.go @@ -17,6 +17,7 @@ import ( "go.temporal.io/server/common" "go.temporal.io/server/common/clock" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/payload" "go.temporal.io/server/common/worker_versioning" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -67,19 +68,22 @@ func (b *EventFactory) CreateWorkflowExecutionStartedEvent( FirstWorkflowTaskBackoff: request.FirstWorkflowTaskBackoff, FirstExecutionRunId: firstRunID, OriginalExecutionRunId: originalRunID, - Memo: req.Memo, - SearchAttributes: req.SearchAttributes, - WorkflowId: req.WorkflowId, - SourceVersionStamp: request.SourceVersionStamp, - CompletionCallbacks: req.CompletionCallbacks, - RootWorkflowExecution: request.RootExecutionInfo.GetExecution(), - InheritedBuildId: request.InheritedBuildId, - VersioningOverride: worker_versioning.ConvertOverrideToV32(nonNilVersioningOverride), - Priority: req.GetPriority(), - InheritedPinnedVersion: request.InheritedPinnedVersion, + // Filter nil values here rather than in the API layer because not all + // creation paths go through the frontend (e.g. continue-as-new, child workflows, replication). + Memo: payload.FilterNilMemo(req.Memo), + SearchAttributes: payload.FilterNilSearchAttributes(req.SearchAttributes), + WorkflowId: req.WorkflowId, + SourceVersionStamp: request.SourceVersionStamp, + CompletionCallbacks: req.CompletionCallbacks, + RootWorkflowExecution: request.RootExecutionInfo.GetExecution(), + InheritedBuildId: request.InheritedBuildId, + VersioningOverride: worker_versioning.ConvertOverrideToV32(nonNilVersioningOverride), + Priority: req.GetPriority(), + InheritedPinnedVersion: request.InheritedPinnedVersion, // We expect the API handler to unset RequestEagerExecution if eager execution cannot be accepted. - EagerExecutionAccepted: req.GetRequestEagerExecution(), - InheritedAutoUpgradeInfo: request.InheritedAutoUpgradeInfo, + EagerExecutionAccepted: req.GetRequestEagerExecution(), + InheritedAutoUpgradeInfo: request.InheritedAutoUpgradeInfo, + DeclinedTargetVersionUpgrade: request.DeclinedTargetVersionUpgrade, } parentInfo := request.ParentExecutionInfo @@ -480,9 +484,12 @@ func (b EventFactory) CreateContinuedAsNewEvent( Initiator: command.Initiator, Failure: command.Failure, LastCompletionResult: command.LastCompletionResult, - Memo: command.Memo, - SearchAttributes: command.SearchAttributes, - InheritBuildId: command.InheritBuildId, + // Filter nil values here rather than in the API layer because not all + // creation paths go through the frontend (continue-as-new, child workflows, replication). + // This CaN event is created on the source workflow, so we need to filter nil values here. + Memo: payload.FilterNilMemo(command.Memo), + SearchAttributes: payload.FilterNilSearchAttributes(command.SearchAttributes), + InheritBuildId: command.InheritBuildId, //nolint:staticcheck // SA1019: worker versioning v0.2 } event.Attributes = &historypb.HistoryEvent_WorkflowExecutionContinuedAsNewEventAttributes{ WorkflowExecutionContinuedAsNewEventAttributes: attributes, @@ -847,11 +854,14 @@ func (b *EventFactory) CreateStartChildWorkflowExecutionInitiatedEvent( WorkflowIdReusePolicy: command.WorkflowIdReusePolicy, RetryPolicy: command.RetryPolicy, CronSchedule: command.CronSchedule, - Memo: command.Memo, - SearchAttributes: command.SearchAttributes, - ParentClosePolicy: command.GetParentClosePolicy(), - InheritBuildId: command.InheritBuildId, - Priority: command.Priority, + // Filter nil values here rather than in the API layer because not all + // creation paths go through the frontend (continue-as-new, child workflows, replication). + // This CaN event is created on the parent workflow, so we need to filter nil values here. + Memo: payload.FilterNilMemo(command.Memo), + SearchAttributes: payload.FilterNilSearchAttributes(command.SearchAttributes), + ParentClosePolicy: command.GetParentClosePolicy(), + InheritBuildId: command.InheritBuildId, //nolint:staticcheck // SA1019: worker versioning v0.2 + Priority: command.Priority, }, } return event diff --git a/service/history/historybuilder/event_store.go b/service/history/historybuilder/event_store.go index 3d663935ffd..c4918c45219 100644 --- a/service/history/historybuilder/event_store.go +++ b/service/history/historybuilder/event_store.go @@ -228,7 +228,7 @@ func (b *EventStore) assignTaskIDs( } taskIDCount := 0 - for i := 0; i < len(dbEventsBatches); i++ { + for i := range dbEventsBatches { taskIDCount += len(dbEventsBatches[i]) } taskIDs, err := b.taskIDGenerator(taskIDCount) @@ -238,9 +238,9 @@ func (b *EventStore) assignTaskIDs( taskIDPointer := 0 height := len(dbEventsBatches) - for i := 0; i < height; i++ { + for i := range height { width := len(dbEventsBatches[i]) - for j := 0; j < width; j++ { + for j := range width { dbEventsBatches[i][j].TaskId = taskIDs[taskIDPointer] taskIDPointer++ } diff --git a/service/history/historybuilder/history_builder_test.go b/service/history/historybuilder/history_builder_test.go index c03f07d4ec9..6fbe78f574e 100644 --- a/service/history/historybuilder/history_builder_test.go +++ b/service/history/historybuilder/history_builder_test.go @@ -17,6 +17,7 @@ import ( taskqueuepb "go.temporal.io/api/taskqueue/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/converter" "go.temporal.io/server/api/historyservice/v1" workflowspb "go.temporal.io/server/api/workflow/v1" "go.temporal.io/server/common" @@ -2426,6 +2427,159 @@ func (s *historyBuilderSuite) TestLastEventVersion() { } +func (s *historyBuilderSuite) TestWorkflowExecutionStarted_NilSearchAttributesFiltered() { + // Create a payload that represents nil + nilPayload, err := s.payloadEncode(nil) + s.NoError(err) + + validPayload, err := s.payloadEncode("valid-value") + s.NoError(err) + + // SearchAttributes with a mix of valid and nil values + searchAttributesWithNil := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "validKey": validPayload, + "nilKey": nilPayload, + }, + } + + request := &historyservice.StartWorkflowExecutionRequest{ + NamespaceId: testNamespaceID.String(), + StartRequest: &workflowservice.StartWorkflowExecutionRequest{ + Namespace: testNamespaceName.String(), + WorkflowId: testWorkflowID, + WorkflowType: testWorkflowType, + TaskQueue: testTaskQueue, + SearchAttributes: searchAttributesWithNil, + }, + } + + event := s.historyBuilder.AddWorkflowExecutionStartedEvent( + s.now, + request, + nil, + "", + "", + "", + ) + + // Verify that nil search attributes are filtered out + attrs := event.GetWorkflowExecutionStartedEventAttributes() + s.NotNil(attrs.SearchAttributes) + s.Len(attrs.SearchAttributes.IndexedFields, 1) + s.NotNil(attrs.SearchAttributes.IndexedFields["validKey"]) + s.Nil(attrs.SearchAttributes.IndexedFields["nilKey"]) +} + +func (s *historyBuilderSuite) TestWorkflowExecutionStarted_AllNilSearchAttributesFiltered() { + // Create a payload that represents nil + nilPayload, err := s.payloadEncode(nil) + s.NoError(err) + + // SearchAttributes with only nil values + searchAttributesAllNil := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "nilKey1": nilPayload, + "nilKey2": nilPayload, + }, + } + + request := &historyservice.StartWorkflowExecutionRequest{ + NamespaceId: testNamespaceID.String(), + StartRequest: &workflowservice.StartWorkflowExecutionRequest{ + Namespace: testNamespaceName.String(), + WorkflowId: testWorkflowID, + WorkflowType: testWorkflowType, + TaskQueue: testTaskQueue, + SearchAttributes: searchAttributesAllNil, + }, + } + + event := s.historyBuilder.AddWorkflowExecutionStartedEvent( + s.now, + request, + nil, + "", + "", + "", + ) + + // Verify that when all search attributes are nil, the entire SearchAttributes is nil + attrs := event.GetWorkflowExecutionStartedEventAttributes() + s.Nil(attrs.SearchAttributes) +} + +func (s *historyBuilderSuite) TestContinuedAsNew_NilSearchAttributesFiltered() { + nilPayload, err := s.payloadEncode(nil) + s.NoError(err) + + validPayload, err := s.payloadEncode("valid-value") + s.NoError(err) + + command := &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: testWorkflowType, + TaskQueue: testTaskQueue, + SearchAttributes: &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "validKey": validPayload, + "nilKey": nilPayload, + }, + }, + } + + event := s.historyBuilder.AddContinuedAsNewEvent( + rand.Int63(), + testRunID, + command, + ) + + // Verify that nil search attributes are filtered out + attrs := event.GetWorkflowExecutionContinuedAsNewEventAttributes() + s.NotNil(attrs.SearchAttributes) + s.Len(attrs.SearchAttributes.IndexedFields, 1) + s.NotNil(attrs.SearchAttributes.IndexedFields["validKey"]) + s.Nil(attrs.SearchAttributes.IndexedFields["nilKey"]) +} + +func (s *historyBuilderSuite) TestStartChildWorkflowExecutionInitiated_NilSearchAttributesFiltered() { + nilPayload, err := s.payloadEncode(nil) + s.NoError(err) + + validPayload, err := s.payloadEncode("valid-value") + s.NoError(err) + + command := &commandpb.StartChildWorkflowExecutionCommandAttributes{ + Namespace: testNamespaceName.String(), + WorkflowId: "child-workflow-id", + WorkflowType: testWorkflowType, + TaskQueue: testTaskQueue, + SearchAttributes: &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "validKey": validPayload, + "nilKey": nilPayload, + }, + }, + } + + event := s.historyBuilder.AddStartChildWorkflowExecutionInitiatedEvent( + rand.Int63(), + command, + testNamespaceID, + ) + + // Verify that nil search attributes are filtered out + attrs := event.GetStartChildWorkflowExecutionInitiatedEventAttributes() + s.NotNil(attrs.SearchAttributes) + s.Len(attrs.SearchAttributes.IndexedFields, 1) + s.NotNil(attrs.SearchAttributes.IndexedFields["validKey"]) + s.Nil(attrs.SearchAttributes.IndexedFields["nilKey"]) +} + +func (s *historyBuilderSuite) payloadEncode(value any) (*commonpb.Payload, error) { + dataConverter := converter.GetDefaultDataConverter() + return dataConverter.ToPayload(value) +} + func (s *historyBuilderSuite) assertEventIDTaskID( historyMutation *HistoryMutation, ) { @@ -2473,7 +2627,7 @@ func (s *historyBuilderSuite) flush() *historypb.HistoryEvent { func (s *historyBuilderSuite) taskIDGenerator(number int) ([]int64, error) { nextTaskID := s.nextTaskID result := make([]int64, number) - for i := 0; i < number; i++ { + for i := range number { result[i] = nextTaskID nextTaskID++ } diff --git a/service/history/hsm/tree_test.go b/service/history/hsm/tree_test.go index 205b2238b3f..766e1fe8f48 100644 --- a/service/history/hsm/tree_test.go +++ b/service/history/hsm/tree_test.go @@ -645,7 +645,7 @@ func TestNode_DeleteDeepHierarchy(t *testing.T) { // Build hierarchy current := root var nodes []*hsm.Node - for i := 0; i < 5; i++ { + for i := range 5 { node, err := current.AddChild(hsm.Key{Type: def1.Type(), ID: fmt.Sprintf("node%d", i)}, hsmtest.NewData(hsmtest.State1)) require.NoError(t, err) nodes = append(nodes, node) @@ -692,7 +692,7 @@ func TestNode_MixedOperationsBeforeDeletion(t *testing.T) { l1, err := root.AddChild(hsm.Key{Type: def1.Type(), ID: "l1"}, hsmtest.NewData(hsmtest.State1)) require.NoError(t, err) - for i := 0; i < 3; i++ { + for range 3 { err = hsm.MachineTransition(l1, func(d *hsmtest.Data) (hsm.TransitionOutput, error) { d.SetState(hsmtest.State2) return hsm.TransitionOutput{}, nil diff --git a/service/history/interfaces/chasm_tree.go b/service/history/interfaces/chasm_tree.go index a92b3081416..81363db3bab 100644 --- a/service/history/interfaces/chasm_tree.go +++ b/service/history/interfaces/chasm_tree.go @@ -18,6 +18,7 @@ var _ ChasmTree = (*chasm.Node)(nil) type ChasmTree interface { CloseTransaction() (chasm.NodesMutation, error) Snapshot(*persistencespb.VersionedTransition) chasm.NodesSnapshot + ApplySystemMutation(chasm.NodesMutation) error ApplyMutation(chasm.NodesMutation) error ApplySnapshot(chasm.NodesSnapshot) error RefreshTasks() error @@ -32,7 +33,12 @@ type ChasmTree interface { ) error ExecuteSideEffectTask( ctx context.Context, - registry *chasm.Registry, + executionKey chasm.ExecutionKey, + task *tasks.ChasmTask, + validate func(chasm.NodeBackend, chasm.Context, chasm.Component) error, + ) error + ExecuteSideEffectDiscardTask( + ctx context.Context, executionKey chasm.ExecutionKey, task *tasks.ChasmTask, validate func(chasm.NodeBackend, chasm.Context, chasm.Component) error, diff --git a/service/history/interfaces/chasm_tree_mock.go b/service/history/interfaces/chasm_tree_mock.go index e7b92f4da8a..ab855b72c23 100644 --- a/service/history/interfaces/chasm_tree_mock.go +++ b/service/history/interfaces/chasm_tree_mock.go @@ -72,6 +72,20 @@ func (mr *MockChasmTreeMockRecorder) ApplySnapshot(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplySnapshot", reflect.TypeOf((*MockChasmTree)(nil).ApplySnapshot), arg0) } +// ApplySystemMutation mocks base method. +func (m *MockChasmTree) ApplySystemMutation(arg0 chasm.NodesMutation) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApplySystemMutation", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// ApplySystemMutation indicates an expected call of ApplySystemMutation. +func (mr *MockChasmTreeMockRecorder) ApplySystemMutation(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplySystemMutation", reflect.TypeOf((*MockChasmTree)(nil).ApplySystemMutation), arg0) +} + // Archetype mocks base method. func (m *MockChasmTree) Archetype() (chasm.Archetype, error) { m.ctrl.T.Helper() @@ -160,18 +174,32 @@ func (mr *MockChasmTreeMockRecorder) EachPureTask(deadline, callback any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EachPureTask", reflect.TypeOf((*MockChasmTree)(nil).EachPureTask), deadline, callback) } +// ExecuteSideEffectDiscardTask mocks base method. +func (m *MockChasmTree) ExecuteSideEffectDiscardTask(ctx context.Context, executionKey chasm.ExecutionKey, task *tasks.ChasmTask, validate func(chasm.NodeBackend, chasm.Context, chasm.Component) error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteSideEffectDiscardTask", ctx, executionKey, task, validate) + ret0, _ := ret[0].(error) + return ret0 +} + +// ExecuteSideEffectDiscardTask indicates an expected call of ExecuteSideEffectDiscardTask. +func (mr *MockChasmTreeMockRecorder) ExecuteSideEffectDiscardTask(ctx, executionKey, task, validate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSideEffectDiscardTask", reflect.TypeOf((*MockChasmTree)(nil).ExecuteSideEffectDiscardTask), ctx, executionKey, task, validate) +} + // ExecuteSideEffectTask mocks base method. -func (m *MockChasmTree) ExecuteSideEffectTask(ctx context.Context, registry *chasm.Registry, executionKey chasm.ExecutionKey, task *tasks.ChasmTask, validate func(chasm.NodeBackend, chasm.Context, chasm.Component) error) error { +func (m *MockChasmTree) ExecuteSideEffectTask(ctx context.Context, executionKey chasm.ExecutionKey, task *tasks.ChasmTask, validate func(chasm.NodeBackend, chasm.Context, chasm.Component) error) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExecuteSideEffectTask", ctx, registry, executionKey, task, validate) + ret := m.ctrl.Call(m, "ExecuteSideEffectTask", ctx, executionKey, task, validate) ret0, _ := ret[0].(error) return ret0 } // ExecuteSideEffectTask indicates an expected call of ExecuteSideEffectTask. -func (mr *MockChasmTreeMockRecorder) ExecuteSideEffectTask(ctx, registry, executionKey, task, validate any) *gomock.Call { +func (mr *MockChasmTreeMockRecorder) ExecuteSideEffectTask(ctx, executionKey, task, validate any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSideEffectTask", reflect.TypeOf((*MockChasmTree)(nil).ExecuteSideEffectTask), ctx, registry, executionKey, task, validate) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSideEffectTask", reflect.TypeOf((*MockChasmTree)(nil).ExecuteSideEffectTask), ctx, executionKey, task, validate) } // IsDirty mocks base method. diff --git a/service/history/ndc/conflict_resolver.go b/service/history/ndc/conflict_resolver.go index 719bd72b9e1..3ca3041098f 100644 --- a/service/history/ndc/conflict_resolver.go +++ b/service/history/ndc/conflict_resolver.go @@ -5,7 +5,6 @@ package ndc import ( "context" - "github.com/google/uuid" "go.temporal.io/api/serviceerror" "go.temporal.io/server/common/definition" "go.temporal.io/server/common/log" @@ -107,7 +106,7 @@ func (r *ConflictResolverImpl) getOrRebuildMutableStateByIndex( // task.getVersion() > currentLastItem // incoming replication task, after application, will become the current branch // (because higher version wins), we need to Rebuild the mutable state for that - rebuiltMutableState, err := r.rebuild(ctx, branchIndex, uuid.NewString()) + rebuiltMutableState, err := r.rebuild(ctx, branchIndex) if err != nil { return nil, false, err } @@ -117,7 +116,6 @@ func (r *ConflictResolverImpl) getOrRebuildMutableStateByIndex( func (r *ConflictResolverImpl) rebuild( ctx context.Context, branchIndex int32, - requestID string, ) (historyi.MutableState, error) { versionHistories := r.mutableState.GetExecutionInfo().GetVersionHistories() @@ -150,7 +148,7 @@ func (r *ConflictResolverImpl) rebuild( util.Ptr(lastItem.GetVersion()), workflowKey, replayVersionHistory.GetBranchToken(), - requestID, + findStartRequestID(executionState), ) if err != nil { return nil, err diff --git a/service/history/ndc/conflict_resolver_test.go b/service/history/ndc/conflict_resolver_test.go index d075c2ed72a..1ad746d998d 100644 --- a/service/history/ndc/conflict_resolver_test.go +++ b/service/history/ndc/conflict_resolver_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + enumspb "go.temporal.io/api/enums/v1" historyspb "go.temporal.io/server/api/history/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/definition" @@ -116,6 +117,9 @@ func (s *conflictResolverSuite) TestRebuild() { }).AnyTimes() s.mockMutableState.EXPECT().GetExecutionState().Return(&persistencespb.WorkflowExecutionState{ RunId: s.runID, + RequestIds: map[string]*persistencespb.RequestIDInfo{ + requestID: {EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED}, + }, }).AnyTimes() s.mockMutableState.EXPECT().GetHistorySize().Return(historySize).AnyTimes() s.mockMutableState.EXPECT().GetExternalPayloadSize().Return(externalPayloadSize).AnyTimes() @@ -159,7 +163,7 @@ func (s *conflictResolverSuite) TestRebuild() { }, nil) s.mockContext.EXPECT().Clear() - rebuiltMutableState, err := s.nDCConflictResolver.rebuild(ctx, 1, requestID) + rebuiltMutableState, err := s.nDCConflictResolver.rebuild(ctx, 1) s.NoError(err) s.NotNil(rebuiltMutableState) s.Equal(int32(1), versionHistories.GetCurrentVersionHistoryIndex()) diff --git a/service/history/ndc/state_rebuilder.go b/service/history/ndc/state_rebuilder.go index e57b51ccc7a..84b136a5d91 100644 --- a/service/history/ndc/state_rebuilder.go +++ b/service/history/ndc/state_rebuilder.go @@ -7,6 +7,7 @@ import ( "time" commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" historypb "go.temporal.io/api/history/v1" "go.temporal.io/api/serviceerror" persistencespb "go.temporal.io/server/api/persistence/v1" @@ -48,7 +49,6 @@ type ( baseLastEventVersion *int64, targetWorkflowIdentifier definition.WorkflowKey, targetBranchToken []byte, - requestID string, currentMutableState *persistencespb.WorkflowMutableState, ) (historyi.MutableState, RebuildStats, error) } @@ -158,9 +158,9 @@ func (r *StateRebuilderImpl) RebuildWithCurrentMutableState( baseLastEventVersion *int64, targetWorkflowIdentifier definition.WorkflowKey, targetBranchToken []byte, - requestID string, currentMutableState *persistencespb.WorkflowMutableState, ) (historyi.MutableState, RebuildStats, error) { + // Use the original start request ID handlers can still correlate rebuilt callbacks to the correct BufferedStart entry. rebuiltMutableState, lastTxnId, err := r.buildMutableStateFromEvent( ctx, now, @@ -170,7 +170,7 @@ func (r *StateRebuilderImpl) RebuildWithCurrentMutableState( baseLastEventVersion, targetWorkflowIdentifier, targetBranchToken, - requestID, + findStartRequestID(currentMutableState.GetExecutionState()), ) if err != nil { return nil, RebuildStats{}, err @@ -398,3 +398,14 @@ func (r *StateRebuilderImpl) getPaginationFn( return paginateItems, resp.NextPageToken, nil } } + +// findStartRequestID returns the request ID associated with the WorkflowExecutionStarted +// event from the RequestIds map, or the create request ID if not found. +func findStartRequestID(executionState *persistencespb.WorkflowExecutionState) string { + for reqID, info := range executionState.GetRequestIds() { + if info.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + return reqID + } + } + return executionState.GetCreateRequestId() +} diff --git a/service/history/ndc/state_rebuilder_mock.go b/service/history/ndc/state_rebuilder_mock.go index 7594d8bbf2b..fe55b8b2092 100644 --- a/service/history/ndc/state_rebuilder_mock.go +++ b/service/history/ndc/state_rebuilder_mock.go @@ -61,9 +61,9 @@ func (mr *MockStateRebuilderMockRecorder) Rebuild(ctx, now, baseWorkflowIdentifi } // RebuildWithCurrentMutableState mocks base method. -func (m *MockStateRebuilder) RebuildWithCurrentMutableState(ctx context.Context, now time.Time, baseWorkflowIdentifier definition.WorkflowKey, baseBranchToken []byte, baseLastEventID int64, baseLastEventVersion *int64, targetWorkflowIdentifier definition.WorkflowKey, targetBranchToken []byte, requestID string, currentMutableState *persistence.WorkflowMutableState) (interfaces.MutableState, RebuildStats, error) { +func (m *MockStateRebuilder) RebuildWithCurrentMutableState(ctx context.Context, now time.Time, baseWorkflowIdentifier definition.WorkflowKey, baseBranchToken []byte, baseLastEventID int64, baseLastEventVersion *int64, targetWorkflowIdentifier definition.WorkflowKey, targetBranchToken []byte, currentMutableState *persistence.WorkflowMutableState) (interfaces.MutableState, RebuildStats, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RebuildWithCurrentMutableState", ctx, now, baseWorkflowIdentifier, baseBranchToken, baseLastEventID, baseLastEventVersion, targetWorkflowIdentifier, targetBranchToken, requestID, currentMutableState) + ret := m.ctrl.Call(m, "RebuildWithCurrentMutableState", ctx, now, baseWorkflowIdentifier, baseBranchToken, baseLastEventID, baseLastEventVersion, targetWorkflowIdentifier, targetBranchToken, currentMutableState) ret0, _ := ret[0].(interfaces.MutableState) ret1, _ := ret[1].(RebuildStats) ret2, _ := ret[2].(error) @@ -71,7 +71,7 @@ func (m *MockStateRebuilder) RebuildWithCurrentMutableState(ctx context.Context, } // RebuildWithCurrentMutableState indicates an expected call of RebuildWithCurrentMutableState. -func (mr *MockStateRebuilderMockRecorder) RebuildWithCurrentMutableState(ctx, now, baseWorkflowIdentifier, baseBranchToken, baseLastEventID, baseLastEventVersion, targetWorkflowIdentifier, targetBranchToken, requestID, currentMutableState any) *gomock.Call { +func (mr *MockStateRebuilderMockRecorder) RebuildWithCurrentMutableState(ctx, now, baseWorkflowIdentifier, baseBranchToken, baseLastEventID, baseLastEventVersion, targetWorkflowIdentifier, targetBranchToken, currentMutableState any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RebuildWithCurrentMutableState", reflect.TypeOf((*MockStateRebuilder)(nil).RebuildWithCurrentMutableState), ctx, now, baseWorkflowIdentifier, baseBranchToken, baseLastEventID, baseLastEventVersion, targetWorkflowIdentifier, targetBranchToken, requestID, currentMutableState) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RebuildWithCurrentMutableState", reflect.TypeOf((*MockStateRebuilder)(nil).RebuildWithCurrentMutableState), ctx, now, baseWorkflowIdentifier, baseBranchToken, baseLastEventID, baseLastEventVersion, targetWorkflowIdentifier, targetBranchToken, currentMutableState) } diff --git a/service/history/ndc/state_rebuilder_test.go b/service/history/ndc/state_rebuilder_test.go index 7604968bf1f..763bb49a860 100644 --- a/service/history/ndc/state_rebuilder_test.go +++ b/service/history/ndc/state_rebuilder_test.go @@ -373,7 +373,7 @@ func (s *stateRebuilderSuite) TestRebuild() { } func (s *stateRebuilderSuite) TestRebuildWithCurrentMutableState() { - requestID := uuid.NewString() + startRequestID := uuid.NewString() version := int64(12) lastEventID := int64(2) branchToken := []byte("other random branch token") @@ -460,6 +460,11 @@ func (s *stateRebuilderSuite) TestRebuildWithCurrentMutableState() { s.mockTaskRefresher.EXPECT().Refresh(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) currentMutableState := &persistencespb.WorkflowMutableState{ + ExecutionState: &persistencespb.WorkflowExecutionState{ + RequestIds: map[string]*persistencespb.RequestIDInfo{ + startRequestID: {EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED}, + }, + }, ExecutionInfo: &persistencespb.WorkflowExecutionInfo{ TransitionHistory: []*persistencespb.VersionedTransition{ { @@ -479,7 +484,6 @@ func (s *stateRebuilderSuite) TestRebuildWithCurrentMutableState() { util.Ptr(version), definition.NewWorkflowKey(targetNamespaceID.String(), targetWorkflowID, targetRunID), targetBranchToken, - requestID, currentMutableState, ) s.NoError(err) @@ -498,4 +502,5 @@ func (s *stateRebuilderSuite) TestRebuildWithCurrentMutableState() { s.Equal(timestamp.TimeValue(rebuildMutableState.GetExecutionState().StartTime), s.now) s.Equal(expectedLastFirstTransactionID, rebuildExecutionInfo.LastFirstEventTxnId) s.Equal(int64(11), rebuildExecutionInfo.TransitionHistory[0].TransitionCount) + s.Equal(startRequestID, rebuildMutableState.GetExecutionState().CreateRequestId) } diff --git a/service/history/ndc/transaction_manager.go b/service/history/ndc/transaction_manager.go index f6dbeb1957c..1a0bedad11b 100644 --- a/service/history/ndc/transaction_manager.go +++ b/service/history/ndc/transaction_manager.go @@ -20,6 +20,7 @@ import ( "go.temporal.io/server/common/persistence/serialization" "go.temporal.io/server/common/persistence/versionhistory" historyi "go.temporal.io/server/service/history/interfaces" + "go.temporal.io/server/service/history/workflow" wcache "go.temporal.io/server/service/history/workflow/cache" ) @@ -175,8 +176,9 @@ func NewTransactionManager( createMgr: nil, updateMgr: nil, } - transactionMgr.createMgr = newTransactionMgrForNewWorkflow(shardContext, transactionMgr, bypassVersionSemanticsCheck) - transactionMgr.updateMgr = newNDCTransactionMgrForExistingWorkflow(shardContext, transactionMgr, bypassVersionSemanticsCheck) + taskRefresher := workflow.NewTaskRefresher(shardContext) + transactionMgr.createMgr = newTransactionMgrForNewWorkflow(shardContext, transactionMgr, bypassVersionSemanticsCheck, taskRefresher) + transactionMgr.updateMgr = newNDCTransactionMgrForExistingWorkflow(shardContext, transactionMgr, bypassVersionSemanticsCheck, taskRefresher) return transactionMgr } @@ -329,7 +331,6 @@ func (r *transactionMgrImpl) backfillWorkflowEventsReapply( baseRebuildLastEventVersion, baseNextEventID, resetRunID, - uuid.NewString(), targetWorkflow, targetWorkflow, EventsReapplicationResetWorkflowReason, diff --git a/service/history/ndc/transaction_manager_existing_workflow.go b/service/history/ndc/transaction_manager_existing_workflow.go index 8f26392acbd..a79ae627c15 100644 --- a/service/history/ndc/transaction_manager_existing_workflow.go +++ b/service/history/ndc/transaction_manager_existing_workflow.go @@ -10,6 +10,7 @@ import ( "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" historyi "go.temporal.io/server/service/history/interfaces" + "go.temporal.io/server/service/history/workflow" ) type ( @@ -27,6 +28,7 @@ type ( shardContext historyi.ShardContext transactionMgr TransactionManager bypassVersionSemanticsCheck bool + taskRefresher workflow.TaskRefresher } ) @@ -36,12 +38,14 @@ func newNDCTransactionMgrForExistingWorkflow( shardContext historyi.ShardContext, transactionMgr TransactionManager, bypassVersionSemanticsCheck bool, + taskRefresher workflow.TaskRefresher, ) *nDCTransactionMgrForExistingWorkflowImpl { return &nDCTransactionMgrForExistingWorkflowImpl{ shardContext: shardContext, transactionMgr: transactionMgr, bypassVersionSemanticsCheck: bypassVersionSemanticsCheck, + taskRefresher: taskRefresher, } } @@ -317,7 +321,7 @@ func (r *nDCTransactionMgrForExistingWorkflowImpl) suppressCurrentAndUpdateAsCur return err } } - if err := targetWorkflow.Revive(); err != nil { + if err := targetWorkflow.Revive(ctx, r.taskRefresher); err != nil { return err } @@ -327,7 +331,7 @@ func (r *nDCTransactionMgrForExistingWorkflowImpl) suppressCurrentAndUpdateAsCur if newWorkflow != nil { newContext = newWorkflow.GetContext() newMutableState = newWorkflow.GetMutableState() - if err := newWorkflow.Revive(); err != nil { + if err := newWorkflow.Revive(ctx, r.taskRefresher); err != nil { return err } newWorkflowPolicy = historyi.TransactionPolicyPassive.Ptr() diff --git a/service/history/ndc/transaction_manager_existing_workflow_test.go b/service/history/ndc/transaction_manager_existing_workflow_test.go index c34bfbca249..92585ee64e4 100644 --- a/service/history/ndc/transaction_manager_existing_workflow_test.go +++ b/service/history/ndc/transaction_manager_existing_workflow_test.go @@ -46,7 +46,9 @@ func (s *transactionMgrForExistingWorkflowSuite) SetupTest() { s.NoError(err) s.mockShard.EXPECT().StateMachineRegistry().Return(reg).AnyTimes() - s.updateMgr = newNDCTransactionMgrForExistingWorkflow(s.mockShard, s.mockTransactionMgr, false) + mockTaskRefresher := workflow.NewMockTaskRefresher(s.controller) + mockTaskRefresher.EXPECT().Refresh(gomock.Any(), gomock.Any(), false).Return(nil).AnyTimes() + s.updateMgr = newNDCTransactionMgrForExistingWorkflow(s.mockShard, s.mockTransactionMgr, false, mockTaskRefresher) } func (s *transactionMgrForExistingWorkflowSuite) TearDownTest() { @@ -121,7 +123,7 @@ func (s *transactionMgrForExistingWorkflowSuite) TestDispatchForExistingWorkflow newWorkflow.EXPECT().GetContext().Return(newContext).AnyTimes() newWorkflow.EXPECT().GetMutableState().Return(newMutableState).AnyTimes() newWorkflow.EXPECT().GetReleaseFn().Return(newReleaseFn).AnyTimes() - newWorkflow.EXPECT().Revive().Return(nil) + newWorkflow.EXPECT().Revive(gomock.Any(), gomock.Any()).Return(nil) currentWorkflow := NewMockWorkflow(s.controller) currentContext := historyi.NewMockWorkflowContext(s.controller) @@ -145,7 +147,7 @@ func (s *transactionMgrForExistingWorkflowSuite) TestDispatchForExistingWorkflow targetWorkflow.EXPECT().HappensAfter(currentWorkflow).Return(true, nil) currentMutableState.EXPECT().IsWorkflowExecutionRunning().Return(true).AnyTimes() currentWorkflow.EXPECT().SuppressBy(targetWorkflow).Return(historyi.TransactionPolicyPassive, nil) - targetWorkflow.EXPECT().Revive().Return(nil) + targetWorkflow.EXPECT().Revive(gomock.Any(), gomock.Any()).Return(nil) targetContext.EXPECT().ConflictResolveWorkflowExecution( gomock.Any(), @@ -197,7 +199,7 @@ func (s *transactionMgrForExistingWorkflowSuite) TestDispatchForExistingWorkflow newWorkflow.EXPECT().GetContext().Return(newContext).AnyTimes() newWorkflow.EXPECT().GetMutableState().Return(newMutableState).AnyTimes() newWorkflow.EXPECT().GetReleaseFn().Return(newReleaseFn).AnyTimes() - newWorkflow.EXPECT().Revive().Return(nil) + newWorkflow.EXPECT().Revive(gomock.Any(), gomock.Any()).Return(nil) currentWorkflow := NewMockWorkflow(s.controller) currentContext := historyi.NewMockWorkflowContext(s.controller) @@ -221,7 +223,7 @@ func (s *transactionMgrForExistingWorkflowSuite) TestDispatchForExistingWorkflow targetWorkflow.EXPECT().HappensAfter(currentWorkflow).Return(true, nil) currentMutableState.EXPECT().IsWorkflowExecutionRunning().Return(false).AnyTimes() currentWorkflow.EXPECT().SuppressBy(targetWorkflow).Return(historyi.TransactionPolicyPassive, nil).Times(0) - targetWorkflow.EXPECT().Revive().Return(nil) + targetWorkflow.EXPECT().Revive(gomock.Any(), gomock.Any()).Return(nil) targetContext.EXPECT().ConflictResolveWorkflowExecution( gomock.Any(), @@ -480,7 +482,7 @@ func (s *transactionMgrForExistingWorkflowSuite) TestDispatchForExistingWorkflow newWorkflow.EXPECT().GetContext().Return(newContext).AnyTimes() newWorkflow.EXPECT().GetMutableState().Return(newMutableState).AnyTimes() newWorkflow.EXPECT().GetReleaseFn().Return(newReleaseFn).AnyTimes() - newWorkflow.EXPECT().Revive().Return(nil) + newWorkflow.EXPECT().Revive(gomock.Any(), gomock.Any()).Return(nil) currentWorkflow := NewMockWorkflow(s.controller) currentContext := historyi.NewMockWorkflowContext(s.controller) @@ -503,7 +505,7 @@ func (s *transactionMgrForExistingWorkflowSuite) TestDispatchForExistingWorkflow targetWorkflow.EXPECT().HappensAfter(currentWorkflow).Return(true, nil) currentMutableState.EXPECT().IsWorkflowExecutionRunning().Return(true).AnyTimes() currentWorkflow.EXPECT().SuppressBy(targetWorkflow).Return(historyi.TransactionPolicyActive, nil) - targetWorkflow.EXPECT().Revive().Return(nil) + targetWorkflow.EXPECT().Revive(gomock.Any(), gomock.Any()).Return(nil) targetContext.EXPECT().ConflictResolveWorkflowExecution( gomock.Any(), diff --git a/service/history/ndc/transaction_manager_new_workflow.go b/service/history/ndc/transaction_manager_new_workflow.go index eb8e91306ff..62ed01b8a72 100644 --- a/service/history/ndc/transaction_manager_new_workflow.go +++ b/service/history/ndc/transaction_manager_new_workflow.go @@ -11,6 +11,7 @@ import ( "go.temporal.io/server/common/persistence" "go.temporal.io/server/service/history/consts" historyi "go.temporal.io/server/service/history/interfaces" + "go.temporal.io/server/service/history/workflow" ) type ( @@ -26,6 +27,7 @@ type ( shardContext historyi.ShardContext transactionMgr TransactionManager bypassVersionSemanticsCheck bool + taskRefresher workflow.TaskRefresher } ) @@ -35,12 +37,14 @@ func newTransactionMgrForNewWorkflow( shardContext historyi.ShardContext, transactionMgr TransactionManager, bypassVersionSemanticsCheck bool, + taskRefresher workflow.TaskRefresher, ) *nDCTransactionMgrForNewWorkflowImpl { return &nDCTransactionMgrForNewWorkflowImpl{ shardContext: shardContext, transactionMgr: transactionMgr, bypassVersionSemanticsCheck: bypassVersionSemanticsCheck, + taskRefresher: taskRefresher, } } @@ -278,7 +282,7 @@ func (r *nDCTransactionMgrForNewWorkflowImpl) suppressCurrentAndCreateAsCurrent( if err != nil { return err } - if err := targetWorkflow.Revive(); err != nil { + if err := targetWorkflow.Revive(ctx, r.taskRefresher); err != nil { return err } diff --git a/service/history/ndc/transaction_manager_new_workflow_test.go b/service/history/ndc/transaction_manager_new_workflow_test.go index aa1146e8cd4..30aedd5b0f4 100644 --- a/service/history/ndc/transaction_manager_new_workflow_test.go +++ b/service/history/ndc/transaction_manager_new_workflow_test.go @@ -15,6 +15,7 @@ import ( "go.temporal.io/server/common/persistence" "go.temporal.io/server/service/history/consts" historyi "go.temporal.io/server/service/history/interfaces" + "go.temporal.io/server/service/history/workflow" "go.uber.org/mock/gomock" ) @@ -43,7 +44,9 @@ func (s *transactionMgrForNewWorkflowSuite) SetupTest() { s.mockTransactionMgr = NewMockTransactionManager(s.controller) s.mockShard = historyi.NewMockShardContext(s.controller) - s.createMgr = newTransactionMgrForNewWorkflow(s.mockShard, s.mockTransactionMgr, false) + mockTaskRefresher := workflow.NewMockTaskRefresher(s.controller) + mockTaskRefresher.EXPECT().Refresh(gomock.Any(), gomock.Any(), false).Return(nil).AnyTimes() + s.createMgr = newTransactionMgrForNewWorkflow(s.mockShard, s.mockTransactionMgr, false, mockTaskRefresher) } func (s *transactionMgrForNewWorkflowSuite) TearDownTest() { @@ -464,7 +467,7 @@ func (s *transactionMgrForNewWorkflowSuite) TestDispatchForNewWorkflow_SuppressC currentMutableState.EXPECT().IsWorkflowExecutionRunning().Return(true).AnyTimes() currentWorkflowPolicy := historyi.TransactionPolicyActive currentWorkflow.EXPECT().SuppressBy(targetWorkflow).Return(currentWorkflowPolicy, nil) - targetWorkflow.EXPECT().Revive().Return(nil) + targetWorkflow.EXPECT().Revive(gomock.Any(), gomock.Any()).Return(nil) currentContext.EXPECT().UpdateWorkflowExecutionWithNew( gomock.Any(), diff --git a/service/history/ndc/transaction_manager_test.go b/service/history/ndc/transaction_manager_test.go index 37eae7c5f93..3023dd0a664 100644 --- a/service/history/ndc/transaction_manager_test.go +++ b/service/history/ndc/transaction_manager_test.go @@ -225,7 +225,6 @@ func (s *transactionMgrSuite) TestBackfillWorkflow_CurrentWorkflow_Active_Closed lastWorkflowTaskStartedVersion, nextEventID, gomock.Any(), - gomock.Any(), targetWorkflow, targetWorkflow, EventsReapplicationResetWorkflowReason, @@ -309,7 +308,6 @@ func (s *transactionMgrSuite) TestBackfillWorkflow_CurrentWorkflow_Closed_ResetF lastWorkflowTaskStartedVersion, nextEventID, gomock.Any(), - gomock.Any(), targetWorkflow, targetWorkflow, EventsReapplicationResetWorkflowReason, diff --git a/service/history/ndc/workflow.go b/service/history/ndc/workflow.go index a933f334393..4b0a4465a14 100644 --- a/service/history/ndc/workflow.go +++ b/service/history/ndc/workflow.go @@ -3,6 +3,7 @@ package ndc import ( + "context" "fmt" enumspb "go.temporal.io/api/enums/v1" @@ -13,8 +14,10 @@ import ( "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/primitives" "go.temporal.io/server/service/history/consts" historyi "go.temporal.io/server/service/history/interfaces" + "go.temporal.io/server/service/history/workflow" ) type ( @@ -25,7 +28,7 @@ type ( GetVectorClock() (int64, int64, error) HappensAfter(that Workflow) (bool, error) - Revive() error + Revive(ctx context.Context, taskRefresher workflow.TaskRefresher) error SuppressBy(incomingWorkflow Workflow) (historyi.TransactionPolicy, error) FlushBufferedEvents() error } @@ -112,14 +115,11 @@ func (r *WorkflowImpl) HappensAfter( ), nil } -func (r *WorkflowImpl) Revive() error { +func (r *WorkflowImpl) Revive(ctx context.Context, taskRefresher workflow.TaskRefresher) error { state, _ := r.mutableState.GetWorkflowStateStatus() if state != enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE { return nil - } else if state == enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED { - // workflow already finished - return nil } // mutable state is in zombie state, need to set the state correctly accordingly @@ -131,7 +131,10 @@ func (r *WorkflowImpl) Revive() error { state, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, ) - return err + if err != nil { + return err + } + return taskRefresher.Refresh(ctx, r.mutableState, false) } func (r *WorkflowImpl) SuppressBy( @@ -271,9 +274,10 @@ func (r *WorkflowImpl) terminateMutableState( if !r.mutableState.IsWorkflow() { return r.mutableState.ChasmTree().Terminate(chasm.TerminateComponentRequest{ - Identity: consts.IdentityHistoryService, - Reason: common.FailureReasonWorkflowTerminationDueToVersionConflict, - Details: payloads.EncodeString(fmt.Sprintf("terminated by version: %v", incomingLastWriteVersion)), + Identity: consts.IdentityHistoryService, + Reason: common.FailureReasonWorkflowTerminationDueToVersionConflict, + Details: payloads.EncodeString(fmt.Sprintf("terminated by version: %v", incomingLastWriteVersion)), + RequestID: primitives.NewUUID().String(), }) } diff --git a/service/history/ndc/workflow_mock.go b/service/history/ndc/workflow_mock.go index 3b174e7232a..96ab7cacb6c 100644 --- a/service/history/ndc/workflow_mock.go +++ b/service/history/ndc/workflow_mock.go @@ -10,9 +10,11 @@ package ndc import ( + context "context" reflect "reflect" interfaces "go.temporal.io/server/service/history/interfaces" + workflow "go.temporal.io/server/service/history/workflow" gomock "go.uber.org/mock/gomock" ) @@ -128,17 +130,17 @@ func (mr *MockWorkflowMockRecorder) HappensAfter(that any) *gomock.Call { } // Revive mocks base method. -func (m *MockWorkflow) Revive() error { +func (m *MockWorkflow) Revive(ctx context.Context, taskRefresher workflow.TaskRefresher) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Revive") + ret := m.ctrl.Call(m, "Revive", ctx, taskRefresher) ret0, _ := ret[0].(error) return ret0 } // Revive indicates an expected call of Revive. -func (mr *MockWorkflowMockRecorder) Revive() *gomock.Call { +func (mr *MockWorkflowMockRecorder) Revive(ctx, taskRefresher any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Revive", reflect.TypeOf((*MockWorkflow)(nil).Revive)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Revive", reflect.TypeOf((*MockWorkflow)(nil).Revive), ctx, taskRefresher) } // SuppressBy mocks base method. diff --git a/service/history/ndc/workflow_resetter.go b/service/history/ndc/workflow_resetter.go index 8e10e213aec..3005eb1d2c1 100644 --- a/service/history/ndc/workflow_resetter.go +++ b/service/history/ndc/workflow_resetter.go @@ -59,7 +59,6 @@ type ( baseRebuildLastEventVersion int64, baseNextEventID int64, resetRunID string, - resetRequestID string, baseWorkflow Workflow, currentWorkflow Workflow, resetReason string, @@ -115,7 +114,6 @@ func (r *workflowResetterImpl) ResetWorkflow( baseRebuildLastEventVersion int64, baseNextEventID int64, resetRunID string, - resetRequestID string, baseWorkflow Workflow, currentWorkflow Workflow, resetReason string, @@ -203,6 +201,14 @@ func (r *workflowResetterImpl) ResetWorkflow( } } + // Use the original start request ID from the base run so callbacks on the + // reset workflow are associated with the original start request. + // The run ID provides uniqueness per execution, so the start request ID can + // be used consistently across resets. + // + // Read from the base run's RequestIds map; fall back to CreateRequestId otherwise. + startRequestID := findStartRequestID(baseWorkflow.GetMutableState().GetExecutionState()) + resetWorkflow, err := r.prepareResetWorkflow( ctx, namespaceID, @@ -212,7 +218,7 @@ func (r *workflowResetterImpl) ResetWorkflow( baseRebuildLastEventID, baseRebuildLastEventVersion, resetRunID, - resetRequestID, + startRequestID, resetWorkflowVersion, resetReason, allowResetWithPendingChildren, @@ -263,7 +269,7 @@ func (r *workflowResetterImpl) prepareResetWorkflow( baseRebuildLastEventID int64, baseRebuildLastEventVersion int64, resetRunID string, - resetRequestID string, + requestID string, resetWorkflowVersion int64, resetReason string, allowResetWithPendingChildren bool, @@ -278,7 +284,7 @@ func (r *workflowResetterImpl) prepareResetWorkflow( baseRebuildLastEventID, baseRebuildLastEventVersion, resetRunID, - resetRequestID, + requestID, ) if err != nil { return nil, err diff --git a/service/history/ndc/workflow_resetter_mock.go b/service/history/ndc/workflow_resetter_mock.go index 2e9b1aee7bc..b4bcd97fe56 100644 --- a/service/history/ndc/workflow_resetter_mock.go +++ b/service/history/ndc/workflow_resetter_mock.go @@ -45,15 +45,15 @@ func (m *MockWorkflowResetter) EXPECT() *MockWorkflowResetterMockRecorder { } // ResetWorkflow mocks base method. -func (m *MockWorkflowResetter) ResetWorkflow(ctx context.Context, namespaceID namespace.ID, workflowID, baseRunID string, baseBranchToken []byte, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID int64, resetRunID, resetRequestID string, baseWorkflow, currentWorkflow Workflow, resetReason string, additionalReapplyEvents []*history.HistoryEvent, resetReapplyExcludeTypes map[enums.ResetReapplyExcludeType]struct{}, allowResetWithPendingChildren bool, postResetOperations []*workflow.PostResetOperation) error { +func (m *MockWorkflowResetter) ResetWorkflow(ctx context.Context, namespaceID namespace.ID, workflowID, baseRunID string, baseBranchToken []byte, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID int64, resetRunID string, baseWorkflow, currentWorkflow Workflow, resetReason string, additionalReapplyEvents []*history.HistoryEvent, resetReapplyExcludeTypes map[enums.ResetReapplyExcludeType]struct{}, allowResetWithPendingChildren bool, postResetOperations []*workflow.PostResetOperation) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ResetWorkflow", ctx, namespaceID, workflowID, baseRunID, baseBranchToken, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID, resetRunID, resetRequestID, baseWorkflow, currentWorkflow, resetReason, additionalReapplyEvents, resetReapplyExcludeTypes, allowResetWithPendingChildren, postResetOperations) + ret := m.ctrl.Call(m, "ResetWorkflow", ctx, namespaceID, workflowID, baseRunID, baseBranchToken, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID, resetRunID, baseWorkflow, currentWorkflow, resetReason, additionalReapplyEvents, resetReapplyExcludeTypes, allowResetWithPendingChildren, postResetOperations) ret0, _ := ret[0].(error) return ret0 } // ResetWorkflow indicates an expected call of ResetWorkflow. -func (mr *MockWorkflowResetterMockRecorder) ResetWorkflow(ctx, namespaceID, workflowID, baseRunID, baseBranchToken, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID, resetRunID, resetRequestID, baseWorkflow, currentWorkflow, resetReason, additionalReapplyEvents, resetReapplyExcludeTypes, allowResetWithPendingChildren, postResetOperations any) *gomock.Call { +func (mr *MockWorkflowResetterMockRecorder) ResetWorkflow(ctx, namespaceID, workflowID, baseRunID, baseBranchToken, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID, resetRunID, baseWorkflow, currentWorkflow, resetReason, additionalReapplyEvents, resetReapplyExcludeTypes, allowResetWithPendingChildren, postResetOperations any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetWorkflow", reflect.TypeOf((*MockWorkflowResetter)(nil).ResetWorkflow), ctx, namespaceID, workflowID, baseRunID, baseBranchToken, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID, resetRunID, resetRequestID, baseWorkflow, currentWorkflow, resetReason, additionalReapplyEvents, resetReapplyExcludeTypes, allowResetWithPendingChildren, postResetOperations) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetWorkflow", reflect.TypeOf((*MockWorkflowResetter)(nil).ResetWorkflow), ctx, namespaceID, workflowID, baseRunID, baseBranchToken, baseRebuildLastEventID, baseRebuildLastEventVersion, baseNextEventID, resetRunID, baseWorkflow, currentWorkflow, resetReason, additionalReapplyEvents, resetReapplyExcludeTypes, allowResetWithPendingChildren, postResetOperations) } diff --git a/service/history/ndc/workflow_state_replicator_test.go b/service/history/ndc/workflow_state_replicator_test.go index e9e6d9415db..de61cfd9906 100644 --- a/service/history/ndc/workflow_state_replicator_test.go +++ b/service/history/ndc/workflow_state_replicator_test.go @@ -580,7 +580,7 @@ type VersionedTransitionMatcher struct { } // Matches implements gomock.Matcher -func (m *VersionedTransitionMatcher) Matches(x interface{}) bool { +func (m *VersionedTransitionMatcher) Matches(x any) bool { // Type assertion to ensure the argument is of the correct type got, ok := x.(*persistencespb.VersionedTransition) if !ok { @@ -1044,7 +1044,7 @@ type historyEventMatcher struct { expected *historypb.HistoryEvent } -func (m *historyEventMatcher) Matches(x interface{}) bool { +func (m *historyEventMatcher) Matches(x any) bool { evt, ok := x.(*historypb.HistoryEvent) return ok && proto.Equal(evt, m.expected) } diff --git a/service/history/ndc_standby_task_util.go b/service/history/ndc_standby_task_util.go index 08b6c2451af..2b17070ecd2 100644 --- a/service/history/ndc_standby_task_util.go +++ b/service/history/ndc_standby_task_util.go @@ -24,8 +24,8 @@ import ( ) type ( - standbyActionFn func(context.Context, historyi.WorkflowContext, historyi.MutableState, historyi.ReleaseWorkflowContextFunc) (interface{}, error) - standbyPostActionFn func(context.Context, tasks.Task, interface{}, log.Logger) error + standbyActionFn func(context.Context, historyi.WorkflowContext, historyi.MutableState, historyi.ReleaseWorkflowContextFunc) (any, error) + standbyPostActionFn func(context.Context, tasks.Task, any, log.Logger) error standbyCurrentTimeFn func() time.Time ) @@ -33,7 +33,7 @@ type ( func standbyTaskPostActionNoOp( _ context.Context, _ tasks.Task, - postActionInfo interface{}, + postActionInfo any, _ log.Logger, ) error { @@ -52,7 +52,7 @@ func standbyTaskPostActionNoOp( func standbyTransferTaskPostActionTaskDiscarded( _ context.Context, taskInfo tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { @@ -67,7 +67,7 @@ func standbyTransferTaskPostActionTaskDiscarded( func standbyTimerTaskPostActionTaskDiscarded( _ context.Context, taskInfo tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { diff --git a/service/history/ndc_task_util.go b/service/history/ndc_task_util.go index e36ae455562..56f50e2d5d8 100644 --- a/service/history/ndc_task_util.go +++ b/service/history/ndc_task_util.go @@ -8,6 +8,7 @@ import ( "go.temporal.io/api/serviceerror" enumsspb "go.temporal.io/server/api/enums/v1" persistencespb "go.temporal.io/server/api/persistence/v1" + "go.temporal.io/server/chasm" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" @@ -34,7 +35,7 @@ func CheckTaskVersion( namespace *namespace.Namespace, version int64, taskVersion int64, - task interface{}, + task any, ) error { if !shard.GetClusterMetadata().IsGlobalNamespaceEnabled() { @@ -193,7 +194,12 @@ func loadMutableStateForTask( // After reloading mutable state from a database, task's event ID is still not valid, // means that task is obsolete and can be safely skipped. getNamespaceTagByID(shardContext.GetNamespaceRegistry(), task.GetNamespaceID()) - metrics.TaskSkipped.With(metricsHandler).Record(1, getNamespaceTagByID(shardContext.GetNamespaceRegistry(), task.GetNamespaceID()), metrics.TaskTypeTag(taskTypeTag)) + metrics.TaskSkipped.With(metricsHandler).Record( + 1, + getNamespaceTagByID(shardContext.GetNamespaceRegistry(), task.GetNamespaceID()), + metrics.TaskTypeTag(taskTypeTag), + metrics.ArchetypeTag(chasm.WorkflowComponentName), + ) logger.Info("Task processor skipping task: task event ID >= MS NextEventID.", tag.WorkflowNextEventID(mutableState.GetNextEventID()), ) @@ -314,11 +320,12 @@ func getNamespaceTagByID( func getNamespaceTagAndReplicationStateByID( registry namespace.Registry, namespaceID string, + businessID string, ) (metrics.Tag, enumspb.ReplicationState) { namespaceName, err := registry.GetNamespaceByID(namespace.ID(namespaceID)) if err != nil { return metrics.NamespaceUnknownTag(), enumspb.REPLICATION_STATE_UNSPECIFIED } - return metrics.NamespaceTag(namespaceName.Name().String()), namespaceName.ReplicationState() + return metrics.NamespaceTag(namespaceName.Name().String()), namespaceName.ReplicationState(businessID) } diff --git a/service/history/outbound_queue_active_task_executor.go b/service/history/outbound_queue_active_task_executor.go index bc3af188228..1876dd0624a 100644 --- a/service/history/outbound_queue_active_task_executor.go +++ b/service/history/outbound_queue_active_task_executor.go @@ -57,6 +57,7 @@ func (e *outboundQueueActiveTaskExecutor) Execute( namespaceTag, replicationState := getNamespaceTagAndReplicationStateByID( e.shardContext.GetNamespaceRegistry(), task.GetNamespaceID(), + executable.GetWorkflowID(), ) taskType := queues.GetOutboundTaskTypeTagValue(task, true, e.shardContext.ChasmRegistry()) respond := func(err error) queues.ExecuteResponse { @@ -124,7 +125,6 @@ func (e *outboundQueueActiveTaskExecutor) executeChasmSideEffectTask( err = executeChasmSideEffectTask( ctx, e.chasmEngine, - e.shardContext.ChasmRegistry(), tree, task, ) diff --git a/service/history/outbound_queue_active_task_executor_test.go b/service/history/outbound_queue_active_task_executor_test.go index 511b4e28101..6e5111138b1 100644 --- a/service/history/outbound_queue_active_task_executor_test.go +++ b/service/history/outbound_queue_active_task_executor_test.go @@ -156,7 +156,6 @@ func (s *outboundQueueActiveTaskExecutorSuite) TestExecute_ChasmTask() { gomock.Any(), gomock.Any(), gomock.Any(), - gomock.Any(), ) }, expectHandlerCalled: true, @@ -194,6 +193,7 @@ func (s *outboundQueueActiveTaskExecutorSuite) TestExecute_ChasmTask() { tc.setupMocks(task) s.mockExecutable.EXPECT().GetTask().Return(task).AnyTimes() + s.mockExecutable.EXPECT().GetWorkflowID().Return("").AnyTimes() result := s.executor.Execute(ctx, s.mockExecutable) @@ -251,6 +251,7 @@ func (s *outboundQueueActiveTaskExecutorSuite) TestExecute_PreValidationFails() task := tc.setupTask() tc.setupMocks(task) s.mockExecutable.EXPECT().GetTask().Return(task) + s.mockExecutable.EXPECT().GetWorkflowID().Return("").AnyTimes() result := s.executor.Execute(ctx, s.mockExecutable) diff --git a/service/history/outbound_queue_factory.go b/service/history/outbound_queue_factory.go index 77dffccef67..91ebcf8a547 100644 --- a/service/history/outbound_queue_factory.go +++ b/service/history/outbound_queue_factory.go @@ -3,6 +3,7 @@ package history import ( "fmt" + "go.temporal.io/server/client" "go.temporal.io/server/common/collection" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" @@ -30,6 +31,7 @@ type outboundQueueFactoryParams struct { fx.In QueueFactoryBaseParams + ClientBean client.Bean CircuitBreakerPool *circuitbreakerpool.OutboundQueueCircuitBreakerPool } @@ -236,6 +238,7 @@ func (f *outboundQueueFactory) CreateQueue( logger, metricsHandler, f.ChasmEngine, + f.ClientBean, ) executor := queues.NewActiveStandbyExecutor( diff --git a/service/history/outbound_queue_standby_task_executor.go b/service/history/outbound_queue_standby_task_executor.go index 403fb7a3142..85f76f0dd7a 100644 --- a/service/history/outbound_queue_standby_task_executor.go +++ b/service/history/outbound_queue_standby_task_executor.go @@ -6,6 +6,7 @@ import ( "fmt" "go.temporal.io/server/chasm" + "go.temporal.io/server/client" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" @@ -26,6 +27,7 @@ type outboundQueueStandbyTaskExecutor struct { config *configs.Config clusterName string + clientBean client.Bean } var _ queues.Executor = &outboundQueueStandbyTaskExecutor{} @@ -37,6 +39,7 @@ func newOutboundQueueStandbyTaskExecutor( logger log.Logger, metricsHandler metrics.Handler, chasmEngine chasm.Engine, + clientBean client.Bean, ) *outboundQueueStandbyTaskExecutor { return &outboundQueueStandbyTaskExecutor{ stateMachineEnvironment: stateMachineEnvironment{ @@ -50,6 +53,7 @@ func newOutboundQueueStandbyTaskExecutor( config: shardCtx.GetConfig(), clusterName: clusterName, chasmEngine: chasmEngine, + clientBean: clientBean, } } @@ -62,6 +66,7 @@ func (e *outboundQueueStandbyTaskExecutor) Execute( namespaceTag, _ := getNamespaceTagAndReplicationStateByID( e.shardContext.GetNamespaceRegistry(), task.GetNamespaceID(), + executable.GetWorkflowID(), ) respond := func(err error) queues.ExecuteResponse { metricsTags := []metrics.Tag{ @@ -178,14 +183,27 @@ func (e *outboundQueueStandbyTaskExecutor) executeChasmSideEffectTask( return err } - shouldRetry, err := validateChasmSideEffectTask( + valid, err := validateChasmSideEffectTask(ctx, ms, task) + if err != nil || !valid { + return err + } + + // Task is still valid — check discard delay. + chasmTaskType, _ := e.shardContext.ChasmRegistry().TaskFqnByID(task.Info.GetTypeId()) + discardTime := task.GetVisibilityTime().Add(e.config.ChasmStandbyTaskDiscardDelay(chasmTaskType)) + if !e.Now().After(discardTime) { + return consts.ErrTaskRetry + } + + return discardChasmSideEffectTask( ctx, - ms, + e.chasmEngine, + e.shardContext.ChasmRegistry(), + ms.ChasmTree(), task, + e.logger, + e.clusterName, + e.clientBean, + e.shardContext.GetNamespaceRegistry(), ) - if shouldRetry != nil { - err = consts.ErrTaskRetry - } - - return err } diff --git a/service/history/outbound_queue_standby_task_executor_test.go b/service/history/outbound_queue_standby_task_executor_test.go index 14146586e71..1a6308c78bf 100644 --- a/service/history/outbound_queue_standby_task_executor_test.go +++ b/service/history/outbound_queue_standby_task_executor_test.go @@ -12,11 +12,13 @@ import ( enumsspb "go.temporal.io/server/api/enums/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/chasm" + "go.temporal.io/server/client" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/log" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/service/history/consts" "go.temporal.io/server/service/history/hsm" historyi "go.temporal.io/server/service/history/interfaces" "go.temporal.io/server/service/history/queues" @@ -41,6 +43,7 @@ type outboundQueueStandbyTaskExecutorSuite struct { mockMutableState *historyi.MockMutableState mockExecutable *queues.MockExecutable mockChasmTree *historyi.MockChasmTree + mockClientBean *client.MockBean logger log.Logger metricsHandler metrics.Handler @@ -85,6 +88,7 @@ func (s *outboundQueueStandbyTaskExecutorSuite) SetupTest() { s.mockMutableState = historyi.NewMockMutableState(s.controller) s.mockExecutable = queues.NewMockExecutable(s.controller) s.mockChasmTree = historyi.NewMockChasmTree(s.controller) + s.mockClientBean = client.NewMockBean(s.controller) s.logger = s.mockShard.GetLogger() s.metricsHandler = s.mockShard.GetMetricsHandler() @@ -127,6 +131,7 @@ func (s *outboundQueueStandbyTaskExecutorSuite) SetupTest() { s.logger, s.metricsHandler, s.mockChasmEngine, + s.mockClientBean, ) } @@ -204,6 +209,7 @@ func (s *outboundQueueStandbyTaskExecutorSuite) TestExecute_ChasmTask() { tc.setupMocks(task) s.mockExecutable.EXPECT().GetTask().Return(task).AnyTimes() + s.mockExecutable.EXPECT().GetWorkflowID().Return("").AnyTimes() result := s.executor.Execute(ctx, s.mockExecutable) @@ -264,6 +270,7 @@ func (s *outboundQueueStandbyTaskExecutorSuite) TestExecute_PreValidationFails() task := tc.setupTask() tc.setupMocks(task) s.mockExecutable.EXPECT().GetTask().Return(task) + s.mockExecutable.EXPECT().GetWorkflowID().Return("").AnyTimes() result := s.executor.Execute(ctx, s.mockExecutable) @@ -275,6 +282,89 @@ func (s *outboundQueueStandbyTaskExecutorSuite) TestExecute_PreValidationFails() } } +func (s *outboundQueueStandbyTaskExecutorSuite) TestExecute_ChasmTask_Discard() { + chasmDiscardDuration := s.mockShard.GetConfig().ChasmStandbyTaskDiscardDelay("") + + setupDiscard := func(lib chasm.Library, taskName string, treeMockFn func(*historyi.MockChasmTree)) (*outboundQueueStandbyTaskExecutor, queues.Executable) { + registry := chasm.NewRegistry(s.logger) + s.NoError(registry.Register(lib)) + s.mockShard.SetChasmRegistry(registry) + typeID := chasm.GenerateTypeID(chasm.FullyQualifiedName(lib.Name(), taskName)) + + chasmTree := historyi.NewMockChasmTree(s.controller) + chasmTree.EXPECT().ValidateSideEffectTask(gomock.Any(), gomock.Any()).Return(true, nil).Times(1) + treeMockFn(chasmTree) + + ms := historyi.NewMockMutableState(s.controller) + ms.EXPECT().GetCurrentVersion().Return(int64(1)).AnyTimes() + ms.EXPECT().NextTransitionCount().Return(int64(0)).AnyTimes() + ms.EXPECT().GetWorkflowKey().Return(tests.WorkflowKey).AnyTimes() + ms.EXPECT().GetExecutionState().Return(&persistencespb.WorkflowExecutionState{ + State: enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING, + }).AnyTimes() + ms.EXPECT().ChasmTree().Return(chasmTree).AnyTimes() + + task := &tasks.ChasmTask{ + WorkflowKey: tests.WorkflowKey, + TaskID: s.mustGenerateTaskID(), + Category: tasks.CategoryOutbound, + Destination: "test-destination", + VisibilityTimestamp: s.now.Add(-chasmDiscardDuration - time.Second), + Info: &persistencespb.ChasmTaskInfo{ + TypeId: typeID, + ArchetypeId: tests.ArchetypeID, + }, + } + + wfCtx := historyi.NewMockWorkflowContext(s.controller) + wfCtx.EXPECT().LoadMutableState(gomock.Any(), gomock.Any()).Return(ms, nil) + + mockCache := cache.NewMockCache(s.controller) + mockCache.EXPECT().GetOrCreateChasmExecution( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), tests.ArchetypeID, gomock.Any(), + ).Return(wfCtx, func(error) {}, nil) + + executable := queues.NewMockExecutable(s.controller) + executable.EXPECT().GetTask().Return(task).AnyTimes() + executable.EXPECT().GetWorkflowID().Return(task.WorkflowKey.WorkflowID).AnyTimes() + + executor := newOutboundQueueStandbyTaskExecutor( + s.mockShard, + mockCache, + s.clusterName, + s.logger, + s.metricsHandler, + s.mockChasmEngine, + s.mockClientBean, + ) + + return executor, executable + } + + s.Run("WithHandler", func() { + executor, executable := setupDiscard(&discardableTaskTestLibrary{}, "discard_task", func(tree *historyi.MockChasmTree) { + tree.EXPECT().ExecuteSideEffectDiscardTask( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(nil).Times(1) + }) + result := executor.Execute(context.Background(), executable) + s.NoError(result.ExecutionErr) + s.False(result.ExecutedAsActive) + }) + + s.Run("WithoutHandler", func() { + executor, executable := setupDiscard(&nonDiscardableTaskTestLibrary{}, "non_discard_task", func(tree *historyi.MockChasmTree) { + // The default Discard (from SideEffectTaskHandlerBase) returns ErrTaskDiscarded. + tree.EXPECT().ExecuteSideEffectDiscardTask( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(chasm.ErrTaskDiscarded).Times(1) + }) + result := executor.Execute(context.Background(), executable) + s.ErrorIs(result.ExecutionErr, consts.ErrTaskDiscarded) + s.False(result.ExecutedAsActive) + }) +} + func (s *outboundQueueStandbyTaskExecutorSuite) mustGenerateTaskID() int64 { taskID, err := s.mockShard.GenerateTaskID() s.NoError(err) diff --git a/service/history/queue_factory_base.go b/service/history/queue_factory_base.go index bcc2ad40052..aada788432d 100644 --- a/service/history/queue_factory_base.go +++ b/service/history/queue_factory_base.go @@ -133,9 +133,7 @@ func getOptionalQueueFactories( if _, ok := registry.GetCategoryByID(tasks.CategoryIDArchival); ok { factories = append(factories, NewArchivalQueueFactory(archivalParams)) } - if config.EnableNexus() { - factories = append(factories, NewOutboundQueueFactory(outboundParams)) - } + factories = append(factories, NewOutboundQueueFactory(outboundParams)) return additionalQueueFactories{ Factories: factories, } diff --git a/service/history/queues/dlq_writer_test.go b/service/history/queues/dlq_writer_test.go index c89a24ec681..07ca5e60752 100644 --- a/service/history/queues/dlq_writer_test.go +++ b/service/history/queues/dlq_writer_test.go @@ -157,7 +157,7 @@ func TestDLQWriter_ConcurrentWrites(t *testing.T) { // Create tasks that will write to the same DLQ (same category, source, target) testTasks := make([]*tasks.WorkflowTask, numConcurrentWrites) - for i := 0; i < numConcurrentWrites; i++ { + for i := range numConcurrentWrites { testTasks[i] = &tasks.WorkflowTask{ WorkflowKey: definition.WorkflowKey{ NamespaceID: string(tests.NamespaceID), @@ -190,7 +190,7 @@ func TestDLQWriter_ConcurrentWrites(t *testing.T) { } // Launch concurrent writes to the same DLQ - for i := 0; i < numConcurrentWrites; i++ { + for i := range numConcurrentWrites { task := testTasks[i] g.Go(func() error { err := writer.WriteTaskToDLQ( @@ -254,7 +254,7 @@ func TestDLQWriter_ConcurrentWritesDifferentQueues(t *testing.T) { } // Launch concurrent writes to DIFFERENT target clusters (different DLQs) - for i := 0; i < numConcurrentWrites; i++ { + for i := range numConcurrentWrites { index := i g.Go(func() error { task := &tasks.WorkflowTask{ diff --git a/service/history/queues/executable.go b/service/history/queues/executable.go index 34f42afc5fb..5231da819bb 100644 --- a/service/history/queues/executable.go +++ b/service/history/queues/executable.go @@ -85,12 +85,6 @@ var ( dependencyTaskNotCompletedReschedulePolicy = common.CreateDependencyTaskNotCompletedReschedulePolicy() ) -var defaultExecutableMetricsTags = []metrics.Tag{ - metrics.NamespaceUnknownTag(), - metrics.TaskTypeTag("__unknown__"), - metrics.OperationTag("__unknown__"), -} - const ( // resubmitMaxAttempts is the max number of attempts we may skip rescheduler when a task is Nacked. // check the comment in shouldResubmitOnNack() for more details @@ -112,19 +106,21 @@ type ( sync.Mutex state ctasks.State - executor Executor - scheduler Scheduler - rescheduler Rescheduler - priorityAssigner PriorityAssigner - timeSource clock.TimeSource - namespaceRegistry namespace.Registry - clusterMetadata cluster.Metadata - chasmRegistry *chasm.Registry - taskTypeTagProvider TaskTypeTagProvider - logger log.Logger - metricsHandler metrics.Handler - tracer trace.Tracer - dlqWriter *DLQWriter + executor Executor + scheduler Scheduler + rescheduler Rescheduler + priorityAssigner PriorityAssigner + timeSource clock.TimeSource + namespaceRegistry namespace.Registry + clusterMetadata cluster.Metadata + chasmRegistry *chasm.Registry + taskTypeTagProvider TaskTypeTagProvider + logger log.Logger + baseMetricsHandler metrics.Handler + defaultMetricsHandler metrics.Handler + chasmMetricsHandler metrics.Handler // contains archetype tag + tracer trace.Tracer + dlqWriter *DLQWriter readerID int64 attempt int @@ -211,13 +207,7 @@ func NewExecutable( return tasks.Tags(task) }, ), - metricsHandler: metricsHandler.WithTags(estimateTaskMetricTags( - task, - namespaceRegistry, - clusterMetadata.GetCurrentClusterName(), - chasmRegistry, - taskTypeTagProvider, - )...), + baseMetricsHandler: metricsHandler, tracer: tracer, dlqWriter: params.DLQWriter, dlqEnabled: params.DLQEnabled, @@ -225,10 +215,11 @@ func NewExecutable( dlqInternalErrors: params.DLQInternalErrors, dlqErrorPattern: params.DLQErrorPattern, } + e.refreshMetricsHandlers(nil) e.priority = priorityAssigner.Assign(e) loadTime := util.MaxTime(timeSource.Now(), task.GetKey().FireTime) - metrics.TaskLoadLatency.With(e.metricsHandler).Record( + metrics.TaskLoadLatency.With(e.chasmMetricsHandler).Record( loadTime.Sub(task.GetVisibilityTime()), metrics.QueueReaderIDTag(readerID), ) @@ -304,14 +295,7 @@ func (e *executableImpl) Execute() (retErr error) { // we need to guess the metrics tags here as we don't know which execution logic // is actually used which is upto the executor implementation - e.metricsHandler = e.metricsHandler.WithTags( - estimateTaskMetricTags( - e.GetTask(), - e.namespaceRegistry, - e.clusterMetadata.GetCurrentClusterName(), - e.chasmRegistry, - e.taskTypeTagProvider, - )...) + e.refreshMetricsHandlers(nil) } attemptUserLatency := time.Duration(0) @@ -322,12 +306,17 @@ func (e *executableImpl) Execute() (retErr error) { attemptLatency := e.timeSource.Now().Sub(startTime) e.attemptNoUserLatency = attemptLatency - attemptUserLatency // emit total attempt latency so that we know how much time a task will occpy a worker goroutine - metrics.TaskProcessingLatency.With(e.metricsHandler).Record(attemptLatency) + metrics.TaskProcessingLatency.With(e.chasmMetricsHandler).Record(attemptLatency) + + if persistenceDuration, ok := metrics.ContextCounterGet(ctx, metrics.TaskPersistenceLatency.Name()); ok { + attemptNoPersistence := attemptLatency - time.Duration(persistenceDuration) + metrics.TaskProcessingNoPersistenceLatency.With(e.chasmMetricsHandler).Record(attemptNoPersistence) + } - priorityTaggedProvider := e.metricsHandler.WithTags(metrics.TaskPriorityTag(e.priority.String())) + priorityTaggedProvider := e.chasmMetricsHandler.WithTags(metrics.TaskPriorityTag(e.priority.String())) metrics.TaskRequests.With(priorityTaggedProvider).Record(1) metrics.TaskScheduleLatency.With(priorityTaggedProvider).Record(e.scheduleLatency) - metrics.OperationCounter.With(e.metricsHandler).Record(1) + metrics.OperationCounter.With(e.defaultMetricsHandler).Record(1) if retErr == nil { e.inMemoryNoUserLatency += e.scheduleLatency + e.attemptNoUserLatency @@ -354,7 +343,7 @@ func (e *executableImpl) Execute() (retErr error) { } resp := e.executor.Execute(ctx, e) - e.metricsHandler = e.metricsHandler.WithTags(resp.ExecutionMetricTags...) + e.refreshMetricsHandlers(resp.ExecutionMetricTags) if resp.ExecutedAsActive != e.lastActiveness { // namespace did a failover, @@ -383,10 +372,10 @@ func (e *executableImpl) writeToDLQ(ctx context.Context) error { e.lastActiveness, ) if err != nil { - metrics.TaskDLQFailures.With(e.metricsHandler).Record(1) + metrics.TaskDLQFailures.With(e.chasmMetricsHandler).Record(1) e.logger.Error("Failed to write task to DLQ", tag.Error(err)) } - metrics.TaskDLQSendLatency.With(e.metricsHandler).Record(e.timeSource.Now().Sub(start)) + metrics.TaskDLQSendLatency.With(e.chasmMetricsHandler).Record(e.timeSource.Now().Sub(start)) return err } @@ -404,7 +393,7 @@ func (e *executableImpl) isInvalidTaskError(err error) bool { // The task is stale and is safe to be dropped. // Even though ErrStaleReference is castable to serviceerror.NotFound, we give this error special treatment // because we're interested in the metric. - metrics.TaskSkipped.With(e.metricsHandler).Record(1) + metrics.TaskSkipped.With(e.chasmMetricsHandler).Record(1) e.logger.Info("Skipped task due to stale reference", tag.Error(err)) return true } @@ -419,7 +408,7 @@ func (e *executableImpl) isInvalidTaskError(err error) bool { } if err == consts.ErrTaskVersionMismatch { - metrics.TaskVersionMisMatch.With(e.metricsHandler).Record(1) + metrics.TaskVersionMisMatch.With(e.chasmMetricsHandler).Record(1) return true } @@ -428,7 +417,7 @@ func (e *executableImpl) isInvalidTaskError(err error) bool { func (e *executableImpl) isSafeToDropError(err error) bool { if err == consts.ErrTaskDiscarded { - metrics.TaskDiscarded.With(e.metricsHandler).Record(1) + metrics.TaskDiscarded.With(e.chasmMetricsHandler).Record(1) return true } @@ -457,7 +446,7 @@ func (e *executableImpl) isExpectedRetryableError(err error) (isRetryable bool, e.resourceExhaustedCount++ } - metrics.TaskThrottledCounter.With(e.metricsHandler).Record( + metrics.TaskThrottledCounter.With(e.chasmMetricsHandler).Record( 1, metrics.ResourceExhaustedCauseTag(resourceExhaustedErr.Cause)) return true, err } @@ -466,22 +455,22 @@ func (e *executableImpl) isExpectedRetryableError(err error) (isRetryable bool, if _, ok := err.(*serviceerror.NamespaceNotActive); ok { // error is expected when there's namespace failover, // so don't count it into task failures. - metrics.TaskNotActiveCounter.With(e.metricsHandler).Record(1) + metrics.TaskNotActiveCounter.With(e.chasmMetricsHandler).Record(1) return true, err } if err == consts.ErrDependencyTaskNotCompleted { - metrics.TasksDependencyTaskNotCompleted.With(e.metricsHandler).Record(1) + metrics.TasksDependencyTaskNotCompleted.With(e.chasmMetricsHandler).Record(1) return true, err } if err == consts.ErrTaskRetry { - metrics.TaskStandbyRetryCounter.With(e.metricsHandler).Record(1) + metrics.TaskStandbyRetryCounter.With(e.chasmMetricsHandler).Record(1) return true, err } if err.Error() == consts.ErrNamespaceHandover.Error() { - metrics.TaskNamespaceHandoverCounter.With(e.metricsHandler).Record(1) + metrics.TaskNamespaceHandoverCounter.With(e.chasmMetricsHandler).Record(1) return true, consts.ErrNamespaceHandover } @@ -500,7 +489,7 @@ func (e *executableImpl) isUnexpectedNonRetryableError(err error) bool { isInternalError := common.IsInternalError(err) if isInternalError { - metrics.TaskInternalErrorCounter.With(e.metricsHandler).Record(1) + metrics.TaskInternalErrorCounter.With(e.chasmMetricsHandler).Record(1) // Only DQL/drop when configured to shouldDLQ := e.dlqInternalErrors() return shouldDLQ @@ -548,7 +537,7 @@ func (e *executableImpl) HandleErr(err error) (retErr error) { // Unexpected errors handled below e.unexpectedErrorAttempts++ - metrics.TaskFailures.With(e.metricsHandler).Record(1) + metrics.TaskFailures.With(e.chasmMetricsHandler).Record(1) logger := log.With(e.logger, tag.Error(err), tag.ErrorType(err), @@ -567,12 +556,12 @@ func (e *executableImpl) HandleErr(err error) (retErr error) { // Terminal errors are likely due to data corruption. // Drop the task by returning nil so that task will be marked as completed, // or send it to the DLQ if that is enabled. - metrics.TaskCorruptionCounter.With(e.metricsHandler).Record(1) + metrics.TaskCorruptionCounter.With(e.chasmMetricsHandler).Record(1) if e.dlqEnabled() { // Keep this message in sync with the log line mentioned in Investigation section of docs/admin/dlq.md e.logger.Error("Marking task as terminally failed, will send to DLQ", tag.Error(err), tag.ErrorType(err)) e.terminalFailureCause = err // <- Execute() examines this attribute on the next attempt. - metrics.TaskTerminalFailures.With(e.metricsHandler).Record(1) + metrics.TaskTerminalFailures.With(e.chasmMetricsHandler).Record(1) return fmt.Errorf("%w: %v", ErrTerminalTaskFailure, err) } e.logger.Error("Dropping task due to terminal error", tag.Error(err), tag.ErrorType(err)) @@ -585,7 +574,7 @@ func (e *executableImpl) HandleErr(err error) (retErr error) { e.logger.Error("Marking task as terminally failed, will send to DLQ. Maximum number of attempts with unexpected errors", tag.UnexpectedErrorAttempts(int32(e.unexpectedErrorAttempts)), tag.Error(err)) e.terminalFailureCause = err // <- Execute() examines this attribute on the next attempt. - metrics.TaskTerminalFailures.With(e.metricsHandler).Record(1) + metrics.TaskTerminalFailures.With(e.chasmMetricsHandler).Record(1) return fmt.Errorf("%w: %w", ErrTerminalTaskFailure, e.terminalFailureCause) } @@ -611,7 +600,7 @@ func (e *executableImpl) matchDLQErrorPattern(err error) error { tag.Error(err), tag.ErrorType(err)) e.terminalFailureCause = err - metrics.TaskTerminalFailures.With(e.metricsHandler).Record(1) + metrics.TaskTerminalFailures.With(e.chasmMetricsHandler).Record(1) return fmt.Errorf("%w: %v", ErrTerminalTaskFailure, err) } @@ -664,9 +653,9 @@ func (e *executableImpl) Ack() { return } - metrics.TaskAttempt.With(e.metricsHandler).Record(int64(e.attempt)) + metrics.TaskAttempt.With(e.chasmMetricsHandler).Record(int64(e.attempt)) - priorityTaggedProvider := e.metricsHandler.WithTags(metrics.TaskPriorityTag(e.priority.String())) + priorityTaggedProvider := e.chasmMetricsHandler.WithTags(metrics.TaskPriorityTag(e.priority.String())) metrics.TaskLatency.With(priorityTaggedProvider).Record(e.inMemoryNoUserLatency) metrics.TaskQueueLatency.With(priorityTaggedProvider.WithTags(metrics.QueueReaderIDTag(e.readerID))). Record(time.Since(e.GetVisibilityTime())) @@ -678,6 +667,17 @@ func (e *executableImpl) Nack(err error) { return } + // Check if this is a busy workflow error and if the scheduler supports + // routing to a sequential scheduler for contended workflows + if errors.Is(err, consts.ErrResourceExhaustedBusyWorkflow) { + if handler, ok := e.scheduler.(BusyWorkflowHandler); ok { + e.SetScheduledTime(e.timeSource.Now()) + if handler.HandleBusyWorkflow(e) { + return + } + } + } + submitted := false if e.shouldResubmitOnNack(err) { // we do not need to know if there any error during submission @@ -808,11 +808,37 @@ func (e *executableImpl) incAttempt() { e.attempt++ if e.attempt > taskCriticalLogMetricAttempts { - metrics.TaskAttempt.With(e.metricsHandler).Record(int64(e.attempt)) + metrics.TaskAttempt.With(e.chasmMetricsHandler).Record(int64(e.attempt)) + } +} + +func (e *executableImpl) refreshMetricsHandlers(executionMetricTags []metrics.Tag) { + sharedTags := taskBaseMetricTagsWithoutArchetype( + e.GetTask(), + e.namespaceRegistry, + e.clusterMetadata.GetCurrentClusterName(), + e.chasmRegistry, + e.taskTypeTagProvider, + ) + if len(executionMetricTags) > 0 { + sharedTags = append(sharedTags, executionMetricTags...) } + e.defaultMetricsHandler = e.baseMetricsHandler.WithTags(sharedTags...) + e.chasmMetricsHandler = e.defaultMetricsHandler.WithTags(getArchetypeTag(e.GetTask(), e.chasmRegistry)) } -func estimateTaskMetricTags( +func taskBaseMetricTags( + task tasks.Task, + namespaceRegistry namespace.Registry, + currentClusterName string, + chasmRegistry *chasm.Registry, + taskTypeTagProvider TaskTypeTagProvider, +) []metrics.Tag { + tags := taskBaseMetricTagsWithoutArchetype(task, namespaceRegistry, currentClusterName, chasmRegistry, taskTypeTagProvider) + return append(tags, getArchetypeTag(task, chasmRegistry)) +} + +func taskBaseMetricTagsWithoutArchetype( task tasks.Task, namespaceRegistry namespace.Registry, currentClusterName string, @@ -837,6 +863,15 @@ func estimateTaskMetricTags( } } +func getArchetypeTag(task tasks.Task, chasmRegistry *chasm.Registry) metrics.Tag { + if t, ok := task.(tasks.HasArchetypeID); ok { + if name, ok := chasmRegistry.ArchetypeDisplayName(t.GetArchetypeID()); ok { + return metrics.ArchetypeTag(name) + } + } + return metrics.ArchetypeTag(chasm.WorkflowComponentName) +} + // CircuitBreakerExecutable wraps Executable with a circuit breaker. // If the executable returns DestinationDownError, it will signal the circuit breaker // of failure, and return the inner error. diff --git a/service/history/queues/executable_factory.go b/service/history/queues/executable_factory.go index 6cc47a7c148..072ad826d0b 100644 --- a/service/history/queues/executable_factory.go +++ b/service/history/queues/executable_factory.go @@ -75,7 +75,7 @@ func NewExecutableFactory( chasmRegistry: chasmRegistry, taskTypeTagProvider: taskTypeTagProvider, logger: logger, - metricsHandler: metricsHandler.WithTags(defaultExecutableMetricsTags...), + metricsHandler: metricsHandler, tracer: tracer, dlqWriter: dlqWriter, dlqEnabled: dlqEnabled, diff --git a/service/history/queues/executable_test.go b/service/history/queues/executable_test.go index d819c855d01..b8bee618e76 100644 --- a/service/history/queues/executable_test.go +++ b/service/history/queues/executable_test.go @@ -199,7 +199,7 @@ func (s *executableSuite) TestExecute_InMemoryNoUserLatency_SingleAttempt() { now = now.Add(scheduleLatency) s.timeSource.Update(now) - s.mockExecutor.EXPECT().Execute(gomock.Any(), executable).Do(func(ctx context.Context, taskInfo interface{}) { + s.mockExecutor.EXPECT().Execute(gomock.Any(), executable).Do(func(ctx context.Context, taskInfo any) { metrics.ContextCounterAdd( ctx, metrics.HistoryWorkflowExecutionCacheLatency.Name(), @@ -275,7 +275,7 @@ func (s *executableSuite) TestExecute_InMemoryNoUserLatency_MultipleAttempts() { now = now.Add(scheduleLatencies[i]) s.timeSource.Update(now) - s.mockExecutor.EXPECT().Execute(gomock.Any(), executable).Do(func(ctx context.Context, taskInfo interface{}) { + s.mockExecutor.EXPECT().Execute(gomock.Any(), executable).Do(func(ctx context.Context, taskInfo any) { metrics.ContextCounterAdd( ctx, metrics.HistoryWorkflowExecutionCacheLatency.Name(), @@ -370,6 +370,75 @@ func (s *executableSuite) TestExecute_CallerInfo() { s.NoError(executable.Execute()) } +func (s *executableSuite) TestExecute_TaskProcessingNoPersistenceLatency_RecordedWhenPersistenceInContext() { + scheduleLatency := 100 * time.Millisecond + persistenceDuration := 50 * time.Millisecond + attemptLatency := 200 * time.Millisecond + expectedNoPersistence := attemptLatency - persistenceDuration + + executable := s.newTestExecutable() + + now := time.Now() + s.timeSource.Update(now) + executable.SetScheduledTime(now) + + now = now.Add(scheduleLatency) + s.timeSource.Update(now) + + capture := s.metricsHandler.StartCapture() + + s.mockExecutor.EXPECT().Execute(gomock.Any(), executable).Do(func(ctx context.Context, _ any) { + metrics.ContextCounterAdd(ctx, metrics.TaskPersistenceLatency.Name(), int64(persistenceDuration)) + now = now.Add(attemptLatency) + s.timeSource.Update(now) + }).Return(queues.ExecuteResponse{ + ExecutionMetricTags: nil, + ExecutedAsActive: true, + ExecutionErr: nil, + }) + + s.NoError(executable.Execute()) + + snapshot := capture.Snapshot() + recordings := snapshot[metrics.TaskProcessingNoPersistenceLatency.Name()] + s.Require().Len(recordings, 1) + actualNoPersistence, ok := recordings[0].Value.(time.Duration) + s.Require().True(ok) + s.Equal(expectedNoPersistence, actualNoPersistence) +} + +func (s *executableSuite) TestExecute_TaskProcessingNoPersistenceLatency_NotRecordedWhenNoPersistence() { + scheduleLatency := 100 * time.Millisecond + attemptLatency := 200 * time.Millisecond + + executable := s.newTestExecutable() + + now := time.Now() + s.timeSource.Update(now) + executable.SetScheduledTime(now) + + now = now.Add(scheduleLatency) + s.timeSource.Update(now) + + capture := s.metricsHandler.StartCapture() + + s.mockExecutor.EXPECT().Execute(gomock.Any(), executable).Do(func(ctx context.Context, _ any) { + // Do not add TaskPersistenceLatency to context (simulates transfer/timer task that never calls visibility). + now = now.Add(attemptLatency) + s.timeSource.Update(now) + }).Return(queues.ExecuteResponse{ + ExecutionMetricTags: nil, + ExecutedAsActive: true, + ExecutionErr: nil, + }) + + s.NoError(executable.Execute()) + + snapshot := capture.Snapshot() + recordings := snapshot[metrics.TaskProcessingNoPersistenceLatency.Name()] + s.Empty(recordings) +} + func (s *executableSuite) TestExecuteHandleErr_ResetAttempt() { testCases := []struct { name string @@ -1192,6 +1261,121 @@ func (s *executableSuite) newTestExecutable(opts ...option) queues.Executable { ) } +// mockSchedulerWithBusyHandler is a mock scheduler that also implements BusyWorkflowHandler +type mockSchedulerWithBusyHandler struct { + *queues.MockScheduler + handleBusyWorkflowCalled bool + handleBusyWorkflowReturn bool +} + +func (m *mockSchedulerWithBusyHandler) HandleBusyWorkflow(executable queues.Executable) bool { + m.handleBusyWorkflowCalled = true + return m.handleBusyWorkflowReturn +} + +func (s *executableSuite) TestTaskNack_BusyWorkflow_HandledByBusyWorkflowHandler() { + // Create a mock scheduler that implements BusyWorkflowHandler + mockScheduler := &mockSchedulerWithBusyHandler{ + MockScheduler: queues.NewMockScheduler(s.controller), + handleBusyWorkflowReturn: true, // Handler successfully handles the task + } + + executable := queues.NewExecutable( + queues.DefaultReaderId, + tasks.NewFakeTask( + definition.NewWorkflowKey( + tests.NamespaceID.String(), + tests.WorkflowID, + tests.RunID, + ), + tasks.CategoryTransfer, + s.timeSource.Now(), + ), + s.mockExecutor, + mockScheduler, + s.mockRescheduler, + queues.NewNoopPriorityAssigner(), + s.timeSource, + s.mockNamespaceRegistry, + s.mockClusterMetadata, + s.chasmRegistry, + queues.GetTaskTypeTagValue, + log.NewTestLogger(), + s.metricsHandler, + telemetry.NoopTracer, + func(params *queues.ExecutableParams) { + params.DLQEnabled = func() bool { return false } + }, + ) + + // Nack with ErrResourceExhaustedBusyWorkflow should trigger HandleBusyWorkflow + executable.Nack(consts.ErrResourceExhaustedBusyWorkflow) + + // Verify HandleBusyWorkflow was called + s.True(mockScheduler.handleBusyWorkflowCalled, "HandleBusyWorkflow should be called for busy workflow errors") +} + +func (s *executableSuite) TestTaskNack_BusyWorkflow_FallbackToReschedule() { + // Create a mock scheduler that implements BusyWorkflowHandler but returns false + mockScheduler := &mockSchedulerWithBusyHandler{ + MockScheduler: queues.NewMockScheduler(s.controller), + handleBusyWorkflowReturn: false, // Handler does not handle the task + } + + executable := queues.NewExecutable( + queues.DefaultReaderId, + tasks.NewFakeTask( + definition.NewWorkflowKey( + tests.NamespaceID.String(), + tests.WorkflowID, + tests.RunID, + ), + tasks.CategoryTransfer, + s.timeSource.Now(), + ), + s.mockExecutor, + mockScheduler, + s.mockRescheduler, + queues.NewNoopPriorityAssigner(), + s.timeSource, + s.mockNamespaceRegistry, + s.mockClusterMetadata, + s.chasmRegistry, + queues.GetTaskTypeTagValue, + log.NewTestLogger(), + s.metricsHandler, + telemetry.NoopTracer, + func(params *queues.ExecutableParams) { + params.DLQEnabled = func() bool { return false } + }, + ) + + // When HandleBusyWorkflow returns false, normal Nack logic kicks in. + // For busy workflow errors, shouldResubmitOnNack returns true, so TrySubmit is called first. + // If TrySubmit fails (returns false), then rescheduler.Add is called. + mockScheduler.EXPECT().TrySubmit(executable).Return(false).Times(1) + s.mockRescheduler.EXPECT().Add(executable, gomock.AssignableToTypeOf(time.Now())).Times(1) + + executable.Nack(consts.ErrResourceExhaustedBusyWorkflow) + + // Verify HandleBusyWorkflow was called first + s.True(mockScheduler.handleBusyWorkflowCalled, "HandleBusyWorkflow should be called for busy workflow errors") +} + +func (s *executableSuite) TestTaskNack_BusyWorkflow_NoHandlerFallsBackToReschedule() { + // Test that when scheduler does not implement BusyWorkflowHandler, + // busy workflow errors fall back to normal Nack path (TrySubmit then reschedule) + executable := s.newTestExecutable() + + // When scheduler doesn't implement BusyWorkflowHandler, normal Nack flow is used. + // For busy workflow errors, shouldResubmitOnNack returns true, so TrySubmit is called first. + // If TrySubmit fails (returns false), then rescheduler.Add is called. + s.mockScheduler.EXPECT().TrySubmit(executable).Return(false).Times(1) + s.mockRescheduler.EXPECT().Add(executable, gomock.AssignableToTypeOf(time.Now())).Times(1) + + executable.Nack(consts.ErrResourceExhaustedBusyWorkflow) +} + func (s *executableSuite) accessInternalState(executable queues.Executable) { _ = fmt.Sprintf("%v", executable) } diff --git a/service/history/queues/memory_scheduled_queue_test.go b/service/history/queues/memory_scheduled_queue_test.go index 1f19af19c99..2ae3ecc4602 100644 --- a/service/history/queues/memory_scheduled_queue_test.go +++ b/service/history/queues/memory_scheduled_queue_test.go @@ -124,7 +124,7 @@ func (s *memoryScheduledQueueSuite) Test_1KRandomTasks() { t := make([]*speculativeWorkflowTaskTimeoutExecutable, 1000) calls := atomic.Int32{} - for i := 0; i < 1000; i++ { + for i := range 1000 { t[i] = s.newSpeculativeWorkflowTaskTimeoutTestExecutable(now.Add(time.Duration(rand.Intn(100)) * time.Microsecond)) // Randomly cancel some tasks. @@ -135,13 +135,13 @@ func (s *memoryScheduledQueueSuite) Test_1KRandomTasks() { } } - for i := 0; i < 1000; i++ { + for i := range 1000 { if t[i].State() != ctasks.TaskStateCancelled { s.mockScheduler.EXPECT().TrySubmit(t[i]).Return(true).Do(func(_ ctasks.Task) { calls.Add(-1) }) } } - for i := 0; i < 1000; i++ { + for i := range 1000 { s.scheduledQueue.Add(t[i]) } diff --git a/service/history/queues/metrics_test.go b/service/history/queues/metrics_test.go index 6644905685a..2ec817b85b6 100644 --- a/service/history/queues/metrics_test.go +++ b/service/history/queues/metrics_test.go @@ -4,6 +4,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + "go.temporal.io/server/chasm" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/service/history/tasks" ) @@ -13,3 +16,19 @@ func TestGetArchivalTaskTypeTagValue(t *testing.T) { unknownTask := &tasks.CloseExecutionTask{} assert.Equal(t, unknownTask.GetType().String(), GetArchivalTaskTypeTagValue(unknownTask)) } + +func TestGetArchetypeTag(t *testing.T) { + registry := chasm.NewRegistry(log.NewTestLogger()) + + t.Run("legacy task without HasArchetypeID defaults to workflow", func(t *testing.T) { + task := &tasks.ActivityTask{} + tag := getArchetypeTag(task, registry) + assert.Equal(t, metrics.ArchetypeTag(chasm.WorkflowComponentName), tag) + }) + + t.Run("HasArchetypeID task with unregistered ID defaults to workflow", func(t *testing.T) { + task := &tasks.ChasmTaskPure{ArchetypeID: 9999} + tag := getArchetypeTag(task, registry) + assert.Equal(t, metrics.ArchetypeTag(chasm.WorkflowComponentName), tag) + }) +} diff --git a/service/history/queues/queue_base.go b/service/history/queues/queue_base.go index 640907ec414..fc0140b6d53 100644 --- a/service/history/queues/queue_base.go +++ b/service/history/queues/queue_base.go @@ -144,14 +144,17 @@ func newQueueBase( // non-default reader should not trigger task unloading // otherwise those readers will keep loading, hit pending task count limit, unload, throttle, load, etc... // use a limit lower than the critical pending task count instead - + // // Use lower maxPendingTaskCount for lower reader to guarantee that higher reader can // always have some tasks loaded. readerOptions.MaxPendingTasksCount = func() int { - return max( - minMaxPendingTaskCount, - int(float64(options.PendingTasksCriticalCount())* - math.Pow(maxPendingTaskMultiplier, float64(readerID))), + return min( + options.MaxPendingTasksCount(), + max( + minMaxPendingTaskCount, + int(float64(options.PendingTasksCriticalCount())* + math.Pow(maxPendingTaskMultiplier, float64(readerID))), + ), ) } } diff --git a/service/history/queues/queue_base_test.go b/service/history/queues/queue_base_test.go index 65bc4988836..a93c6811de9 100644 --- a/service/history/queues/queue_base_test.go +++ b/service/history/queues/queue_base_test.go @@ -549,7 +549,7 @@ func (s *queueBaseSuite) TestCheckPoint_MoveTaskGroupAction() { addExecutableToSlice := func(readerID int64, slice Slice, namespaceID string, count int) { sliceRange := slice.Scope().Range - for i := 0; i < count; i++ { + for range count { mockTask := tasks.NewMockTask(s.controller) mockTask.EXPECT().GetKey().Return(NewRandomKeyInRange(sliceRange)).AnyTimes() mockTask.EXPECT().GetNamespaceID().Return(namespaceID).AnyTimes() diff --git a/service/history/queues/queue_scheduled_test.go b/service/history/queues/queue_scheduled_test.go index feeeb66831b..8a9dd47dbfa 100644 --- a/service/history/queues/queue_scheduled_test.go +++ b/service/history/queues/queue_scheduled_test.go @@ -18,6 +18,7 @@ import ( "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/predicates" + ctasks "go.temporal.io/server/common/tasks" "go.temporal.io/server/common/telemetry" "go.temporal.io/server/common/timer" "go.temporal.io/server/service/history/shard" @@ -83,9 +84,16 @@ func (s *scheduledQueueSuite) SetupTest() { WorkerCount: s.mockShard.GetConfig().TimerProcessorSchedulerWorkerCount, ActiveNamespaceWeights: s.mockShard.GetConfig().TimerProcessorSchedulerActiveRoundRobinWeights, StandbyNamespaceWeights: s.mockShard.GetConfig().TimerProcessorSchedulerStandbyRoundRobinWeights, + ExecutionAwareSchedulerOptions: ctasks.ExecutionAwareSchedulerOptions{ + Enabled: func() bool { return false }, + MaxQueues: func() int { return 500 }, + QueueTTL: func() time.Duration { return 5 * time.Second }, + }, }, s.mockShard.GetNamespaceRegistry(), logger, + metrics.NoopMetricsHandler, + s.mockShard.GetTimeSource(), ) scheduler = NewRateLimitedScheduler( scheduler, diff --git a/service/history/queues/reader_group_test.go b/service/history/queues/reader_group_test.go index ef85cde0859..5a60cda928f 100644 --- a/service/history/queues/reader_group_test.go +++ b/service/history/queues/reader_group_test.go @@ -71,7 +71,7 @@ func (s *readerGroupSuite) TestAddGetReader() { s.False(ok) s.Nil(r) - for i := int64(0); i < 3; i++ { + for i := range int64(3) { r := s.readerGroup.NewReader(i) readers := s.readerGroup.Readers() diff --git a/service/history/queues/scheduler.go b/service/history/queues/scheduler.go index 2dd0d0e0d46..9edcff17597 100644 --- a/service/history/queues/scheduler.go +++ b/service/history/queues/scheduler.go @@ -5,6 +5,7 @@ package queues import ( "go.temporal.io/server/chasm" "go.temporal.io/server/common/clock" + "go.temporal.io/server/common/definition" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" @@ -25,6 +26,15 @@ const ( ) type ( + // BusyWorkflowHandler is an interface for schedulers that can handle busy workflow errors + // by routing tasks to an ExecutionQueueScheduler. + BusyWorkflowHandler interface { + // HandleBusyWorkflow is called when a task encounters a busy workflow error. + // It routes the task to the ExecutionQueueScheduler for sequential processing. + // Returns true if the task was handled, false if the caller should handle it. + HandleBusyWorkflow(Executable) bool + } + // Scheduler is the component for scheduling and processing // task executables, it's based on the common/tasks.Scheduler // interface and provide the additional information of how @@ -51,6 +61,9 @@ type ( ActiveNamespaceWeights dynamicconfig.MapPropertyFnWithNamespaceFilter StandbyNamespaceWeights dynamicconfig.MapPropertyFnWithNamespaceFilter InactiveNamespaceDeletionDelay dynamicconfig.DurationPropertyFn + + // ExecutionAwareSchedulerOptions contains options for sequential per-workflow scheduling + tasks.ExecutionAwareSchedulerOptions } RateLimitedSchedulerOptions struct { @@ -66,6 +79,10 @@ type ( taskChannelKeyFn TaskChannelKeyFn channelWeightFn ChannelWeightFn channelWeightUpdateCh chan struct{} + + // executionAwareScheduler holds a reference to the workflow-aware scheduler + // for handling busy workflow errors + executionAwareScheduler *tasks.ExecutionAwareScheduler[Executable] } rateLimitedSchedulerImpl struct { @@ -80,6 +97,8 @@ func NewScheduler( options SchedulerOptions, namespaceRegistry namespace.Registry, logger log.Logger, + metricsHandler metrics.Handler, + timeSource clock.TimeSource, ) Scheduler { var scheduler tasks.Scheduler[Executable] @@ -128,6 +147,21 @@ func NewScheduler( WorkerCount: options.WorkerCount, } + fifoScheduler := tasks.NewFIFOScheduler[Executable]( + fifoSchedulerOptions, + logger, + ) + + // Wrap the FIFO scheduler with ExecutionAwareScheduler for sequential per-execution processing + executionAwareScheduler := tasks.NewExecutionAwareScheduler[Executable]( + fifoScheduler, + options.ExecutionAwareSchedulerOptions, + executableQueueKeyFn, + logger, + metricsHandler, + timeSource, + ) + scheduler = tasks.NewInterleavedWeightedRoundRobinScheduler( tasks.InterleavedWeightedRoundRobinSchedulerOptions[Executable, TaskChannelKey]{ TaskChannelKeyFn: taskChannelKeyFn, @@ -135,19 +169,17 @@ func NewScheduler( ChannelWeightUpdateCh: channelWeightUpdateCh, InactiveChannelDeletionDelay: options.InactiveNamespaceDeletionDelay, }, - tasks.Scheduler[Executable](tasks.NewFIFOScheduler[Executable]( - fifoSchedulerOptions, - logger, - )), + executionAwareScheduler, logger, ) return &schedulerImpl{ - Scheduler: scheduler, - namespaceRegistry: namespaceRegistry, - taskChannelKeyFn: taskChannelKeyFn, - channelWeightFn: channelWeightFn, - channelWeightUpdateCh: channelWeightUpdateCh, + Scheduler: scheduler, + namespaceRegistry: namespaceRegistry, + taskChannelKeyFn: taskChannelKeyFn, + channelWeightFn: channelWeightFn, + channelWeightUpdateCh: channelWeightUpdateCh, + executionAwareScheduler: executionAwareScheduler, } } @@ -182,6 +214,13 @@ func (s *schedulerImpl) TaskChannelKeyFn() TaskChannelKeyFn { return s.taskChannelKeyFn } +// HandleBusyWorkflow implements BusyWorkflowHandler by delegating to the +// underlying ExecutionAwareScheduler. This is called when a task encounters +// a busy workflow error and needs to be routed to the sequential scheduler. +func (s *schedulerImpl) HandleBusyWorkflow(executable Executable) bool { + return s.executionAwareScheduler.HandleBusyWorkflow(executable) +} + // CommonSchedulerWrapper is an adapter that converts a common [task.Scheduler] to a [Scheduler] with an injectable // TaskChannelKeyFn. type CommonSchedulerWrapper struct { @@ -227,7 +266,7 @@ func NewRateLimitedScheduler( } taskMetricsTagsFn := func(e Executable) []metrics.Tag { return append( - estimateTaskMetricTags(e.GetTask(), namespaceRegistry, currentClusterName, chasmRegistry, GetTaskTypeTagValue), + taskBaseMetricTags(e.GetTask(), namespaceRegistry, currentClusterName, chasmRegistry, GetTaskTypeTagValue), metrics.TaskPriorityTag(e.GetPriority().String()), ) } @@ -263,3 +302,22 @@ func (s *rateLimitedSchedulerImpl) Stop() { func (s *rateLimitedSchedulerImpl) TaskChannelKeyFn() TaskChannelKeyFn { return s.baseScheduler.TaskChannelKeyFn() } + +// HandleBusyWorkflow implements BusyWorkflowHandler by delegating to the +// underlying baseScheduler. This is called when a task encounters a busy +// workflow error and needs to be routed to the sequential scheduler. +func (s *rateLimitedSchedulerImpl) HandleBusyWorkflow(executable Executable) bool { + if handler, ok := s.baseScheduler.(BusyWorkflowHandler); ok { + return handler.HandleBusyWorkflow(executable) + } + return false +} + +// executableQueueKeyFn extracts the workflow key from an Executable for queue routing. +func executableQueueKeyFn(e Executable) any { + return definition.NewWorkflowKey( + e.GetNamespaceID(), + e.GetWorkflowID(), + e.GetRunID(), + ) +} diff --git a/service/history/queues/scheduler_mock.go b/service/history/queues/scheduler_mock.go index 22540368242..5aac29e1c3b 100644 --- a/service/history/queues/scheduler_mock.go +++ b/service/history/queues/scheduler_mock.go @@ -15,6 +15,44 @@ import ( gomock "go.uber.org/mock/gomock" ) +// MockBusyWorkflowHandler is a mock of BusyWorkflowHandler interface. +type MockBusyWorkflowHandler struct { + ctrl *gomock.Controller + recorder *MockBusyWorkflowHandlerMockRecorder + isgomock struct{} +} + +// MockBusyWorkflowHandlerMockRecorder is the mock recorder for MockBusyWorkflowHandler. +type MockBusyWorkflowHandlerMockRecorder struct { + mock *MockBusyWorkflowHandler +} + +// NewMockBusyWorkflowHandler creates a new mock instance. +func NewMockBusyWorkflowHandler(ctrl *gomock.Controller) *MockBusyWorkflowHandler { + mock := &MockBusyWorkflowHandler{ctrl: ctrl} + mock.recorder = &MockBusyWorkflowHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBusyWorkflowHandler) EXPECT() *MockBusyWorkflowHandlerMockRecorder { + return m.recorder +} + +// HandleBusyWorkflow mocks base method. +func (m *MockBusyWorkflowHandler) HandleBusyWorkflow(arg0 Executable) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleBusyWorkflow", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HandleBusyWorkflow indicates an expected call of HandleBusyWorkflow. +func (mr *MockBusyWorkflowHandlerMockRecorder) HandleBusyWorkflow(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleBusyWorkflow", reflect.TypeOf((*MockBusyWorkflowHandler)(nil).HandleBusyWorkflow), arg0) +} + // MockScheduler is a mock of Scheduler interface. type MockScheduler struct { ctrl *gomock.Controller diff --git a/service/history/queues/speculative_workflow_task_timeout_queue.go b/service/history/queues/speculative_workflow_task_timeout_queue.go index 749612f8f12..fe5c92c8944 100644 --- a/service/history/queues/speculative_workflow_task_timeout_queue.go +++ b/service/history/queues/speculative_workflow_task_timeout_queue.go @@ -90,7 +90,7 @@ func (q SpeculativeWorkflowTaskTimeoutQueue) NotifyNewTasks(ts []tasks.Task) { q.chasmRegistry, GetTaskTypeTagValue, q.logger, - q.metricsHandler.WithTags(defaultExecutableMetricsTags...), + q.metricsHandler, q.tracer, ), wttt) q.timeoutQueue.Add(executable) diff --git a/service/history/replication/ack_manager.go b/service/history/replication/ack_manager.go index b0ff97187eb..b3b576ce293 100644 --- a/service/history/replication/ack_manager.go +++ b/service/history/replication/ack_manager.go @@ -444,6 +444,8 @@ func (p *ackMgrImpl) ConvertTask( task, p.workflowCache, ) + case *tasks.DeleteExecutionReplicationTask: + return convertDeleteExecutionReplicationTask(task) default: return nil, errUnknownReplicationTask } diff --git a/service/history/replication/batchable_task.go b/service/history/replication/batchable_task.go index 6fde6074341..175a97aefe3 100644 --- a/service/history/replication/batchable_task.go +++ b/service/history/replication/batchable_task.go @@ -45,7 +45,7 @@ const ( var _ TrackableExecutableTask = (*batchedTask)(nil) -func (w *batchedTask) QueueID() interface{} { +func (w *batchedTask) QueueID() any { return w.batchedTask.QueueID() } diff --git a/service/history/replication/eventhandler/event_importer_test.go b/service/history/replication/eventhandler/event_importer_test.go index 2007f23c735..e806f7e770a 100644 --- a/service/history/replication/eventhandler/event_importer_test.go +++ b/service/history/replication/eventhandler/event_importer_test.go @@ -53,7 +53,7 @@ type ImportWorkflowExecutionRequestMatcher struct { ExpectedRequest *historyservice.ImportWorkflowExecutionRequest } -func (m *ImportWorkflowExecutionRequestMatcher) Matches(x interface{}) bool { +func (m *ImportWorkflowExecutionRequestMatcher) Matches(x any) bool { return m.ExpectedRequest.Equal(x) } diff --git a/service/history/replication/eventhandler/resend_handler_test.go b/service/history/replication/eventhandler/resend_handler_test.go index 9ab24aedd34..5374ed19639 100644 --- a/service/history/replication/eventhandler/resend_handler_test.go +++ b/service/history/replication/eventhandler/resend_handler_test.go @@ -126,7 +126,7 @@ type historyEventMatrixMatcher struct { expected [][]*historypb.HistoryEvent } -func (m *historyEventMatrixMatcher) Matches(x interface{}) bool { +func (m *historyEventMatrixMatcher) Matches(x any) bool { actual, ok := x.([][]*historypb.HistoryEvent) if !ok { return false diff --git a/service/history/replication/executable_activity_state_task.go b/service/history/replication/executable_activity_state_task.go index a8b583cac9f..8c1e1905683 100644 --- a/service/history/replication/executable_activity_state_task.go +++ b/service/history/replication/executable_activity_state_task.go @@ -118,7 +118,7 @@ func NewExecutableActivityStateTask( } } -func (e *ExecutableActivityStateTask) QueueID() interface{} { +func (e *ExecutableActivityStateTask) QueueID() any { return e.WorkflowKey } diff --git a/service/history/replication/executable_backfill_history_events_task.go b/service/history/replication/executable_backfill_history_events_task.go index 5f2f5b33eae..0cae12f261f 100644 --- a/service/history/replication/executable_backfill_history_events_task.go +++ b/service/history/replication/executable_backfill_history_events_task.go @@ -66,7 +66,7 @@ func NewExecutableBackfillHistoryEventsTask( } } -func (e *ExecutableBackfillHistoryEventsTask) QueueID() interface{} { +func (e *ExecutableBackfillHistoryEventsTask) QueueID() any { return e.WorkflowKey } diff --git a/service/history/replication/executable_delete_execution_task.go b/service/history/replication/executable_delete_execution_task.go new file mode 100644 index 00000000000..e73da5264b1 --- /dev/null +++ b/service/history/replication/executable_delete_execution_task.go @@ -0,0 +1,180 @@ +package replication + +import ( + "context" + "fmt" + "time" + + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + enumsspb "go.temporal.io/server/api/enums/v1" + "go.temporal.io/server/api/historyservice/v1" + persistencespb "go.temporal.io/server/api/persistence/v1" + replicationspb "go.temporal.io/server/api/replication/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/common/definition" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/softassert" + ctasks "go.temporal.io/server/common/tasks" +) + +type ExecutableDeleteExecutionTask struct { + ProcessToolBox + + chasm.ComponentRef + ExecutableTask +} + +var _ ctasks.Task = (*ExecutableDeleteExecutionTask)(nil) +var _ TrackableExecutableTask = (*ExecutableDeleteExecutionTask)(nil) + +func NewExecutableDeleteExecutionTask( + processToolBox ProcessToolBox, + taskID int64, + taskCreationTime time.Time, + sourceClusterName string, + sourceShardKey ClusterShardKey, + replicationTask *replicationspb.ReplicationTask, +) *ExecutableDeleteExecutionTask { + rawInfo := replicationTask.GetRawTaskInfo() + + // ArchetypeID should never be unspecified. Default to WorkflowArchetypeID. + archetypeID := chasm.WorkflowArchetypeID + if rawInfo != nil && rawInfo.ArchetypeId != chasm.UnspecifiedArchetypeID { + archetypeID = rawInfo.ArchetypeId + } else { + softassert.That(processToolBox.Logger, false, "delete execution replication task has unspecified archetype ID") + } + + return &ExecutableDeleteExecutionTask{ + ProcessToolBox: processToolBox, + ComponentRef: chasm.NewComponentRefByArchetypeID( + chasm.ExecutionKey{ + NamespaceID: rawInfo.GetNamespaceId(), + BusinessID: rawInfo.GetWorkflowId(), + RunID: rawInfo.GetRunId(), + }, + archetypeID, + ), + ExecutableTask: NewExecutableTask( + processToolBox, + taskID, + metrics.DeleteExecutionReplicationTaskScope, + taskCreationTime, + time.Now().UTC(), + sourceClusterName, + sourceShardKey, + replicationTask, + ), + } +} + +func (e *ExecutableDeleteExecutionTask) QueueID() any { + return definition.NewWorkflowKey(e.NamespaceID, e.BusinessID, e.RunID) +} + +func (e *ExecutableDeleteExecutionTask) Execute() error { + if e.TerminalState() { + return nil + } + e.MarkExecutionStart() + + callerInfo := getReplicaitonCallerInfo(e.GetPriority()) + namespaceName, apply, err := e.GetNamespaceInfo(headers.SetCallerInfo( + context.Background(), + callerInfo, + ), e.NamespaceID, e.BusinessID) + if err != nil { + return err + } else if !apply { + e.Logger.Warn("Skipping the replication task", + tag.WorkflowNamespaceID(e.NamespaceID), + tag.WorkflowID(e.BusinessID), + tag.WorkflowRunID(e.RunID), + tag.TaskID(e.TaskID()), + ) + metrics.ReplicationTasksSkipped.With(e.MetricsHandler).Record( + 1, + metrics.OperationTag(metrics.DeleteExecutionReplicationTaskScope), + metrics.NamespaceTag(namespaceName), + ) + return nil + } + + ctx, cancel := newTaskContext(namespaceName, e.Config.ReplicationTaskApplyTimeout(), callerInfo) + defer cancel() + + archetypeID, _ := e.ArchetypeID(nil) + switch archetypeID { + case chasm.WorkflowArchetypeID: + return e.deleteWorkflowExecution(ctx) + default: + return e.deleteChasmExecution(ctx) + } +} + +func (e *ExecutableDeleteExecutionTask) deleteWorkflowExecution(ctx context.Context) error { + shardContext, err := e.ShardController.GetShardByNamespaceWorkflow( + namespace.ID(e.NamespaceID), + e.BusinessID, + ) + if err != nil { + return err + } + engine, err := shardContext.GetEngine(ctx) + if err != nil { + return err + } + + _, err = engine.DeleteWorkflowExecution(ctx, &historyservice.DeleteWorkflowExecutionRequest{ + NamespaceId: e.NamespaceID, + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: e.BusinessID, + RunId: e.RunID, + }, + }) + return err +} + +func (e *ExecutableDeleteExecutionTask) deleteChasmExecution(ctx context.Context) error { + return e.ChasmEngine.DeleteExecution(ctx, e.ComponentRef, chasm.DeleteExecutionRequest{}) +} + +func (e *ExecutableDeleteExecutionTask) HandleErr(err error) error { + metrics.ReplicationTasksErrorByType.With(e.MetricsHandler).Record( + 1, + metrics.OperationTag(metrics.DeleteExecutionReplicationTaskScope), + metrics.NamespaceTag(e.NamespaceName()), + metrics.ServiceErrorTypeTag(err), + ) + switch err.(type) { + case nil, *serviceerror.NotFound: + return nil + default: + e.Logger.Error("delete execution replication task encountered error", + tag.WorkflowNamespaceID(e.NamespaceID), + tag.WorkflowID(e.BusinessID), + tag.WorkflowRunID(e.RunID), + tag.TaskID(e.TaskID()), + tag.Error(err), + ) + return fmt.Errorf("delete execution replication task error: %w", err) + } +} + +func (e *ExecutableDeleteExecutionTask) MarkPoisonPill() error { + if e.ReplicationTask().GetRawTaskInfo() == nil { + e.ReplicationTask().RawTaskInfo = &persistencespb.ReplicationTaskInfo{ + NamespaceId: e.NamespaceID, + WorkflowId: e.BusinessID, + RunId: e.RunID, + TaskId: e.TaskID(), + TaskType: enumsspb.TASK_TYPE_REPLICATION_DELETE_EXECUTION, + } + } + + return e.ExecutableTask.MarkPoisonPill() +} diff --git a/service/history/replication/executable_history_task.go b/service/history/replication/executable_history_task.go index 34a75b18152..aecf7e6c33a 100644 --- a/service/history/replication/executable_history_task.go +++ b/service/history/replication/executable_history_task.go @@ -90,7 +90,7 @@ func NewExecutableHistoryTask( } } -func (e *ExecutableHistoryTask) QueueID() interface{} { +func (e *ExecutableHistoryTask) QueueID() any { return e.WorkflowKey } diff --git a/service/history/replication/executable_noop_task.go b/service/history/replication/executable_noop_task.go index 72d9af555c3..32209da81ab 100644 --- a/service/history/replication/executable_noop_task.go +++ b/service/history/replication/executable_noop_task.go @@ -43,7 +43,7 @@ func NewExecutableNoopTask( } } -func (e *ExecutableNoopTask) QueueID() interface{} { +func (e *ExecutableNoopTask) QueueID() any { return noopTaskID } diff --git a/service/history/replication/executable_sync_hsm_task.go b/service/history/replication/executable_sync_hsm_task.go index b5a356bda68..1345365df00 100644 --- a/service/history/replication/executable_sync_hsm_task.go +++ b/service/history/replication/executable_sync_hsm_task.go @@ -68,7 +68,7 @@ func NewExecutableSyncHSMTask( } } -func (e *ExecutableSyncHSMTask) QueueID() interface{} { +func (e *ExecutableSyncHSMTask) QueueID() any { return e.WorkflowKey } diff --git a/service/history/replication/executable_sync_versioned_transition_task.go b/service/history/replication/executable_sync_versioned_transition_task.go index 071564706e1..ff2b9688b98 100644 --- a/service/history/replication/executable_sync_versioned_transition_task.go +++ b/service/history/replication/executable_sync_versioned_transition_task.go @@ -64,7 +64,7 @@ func NewExecutableSyncVersionedTransitionTask( } } -func (e *ExecutableSyncVersionedTransitionTask) QueueID() interface{} { +func (e *ExecutableSyncVersionedTransitionTask) QueueID() any { return e.WorkflowKey } diff --git a/service/history/replication/executable_task.go b/service/history/replication/executable_task.go index c962cb54f5e..252bee79bde 100644 --- a/service/history/replication/executable_task.go +++ b/service/history/replication/executable_task.go @@ -215,7 +215,7 @@ func (e *ExecutableTaskImpl) Abort() { e.Abort() // retry abort } - e.Logger.Debug(fmt.Sprintf( + e.ThrottledLogger.Debug(fmt.Sprintf( "replication task: %v encountered abort event", e.taskID, )) @@ -230,7 +230,7 @@ func (e *ExecutableTaskImpl) Cancel() { e.Cancel() // retry cancel } - e.Logger.Debug(fmt.Sprintf( + e.ThrottledLogger.Debug(fmt.Sprintf( "replication task: %v encountered cancellation event", e.taskID, )) @@ -243,7 +243,7 @@ func (e *ExecutableTaskImpl) Reschedule() { return } - e.Logger.Info(fmt.Sprintf( + e.ThrottledLogger.Info(fmt.Sprintf( "replication task: %v scheduled for retry", e.taskID, )) @@ -338,7 +338,7 @@ func (e *ExecutableTaskImpl) emitFinishMetrics( nsTag, ) if processingLatency > 10*time.Second && e.replicationTask != nil && e.replicationTask.RawTaskInfo != nil { - e.Logger.Warn(fmt.Sprintf( + e.ThrottledLogger.Warn(fmt.Sprintf( "replication task latency is too long: queue=%.2fs processing=%.2fs", queueLatency.Seconds(), processingLatency.Seconds(), @@ -812,7 +812,7 @@ func (e *ExecutableTaskImpl) GetNamespaceInfo( case *serviceerror.NamespaceNotFound: _, err = e.ProcessToolBox.EagerNamespaceRefresher.SyncNamespaceFromSourceCluster(ctx, namespace.ID(namespaceID), e.sourceClusterName) if err != nil { - e.Logger.Info("Failed to SyncNamespaceFromSourceCluster", tag.Error(err)) + e.ThrottledLogger.Error("Failed to SyncNamespaceFromSourceCluster", tag.Error(err)) return "", false, nil } default: diff --git a/service/history/replication/executable_task_converter.go b/service/history/replication/executable_task_converter.go index 2a54a1484ed..3a2de1c6916 100644 --- a/service/history/replication/executable_task_converter.go +++ b/service/history/replication/executable_task_converter.go @@ -147,6 +147,15 @@ func (e *executableTaskConverterImpl) convertOne( sourceShardKey, replicationTask, ) + case enumsspb.REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK: + return NewExecutableDeleteExecutionTask( + e.processToolBox, + replicationTask.SourceTaskId, + taskCreationTime, + sourceClusterName, + sourceShardKey, + replicationTask, + ) default: e.processToolBox.Logger.Error(fmt.Sprintf("unknown replication task: %v", replicationTask)) return NewExecutableUnknownTask( diff --git a/service/history/replication/executable_task_test.go b/service/history/replication/executable_task_test.go index 778dc440a9b..02fb284118a 100644 --- a/service/history/replication/executable_task_test.go +++ b/service/history/replication/executable_task_test.go @@ -126,6 +126,7 @@ func (s *executableTaskSuite) SetupTest() { NamespaceCache: s.namespaceCache, MetricsHandler: s.metricsHandler, Logger: s.logger, + ThrottledLogger: s.logger, EagerNamespaceRefresher: s.eagerNamespaceRefresher, DLQWriter: NewExecutionManagerDLQWriter(s.mockExecutionManager), Serializer: s.serializer, diff --git a/service/history/replication/executable_task_tool_box.go b/service/history/replication/executable_task_tool_box.go index 17625b18c3a..7cebad73ccf 100644 --- a/service/history/replication/executable_task_tool_box.go +++ b/service/history/replication/executable_task_tool_box.go @@ -1,6 +1,7 @@ package replication import ( + "go.temporal.io/server/chasm" "go.temporal.io/server/client" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/log" @@ -23,6 +24,7 @@ type ( ClusterMetadata cluster.Metadata ClientBean client.Bean ShardController shard.Controller + ChasmEngine chasm.Engine NamespaceCache namespace.Registry EagerNamespaceRefresher EagerNamespaceRefresher ResendHandler eventhandler.ResendHandler @@ -31,6 +33,7 @@ type ( LowPriorityTaskScheduler ctasks.Scheduler[TrackableExecutableTask] `name:"LowPriorityTaskScheduler"` MetricsHandler metrics.Handler Logger log.Logger + ThrottledLogger log.ThrottledLogger Serializer serialization.Serializer DLQWriter DLQWriter HistoryEventsHandler eventhandler.HistoryEventsHandler diff --git a/service/history/replication/executable_task_tracker.go b/service/history/replication/executable_task_tracker.go index 93aa214f483..459158de793 100644 --- a/service/history/replication/executable_task_tracker.go +++ b/service/history/replication/executable_task_tracker.go @@ -20,7 +20,7 @@ const MarkPoisonPillMaxAttempts = 3 type ( TrackableExecutableTask interface { ctasks.Task - QueueID() interface{} + QueueID() any TaskID() int64 TaskCreationTime() time.Time MarkPoisonPill() error diff --git a/service/history/replication/executable_unknown_task.go b/service/history/replication/executable_unknown_task.go index 93a3e646ed6..92058b69673 100644 --- a/service/history/replication/executable_unknown_task.go +++ b/service/history/replication/executable_unknown_task.go @@ -52,7 +52,7 @@ func NewExecutableUnknownTask( } } -func (e *ExecutableUnknownTask) QueueID() interface{} { +func (e *ExecutableUnknownTask) QueueID() any { return unknownTaskID } diff --git a/service/history/replication/executable_verify_versioned_transition_task.go b/service/history/replication/executable_verify_versioned_transition_task.go index 5e8085f5757..15628c7d1a3 100644 --- a/service/history/replication/executable_verify_versioned_transition_task.go +++ b/service/history/replication/executable_verify_versioned_transition_task.go @@ -71,7 +71,7 @@ func NewExecutableVerifyVersionedTransitionTask( } } -func (e *ExecutableVerifyVersionedTransitionTask) QueueID() interface{} { +func (e *ExecutableVerifyVersionedTransitionTask) QueueID() any { return e.WorkflowKey } diff --git a/service/history/replication/executable_workflow_state_task.go b/service/history/replication/executable_workflow_state_task.go index 017f5111aaa..f481028a317 100644 --- a/service/history/replication/executable_workflow_state_task.go +++ b/service/history/replication/executable_workflow_state_task.go @@ -71,7 +71,7 @@ func NewExecutableWorkflowStateTask( } } -func (e *ExecutableWorkflowStateTask) QueueID() interface{} { +func (e *ExecutableWorkflowStateTask) QueueID() any { return e.WorkflowKey } diff --git a/service/history/replication/fx.go b/service/history/replication/fx.go index 8bceb6d142d..28459595c8f 100644 --- a/service/history/replication/fx.go +++ b/service/history/replication/fx.go @@ -44,6 +44,7 @@ var Module = fx.Provide( func(m persistence.ExecutionManager) ExecutionManager { return m }, + nsreplication.NewNoopDataMerger, NewExecutionManagerDLQWriter, ClientSchedulerRateLimiterProvider, ServerSchedulerRateLimiterProvider, @@ -79,6 +80,7 @@ func eagerNamespaceRefresherProvider( logger log.Logger, clientBean client.Bean, clusterMetadata cluster.Metadata, + dataMerger nsreplication.NamespaceDataMerger, metricsHandler metrics.Handler, ) EagerNamespaceRefresher { return NewEagerNamespaceRefresher( @@ -89,6 +91,7 @@ func eagerNamespaceRefresherProvider( nsreplication.NewTaskExecutor( clusterMetadata.GetCurrentClusterName(), metadataManager, + dataMerger, logger, ), clusterMetadata.GetCurrentClusterName(), @@ -183,7 +186,7 @@ func replicationStreamLowPrioritySchedulerProvider( } return NewSequentialTaskQueueWithID(workflowKey.NamespaceID + "_" + workflowKey.WorkflowID) } - taskQueueHashFunc := func(item interface{}) uint32 { + taskQueueHashFunc := func(item any) uint32 { workflowKey, ok := item.(definition.WorkflowKey) if !ok { return 0 diff --git a/service/history/replication/metrics.go b/service/history/replication/metrics.go index 86553791e89..2525283f345 100644 --- a/service/history/replication/metrics.go +++ b/service/history/replication/metrics.go @@ -31,6 +31,8 @@ func TaskOperationTag( return metrics.BackfillHistoryEventsTaskScope case enumsspb.REPLICATION_TASK_TYPE_VERIFY_VERSIONED_TRANSITION_TASK: return metrics.VerifyVersionedTransitionTaskScope + case enumsspb.REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK: + return metrics.DeleteExecutionReplicationTaskScope default: return metrics.NoopTaskScope } @@ -50,6 +52,8 @@ func TaskOperationTagFromTask( return metrics.SyncWorkflowStateTaskScope case enumsspb.TASK_TYPE_REPLICATION_HISTORY: return metrics.HistoryReplicationTaskScope + case enumsspb.TASK_TYPE_REPLICATION_DELETE_EXECUTION: + return metrics.DeleteExecutionReplicationTaskScope default: return metrics.UnknownTaskScope } diff --git a/service/history/replication/raw_task_converter.go b/service/history/replication/raw_task_converter.go index e69f6167784..dc301012607 100644 --- a/service/history/replication/raw_task_converter.go +++ b/service/history/replication/raw_task_converter.go @@ -321,6 +321,16 @@ func convertSyncVersionedTransitionTask( ) } +func convertDeleteExecutionReplicationTask( + taskInfo *tasks.DeleteExecutionReplicationTask, +) (*replicationspb.ReplicationTask, error) { + return &replicationspb.ReplicationTask{ + TaskType: enumsspb.REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK, + SourceTaskId: taskInfo.TaskID, + VisibilityTime: timestamppb.New(taskInfo.VisibilityTimestamp), + }, nil +} + func convertHistoryReplicationTask( ctx context.Context, shardContext historyi.ShardContext, diff --git a/service/history/replication/raw_task_converter_test.go b/service/history/replication/raw_task_converter_test.go index 074225b47a6..20da37a8636 100644 --- a/service/history/replication/raw_task_converter_test.go +++ b/service/history/replication/raw_task_converter_test.go @@ -1962,3 +1962,23 @@ func (s *rawTaskConverterSuite) TestIsCloseTransferTaskAcked_TaskNotAcked_Contai result := converter.isCloseTransferTaskAcked(closeTransferTask) s.False(result) } + +func (s *rawTaskConverterSuite) TestConvertDeleteExecutionReplicationTask() { + taskID := int64(1444) + task := &tasks.DeleteExecutionReplicationTask{ + WorkflowKey: definition.NewWorkflowKey( + s.namespaceID, + s.workflowID, + s.runID, + ), + VisibilityTimestamp: time.Now().UTC(), + TaskID: taskID, + ArchetypeID: chasm.WorkflowArchetypeID, + } + + result, err := convertDeleteExecutionReplicationTask(task) + s.NoError(err) + s.NotNil(result) + s.Equal(enumsspb.REPLICATION_TASK_TYPE_DELETE_EXECUTION_TASK, result.TaskType) + s.Equal(taskID, result.SourceTaskId) +} diff --git a/service/history/replication/sequential_batch_queue.go b/service/history/replication/sequential_batch_queue.go index b649b7310ff..8d8fa583d0b 100644 --- a/service/history/replication/sequential_batch_queue.go +++ b/service/history/replication/sequential_batch_queue.go @@ -11,7 +11,7 @@ import ( type ( SequentialBatchableTaskQueue struct { - id interface{} + id any sync.Mutex taskQueue collection.Queue[*batchedTask] @@ -41,7 +41,7 @@ func NewSequentialBatchableTaskQueue( } } -func (q *SequentialBatchableTaskQueue) ID() interface{} { +func (q *SequentialBatchableTaskQueue) ID() any { return q.id } diff --git a/service/history/replication/sequential_queue.go b/service/history/replication/sequential_queue.go index 3a153d9c4ac..984c7bb3db2 100644 --- a/service/history/replication/sequential_queue.go +++ b/service/history/replication/sequential_queue.go @@ -11,7 +11,7 @@ import ( type ( SequentialTaskQueue struct { - id interface{} + id any sync.Mutex taskQueue collection.Queue[TrackableExecutableTask] @@ -28,7 +28,7 @@ func NewSequentialTaskQueue(task TrackableExecutableTask) ctasks.SequentialTaskQ } } -func NewSequentialTaskQueueWithID(id interface{}) ctasks.SequentialTaskQueue[TrackableExecutableTask] { +func NewSequentialTaskQueueWithID(id any) ctasks.SequentialTaskQueue[TrackableExecutableTask] { return &SequentialTaskQueue{ id: id, @@ -38,7 +38,7 @@ func NewSequentialTaskQueueWithID(id interface{}) ctasks.SequentialTaskQueue[Tra } } -func (q *SequentialTaskQueue) ID() interface{} { +func (q *SequentialTaskQueue) ID() any { return q.id } @@ -77,7 +77,7 @@ func SequentialTaskQueueCompareLess(this TrackableExecutableTask, that Trackable } func WorkflowKeyHashFn( - item interface{}, + item any, ) uint32 { workflowKey, ok := item.(definition.WorkflowKey) if !ok { diff --git a/service/history/replication/stream_sender.go b/service/history/replication/stream_sender.go index ed67d3e9bf9..928034359f5 100644 --- a/service/history/replication/stream_sender.go +++ b/service/history/replication/stream_sender.go @@ -559,7 +559,7 @@ Loop: }() task, err := s.taskConverter.Convert(item, s.clientShardKey.ClusterID, priority) if err != nil { - return err + return s.recordRetry(item, attempt, fmt.Errorf("convert: %w", err)) } if task == nil { return nil @@ -590,7 +590,7 @@ Loop: 0, "", )); err != nil { - return err + return s.recordRetry(item, attempt, fmt.Errorf("rate_limit: %w", err)) } metrics.ReplicationRateLimitLatency.With(s.metrics).Record(time.Since(rlStartTime), metrics.OperationTag(TaskOperationTag(task))) } @@ -604,7 +604,7 @@ Loop: }, }, }); err != nil { - return err + return s.recordRetry(item, attempt, fmt.Errorf("send: %w", err)) } skipCount = 0 metrics.ReplicationTasksSend.With(s.metrics).Record( @@ -731,3 +731,18 @@ func (s *StreamSenderImpl) getTaskTargetCluster(task tasks.Task) []string { return nil } } + +func (s *StreamSenderImpl) recordRetry( + item tasks.Task, + attempt int64, + err error, +) error { + s.shardContext.GetThrottledLogger().Warn("Replication task send retry", + tag.TaskID(item.GetTaskID()), + tag.WorkflowNamespaceID(item.GetNamespaceID()), + tag.WorkflowID(item.GetWorkflowID()), + tag.Counter(int(attempt)), + tag.Error(err), + ) + return err +} diff --git a/service/history/replication/task_fetcher.go b/service/history/replication/task_fetcher.go index 95cb6766692..10a6f5f4087 100644 --- a/service/history/replication/task_fetcher.go +++ b/service/history/replication/task_fetcher.go @@ -201,7 +201,7 @@ func newReplicationTaskFetcher( ) workers := make(map[int]*replicationTaskFetcherWorker) - for i := 0; i < numWorker; i++ { + for i := range numWorker { workers[i] = newReplicationTaskFetcherWorker( logger, sourceCluster, diff --git a/service/history/replication/task_fetcher_test.go b/service/history/replication/task_fetcher_test.go index 338ac1f4687..93d8ff43fec 100644 --- a/service/history/replication/task_fetcher_test.go +++ b/service/history/replication/task_fetcher_test.go @@ -294,7 +294,7 @@ func (s *taskFetcherSuite) TestConcurrentFetchAndProcess_Success() { waitGroup := sync.WaitGroup{} waitGroup.Add(numShards) - for i := 0; i < numShards; i++ { + for i := range numShards { shardID := int32(i) go func() { defer waitGroup.Done() @@ -338,7 +338,7 @@ func (s *taskFetcherSuite) TestConcurrentFetchAndProcess_Error() { waitGroup := sync.WaitGroup{} waitGroup.Add(numShards) - for i := 0; i < numShards; i++ { + for i := range numShards { shardID := int32(i) go func() { defer waitGroup.Done() @@ -372,7 +372,7 @@ func newGetReplicationMessagesRequestMatcher( } } -func (m *getReplicationMessagesRequestMatcher) Matches(x interface{}) bool { +func (m *getReplicationMessagesRequestMatcher) Matches(x any) bool { req, ok := x.(*adminservice.GetReplicationMessagesRequest) if !ok { return false diff --git a/service/history/replication/task_processor.go b/service/history/replication/task_processor.go index 3a3ea03d2f2..94015b05650 100644 --- a/service/history/replication/task_processor.go +++ b/service/history/replication/task_processor.go @@ -471,7 +471,7 @@ func (p *taskProcessorImpl) convertTaskToDLQTask( } } -func (p *taskProcessorImpl) paginationFn(_ []byte) ([]interface{}, []byte, error) { +func (p *taskProcessorImpl) paginationFn(_ []byte) ([]any, []byte, error) { respChan := make(chan *replicationspb.ReplicationMessages, 1) var lastProcessedVisTime *timestamppb.Timestamp if !p.maxRxProcessedTimestamp.IsZero() { @@ -501,7 +501,7 @@ func (p *taskProcessorImpl) paginationFn(_ []byte) ([]interface{}, []byte, error // since sync shard status are periodically updated } - var tasks []interface{} + var tasks []any for _, task := range resp.GetReplicationTasks() { tasks = append(tasks, task) } diff --git a/service/history/shard/context_impl.go b/service/history/shard/context_impl.go index c2ab570b1f1..3d1b33b638d 100644 --- a/service/history/shard/context_impl.go +++ b/service/history/shard/context_impl.go @@ -163,7 +163,7 @@ type ( } // These are the requests that can be passed to transition to change state: - contextRequest interface{} + contextRequest any contextRequestAcquire struct{} contextRequestAcquired struct{ engine historyi.Engine } @@ -329,7 +329,7 @@ func (s *ContextImpl) GenerateTaskIDs(number int) ([]int64, error) { defer s.wUnlock() result := []int64{} - for i := 0; i < number; i++ { + for range number { id, err := s.generateTaskIDLocked() if err != nil { return nil, err @@ -476,7 +476,7 @@ func (s *ContextImpl) UpdateHandoverNamespace(ns *namespace.Namespace, deletedFr // it here to be more safe in case above assumption no longer holds in the future. isHandoverNamespace := ns.IsGlobalNamespace() && ns.ActiveInCluster(s.GetClusterMetadata().GetCurrentClusterName()) && - ns.ReplicationState() == enumspb.REPLICATION_STATE_HANDOVER + ns.ReplicationState("") == enumspb.REPLICATION_STATE_HANDOVER s.wLock() if deletedFromDb || !isHandoverNamespace { @@ -695,7 +695,8 @@ func (s *ContextImpl) updateCloseTaskIDs(executionInfo *persistencespb.WorkflowE } } for _, t := range tasksByCategory[tasks.CategoryVisibility] { - if t.GetType() == enumsspb.TASK_TYPE_VISIBILITY_CLOSE_EXECUTION { + if t.GetType() == enumsspb.TASK_TYPE_VISIBILITY_CLOSE_EXECUTION || + t.GetType() == enumsspb.TASK_TYPE_CHASM { executionInfo.CloseVisibilityTaskId = t.GetTaskID() break } @@ -947,7 +948,7 @@ func (s *ContextImpl) DeleteWorkflowExecution( stage *tasks.DeleteWorkflowExecutionStage, ) (retErr error) { // DeleteWorkflowExecution is a 4 stages process (order is very important and should not be changed): - // 1. Add visibility delete task, i.e. schedule visibility record delete, + // 1. Add visibility delete task, i.e. schedule visibility record delete, and execution replication delete task, // 2. Delete current workflow execution pointer, // 3. Delete workflow mutable state, // 4. Delete history branch. @@ -999,6 +1000,7 @@ func (s *ContextImpl) DeleteWorkflowExecution( // Don't acquire shard lock or io semaphore if all stages that require lock are already processed. if !stage.IsProcessed( tasks.DeleteWorkflowExecutionStageVisibility | + tasks.DeleteWorkflowExecutionStageReplication | tasks.DeleteWorkflowExecutionStageCurrent | tasks.DeleteWorkflowExecutionStageMutableState) { @@ -1010,11 +1012,11 @@ func (s *ContextImpl) DeleteWorkflowExecution( } defer s.ioSemaphoreRelease() - // Stage 1. Delete visibility. - if deleteVisibilityRecord && !stage.IsProcessed(tasks.DeleteWorkflowExecutionStageVisibility) { - // TODO: move to existing task generator logic - newTasks := map[tasks.Category][]tasks.Task{ - tasks.CategoryVisibility: { + // Stage 1. Add visibility delete task and delete execution replication task. + if !stage.IsProcessed(tasks.DeleteWorkflowExecutionStageVisibility) { + newTasks := make(map[tasks.Category][]tasks.Task) + if deleteVisibilityRecord { + newTasks[tasks.CategoryVisibility] = []tasks.Task{ &tasks.DeleteExecutionVisibilityTask{ // TaskID is set by addTasks WorkflowKey: key, @@ -1022,25 +1024,43 @@ func (s *ContextImpl) DeleteWorkflowExecution( CloseExecutionVisibilityTaskID: closeVisibilityTaskId, CloseTime: workflowCloseTime, }, - }, - } - addTasksRequest := &persistence.AddHistoryTasksRequest{ - ShardID: s.shardID, - NamespaceID: key.NamespaceID, - WorkflowID: key.WorkflowID, - ArchetypeID: archetypeID, - - Tasks: newTasks, + } } - err := s.addTasksSemaphoreAcquired(ctx, addTasksRequest) - if persistence.OperationPossiblySucceeded(err) { - engine.NotifyNewTasks(newTasks) + // Piggyback delete execution replication task on the same write to save a DB operation. + if s.config.EnableDeleteWorkflowExecutionReplication() && + !stage.IsProcessed(tasks.DeleteWorkflowExecutionStageReplication) { + if nsEntry, err := s.GetNamespaceRegistry().GetNamespaceByID( + namespace.ID(key.NamespaceID), + ); err == nil && + nsEntry.ActiveInCluster(s.GetClusterMetadata().GetCurrentClusterName()) && + nsEntry.ReplicationPolicy() == namespace.ReplicationPolicyMultiCluster { + newTasks[tasks.CategoryReplication] = []tasks.Task{ + &tasks.DeleteExecutionReplicationTask{ + WorkflowKey: key, + ArchetypeID: archetypeID, + }, + } + } } - if err != nil { - return err + if len(newTasks) > 0 { + addTasksRequest := &persistence.AddHistoryTasksRequest{ + ShardID: s.shardID, + NamespaceID: key.NamespaceID, + WorkflowID: key.WorkflowID, + ArchetypeID: archetypeID, + Tasks: newTasks, + } + err := s.addTasksSemaphoreAcquired(ctx, addTasksRequest) + if persistence.OperationPossiblySucceeded(err) { + engine.NotifyNewTasks(newTasks) + } + if err != nil { + return err + } } } stage.MarkProcessed(tasks.DeleteWorkflowExecutionStageVisibility) + stage.MarkProcessed(tasks.DeleteWorkflowExecutionStageReplication) // Stage 2. Delete current workflow execution pointer. if !stage.IsProcessed(tasks.DeleteWorkflowExecutionStageCurrent) { diff --git a/service/history/shard/context_test.go b/service/history/shard/context_test.go index 303014d4ad7..294d0d1f263 100644 --- a/service/history/shard/context_test.go +++ b/service/history/shard/context_test.go @@ -206,7 +206,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_Success() { ) s.NoError(err) - s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication, stage) } func (s *contextSuite) TestDeleteWorkflowExecution_Continue_Success() { @@ -231,7 +231,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_Continue_Success() { &stage, ) s.NoError(err) - s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication, stage) s.mockExecutionManager.EXPECT().DeleteWorkflowExecution(gomock.Any(), gomock.Any()).Return(nil) s.mockExecutionManager.EXPECT().DeleteHistoryBranch(gomock.Any(), gomock.Any()).Return(nil) @@ -246,7 +246,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_Continue_Success() { &stage, ) s.NoError(err) - s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication, stage) s.mockExecutionManager.EXPECT().DeleteHistoryBranch(gomock.Any(), gomock.Any()).Return(nil) stage = tasks.DeleteWorkflowExecutionStageVisibility | tasks.DeleteWorkflowExecutionStageCurrent | tasks.DeleteWorkflowExecutionStageMutableState @@ -260,7 +260,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_Continue_Success() { &stage, ) s.NoError(err) - s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication, stage) } func (s *contextSuite) TestDeleteWorkflowExecution_ErrorAndContinue_Success() { @@ -285,7 +285,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_ErrorAndContinue_Success() { &stage, ) s.Error(err) - s.Equal(tasks.DeleteWorkflowExecutionStageVisibility, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication, stage) s.mockExecutionManager.EXPECT().DeleteCurrentWorkflowExecution(gomock.Any(), gomock.Any()).Return(nil) s.mockExecutionManager.EXPECT().DeleteWorkflowExecution(gomock.Any(), gomock.Any()).Return(errors.New("some error")) @@ -299,7 +299,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_ErrorAndContinue_Success() { &stage, ) s.Error(err) - s.Equal(tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageCurrent, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication|tasks.DeleteWorkflowExecutionStageCurrent, stage) s.mockExecutionManager.EXPECT().DeleteWorkflowExecution(gomock.Any(), gomock.Any()).Return(nil) s.mockExecutionManager.EXPECT().DeleteHistoryBranch(gomock.Any(), gomock.Any()).Return(errors.New("some error")) @@ -313,7 +313,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_ErrorAndContinue_Success() { &stage, ) s.Error(err) - s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageVisibility, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication, stage) s.mockExecutionManager.EXPECT().DeleteHistoryBranch(gomock.Any(), gomock.Any()).Return(nil) err = s.mockShard.DeleteWorkflowExecution( @@ -326,7 +326,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_ErrorAndContinue_Success() { &stage, ) s.NoError(err) - s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageHistory, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageCurrent|tasks.DeleteWorkflowExecutionStageMutableState|tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageHistory|tasks.DeleteWorkflowExecutionStageReplication, stage) } func (s *contextSuite) TestDeleteWorkflowExecution_DeleteVisibilityTaskNotifiction() { @@ -366,7 +366,7 @@ func (s *contextSuite) TestDeleteWorkflowExecution_DeleteVisibilityTaskNotificti &stage, ) s.Error(err) - s.Equal(tasks.DeleteWorkflowExecutionStageVisibility, stage) + s.Equal(tasks.DeleteWorkflowExecutionStageVisibility|tasks.DeleteWorkflowExecutionStageReplication, stage) } func (s *contextSuite) TestAcquireShardOwnershipLostErrorIsNotRetried() { diff --git a/service/history/shard/controller_test.go b/service/history/shard/controller_test.go index 0cfec226623..0e6678c8b78 100644 --- a/service/history/shard/controller_test.go +++ b/service/history/shard/controller_test.go @@ -319,10 +319,10 @@ func (s *controllerSuite) TestHistoryEngineClosed() { s.shardController.acquireShards(context.Background()) var workerWG sync.WaitGroup - for w := 0; w < 10; w++ { + for range 10 { workerWG.Add(1) go func() { - for attempt := 0; attempt < 10; attempt++ { + for range 10 { for shardID := int32(1); shardID <= numShards; shardID++ { shard, err := s.shardController.GetShardByID(shardID) s.NoError(err) @@ -345,10 +345,10 @@ func (s *controllerSuite) TestHistoryEngineClosed() { s.shardController.CloseShardByID(shardID) } - for w := 0; w < 10; w++ { + for range 10 { workerWG.Add(1) go func() { - for attempt := 0; attempt < 10; attempt++ { + for range 10 { for shardID := int32(3); shardID <= numShards; shardID++ { shard, err := s.shardController.GetShardByID(shardID) s.NoError(err) @@ -363,7 +363,7 @@ func (s *controllerSuite) TestHistoryEngineClosed() { }() } - for w := 0; w < 10; w++ { + for range 10 { workerWG.Add(1) go func() { shardLost := false @@ -417,7 +417,7 @@ func (s *controllerSuite) TestShardControllerClosed() { s.shardController.acquireShards(context.Background()) var workerWG sync.WaitGroup - for w := 0; w < 10; w++ { + for range 10 { workerWG.Add(1) go func() { shardLost := false @@ -672,7 +672,7 @@ func (s *controllerSuite) TestShardControllerFuzz() { s.shardController.acquireShards(context.Background()) var workers goro.Group - for i := 0; i < 10; i++ { + for range 10 { workers.Go(worker) } @@ -1148,7 +1148,7 @@ var _ fmt.Stringer = (*ContextImpl)(nil) type contextMatcher int32 -func (s contextMatcher) Matches(x interface{}) bool { +func (s contextMatcher) Matches(x any) bool { shardContext, ok := x.(historyi.ShardContext) return ok && shardContext.GetShardID() == int32(s) } @@ -1159,7 +1159,7 @@ func (s contextMatcher) String() string { type getOrCreateShardRequestMatcher int32 -func (s getOrCreateShardRequestMatcher) Matches(x interface{}) bool { +func (s getOrCreateShardRequestMatcher) Matches(x any) bool { req, ok := x.(*persistence.GetOrCreateShardRequest) return ok && req.ShardID == int32(s) } @@ -1170,7 +1170,7 @@ func (s getOrCreateShardRequestMatcher) String() string { type updateShardRequestMatcher persistence.UpdateShardRequest -func (m updateShardRequestMatcher) Matches(x interface{}) bool { +func (m updateShardRequestMatcher) Matches(x any) bool { req, ok := x.(*persistence.UpdateShardRequest) if !ok { return false diff --git a/service/history/shard/task_key_generator_test.go b/service/history/shard/task_key_generator_test.go index a62a3e7f104..fbcec6ff13d 100644 --- a/service/history/shard/task_key_generator_test.go +++ b/service/history/shard/task_key_generator_test.go @@ -59,7 +59,7 @@ func (s *taskKeyGeneratorSuite) TestSetTaskKeys_ImmediateTasks() { numTask := 5 transferTasks := make([]tasks.Task, 0, numTask) - for i := 0; i < numTask; i++ { + for range numTask { transferTasks = append( transferTasks, tasks.NewFakeTask( @@ -123,7 +123,7 @@ func (s *taskKeyGeneratorSuite) TestSetTaskKeys_RenewRange() { s.True(numTask > (1 << s.rangeSizeBits)) transferTasks := make([]tasks.Task, 0, numTask) - for i := 0; i < numTask; i++ { + for range numTask { transferTasks = append( transferTasks, tasks.NewFakeTask(tests.WorkflowKey, tasks.CategoryTransfer, now), diff --git a/service/history/shard/task_key_manager_test.go b/service/history/shard/task_key_manager_test.go index 1e79a9c08c5..725c0f6c3be 100644 --- a/service/history/shard/task_key_manager_test.go +++ b/service/history/shard/task_key_manager_test.go @@ -65,7 +65,7 @@ func (s *taskKeyManagerSuite) TestSetAndTrackTaskKeys() { numTask := 5 transferTasks := make([]tasks.Task, 0, numTask) - for i := 0; i < numTask; i++ { + for range numTask { transferTasks = append( transferTasks, tasks.NewFakeTask( diff --git a/service/history/tasks/delete_execution_replication_task.go b/service/history/tasks/delete_execution_replication_task.go new file mode 100644 index 00000000000..160529a2f74 --- /dev/null +++ b/service/history/tasks/delete_execution_replication_task.go @@ -0,0 +1,57 @@ +package tasks + +import ( + "time" + + enumsspb "go.temporal.io/server/api/enums/v1" + "go.temporal.io/server/common/definition" +) + +var _ Task = (*DeleteExecutionReplicationTask)(nil) +var _ HasArchetypeID = (*DeleteExecutionReplicationTask)(nil) + +type DeleteExecutionReplicationTask struct { + definition.WorkflowKey + VisibilityTimestamp time.Time + TaskID int64 + ArchetypeID uint32 +} + +func (a *DeleteExecutionReplicationTask) GetKey() Key { + return NewImmediateKey(a.TaskID) +} + +func (a *DeleteExecutionReplicationTask) GetVersion() int64 { + return 0 +} + +func (a *DeleteExecutionReplicationTask) SetVersion(_ int64) { +} + +func (a *DeleteExecutionReplicationTask) GetTaskID() int64 { + return a.TaskID +} + +func (a *DeleteExecutionReplicationTask) SetTaskID(id int64) { + a.TaskID = id +} + +func (a *DeleteExecutionReplicationTask) GetVisibilityTime() time.Time { + return a.VisibilityTimestamp +} + +func (a *DeleteExecutionReplicationTask) SetVisibilityTime(timestamp time.Time) { + a.VisibilityTimestamp = timestamp +} + +func (a *DeleteExecutionReplicationTask) GetCategory() Category { + return CategoryReplication +} + +func (a *DeleteExecutionReplicationTask) GetType() enumsspb.TaskType { + return enumsspb.TASK_TYPE_REPLICATION_DELETE_EXECUTION +} + +func (a *DeleteExecutionReplicationTask) GetArchetypeID() uint32 { + return a.ArchetypeID +} diff --git a/service/history/tasks/delete_workflow_execution_stage.go b/service/history/tasks/delete_workflow_execution_stage.go index b9e0d218ab2..b35bd32ae45 100644 --- a/service/history/tasks/delete_workflow_execution_stage.go +++ b/service/history/tasks/delete_workflow_execution_stage.go @@ -9,6 +9,7 @@ const ( const ( DeleteWorkflowExecutionStageVisibility DeleteWorkflowExecutionStage = 1 << iota + DeleteWorkflowExecutionStageReplication DeleteWorkflowExecutionStageCurrent DeleteWorkflowExecutionStageMutableState DeleteWorkflowExecutionStageHistory diff --git a/service/history/tasks/key_test.go b/service/history/tasks/key_test.go index ae9e9e49601..993a521a276 100644 --- a/service/history/tasks/key_test.go +++ b/service/history/tasks/key_test.go @@ -83,9 +83,9 @@ func (s *taskKeySuite) TestSort() { numTaskPerInstant := 16 taskKeys := Keys{} - for i := 0; i < numInstant; i++ { + for range numInstant { fireTime := time.Unix(0, rand.Int63()) - for j := 0; j < numTaskPerInstant; j++ { + for range numTaskPerInstant { taskKeys = append(taskKeys, NewKey(fireTime, rand.Int63())) } } diff --git a/service/history/tasks/utils.go b/service/history/tasks/utils.go index 3c446342b6d..1d6a385c08d 100644 --- a/service/history/tasks/utils.go +++ b/service/history/tasks/utils.go @@ -67,7 +67,7 @@ func GetTransferTaskEventID( case *CloseExecutionTask: eventID = common.FirstEventID case *DeleteExecutionTask: - eventID = common.FirstEventID + return getChasmTaskEventID() case *CancelExecutionTask: eventID = task.InitiatedEventID case *SignalExecutionTask: diff --git a/service/history/timer_queue_active_task_executor.go b/service/history/timer_queue_active_task_executor.go index d649691e552..ec7d5280ca9 100644 --- a/service/history/timer_queue_active_task_executor.go +++ b/service/history/timer_queue_active_task_executor.go @@ -79,6 +79,7 @@ func (t *timerQueueActiveTaskExecutor) Execute( namespaceTag, replicationState := getNamespaceTagAndReplicationStateByID( t.shardContext.GetNamespaceRegistry(), executable.GetNamespaceID(), + executable.GetWorkflowID(), ) metricsTags := []metrics.Tag{ namespaceTag, @@ -1014,7 +1015,6 @@ func (t *timerQueueActiveTaskExecutor) executeChasmSideEffectTimerTask( return executeChasmSideEffectTask( ctx, t.chasmEngine, - t.shardContext.ChasmRegistry(), tree, task, ) diff --git a/service/history/timer_queue_active_task_executor_test.go b/service/history/timer_queue_active_task_executor_test.go index df9ceb43910..a8af08a052c 100644 --- a/service/history/timer_queue_active_task_executor_test.go +++ b/service/history/timer_queue_active_task_executor_test.go @@ -1975,7 +1975,6 @@ func (s *timerQueueActiveTaskExecutorSuite) TestExecuteChasmSideEffectTimerTask_ gomock.Any(), gomock.Any(), gomock.Any(), - gomock.Any(), ).Times(1).Return(nil) // Mock mutable state. @@ -2045,7 +2044,11 @@ func (s *timerQueueActiveTaskExecutorSuite) TestExecuteChasmPureTimerTask_Execut } // Mock the CHASM tree and execute interface. - mockEach := &chasm.MockNodePureTask{} + mockEach := &chasm.MockNodePureTask{ + HandleExecutePureTask: func(_ context.Context, _ chasm.TaskAttributes, _ any) (bool, error) { + return true, nil + }, + } chasmTree := historyi.NewMockChasmTree(s.controller) chasmTree.EXPECT().EachPureTask(gomock.Any(), gomock.Any()). Times(1).Do( diff --git a/service/history/timer_queue_factory.go b/service/history/timer_queue_factory.go index 4bbb450a056..3f05af636b0 100644 --- a/service/history/timer_queue_factory.go +++ b/service/history/timer_queue_factory.go @@ -7,6 +7,7 @@ import ( "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/persistence/visibility/manager" "go.temporal.io/server/common/resource" + ctasks "go.temporal.io/server/common/tasks" "go.temporal.io/server/common/telemetry" "go.temporal.io/server/service/history/deletemanager" historyi "go.temporal.io/server/service/history/interfaces" @@ -48,9 +49,17 @@ func NewTimerQueueFactory( ActiveNamespaceWeights: params.Config.TimerProcessorSchedulerActiveRoundRobinWeights, StandbyNamespaceWeights: params.Config.TimerProcessorSchedulerStandbyRoundRobinWeights, InactiveNamespaceDeletionDelay: params.Config.TaskSchedulerInactiveChannelDeletionDelay, + ExecutionAwareSchedulerOptions: ctasks.ExecutionAwareSchedulerOptions{ + Enabled: params.Config.TaskSchedulerEnableExecutionQueueScheduler, + MaxQueues: params.Config.TaskSchedulerExecutionQueueSchedulerMaxQueues, + QueueTTL: params.Config.TaskSchedulerExecutionQueueSchedulerQueueTTL, + QueueConcurrency: params.Config.TaskSchedulerExecutionQueueSchedulerQueueConcurrency, + }, }, params.NamespaceRegistry, params.Logger, + params.MetricsHandler, + params.TimeSource, ), HostPriorityAssigner: queues.NewPriorityAssigner( params.NamespaceRegistry, diff --git a/service/history/timer_queue_standby_task_executor.go b/service/history/timer_queue_standby_task_executor.go index b235a5a0183..57fc3e24c98 100644 --- a/service/history/timer_queue_standby_task_executor.go +++ b/service/history/timer_queue_standby_task_executor.go @@ -178,13 +178,14 @@ func (t *timerQueueStandbyTaskExecutor) executeChasmSideEffectTimerTask( ms historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc, ) (any, error) { - return validateChasmSideEffectTask( - ctx, - ms, - task, - ) + valid, err := validateChasmSideEffectTask(ctx, ms, task) + if err != nil || !valid { + return nil, err + } + return ms.ChasmTree(), nil } + chasmTaskType, _ := t.shardContext.ChasmRegistry().TaskFqnByID(task.Info.GetTypeId()) return t.processTimer( ctx, task, @@ -192,18 +193,49 @@ func (t *timerQueueStandbyTaskExecutor) executeChasmSideEffectTimerTask( getStandbyPostActionFn( task, t.getCurrentTime, - t.config.StandbyTaskMissingEventsDiscardDelay(task.GetType()), - t.checkExecutionStillExistsOnSourceBeforeDiscard, + t.config.ChasmStandbyTaskDiscardDelay(chasmTaskType), + t.discardChasmTask, ), ) } +func (t *timerQueueStandbyTaskExecutor) discardChasmTask( + ctx context.Context, + taskInfo tasks.Task, + postActionInfo any, + logger log.Logger, +) error { + if postActionInfo == nil { + return nil + } + chasmTree, ok := postActionInfo.(historyi.ChasmTree) + if !ok { + return serviceerror.NewInternal("postActionInfo is not a ChasmTree") + } + chasmTask, ok := taskInfo.(*tasks.ChasmTask) + if !ok { + return serviceerror.NewInternal("taskInfo is not a ChasmTask") + } + + return discardChasmSideEffectTask( + ctx, + t.chasmEngine, + t.shardContext.ChasmRegistry(), + chasmTree, + chasmTask, + logger, + t.clusterName, + t.clientBean, + t.shardContext.GetNamespaceRegistry(), + ) +} + func (t *timerQueueStandbyTaskExecutor) executeUserTimerTimeoutTask( ctx context.Context, timerTask *tasks.UserTimerTask, ) error { referenceTime := t.Now() - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { if !mutableState.IsWorkflowExecutionRunning() { // workflow already finished, no need to process the timer return nil, nil @@ -263,7 +295,7 @@ func (t *timerQueueStandbyTaskExecutor) executeActivityTimeoutTask( // the overall solution is to attempt to generate a new activity timer task whenever the // task passed in is safe to be throw away. referenceTime := t.Now() - actionFn := func(ctx context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(ctx context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { if !mutableState.IsWorkflowExecutionRunning() { // workflow already finished, no need to process the timer return nil, nil @@ -357,7 +389,7 @@ func (t *timerQueueStandbyTaskExecutor) executeActivityRetryTimerTask( ctx context.Context, task *tasks.ActivityRetryTimerTask, ) (retError error) { - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { if !mutableState.IsWorkflowExecutionRunning() { // workflow already finished, no need to process the timer return nil, nil @@ -411,7 +443,7 @@ func (t *timerQueueStandbyTaskExecutor) executeWorkflowTaskTimeoutTask( return nil } - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { if !mutableState.IsWorkflowExecutionRunning() { // workflow already finished, no need to process the timer return nil, nil @@ -460,7 +492,7 @@ func (t *timerQueueStandbyTaskExecutor) executeWorkflowBackoffTimerTask( ctx context.Context, timerTask *tasks.WorkflowBackoffTimerTask, ) error { - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { if !mutableState.IsWorkflowExecutionRunning() { // workflow already finished, no need to process the timer return nil, nil @@ -503,7 +535,7 @@ func (t *timerQueueStandbyTaskExecutor) executeWorkflowRunTimeoutTask( timerTask *tasks.WorkflowRunTimeoutTask, ) error { - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { if !t.isValidWorkflowRunTimeoutTask(mutableState, timerTask) { return nil, nil } @@ -542,7 +574,7 @@ func (t *timerQueueStandbyTaskExecutor) executeWorkflowExecutionTimeoutTask( wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc, - ) (interface{}, error) { + ) (any, error) { if !t.isValidWorkflowExecutionTimeoutTask(mutableState, timerTask) { return nil, nil } @@ -692,7 +724,7 @@ func (t *timerQueueStandbyTaskExecutor) processTimer( func (t *timerQueueStandbyTaskExecutor) pushActivity( ctx context.Context, task tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { if postActionInfo == nil { @@ -751,7 +783,7 @@ func (t *timerQueueStandbyTaskExecutor) getCurrentTime() time.Time { func (t *timerQueueStandbyTaskExecutor) checkExecutionStillExistsOnSourceBeforeDiscard( ctx context.Context, taskInfo tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { if postActionInfo == nil { diff --git a/service/history/timer_queue_standby_task_executor_test.go b/service/history/timer_queue_standby_task_executor_test.go index ae4293d4e08..064e2b6b8ac 100644 --- a/service/history/timer_queue_standby_task_executor_test.go +++ b/service/history/timer_queue_standby_task_executor_test.go @@ -89,6 +89,7 @@ type ( timeSource *clock.EventTimeSource fetchHistoryDuration time.Duration discardDuration time.Duration + chasmDiscardDuration time.Duration clientBean *client.MockBean timerQueueStandbyTaskExecutor *timerQueueStandbyTaskExecutor @@ -117,6 +118,7 @@ func (s *timerQueueStandbyTaskExecutorSuite) SetupTest() { s.timeSource = clock.NewEventTimeSource().Update(s.now) s.fetchHistoryDuration = time.Minute * 12 s.discardDuration = time.Minute * 30 + s.chasmDiscardDuration = s.config.ChasmStandbyTaskDiscardDelay("") s.controller = gomock.NewController(s.T()) s.mockTxProcessor = queues.NewMockQueue(s.controller) @@ -2260,6 +2262,98 @@ func (s *timerQueueStandbyTaskExecutorSuite) newTaskExecutable( ) } +func (s *timerQueueStandbyTaskExecutorSuite) TestExecuteChasmSideEffectTimerTask_Discard() { + setupDiscard := func(lib chasm.Library, taskName string, treeMockFn func(*historyi.MockChasmTree)) (*timerQueueStandbyTaskExecutor, *tasks.ChasmTask) { + execution := &commonpb.WorkflowExecution{ + WorkflowId: tests.WorkflowKey.WorkflowID, + RunId: tests.WorkflowKey.RunID, + } + + registry := chasm.NewRegistry(s.logger) + s.NoError(registry.Register(lib)) + s.mockShard.SetChasmRegistry(registry) + typeID := chasm.GenerateTypeID(chasm.FullyQualifiedName(lib.Name(), taskName)) + + chasmTree := historyi.NewMockChasmTree(s.controller) + chasmTree.EXPECT().ValidateSideEffectTask(gomock.Any(), gomock.Any()).Return(true, nil).Times(1) + treeMockFn(chasmTree) + + ms := historyi.NewMockMutableState(s.controller) + ms.EXPECT().GetCurrentVersion().Return(int64(2)).AnyTimes() + ms.EXPECT().NextTransitionCount().Return(int64(0)).AnyTimes() + ms.EXPECT().GetNextEventID().Return(int64(2)).AnyTimes() + ms.EXPECT().GetExecutionInfo().Return(&persistencespb.WorkflowExecutionInfo{}).AnyTimes() + ms.EXPECT().GetWorkflowKey().Return(tests.WorkflowKey).AnyTimes() + ms.EXPECT().GetExecutionState().Return( + &persistencespb.WorkflowExecutionState{Status: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING}, + ).AnyTimes() + ms.EXPECT().ChasmTree().Return(chasmTree).AnyTimes() + + timerTask := &tasks.ChasmTask{ + WorkflowKey: definition.NewWorkflowKey( + s.namespaceID.String(), + execution.GetWorkflowId(), + execution.GetRunId(), + ), + VisibilityTimestamp: s.now, + TaskID: s.mustGenerateTaskID(), + Info: &persistencespb.ChasmTaskInfo{ + ArchetypeId: tests.ArchetypeID, + TypeId: typeID, + }, + } + + wfCtx := historyi.NewMockWorkflowContext(s.controller) + wfCtx.EXPECT().LoadMutableState(gomock.Any(), s.mockShard).Return(ms, nil).AnyTimes() + + mockCache := wcache.NewMockCache(s.controller) + mockCache.EXPECT().GetOrCreateChasmExecution( + gomock.Any(), s.mockShard, gomock.Any(), execution, tests.ArchetypeID, locks.PriorityLow, + ).Return(wfCtx, wcache.NoopReleaseFn, nil).AnyTimes() + + //nolint:revive // unchecked-type-assertion + executor := newTimerQueueStandbyTaskExecutor( + s.mockShard, + mockCache, + s.mockDeleteManager, + s.mockMatchingClient, + s.mockChasmEngine, + s.logger, + metrics.NoopMetricsHandler, + s.clusterName, + s.config, + s.clientBean, + ).(*timerQueueStandbyTaskExecutor) + + // Advance the standby cluster's time past the CHASM discard delay. + s.mockShard.SetCurrentTime(s.clusterName, s.now.Add(s.chasmDiscardDuration+time.Second)) + + return executor, timerTask + } + + s.Run("WithHandler", func() { + executor, task := setupDiscard(&discardableTaskTestLibrary{}, "discard_task", func(tree *historyi.MockChasmTree) { + tree.EXPECT().ExecuteSideEffectDiscardTask( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(nil).Times(1) + }) + resp := executor.Execute(context.Background(), s.newTaskExecutable(task)) + s.NotNil(resp) + s.NoError(resp.ExecutionErr) + }) + + s.Run("WithoutHandler", func() { + executor, task := setupDiscard(&nonDiscardableTaskTestLibrary{}, "non_discard_task", func(tree *historyi.MockChasmTree) { + tree.EXPECT().ExecuteSideEffectDiscardTask( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(chasm.ErrTaskDiscarded).Times(1) + }) + resp := executor.Execute(context.Background(), s.newTaskExecutable(task)) + s.NotNil(resp) + s.ErrorIs(resp.ExecutionErr, consts.ErrTaskDiscarded) + }) +} + func (s *timerQueueStandbyTaskExecutorSuite) mustGenerateTaskID() int64 { taskID, err := s.mockShard.GenerateTaskID() s.NoError(err) diff --git a/service/history/transfer_queue_active_task_executor.go b/service/history/transfer_queue_active_task_executor.go index 3fba82a3e86..c6cfe60148e 100644 --- a/service/history/transfer_queue_active_task_executor.go +++ b/service/history/transfer_queue_active_task_executor.go @@ -106,6 +106,7 @@ func (t *transferQueueActiveTaskExecutor) Execute( namespaceTag, replicationState := getNamespaceTagAndReplicationStateByID( t.shardContext.GetNamespaceRegistry(), task.GetNamespaceID(), + executable.GetWorkflowID(), ) metricsTags := []metrics.Tag{ namespaceTag, @@ -190,7 +191,6 @@ func (t *transferQueueActiveTaskExecutor) executeChasmSideEffectTransferTask( return executeChasmSideEffectTask( ctx, t.chasmEngine, - t.shardContext.ChasmRegistry(), tree, task, ) @@ -1739,7 +1739,6 @@ func (t *transferQueueActiveTaskExecutor) resetWorkflow( baseRebuildLastEventVersion, baseNextEventID, resetRunID, - uuid.NewString(), baseWorkflow, ndc.NewWorkflow( t.shardContext.GetClusterMetadata(), diff --git a/service/history/transfer_queue_active_task_executor_test.go b/service/history/transfer_queue_active_task_executor_test.go index 869f635013a..a3cc66be703 100644 --- a/service/history/transfer_queue_active_task_executor_test.go +++ b/service/history/transfer_queue_active_task_executor_test.go @@ -309,7 +309,6 @@ func (s *transferQueueActiveTaskExecutorSuite) TestExecuteChasmSideEffectTransfe gomock.Any(), gomock.Any(), gomock.Any(), - gomock.Any(), ).Times(1).Return(nil) // Mock mutable state. @@ -1140,7 +1139,7 @@ func (s *transferQueueActiveTaskExecutorSuite) TestProcessCloseExecution_NoParen commandType := enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION parentClosePolicy := enumspb.PARENT_CLOSE_POLICY_TERMINATE var commands []*commandpb.Command - for i := 0; i < 10; i++ { + for i := range 10 { commands = append(commands, &commandpb.Command{ CommandType: commandType, Attributes: &commandpb.Command_StartChildWorkflowExecutionCommandAttributes{StartChildWorkflowExecutionCommandAttributes: &commandpb.StartChildWorkflowExecutionCommandAttributes{ @@ -1160,7 +1159,7 @@ func (s *transferQueueActiveTaskExecutorSuite) TestProcessCloseExecution_NoParen Commands: commands, }, defaultWorkflowTaskCompletionLimits) - for i := 0; i < 10; i++ { + for i := range 10 { _, _, err = mutableState.AddStartChildWorkflowExecutionInitiatedEvent(event.GetEventId(), &commandpb.StartChildWorkflowExecutionCommandAttributes{ WorkflowId: "child workflow" + convert.IntToString(i), WorkflowType: &commonpb.WorkflowType{ @@ -1237,7 +1236,7 @@ func (s *transferQueueActiveTaskExecutorSuite) TestProcessCloseExecution_ParentW commandType := enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION parentClosePolicy := enumspb.PARENT_CLOSE_POLICY_TERMINATE var commands []*commandpb.Command - for i := 0; i < 10; i++ { + for i := range 10 { commands = append(commands, &commandpb.Command{ CommandType: commandType, Attributes: &commandpb.Command_StartChildWorkflowExecutionCommandAttributes{StartChildWorkflowExecutionCommandAttributes: &commandpb.StartChildWorkflowExecutionCommandAttributes{ @@ -1257,7 +1256,7 @@ func (s *transferQueueActiveTaskExecutorSuite) TestProcessCloseExecution_ParentW Commands: commands, }, defaultWorkflowTaskCompletionLimits) - for i := 0; i < 10; i++ { + for i := range 10 { _, _, err = mutableState.AddStartChildWorkflowExecutionInitiatedEvent(event.GetEventId(), &commandpb.StartChildWorkflowExecutionCommandAttributes{ WorkflowId: "child workflow" + convert.IntToString(i), WorkflowType: &commonpb.WorkflowType{ @@ -1327,7 +1326,7 @@ func (s *transferQueueActiveTaskExecutorSuite) TestProcessCloseExecution_NoParen commandType := enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION parentClosePolicy := enumspb.PARENT_CLOSE_POLICY_ABANDON var commands []*commandpb.Command - for i := 0; i < 10; i++ { + for i := range 10 { commands = append(commands, &commandpb.Command{ CommandType: commandType, Attributes: &commandpb.Command_StartChildWorkflowExecutionCommandAttributes{StartChildWorkflowExecutionCommandAttributes: &commandpb.StartChildWorkflowExecutionCommandAttributes{ @@ -1347,7 +1346,7 @@ func (s *transferQueueActiveTaskExecutorSuite) TestProcessCloseExecution_NoParen Commands: commands, }, defaultWorkflowTaskCompletionLimits) - for i := 0; i < 10; i++ { + for i := range 10 { _, _, err = mutableState.AddStartChildWorkflowExecutionInitiatedEvent(event.GetEventId(), &commandpb.StartChildWorkflowExecutionCommandAttributes{ WorkflowId: "child workflow" + convert.IntToString(i), WorkflowType: &commonpb.WorkflowType{ @@ -2996,6 +2995,7 @@ func (s *transferQueueActiveTaskExecutorSuite) TestPendingCloseExecutionTasks() } executable := queues.NewMockExecutable(ctrl) executable.EXPECT().GetTask().Return(task) + executable.EXPECT().GetWorkflowID().Return(workflowKey.WorkflowID).AnyTimes() resp := executor.Execute(context.Background(), executable) if c.ShouldDelete { s.NoError(resp.ExecutionErr) diff --git a/service/history/transfer_queue_factory.go b/service/history/transfer_queue_factory.go index be51dbfd4d3..17e8804d068 100644 --- a/service/history/transfer_queue_factory.go +++ b/service/history/transfer_queue_factory.go @@ -8,6 +8,7 @@ import ( "go.temporal.io/server/common/persistence/visibility/manager" "go.temporal.io/server/common/resource" "go.temporal.io/server/common/sdk" + ctasks "go.temporal.io/server/common/tasks" "go.temporal.io/server/common/telemetry" "go.temporal.io/server/common/worker_versioning" historyi "go.temporal.io/server/service/history/interfaces" @@ -53,9 +54,17 @@ func NewTransferQueueFactory( ActiveNamespaceWeights: params.Config.TransferProcessorSchedulerActiveRoundRobinWeights, StandbyNamespaceWeights: params.Config.TransferProcessorSchedulerStandbyRoundRobinWeights, InactiveNamespaceDeletionDelay: params.Config.TaskSchedulerInactiveChannelDeletionDelay, + ExecutionAwareSchedulerOptions: ctasks.ExecutionAwareSchedulerOptions{ + Enabled: params.Config.TaskSchedulerEnableExecutionQueueScheduler, + MaxQueues: params.Config.TaskSchedulerExecutionQueueSchedulerMaxQueues, + QueueTTL: params.Config.TaskSchedulerExecutionQueueSchedulerQueueTTL, + QueueConcurrency: params.Config.TaskSchedulerExecutionQueueSchedulerQueueConcurrency, + }, }, params.NamespaceRegistry, params.Logger, + params.MetricsHandler, + params.TimeSource, ), HostPriorityAssigner: queues.NewPriorityAssigner( params.NamespaceRegistry, diff --git a/service/history/transfer_queue_standby_task_executor.go b/service/history/transfer_queue_standby_task_executor.go index 4e61c43aafd..d7cc6bc8e93 100644 --- a/service/history/transfer_queue_standby_task_executor.go +++ b/service/history/transfer_queue_standby_task_executor.go @@ -129,13 +129,14 @@ func (t *transferQueueStandbyTaskExecutor) executeChasmSideEffectTransferTask( ms historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc, ) (any, error) { - return validateChasmSideEffectTask( - ctx, - ms, - task, - ) + valid, err := validateChasmSideEffectTask(ctx, ms, task) + if err != nil || !valid { + return nil, err + } + return ms.ChasmTree(), nil } + chasmTaskType, _ := t.shardContext.ChasmRegistry().TaskFqnByID(task.Info.GetTypeId()) return t.processTransfer( ctx, true, @@ -144,18 +145,49 @@ func (t *transferQueueStandbyTaskExecutor) executeChasmSideEffectTransferTask( getStandbyPostActionFn( task, t.getCurrentTime, - t.config.StandbyTaskMissingEventsDiscardDelay(task.GetType()), - t.checkExecutionStillExistsOnSourceBeforeDiscard, + t.config.ChasmStandbyTaskDiscardDelay(chasmTaskType), + t.discardChasmTask, ), ) } +func (t *transferQueueStandbyTaskExecutor) discardChasmTask( + ctx context.Context, + taskInfo tasks.Task, + postActionInfo any, + logger log.Logger, +) error { + if postActionInfo == nil { + return nil + } + chasmTree, ok := postActionInfo.(historyi.ChasmTree) + if !ok { + return serviceerror.NewInternal("postActionInfo is not a ChasmTree") + } + chasmTask, ok := taskInfo.(*tasks.ChasmTask) + if !ok { + return serviceerror.NewInternal("taskInfo is not a ChasmTask") + } + + return discardChasmSideEffectTask( + ctx, + t.chasmEngine, + t.shardContext.ChasmRegistry(), + chasmTree, + chasmTask, + logger, + t.clusterName, + t.clientBean, + t.shardContext.GetNamespaceRegistry(), + ) +} + func (t *transferQueueStandbyTaskExecutor) processActivityTask( ctx context.Context, transferTask *tasks.ActivityTask, ) error { processTaskIfClosed := false - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { activityInfo, ok := mutableState.GetActivityInfo(transferTask.ScheduledEventID) if !ok { return nil, nil @@ -199,7 +231,7 @@ func (t *transferQueueStandbyTaskExecutor) processWorkflowTask( ctx context.Context, transferTask *tasks.WorkflowTask, ) error { - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { wtInfo := mutableState.GetWorkflowTaskByID(transferTask.ScheduledEventID) if wtInfo == nil { return nil, nil @@ -254,7 +286,7 @@ func (t *transferQueueStandbyTaskExecutor) processCloseExecution( transferTask *tasks.CloseExecutionTask, ) error { processTaskIfClosed := true - actionFn := func(ctx context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, release historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(ctx context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, release historyi.ReleaseWorkflowContextFunc) (any, error) { if mutableState.IsWorkflowExecutionRunning() { // this can happen if workflow is reset. return nil, nil @@ -362,7 +394,7 @@ func (t *transferQueueStandbyTaskExecutor) processCancelExecution( transferTask *tasks.CancelExecutionTask, ) error { processTaskIfClosed := false - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { requestCancelInfo, ok := mutableState.GetRequestCancelInfo(transferTask.InitiatedEventID) if !ok { return nil, nil @@ -395,7 +427,7 @@ func (t *transferQueueStandbyTaskExecutor) processSignalExecution( transferTask *tasks.SignalExecutionTask, ) error { processTaskIfClosed := false - actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(_ context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, _ historyi.ReleaseWorkflowContextFunc) (any, error) { signalInfo, ok := mutableState.GetSignalInfo(transferTask.InitiatedEventID) if !ok { return nil, nil @@ -428,7 +460,7 @@ func (t *transferQueueStandbyTaskExecutor) processStartChildExecution( transferTask *tasks.StartChildExecutionTask, ) error { processTaskIfClosed := true - actionFn := func(ctx context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, release historyi.ReleaseWorkflowContextFunc) (interface{}, error) { + actionFn := func(ctx context.Context, wfContext historyi.WorkflowContext, mutableState historyi.MutableState, release historyi.ReleaseWorkflowContextFunc) (any, error) { childWorkflowInfo, ok := mutableState.GetChildExecutionInfo(transferTask.InitiatedEventID) if !ok { return nil, nil @@ -577,7 +609,7 @@ func (t *transferQueueStandbyTaskExecutor) processTransfer( func (t *transferQueueStandbyTaskExecutor) pushActivity( ctx context.Context, task tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { if postActionInfo == nil { @@ -602,7 +634,7 @@ func (t *transferQueueStandbyTaskExecutor) pushActivity( func (t *transferQueueStandbyTaskExecutor) pushWorkflowTask( ctx context.Context, task tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { if postActionInfo == nil { @@ -639,7 +671,7 @@ func (e *verificationErr) Unwrap() error { func (t *transferQueueStandbyTaskExecutor) checkExecutionStillExistsOnSourceBeforeDiscard( ctx context.Context, taskInfo tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { if postActionInfo == nil { @@ -663,7 +695,7 @@ func (t *transferQueueStandbyTaskExecutor) checkExecutionStillExistsOnSourceBefo func (t *transferQueueStandbyTaskExecutor) checkParentWorkflowStillExistOnSourceBeforeDiscard( ctx context.Context, taskInfo tasks.Task, - postActionInfo interface{}, + postActionInfo any, logger log.Logger, ) error { if postActionInfo == nil { diff --git a/service/history/transfer_queue_standby_task_executor_test.go b/service/history/transfer_queue_standby_task_executor_test.go index 9886ebc8b70..ef42953d299 100644 --- a/service/history/transfer_queue_standby_task_executor_test.go +++ b/service/history/transfer_queue_standby_task_executor_test.go @@ -90,6 +90,7 @@ type ( localVerificationDuration time.Duration fetchHistoryDuration time.Duration discardDuration time.Duration + chasmDiscardDuration time.Duration transferQueueStandbyTaskExecutor *transferQueueStandbyTaskExecutor mockSearchAttributesProvider *searchattribute.MockProvider @@ -119,6 +120,7 @@ func (s *transferQueueStandbyTaskExecutorSuite) SetupTest() { s.localVerificationDuration = time.Minute * 10 s.fetchHistoryDuration = time.Minute * 12 s.discardDuration = time.Minute * 30 + s.chasmDiscardDuration = config.ChasmStandbyTaskDiscardDelay("") s.controller = gomock.NewController(s.T()) s.mockShard = shard.NewTestContextWithTimeSource( @@ -1338,6 +1340,98 @@ func (s *transferQueueStandbyTaskExecutorSuite) newTaskExecutable( ) } +func (s *transferQueueStandbyTaskExecutorSuite) TestExecuteChasmSideEffectTransferTask_Discard() { + setupDiscard := func(lib chasm.Library, taskName string, treeMockFn func(*historyi.MockChasmTree)) (*transferQueueStandbyTaskExecutor, *tasks.ChasmTask) { + execution := &commonpb.WorkflowExecution{ + WorkflowId: tests.WorkflowKey.WorkflowID, + RunId: tests.WorkflowKey.RunID, + } + + registry := chasm.NewRegistry(s.logger) + s.NoError(registry.Register(lib)) + s.mockShard.SetChasmRegistry(registry) + typeID := chasm.GenerateTypeID(chasm.FullyQualifiedName(lib.Name(), taskName)) + + chasmTree := historyi.NewMockChasmTree(s.controller) + chasmTree.EXPECT().ValidateSideEffectTask(gomock.Any(), gomock.Any()).Return(true, nil).Times(1) + treeMockFn(chasmTree) + + ms := historyi.NewMockMutableState(s.controller) + ms.EXPECT().GetCurrentVersion().Return(int64(2)).AnyTimes() + ms.EXPECT().NextTransitionCount().Return(int64(0)).AnyTimes() + ms.EXPECT().GetNextEventID().Return(int64(2)).AnyTimes() + ms.EXPECT().GetExecutionInfo().Return(&persistencespb.WorkflowExecutionInfo{}).AnyTimes() + ms.EXPECT().GetWorkflowKey().Return(tests.WorkflowKey).AnyTimes() + ms.EXPECT().GetExecutionState().Return( + &persistencespb.WorkflowExecutionState{Status: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING}, + ).AnyTimes() + ms.EXPECT().ChasmTree().Return(chasmTree).AnyTimes() + + transferTask := &tasks.ChasmTask{ + WorkflowKey: definition.NewWorkflowKey( + s.namespaceID.String(), + execution.GetWorkflowId(), + execution.GetRunId(), + ), + VisibilityTimestamp: s.now, + TaskID: s.mustGenerateTaskID(), + Info: &persistencespb.ChasmTaskInfo{ + ArchetypeId: tests.ArchetypeID, + TypeId: typeID, + }, + } + + wfCtx := historyi.NewMockWorkflowContext(s.controller) + wfCtx.EXPECT().LoadMutableState(gomock.Any(), s.mockShard).Return(ms, nil).AnyTimes() + + mockCache := wcache.NewMockCache(s.controller) + mockCache.EXPECT().GetOrCreateChasmExecution( + gomock.Any(), s.mockShard, gomock.Any(), execution, tests.ArchetypeID, gomock.Any(), + ).Return(wfCtx, wcache.NoopReleaseFn, nil).AnyTimes() + + //nolint:revive // unchecked-type-assertion + executor := newTransferQueueStandbyTaskExecutor( + s.mockShard, + mockCache, + s.logger, + metrics.NoopMetricsHandler, + s.clusterName, + s.mockShard.Resource.HistoryClient, + s.mockShard.Resource.MatchingClient, + s.mockVisibilityManager, + s.mockChasmEngine, + s.clientBean, + ).(*transferQueueStandbyTaskExecutor) + + // Advance the standby cluster's time past the CHASM discard delay. + s.mockShard.SetCurrentTime(s.clusterName, s.now.Add(s.chasmDiscardDuration+time.Second)) + + return executor, transferTask + } + + s.Run("WithHandler", func() { + executor, task := setupDiscard(&discardableTaskTestLibrary{}, "discard_task", func(tree *historyi.MockChasmTree) { + tree.EXPECT().ExecuteSideEffectDiscardTask( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(nil).Times(1) + }) + resp := executor.Execute(context.Background(), s.newTaskExecutable(task)) + s.NotNil(resp) + s.NoError(resp.ExecutionErr) + }) + + s.Run("WithoutHandler", func() { + executor, task := setupDiscard(&nonDiscardableTaskTestLibrary{}, "non_discard_task", func(tree *historyi.MockChasmTree) { + tree.EXPECT().ExecuteSideEffectDiscardTask( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(chasm.ErrTaskDiscarded).Times(1) + }) + resp := executor.Execute(context.Background(), s.newTaskExecutable(task)) + s.NotNil(resp) + s.ErrorIs(resp.ExecutionErr, consts.ErrTaskDiscarded) + }) +} + func (s *transferQueueStandbyTaskExecutorSuite) mustGenerateTaskID() int64 { taskID, err := s.mockShard.GenerateTaskID() s.NoError(err) diff --git a/service/history/visibility_queue_factory.go b/service/history/visibility_queue_factory.go index badcbd138e2..ba77d719cb6 100644 --- a/service/history/visibility_queue_factory.go +++ b/service/history/visibility_queue_factory.go @@ -5,6 +5,7 @@ import ( "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/persistence/visibility/manager" + ctasks "go.temporal.io/server/common/tasks" "go.temporal.io/server/common/telemetry" historyi "go.temporal.io/server/service/history/interfaces" "go.temporal.io/server/service/history/queues" @@ -44,9 +45,17 @@ func NewVisibilityQueueFactory( ActiveNamespaceWeights: params.Config.VisibilityProcessorSchedulerActiveRoundRobinWeights, StandbyNamespaceWeights: params.Config.VisibilityProcessorSchedulerStandbyRoundRobinWeights, InactiveNamespaceDeletionDelay: params.Config.TaskSchedulerInactiveChannelDeletionDelay, + ExecutionAwareSchedulerOptions: ctasks.ExecutionAwareSchedulerOptions{ + Enabled: params.Config.TaskSchedulerEnableExecutionQueueScheduler, + MaxQueues: params.Config.TaskSchedulerExecutionQueueSchedulerMaxQueues, + QueueTTL: params.Config.TaskSchedulerExecutionQueueSchedulerQueueTTL, + QueueConcurrency: params.Config.TaskSchedulerExecutionQueueSchedulerQueueConcurrency, + }, }, params.NamespaceRegistry, params.Logger, + params.MetricsHandler, + params.TimeSource, ), HostPriorityAssigner: queues.NewPriorityAssigner( params.NamespaceRegistry, diff --git a/service/history/visibility_queue_task_executor.go b/service/history/visibility_queue_task_executor.go index 73456b28e30..10615572e13 100644 --- a/service/history/visibility_queue_task_executor.go +++ b/service/history/visibility_queue_task_executor.go @@ -78,6 +78,7 @@ func (t *visibilityQueueTaskExecutor) Execute( namespaceTag, replicationState := getNamespaceTagAndReplicationStateByID( t.shardContext.GetNamespaceRegistry(), task.GetNamespaceID(), + executable.GetWorkflowID(), ) metricsTags := []metrics.Tag{ namespaceTag, @@ -367,7 +368,7 @@ func (t *visibilityQueueTaskExecutor) processChasmTask( } valid, err := validateChasmSideEffectTask(ctx, mutableState, task) - if err != nil || valid == nil { + if err != nil || !valid { return err } diff --git a/service/history/workflow/cache/cache_test.go b/service/history/workflow/cache/cache_test.go index fc172b8c05a..c940b2715a4 100644 --- a/service/history/workflow/cache/cache_test.go +++ b/service/history/workflow/cache/cache_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" @@ -322,7 +323,7 @@ func (s *workflowCacheSuite) TestHistoryCacheConcurrentAccess_Release() { release(errors.New("some random error message")) } - for i := 0; i < coroutineCount; i++ { + for range coroutineCount { go testFn() } stopGroup.Wait() @@ -461,12 +462,17 @@ func (s *workflowCacheSuite) TestHistoryCache_CacheHoldTimeMetricContext() { locks.PriorityHigh, ) s.NoError(err) - s.Eventually(func() bool { - release1(nil) + holdDuration := 100 * time.Millisecond + time.Sleep(holdDuration) //nolint:forbidigo + release1(nil) + s.EventuallyWithT(func(collect *assert.CollectT) { snapshot := capture.Snapshot() - s.Greater(snapshot[metrics.HistoryWorkflowExecutionCacheLockHoldDuration.Name()][0].Value, 100*time.Millisecond) - return tests.NamespaceID.String() == snapshot[metrics.HistoryWorkflowExecutionCacheLockHoldDuration.Name()][0].Tags["namespace_id"] - }, 150*time.Millisecond, 100*time.Millisecond) + recordings := snapshot[metrics.HistoryWorkflowExecutionCacheLockHoldDuration.Name()] + if assert.NotEmpty(collect, recordings) { + assert.Greater(collect, recordings[0].Value, holdDuration) + assert.Equal(collect, tests.NamespaceID.String(), recordings[0].Tags["namespace_id"]) + } + }, time.Second, 10*time.Millisecond) capture = metricsHandler.StartCapture() release2, err := s.cache.GetOrCreateCurrentExecution( @@ -478,12 +484,16 @@ func (s *workflowCacheSuite) TestHistoryCache_CacheHoldTimeMetricContext() { locks.PriorityHigh, ) s.NoError(err) - s.Eventually(func() bool { - release2(nil) + time.Sleep(200 * time.Millisecond) //nolint:forbidigo + release2(nil) + s.EventuallyWithT(func(collect *assert.CollectT) { snapshot := capture.Snapshot() - s.Greater(snapshot[metrics.HistoryWorkflowExecutionCacheLockHoldDuration.Name()][0].Value, 200*time.Millisecond) - return tests.NamespaceID.String() == snapshot[metrics.HistoryWorkflowExecutionCacheLockHoldDuration.Name()][0].Tags["namespace_id"] - }, 300*time.Millisecond, 200*time.Millisecond) + recordings := snapshot[metrics.HistoryWorkflowExecutionCacheLockHoldDuration.Name()] + if assert.NotEmpty(collect, recordings) { + assert.Greater(collect, recordings[0].Value, 200*time.Millisecond) + assert.Equal(collect, tests.NamespaceID.String(), recordings[0].Tags["namespace_id"]) + } + }, time.Second, 10*time.Millisecond) } func (s *workflowCacheSuite) TestCacheImpl_lockWorkflowExecution() { diff --git a/service/history/workflow/context.go b/service/history/workflow/context.go index 90c1484f661..9948b31d03e 100644 --- a/service/history/workflow/context.go +++ b/service/history/workflow/context.go @@ -21,6 +21,7 @@ import ( "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/persistence" + "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/softassert" "go.temporal.io/server/common/util" "go.temporal.io/server/service/history/configs" @@ -737,7 +738,7 @@ func (c *ContextImpl) mergeUpdateWithNewReplicationTasks( } taskUpdated := false - updateTask := func(task interface{}) bool { + updateTask := func(task any) bool { switch t := task.(type) { case *tasks.HistoryReplicationTask: t.NewRunBranchToken = newRunBranchToken @@ -968,6 +969,7 @@ func (c *ContextImpl) UpdateRegistry(ctx context.Context) update.Registry { c.updateRegistry = update.NewRegistry( c.MutableState, + update.WithNamespace(nsName), update.WithLogger(c.logger), update.WithMetrics(c.metricsHandler), update.WithTracerProvider(trace.SpanFromContext(ctx).TracerProvider()), @@ -1135,9 +1137,10 @@ func (c *ContextImpl) forceTerminateWorkflow( if !mutableState.IsWorkflow() { return mutableState.ChasmTree().Terminate(chasm.TerminateComponentRequest{ - Identity: consts.IdentityHistoryService, - Reason: failureReason, - Details: nil, + Identity: consts.IdentityHistoryService, + Reason: failureReason, + Details: nil, + RequestID: primitives.NewUUID().String(), }) } diff --git a/service/history/workflow/metrics.go b/service/history/workflow/metrics.go index 486e4b00397..e32c07c86c4 100644 --- a/service/history/workflow/metrics.go +++ b/service/history/workflow/metrics.go @@ -173,8 +173,11 @@ func RecordActivityCompletionMetrics( tags..., ) - if !completion.AttemptStartedTime.IsZero() && completion.Status != ActivityStatusTimeout { - latency := time.Since(completion.AttemptStartedTime) + now := shard.GetTimeSource().Now() + if completion.Status != ActivityStatusTimeout && + !completion.AttemptStartedTime.IsZero() && + !completion.AttemptStartedTime.After(now) { + latency := now.Sub(completion.AttemptStartedTime) // ActivityE2ELatency is deprecated due to its inaccurate naming. It captures the attempt duration instead of an end-to-end duration as its name suggests. For now record both metrics metrics.ActivityE2ELatency.With(metricsHandler).Record(latency) metrics.ActivityStartToCloseLatency.With(metricsHandler).Record(latency) @@ -182,7 +185,7 @@ func RecordActivityCompletionMetrics( // Record true end-to-end duration only for terminal states (includes retries and backoffs) if completion.Closed && !completion.FirstScheduledTime.IsZero() { - scheduleToCloseLatency := time.Since(completion.FirstScheduledTime) + scheduleToCloseLatency := now.Sub(completion.FirstScheduledTime) metrics.ActivityScheduleToCloseLatency.With(metricsHandler).Record(scheduleToCloseLatency) } diff --git a/service/history/workflow/metrics_test.go b/service/history/workflow/metrics_test.go index 03f4ef2f22e..7ef261e3ce9 100644 --- a/service/history/workflow/metrics_test.go +++ b/service/history/workflow/metrics_test.go @@ -6,12 +6,15 @@ import ( "github.com/stretchr/testify/require" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/common/clock" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/metrics/metricstest" "go.temporal.io/server/common/namespace" "go.temporal.io/server/service/history/configs" + historyi "go.temporal.io/server/service/history/interfaces" + "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -63,3 +66,153 @@ func TestEmitWorkflowCompletionStats_SkipNonWorkflow(t *testing.T) { _, err = snapshot.Histogram("workflow_schedule_to_close_latency_milliseconds") require.Error(t, err) } + +func TestRecordActivityCompletionMetrics_SkipsStartToCloseLatencyWhenStartedTimeMissing(t *testing.T) { + controller := gomock.NewController(t) + handler := metrics.NewMockHandler(controller) + shard := newActivityMetricsTestShard(controller, handler) + + expectActivityMetricsScope(handler, metrics.HistoryRespondActivityTaskCompletedScope) + + scheduleToCloseTimer := metrics.NewMockTimerIface(controller) + scheduleToCloseTimer.EXPECT().Record(gomock.Any()).Times(1) + handler.EXPECT().Timer(metrics.ActivityScheduleToCloseLatency.Name()).Return(scheduleToCloseTimer) + + successCounter := metrics.NewMockCounterIface(controller) + successCounter.EXPECT().Record(int64(1)).Times(1) + handler.EXPECT().Counter(metrics.ActivitySuccess.Name()).Return(successCounter) + + RecordActivityCompletionMetrics( + shard, + namespace.Name("test-namespace"), + "test-task-queue", + ActivityCompletionMetrics{ + Status: ActivityStatusSucceeded, + AttemptStartedTime: time.Time{}, + FirstScheduledTime: time.Now().Add(-2 * time.Minute), + Closed: true, + }, + testActivityMetricTags(metrics.HistoryRespondActivityTaskCompletedScope)..., + ) +} + +func TestRecordActivityCompletionMetrics_SkipsFutureStartTime(t *testing.T) { + controller := gomock.NewController(t) + handler := metrics.NewMockHandler(controller) + shard := newActivityMetricsTestShard(controller, handler) + expectActivityMetricsScope(handler, metrics.HistoryRespondActivityTaskCompletedScope) + + RecordActivityCompletionMetrics( + shard, + namespace.Name("test-namespace"), + "test-task-queue", + ActivityCompletionMetrics{ + Status: ActivityStatusUnknown, + AttemptStartedTime: time.Now().Add(1 * time.Minute), + }, + testActivityMetricTags(metrics.HistoryRespondActivityTaskCompletedScope)..., + ) +} + +func TestRecordActivityCompletionMetrics_RecordsLargeLatency(t *testing.T) { + controller := gomock.NewController(t) + handler := metrics.NewMockHandler(controller) + shard := newActivityMetricsTestShard(controller, handler) + expectActivityMetricsScope(handler, metrics.HistoryRespondActivityTaskCompletedScope) + + e2eTimer := metrics.NewMockTimerIface(controller) + e2eTimer.EXPECT().Record(gomock.Any()).Times(1) + handler.EXPECT().Timer(metrics.ActivityE2ELatency.Name()).Return(e2eTimer) + + startToCloseTimer := metrics.NewMockTimerIface(controller) + startToCloseTimer.EXPECT().Record(gomock.Any()).Times(1) + handler.EXPECT().Timer(metrics.ActivityStartToCloseLatency.Name()).Return(startToCloseTimer) + + RecordActivityCompletionMetrics( + shard, + namespace.Name("test-namespace"), + "test-task-queue", + ActivityCompletionMetrics{ + Status: ActivityStatusUnknown, + AttemptStartedTime: time.Now().Add(-2 * time.Hour), + }, + testActivityMetricTags(metrics.HistoryRespondActivityTaskCompletedScope)..., + ) +} + +func TestRecordActivityCompletionMetrics_TimeoutWithStartedTimeSkipsLatency(t *testing.T) { + controller := gomock.NewController(t) + handler := metrics.NewMockHandler(controller) + shard := newActivityMetricsTestShard(controller, handler) + expectActivityMetricsScope(handler, metrics.TimerActiveTaskActivityTimeoutScope) + + timeoutCounter := metrics.NewMockCounterIface(controller) + timeoutCounter.EXPECT().Record(int64(1), metrics.StringTag("timeout_type", enumspb.TIMEOUT_TYPE_START_TO_CLOSE.String())).Times(1) + handler.EXPECT().Counter(metrics.ActivityTaskTimeout.Name()).Return(timeoutCounter) + + RecordActivityCompletionMetrics( + shard, + namespace.Name("test-namespace"), + "test-task-queue", + ActivityCompletionMetrics{ + Status: ActivityStatusTimeout, + AttemptStartedTime: time.Now().Add(-30 * time.Second), + TimerType: enumspb.TIMEOUT_TYPE_START_TO_CLOSE, + }, + testActivityMetricTags(metrics.TimerActiveTaskActivityTimeoutScope)..., + ) +} + +func TestRecordActivityCompletionMetrics_TimeoutWithMissingStartedTimeSkipsLatencyAndEmitsCounter(t *testing.T) { + controller := gomock.NewController(t) + handler := metrics.NewMockHandler(controller) + shard := newActivityMetricsTestShard(controller, handler) + expectActivityMetricsScope(handler, metrics.TimerActiveTaskActivityTimeoutScope) + + timeoutCounter := metrics.NewMockCounterIface(controller) + timeoutCounter.EXPECT().Record(int64(1), metrics.StringTag("timeout_type", enumspb.TIMEOUT_TYPE_HEARTBEAT.String())).Times(1) + handler.EXPECT().Counter(metrics.ActivityTaskTimeout.Name()).Return(timeoutCounter) + + RecordActivityCompletionMetrics( + shard, + namespace.Name("test-namespace"), + "test-task-queue", + ActivityCompletionMetrics{ + Status: ActivityStatusTimeout, + TimerType: enumspb.TIMEOUT_TYPE_HEARTBEAT, + }, + testActivityMetricTags(metrics.TimerActiveTaskActivityTimeoutScope)..., + ) +} + +func newActivityMetricsTestShard( + controller *gomock.Controller, + handler *metrics.MockHandler, +) *historyi.MockShardContext { + shard := historyi.NewMockShardContext(controller) + shard.EXPECT().GetMetricsHandler().Return(handler).Times(1) + shard.EXPECT().GetConfig().Return(&configs.Config{ + BreakdownMetricsByTaskQueue: dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(true), + }).Times(1) + shard.EXPECT().GetTimeSource().Return(clock.NewRealTimeSource()).Times(1) + return shard +} + +func expectActivityMetricsScope(handler *metrics.MockHandler, operation string) { + scopeTags := []any{ + metrics.OperationTag(operation), + metrics.WorkflowTypeTag("test-workflow"), + metrics.ActivityTypeTag("test-activity"), + metrics.NamespaceTag("test-namespace"), + metrics.UnsafeTaskQueueTag("test-task-queue"), + } + handler.EXPECT().WithTags(scopeTags...).Return(handler).Times(1) +} + +func testActivityMetricTags(operation string) []metrics.Tag { + return []metrics.Tag{ + metrics.OperationTag(operation), + metrics.WorkflowTypeTag("test-workflow"), + metrics.ActivityTypeTag("test-activity"), + } +} diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index ebadcd4c0ff..94e8d8fb511 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -45,6 +45,7 @@ import ( "go.temporal.io/server/common/definition" "go.temporal.io/server/common/enums" "go.temporal.io/server/common/failure" + "go.temporal.io/server/common/headers" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" @@ -56,7 +57,6 @@ import ( "go.temporal.io/server/common/persistence/transitionhistory" "go.temporal.io/server/common/persistence/versionhistory" "go.temporal.io/server/common/primitives/timestamp" - "go.temporal.io/server/common/searchattribute" "go.temporal.io/server/common/searchattribute/sadefs" serviceerrors "go.temporal.io/server/common/serviceerror" "go.temporal.io/server/common/softassert" @@ -381,9 +381,7 @@ func NewMutableState( ExecutionStats: &persistencespb.ExecutionStats{HistorySize: 0}, SubStateMachinesByType: make(map[string]*persistencespb.StateMachineMap), } - if s.config.EnableNexus() { - s.executionInfo.TaskGenerationShardClockTimestamp = shard.CurrentVectorClock().GetClock() - } + s.executionInfo.TaskGenerationShardClockTimestamp = shard.CurrentVectorClock().GetClock() s.approximateSize += s.executionInfo.Size() s.executionState = &persistencespb.WorkflowExecutionState{ @@ -416,6 +414,7 @@ func NewMutableState( s, chasm.DefaultPathEncoder, logger, + shard.GetMetricsHandler().WithTags(metrics.NamespaceTag(namespaceName)), ) } @@ -563,6 +562,7 @@ func NewMutableStateFromDB( mutableState, chasm.DefaultPathEncoder, mutableState.logger, // this logger is tagged with execution key. + shard.GetMetricsHandler().WithTags(metrics.NamespaceTag(namespaceEntry.Name().String())), ) if err != nil { return nil, err @@ -2598,12 +2598,13 @@ func (ms *MutableStateImpl) addWorkflowExecutionStartedEventForContinueAsNew( ContinuedFailure: command.GetFailure(), ContinueAsNewInitiator: command.Initiator, // enforce minimal interval between runs to prevent tight loop continue as new spin. - FirstWorkflowTaskBackoff: previousExecutionState.ContinueAsNewMinBackoff(command.BackoffStartInterval), - SourceVersionStamp: sourceVersionStamp, - RootExecutionInfo: rootExecutionInfo, - InheritedBuildId: inheritedBuildId, - InheritedPinnedVersion: inheritedPinnedVersion, - VersioningOverride: pinnedOverride, + FirstWorkflowTaskBackoff: previousExecutionState.ContinueAsNewMinBackoff(command.BackoffStartInterval), + SourceVersionStamp: sourceVersionStamp, + RootExecutionInfo: rootExecutionInfo, + InheritedBuildId: inheritedBuildId, + InheritedPinnedVersion: inheritedPinnedVersion, + VersioningOverride: pinnedOverride, + DeclinedTargetVersionUpgrade: computeDeclinedTargetVersionUpgrade(previousExecutionInfo), } if command.GetInitiator() == enumspb.CONTINUE_AS_NEW_INITIATOR_RETRY { req.Attempt = previousExecutionState.GetExecutionInfo().Attempt + 1 @@ -2651,6 +2652,20 @@ func (ms *MutableStateImpl) addWorkflowExecutionStartedEventForContinueAsNew( return event, nil } +// computeDeclinedTargetVersionUpgrade determines what declined-upgrade value to +// pass to the next run at continue-as-new time: +// - If the current run was signaled about a target change (last_notified is set), +// that becomes the declined value (the SDK saw it and chose not to upgrade). +// - Otherwise, preserve the existing declined value from a prior CaN chain. +func computeDeclinedTargetVersionUpgrade(info *persistencespb.WorkflowExecutionInfo) *historypb.DeclinedTargetVersionUpgrade { + if lastNotified := info.GetLastNotifiedTargetVersion(); lastNotified != nil { + return &historypb.DeclinedTargetVersionUpgrade{ + DeploymentVersion: lastNotified.GetDeploymentVersion(), + } + } + return info.GetDeclinedTargetVersionUpgrade() +} + func (ms *MutableStateImpl) ContinueAsNewMinBackoff(backoffDuration *durationpb.Duration) *durationpb.Duration { // lifetime of previous execution lifetime := ms.timeSource.Now().Sub(ms.executionState.StartTime.AsTime().UTC()) @@ -2930,6 +2945,13 @@ func (ms *MutableStateImpl) ApplyWorkflowExecutionStartedEvent( ms.executionInfo.VersioningInfo.Behavior = enumspb.VERSIONING_BEHAVIOR_PINNED } + // If the workflow inherited a pinned version from CaN/retry, set the declined + // target version upgrade from the started event. This is the same public API + // type, so no conversion needed. + if event.GetContinuedExecutionRunId() != "" && event.GetInheritedPinnedVersion() != nil { + ms.executionInfo.DeclinedTargetVersionUpgrade = event.GetDeclinedTargetVersionUpgrade() + } + // Populate the versioningInfo if the inheritedAutoUpgradeInfo is present. if event.GetInheritedAutoUpgradeInfo() != nil { ms.SetVersioningRevisionNumber(event.GetInheritedAutoUpgradeInfo().GetSourceDeploymentRevisionNumber()) @@ -3299,7 +3321,7 @@ func (ms *MutableStateImpl) updateBinaryChecksumSearchAttribute() error { recentBinaryChecksums = append(recentBinaryChecksums, rp.BinaryChecksum) } } - checksumsPayload, err := searchattribute.EncodeValue(recentBinaryChecksums, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) + checksumsPayload, err := sadefs.EncodeValue(recentBinaryChecksums, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) if err != nil { return err } @@ -3495,7 +3517,7 @@ func (ms *MutableStateImpl) loadBuildIds() ([]string, error) { if !found { return []string{}, nil } - decoded, err := searchattribute.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) + decoded, err := sadefs.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) if err != nil { return nil, err } @@ -3518,7 +3540,7 @@ func (ms *MutableStateImpl) loadSearchAttributeString(saName string) (string, er if !found { return "", nil } - decoded, err := searchattribute.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD, false) + decoded, err := sadefs.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD, false) if err != nil { return "", err } @@ -3541,7 +3563,7 @@ func (ms *MutableStateImpl) loadUsedDeploymentVersions() ([]string, error) { if !found { return []string{}, nil } - decoded, err := searchattribute.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) + decoded, err := sadefs.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) if err != nil { return nil, err } @@ -3644,7 +3666,7 @@ func (ms *MutableStateImpl) saveBuildIds(buildIds []string, maxSearchAttributeVa hasUnversionedOrAssigned = worker_versioning.IsUnversionedOrAssignedBuildIdSearchAttribute(buildIds[0]) } for { - saPayload, err := searchattribute.EncodeValue(buildIds, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) + saPayload, err := sadefs.EncodeValue(buildIds, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) if err != nil { return err } @@ -3672,7 +3694,7 @@ func (ms *MutableStateImpl) saveUsedDeploymentVersions(usedDeploymentVersions [] } for { - saPayload, err := searchattribute.EncodeValue(usedDeploymentVersions, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) + saPayload, err := sadefs.EncodeValue(usedDeploymentVersions, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) if err != nil { return err } @@ -3700,7 +3722,7 @@ func (ms *MutableStateImpl) saveDeploymentSearchAttributes(deployment, version, if deployment == "" { saPayloads[sadefs.TemporalWorkerDeployment] = nil } else { - deploymentPayload, err := searchattribute.EncodeValue(deployment, enumspb.INDEXED_VALUE_TYPE_KEYWORD) + deploymentPayload, err := sadefs.EncodeValue(deployment, enumspb.INDEXED_VALUE_TYPE_KEYWORD) if err != nil { return err } @@ -3712,7 +3734,7 @@ func (ms *MutableStateImpl) saveDeploymentSearchAttributes(deployment, version, saPayloads[sadefs.TemporalWorkerDeploymentVersion] = nil } else { saPayloads[sadefs.TemporalWorkerDeploymentVersion] = nil - versionPayload, err := searchattribute.EncodeValue(version, enumspb.INDEXED_VALUE_TYPE_KEYWORD) + versionPayload, err := sadefs.EncodeValue(version, enumspb.INDEXED_VALUE_TYPE_KEYWORD) if err != nil { return err } @@ -3723,7 +3745,7 @@ func (ms *MutableStateImpl) saveDeploymentSearchAttributes(deployment, version, if behavior == "" { saPayloads[sadefs.TemporalWorkflowVersioningBehavior] = nil } else { - behaviorPayload, err := searchattribute.EncodeValue(behavior, enumspb.INDEXED_VALUE_TYPE_KEYWORD) + behaviorPayload, err := sadefs.EncodeValue(behavior, enumspb.INDEXED_VALUE_TYPE_KEYWORD) if err != nil { return err } @@ -4431,7 +4453,7 @@ func (ms *MutableStateImpl) AddActivityTaskCanceledEvent( tag.WorkflowEventID(ms.GetNextEventID()), tag.ErrorTypeInvalidHistoryAction, tag.WorkflowScheduledEventID(scheduledEventID), - tag.WorkflowActivityID(ai.ActivityId), + tag.ActivityID(ai.ActivityId), tag.WorkflowStartedEventID(ai.StartedEventId)) return nil, ms.createInternalServerError(opTag) } @@ -6435,7 +6457,7 @@ func (ms *MutableStateImpl) buildTemporalPauseInfoEntries() []string { func (ms *MutableStateImpl) updatePauseInfoSearchAttribute() error { allEntries := ms.buildTemporalPauseInfoEntries() - pauseInfoPayload, err := searchattribute.EncodeValue(allEntries, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) + pauseInfoPayload, err := sadefs.EncodeValue(allEntries, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) if err != nil { return err } @@ -6468,7 +6490,7 @@ func (ms *MutableStateImpl) UpdateReportedProblemsSearchAttribute() error { } } - reportedProblemsPayload, err := searchattribute.EncodeValue(reportedProblems, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) + reportedProblemsPayload, err := sadefs.EncodeValue(reportedProblems, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST) if err != nil { return err } @@ -6478,7 +6500,7 @@ func (ms *MutableStateImpl) UpdateReportedProblemsSearchAttribute() error { exeInfo.SearchAttributes = make(map[string]*commonpb.Payload, 1) } - decodedA, err := searchattribute.DecodeValue(exeInfo.SearchAttributes[sadefs.TemporalReportedProblems], enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, false) + decodedA, err := sadefs.DecodeValue(exeInfo.SearchAttributes[sadefs.TemporalReportedProblems], enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, false) if err != nil { return err } @@ -6544,7 +6566,7 @@ func (ms *MutableStateImpl) decodeReportedProblems(p *commonpb.Payload) []string return nil } - decoded, err := searchattribute.DecodeValue(p, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, false) + decoded, err := sadefs.DecodeValue(p, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, false) if err != nil { ms.logger.Error("Failed to decode TemporalReportedProblems payload for logging", tag.Error(err)) @@ -6685,6 +6707,12 @@ func (ms *MutableStateImpl) AddTasks( ) { now := ms.timeSource.Now() for _, task := range newTasks { + if chasmTask, ok := task.(*tasks.ChasmTask); ok && + chasmTask.GetCategory() == tasks.CategoryVisibility && + ms.stateInDB == enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED { + softassert.Fail(ms.logger, "CHASM visibility task added on already-closed execution") + } + category := task.GetCategory() if category.Type() == tasks.CategoryTypeScheduled && task.GetVisibilityTime().Sub(now) > maxScheduledTaskDuration { @@ -7074,6 +7102,27 @@ func (ms *MutableStateImpl) closeTransaction( return closeTransactionResult{}, err } + // Stamp events with the caller's principal. Only do this on the active + // cluster — standby (passive) replays events that were already stamped by + // the active side, and we must not overwrite those principals. + if transactionPolicy == historyi.TransactionPolicyActive { + principal := headers.GetPrincipal(ctx) + for _, we := range workflowEventsSeq { + for _, event := range we.Events { + // Skip events that already have a principal. Those are previously + // buffered events (e.g., signals) that were stamped when originally + // created and are now being flushed into history by a different caller + // (e.g., the worker completing a workflow task). + if event.Principal == nil { + event.Principal = principal + } + } + } + for _, event := range bufferEvents { + event.Principal = principal + } + } + // CloseTransaction() on chasmTree may update execution state & status, // so must be called before closeTransactionUpdateTransitionHistory(). chasmNodesMutation, err := ms.chasmTree.CloseTransaction() @@ -7874,7 +7923,7 @@ func (ms *MutableStateImpl) dirtyHSMToReplicationTask( return emptyTasks } - // HSM() contains children also implies Nexus is enabled + // Skip if there are no HSM children (no outbound tasks to generate) if len(ms.HSM().InternalRepr().Children) == 0 { return emptyTasks } @@ -8745,12 +8794,12 @@ func (ms *MutableStateImpl) syncExecutionInfo(current *persistencespb.WorkflowEx } } - doNotSync := func(v any) []interface{} { + doNotSync := func(v any) []any { info, ok := v.(*persistencespb.WorkflowExecutionInfo) if !ok || info == nil { return nil } - ignoreFields := []interface{}{ + ignoreFields := []any{ &info.WorkflowTaskVersion, &info.WorkflowTaskScheduledEventId, &info.WorkflowTaskStartedEventId, diff --git a/service/history/workflow/mutable_state_impl_test.go b/service/history/workflow/mutable_state_impl_test.go index 62e48a43ed3..f5a5e441dcf 100644 --- a/service/history/workflow/mutable_state_impl_test.go +++ b/service/history/workflow/mutable_state_impl_test.go @@ -38,13 +38,13 @@ import ( "go.temporal.io/server/common/definition" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/failure" + "go.temporal.io/server/common/headers" "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/payloads" "go.temporal.io/server/common/persistence/transitionhistory" "go.temporal.io/server/common/persistence/versionhistory" "go.temporal.io/server/common/primitives/timestamp" - "go.temporal.io/server/common/searchattribute" "go.temporal.io/server/common/searchattribute/sadefs" serviceerror2 "go.temporal.io/server/common/serviceerror" "go.temporal.io/server/common/testing/fakedata" @@ -1414,7 +1414,7 @@ func (s *mutableStateSuite) TestChecksumProbabilities() { for _, prob := range []int{0, 100} { s.mockConfig.MutableStateChecksumGenProbability = func(namespace string) int { return prob } s.mockConfig.MutableStateChecksumVerifyProbability = func(namespace string) int { return prob } - for i := 0; i < 100; i++ { + for range 100 { shouldGenerate := s.mutableState.shouldGenerateChecksum() shouldVerify := s.mutableState.shouldVerifyChecksum() s.Equal(prob == 100, shouldGenerate) @@ -3919,7 +3919,7 @@ func (s *mutableStateSuite) getBuildIdsFromMutableState() []string { if !found { return []string{} } - decoded, err := searchattribute.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) + decoded, err := sadefs.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) s.NoError(err) buildIDs, ok := decoded.([]string) s.True(ok) @@ -3931,7 +3931,7 @@ func (s *mutableStateSuite) getUsedDeploymentVersionsFromMutableState() []string if !found { return []string{} } - decoded, err := searchattribute.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) + decoded, err := sadefs.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) s.NoError(err) usedDeploymentVersions, ok := decoded.([]string) s.True(ok) @@ -5024,12 +5024,12 @@ func (s *mutableStateSuite) TestExecutionInfoClone() { } clone.NamespaceId = "namespace-id" clone.WorkflowId = "workflow-id" - err := common.MergeProtoExcludingFields(s.mutableState.executionInfo, clone, func(v any) []interface{} { + err := common.MergeProtoExcludingFields(s.mutableState.executionInfo, clone, func(v any) []any { info, ok := v.(*persistencespb.WorkflowExecutionInfo) if !ok || info == nil { return nil } - return []interface{}{ + return []any{ &info.NamespaceId, } }) @@ -5983,7 +5983,7 @@ func (s *mutableStateSuite) TestAddTasks_CHASMPureTask() { totalTasks := 2 * s.mockConfig.ChasmMaxInMemoryPureTasks() visTimestamp := s.mockShard.GetTimeSource().Now() - for i := 0; i < totalTasks; i++ { + for range totalTasks { task := &tasks.ChasmTaskPure{ VisibilityTimestamp: visTimestamp, } @@ -6225,3 +6225,142 @@ func (s *mutableStateSuite) TestAddActivityTaskStartedEventStoresWorkerControlTa s.True(ok) s.Equal(expectedWorkerControlTaskQueue, updatedActivityInfo.WorkerControlTaskQueue) } + +func (s *mutableStateSuite) TestCloseTransaction_PrincipalStamped() { + for _, tc := range []struct { + name string + policy historyi.TransactionPolicy + }{ + {"Active", historyi.TransactionPolicyActive}, + {"Passive", historyi.TransactionPolicyPassive}, + } { + s.Run(tc.name, func() { + namespaceEntry := tests.GlobalNamespaceEntry + s.mockEventsCache.EXPECT().PutEvent(gomock.Any(), gomock.Any()).AnyTimes() + + dbState := s.buildWorkflowMutableState() + dbState.BufferedEvents = nil + + var err error + s.mutableState, err = NewMutableStateFromDB(s.mockShard, s.mockEventsCache, s.logger, namespaceEntry, dbState, 123) + s.NoError(err) + err = s.mutableState.UpdateCurrentVersion(namespaceEntry.FailoverVersion(tests.WorkflowID), false) + s.NoError(err) + + // Complete the workflow task to generate events in workflowEventsSeq. + workflowTaskInfo := s.mutableState.GetStartedWorkflowTask() + _, err = s.mutableState.AddWorkflowTaskCompletedEvent( + workflowTaskInfo, + &workflowservice.RespondWorkflowTaskCompletedRequest{}, + workflowTaskCompletionLimits, + ) + s.NoError(err) + + // Close the transaction with a principal in context. + principal := &commonpb.Principal{Type: "user", Name: "alice"} + ctx := headers.SetPrincipal(context.Background(), principal) + _, eventsSeq, err := s.mutableState.CloseTransactionAsMutation(ctx, tc.policy) + s.NoError(err) + + s.NotEmpty(eventsSeq) + for _, we := range eventsSeq { + for _, event := range we.Events { + if tc.policy == historyi.TransactionPolicyActive { + // Active: all events should be stamped with the caller's principal. + s.Equal("user", event.Principal.GetType(), "event %s should have principal type 'user'", event.EventType) + s.Equal("alice", event.Principal.GetName(), "event %s should have principal name 'alice'", event.EventType) + } else { + // Passive: events must not be stamped + s.Nil(event.Principal, "event %s should not have principal stamped in passive mode", event.EventType) + } + } + } + }) + } +} + +func (s *mutableStateSuite) TestCloseTransaction_PrincipalPreserved() { + namespaceEntry := tests.GlobalNamespaceEntry + s.mockEventsCache.EXPECT().PutEvent(gomock.Any(), gomock.Any()).AnyTimes() + + dbState := s.buildWorkflowMutableState() + + var err error + s.mutableState, err = NewMutableStateFromDB(s.mockShard, s.mockEventsCache, s.logger, namespaceEntry, dbState, 123) + s.NoError(err) + err = s.mutableState.UpdateCurrentVersion(namespaceEntry.FailoverVersion(tests.WorkflowID), false) + s.NoError(err) + + s.mockShard.Resource.ClusterMetadata.EXPECT().GetCurrentClusterName().Return(cluster.TestCurrentClusterName).AnyTimes() + + // Transaction 1: First signal arrives while a workflow task is started. + // The signal gets buffered and stamped with alice's principal. + _, err = s.mutableState.AddWorkflowExecutionSignaledEvent( + "signal-from-alice", + &commonpb.Payloads{}, + "alice-identity", + &commonpb.Header{}, + nil, + nil, + ) + s.NoError(err) + + aliceCtx := headers.SetPrincipal(context.Background(), &commonpb.Principal{Type: "user", Name: "alice"}) + mutation, _, err := s.mutableState.CloseTransactionAsMutation(aliceCtx, historyi.TransactionPolicyActive) + s.NoError(err) + s.Len(mutation.NewBufferedEvents, 1) + s.Equal("alice", mutation.NewBufferedEvents[0].Principal.GetName()) + + // Transaction 2: Second signal arrives from a different caller. + // It gets buffered and stamped with bob's principal. Alice's buffered + // signal must not be overwritten. + _, err = s.mutableState.AddWorkflowExecutionSignaledEvent( + "signal-from-bob", + &commonpb.Payloads{}, + "bob-identity", + &commonpb.Header{}, + nil, + nil, + ) + s.NoError(err) + + bobCtx := headers.SetPrincipal(context.Background(), &commonpb.Principal{Type: "user", Name: "bob"}) + mutation, _, err = s.mutableState.CloseTransactionAsMutation(bobCtx, historyi.TransactionPolicyActive) + s.NoError(err) + s.Len(mutation.NewBufferedEvents, 1) + s.Equal("bob", mutation.NewBufferedEvents[0].Principal.GetName()) + + // Transaction 3: A worker completes the workflow task. Both buffered + // signals are flushed into history. Each should retain its original + // principal. + workflowTaskInfo := s.mutableState.GetStartedWorkflowTask() + _, err = s.mutableState.AddWorkflowTaskCompletedEvent( + workflowTaskInfo, + &workflowservice.RespondWorkflowTaskCompletedRequest{}, + workflowTaskCompletionLimits, + ) + s.NoError(err) + + workerCtx := headers.SetPrincipal(context.Background(), &commonpb.Principal{Type: "worker", Name: "worker-1"}) + _, eventsSeq, err := s.mutableState.CloseTransactionAsMutation(workerCtx, historyi.TransactionPolicyActive) + s.NoError(err) + + s.NotEmpty(eventsSeq) + principalBySignalName := map[string]string{} + foundWorkerEvent := false + for _, we := range eventsSeq { + for _, event := range we.Events { + if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED { + signalName := event.GetWorkflowExecutionSignaledEventAttributes().GetSignalName() + principalBySignalName[signalName] = event.Principal.GetName() + } else { + foundWorkerEvent = true + s.Equal("worker", event.Principal.GetType(), "event %s should have worker's principal type", event.EventType) + s.Equal("worker-1", event.Principal.GetName(), "event %s should have worker's principal name", event.EventType) + } + } + } + s.True(foundWorkerEvent, "expected to find non-signal events in workflowEventsSeq") + s.Equal("alice", principalBySignalName["signal-from-alice"], "alice's signal should retain her principal") + s.Equal("bob", principalBySignalName["signal-from-bob"], "bob's signal should retain his principal") +} diff --git a/service/history/workflow/noop_chasm_tree.go b/service/history/workflow/noop_chasm_tree.go index cfbb3c45397..4f914425436 100644 --- a/service/history/workflow/noop_chasm_tree.go +++ b/service/history/workflow/noop_chasm_tree.go @@ -25,6 +25,10 @@ func (*noopChasmTree) Snapshot(*persistencespb.VersionedTransition) chasm.NodesS return chasm.NodesSnapshot{} } +func (*noopChasmTree) ApplySystemMutation(chasm.NodesMutation) error { + return nil +} + func (*noopChasmTree) ApplyMutation(chasm.NodesMutation) error { return nil } @@ -78,7 +82,15 @@ func (*noopChasmTree) ComponentByPath(chasm.Context, []string) (chasm.Component, func (*noopChasmTree) ExecuteSideEffectTask( ctx context.Context, - registry *chasm.Registry, + executionKey chasm.ExecutionKey, + task *tasks.ChasmTask, + validate func(chasm.NodeBackend, chasm.Context, chasm.Component) error, +) error { + return nil +} + +func (*noopChasmTree) ExecuteSideEffectDiscardTask( + ctx context.Context, executionKey chasm.ExecutionKey, task *tasks.ChasmTask, validate func(chasm.NodeBackend, chasm.Context, chasm.Component) error, diff --git a/service/history/workflow/query_registry_test.go b/service/history/workflow/query_registry_test.go index 77d2a2ac79c..e626be86cd2 100644 --- a/service/history/workflow/query_registry_test.go +++ b/service/history/workflow/query_registry_test.go @@ -29,7 +29,7 @@ func (s *QueryRegistrySuite) TestQueryRegistry() { qr := NewQueryRegistry() ids := make([]string, 100) completionChs := make([]<-chan struct{}, 100) - for i := 0; i < 100; i++ { + for i := range 100 { ids[i], completionChs[i] = qr.BufferQuery(&querypb.WorkflowQuery{}) } s.assertBufferedState(qr, ids...) @@ -37,7 +37,7 @@ func (s *QueryRegistrySuite) TestQueryRegistry() { s.assertQuerySizes(qr, 100, 0, 0, 0) s.assertChanState(false, completionChs...) - for i := 0; i < 25; i++ { + for i := range 25 { err := qr.SetCompletionState(ids[i], &historyi.QueryCompletionState{ Type: QueryCompletionTypeSucceeded, Result: &querypb.WorkflowQueryResult{ @@ -84,7 +84,7 @@ func (s *QueryRegistrySuite) TestQueryRegistry() { s.assertChanState(true, completionChs[0:75]...) s.assertChanState(false, completionChs[75:]...) - for i := 0; i < 75; i++ { + for i := range 75 { switch i % 3 { case 0: s.Equal(errQueryNotExists, qr.SetCompletionState(ids[i], &historyi.QueryCompletionState{ @@ -111,7 +111,7 @@ func (s *QueryRegistrySuite) TestQueryRegistry() { s.assertChanState(true, completionChs[0:75]...) s.assertChanState(false, completionChs[75:]...) - for i := 0; i < 25; i++ { + for i := range 25 { qr.RemoveQuery(ids[i]) s.assertHasQueries(qr, true, i < 24, true, true) s.assertQuerySizes(qr, 25, 25-i-1, 25, 25) diff --git a/service/history/workflow/retry.go b/service/history/workflow/retry.go index 66f8348f9e4..fe40763030d 100644 --- a/service/history/workflow/retry.go +++ b/service/history/workflow/retry.go @@ -321,9 +321,11 @@ func SetupNewWorkflowForRetryOrCron( Attempt: attempt, SourceVersionStamp: sourceVersionStamp, RootExecutionInfo: rootInfo, - InheritedBuildId: startAttr.InheritedBuildId, + InheritedBuildId: startAttr.InheritedBuildId, //nolint:staticcheck InheritedPinnedVersion: inheritedPinnedVersion, InheritedAutoUpgradeInfo: inheritedAutoUpgradeInfo, + // For retries, pass through the declined value from the started event directly. + DeclinedTargetVersionUpgrade: startAttr.GetDeclinedTargetVersionUpgrade(), } workflowTimeoutTime := timestamp.TimeValue(previousExecutionInfo.WorkflowExecutionExpirationTime) if !workflowTimeoutTime.IsZero() { diff --git a/service/history/workflow/task_generator.go b/service/history/workflow/task_generator.go index 47288f6ecec..3bb1d19cb3a 100644 --- a/service/history/workflow/task_generator.go +++ b/service/history/workflow/task_generator.go @@ -14,7 +14,6 @@ import ( historyspb "go.temporal.io/server/api/history/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/chasm" - "go.temporal.io/server/chasm/lib/activity" "go.temporal.io/server/common" "go.temporal.io/server/common/archiver" "go.temporal.io/server/common/backoff" @@ -278,11 +277,6 @@ func (r *TaskGeneratorImpl) GenerateWorkflowCloseTasks( // This method returns an error when the GetNamespaceByID call fails with anything other than // serviceerror.NamespaceNotFound. func (r *TaskGeneratorImpl) getRetention() (time.Duration, error) { - // For standalone activities, use 1 day retention - if r.mutableState.ChasmTree().ArchetypeID() == activity.ArchetypeID { - return 24 * time.Hour, nil - } - retention := defaultWorkflowRetention executionInfo := r.mutableState.GetExecutionInfo() namespaceEntry, err := r.namespaceRegistry.GetNamespaceByID(namespace.ID(executionInfo.NamespaceId)) @@ -861,13 +855,11 @@ func (r *TaskGeneratorImpl) GenerateMigrationTasks(targetClusters []string) ([]t activityIDs, targetClusters, )...) - if r.config.EnableNexus() { - taskEquivalents = append(taskEquivalents, &tasks.SyncHSMTask{ - WorkflowKey: workflowKey, - // TaskID and VisibilityTimestamp are set by shard - TargetClusters: targetClusters, - }) - } + taskEquivalents = append(taskEquivalents, &tasks.SyncHSMTask{ + WorkflowKey: workflowKey, + // TaskID and VisibilityTimestamp are set by shard + TargetClusters: targetClusters, + }) } if r.mutableState.IsTransitionHistoryEnabled() && diff --git a/service/history/workflow/task_generator_test.go b/service/history/workflow/task_generator_test.go index 90cb03f4d56..94d9548ac7d 100644 --- a/service/history/workflow/task_generator_test.go +++ b/service/history/workflow/task_generator_test.go @@ -921,7 +921,7 @@ func TestTaskGeneratorImpl_GenerateDirtySubStateMachineTasks_TrimsTimersForDelet require.Empty(t, ms.GetExecutionInfo().StateMachineTimers) // Timer should be trimmed } -func TestTaskGeneratorImpl_GenerateDeleteHistoryEventTask_ActivityRetention(t *testing.T) { +func TestTaskGeneratorImpl_GenerateDeleteHistoryEventTask_ChasmComponentRetention(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) @@ -931,25 +931,36 @@ func TestTaskGeneratorImpl_GenerateDeleteHistoryEventTask_ActivityRetention(t *t testCases := []struct { name string archetypeID chasm.ArchetypeID - namespaceRetention time.Duration expectedMinRetention time.Duration expectedMaxRetention time.Duration setupNamespaceRegistry func(*namespace.MockRegistry) }{ { - name: "standalone activity uses 1 day retention", + name: "standalone activity uses namespace retention", archetypeID: activity.ArchetypeID, - namespaceRetention: 90 * 24 * time.Hour, // 90 days namespace retention - expectedMinRetention: 24 * time.Hour, // Activity should use 1 day - expectedMaxRetention: 24*time.Hour + retentionJitterDuration*2, + expectedMinRetention: 90 * 24 * time.Hour, + expectedMaxRetention: 90*24*time.Hour + retentionJitterDuration*2, setupNamespaceRegistry: func(nr *namespace.MockRegistry) { - // Namespace registry should not be called for activities + namespaceConfig := &persistencespb.NamespaceConfig{ + Retention: durationpb.New(90 * 24 * time.Hour), + } + namespaceEntry := namespace.NewGlobalNamespaceForTest( + &persistencespb.NamespaceInfo{Id: tests.NamespaceID.String(), Name: tests.Namespace.String()}, + namespaceConfig, + &persistencespb.NamespaceReplicationConfig{ + ActiveClusterName: cluster.TestCurrentClusterName, + Clusters: []string{ + cluster.TestCurrentClusterName, + }, + }, + tests.Version, + ) + nr.EXPECT().GetNamespaceByID(namespaceEntry.ID()).Return(namespaceEntry, nil).AnyTimes() }, }, { name: "workflow uses namespace retention", archetypeID: chasm.WorkflowArchetypeID, - namespaceRetention: 7 * 24 * time.Hour, // 7 days namespace retention expectedMinRetention: 7 * 24 * time.Hour, expectedMaxRetention: 7*24*time.Hour + retentionJitterDuration*2, setupNamespaceRegistry: func(nr *namespace.MockRegistry) { @@ -973,7 +984,6 @@ func TestTaskGeneratorImpl_GenerateDeleteHistoryEventTask_ActivityRetention(t *t { name: "scheduler uses namespace retention", archetypeID: chasm.SchedulerArchetypeID, - namespaceRetention: 30 * 24 * time.Hour, // 30 days namespace retention expectedMinRetention: 30 * 24 * time.Hour, expectedMaxRetention: 30*24*time.Hour + retentionJitterDuration*2, setupNamespaceRegistry: func(nr *namespace.MockRegistry) { diff --git a/service/history/workflow/task_refresher.go b/service/history/workflow/task_refresher.go index 0523a88aed1..b9315cfc00a 100644 --- a/service/history/workflow/task_refresher.go +++ b/service/history/workflow/task_refresher.go @@ -68,10 +68,8 @@ func (r *TaskRefresherImpl) Refresh( mutableState historyi.MutableState, shouldSkipGeneratingCloseTransferTask bool, ) error { - if r.shard.GetConfig().EnableNexus() { - // Invalidate all tasks generated for this mutable state before the refresh. - mutableState.GetExecutionInfo().TaskGenerationShardClockTimestamp = r.shard.CurrentVectorClock().GetClock() - } + // Invalidate all tasks generated for this mutable state before the refresh. + mutableState.GetExecutionInfo().TaskGenerationShardClockTimestamp = r.shard.CurrentVectorClock().GetClock() if err := r.PartialRefresh(ctx, mutableState, EmptyVersionedTransition, nil, shouldSkipGeneratingCloseTransferTask); err != nil { return err diff --git a/service/history/workflow/update/registry.go b/service/history/workflow/update/registry.go index 59309afcd4a..73d9b64b8bf 100644 --- a/service/history/workflow/update/registry.go +++ b/service/history/workflow/update/registry.go @@ -136,6 +136,13 @@ func WithTotalLimitSuggestCAN(f func() float64) Option { } } +// WithNamespace sets the namespace name to be used in Registry metrics and logs. +func WithNamespace(ns string) Option { + return func(r *registry) { + r.instrumentation.namespace = ns + } +} + // WithLogger sets the log.Logger to be used by Registry and its Updates. func WithLogger(l log.Logger) Option { return func(r *registry) { diff --git a/service/history/workflow/update/util.go b/service/history/workflow/update/util.go index 034e4e117c3..40531146f96 100644 --- a/service/history/workflow/update/util.go +++ b/service/history/workflow/update/util.go @@ -14,9 +14,10 @@ import ( type ( instrumentation struct { - log log.Logger - metrics metrics.Handler - tracer trace.Tracer + log log.Logger + metrics metrics.Handler + tracer trace.Tracer + namespace string } ) @@ -50,11 +51,12 @@ func (i *instrumentation) countRateLimited() { func (i *instrumentation) countRegistrySizeLimited(updateCount, registrySize, payloadSize int) { i.oneOf(metrics.WorkflowExecutionUpdateRegistrySizeLimited.Name()) - // TODO: remove log once limit is enforced everywhere i.log.Warn("update registry size limit reached", tag.Int("registry-size", registrySize), tag.Int("payload-size", payloadSize), - tag.Int("update-count", updateCount)) + tag.Int("update-count", updateCount), + tag.String("namespace", i.namespace), + ) } func (i *instrumentation) countTooMany() { @@ -79,7 +81,6 @@ func (i *instrumentation) countSentAgain() { } func (i *instrumentation) invalidStateTransition(updateID string, msg proto.Message, state state) { - i.oneOf(metrics.InvalidStateTransitionWorkflowExecutionUpdateCounter.Name()) softassert.Fail( i.log, "invalid state transition attempted", @@ -87,6 +88,7 @@ func (i *instrumentation) invalidStateTransition(updateID string, msg proto.Mess tag.String("update-id", updateID), tag.String("message", fmt.Sprintf("%T", msg)), tag.Stringer("state", state), + tag.String("namespace", i.namespace), ) } diff --git a/service/history/workflow/workflow_task_state_machine.go b/service/history/workflow/workflow_task_state_machine.go index 186c0b4b7a5..0d94316c657 100644 --- a/service/history/workflow/workflow_task_state_machine.go +++ b/service/history/workflow/workflow_task_state_machine.go @@ -491,17 +491,41 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskStartedEvent( suggestContinueAsNewReasons = append(suggestContinueAsNewReasons, enumspb.SUGGEST_CONTINUE_AS_NEW_REASON_TOO_MANY_UPDATES) } - // checking whether targetDeploymentVersion == nil means that we won't send the targetDeploymentVersionChanged=true - // to workflows that are about to transition to the target version. This is good because if their transition succeeds, - // they don't need to CaN-with-upgrade to start using the new version. var targetDeploymentVersionChanged bool if m.ms.config.EnableSendTargetVersionChanged(m.ms.namespaceEntry.Name().String()) && - m.ms.GetEffectiveVersioningBehavior() != enumspb.VERSIONING_BEHAVIOR_UNSPECIFIED && - targetDeploymentVersion != nil { - if currentDeploymentVersion := m.ms.GetEffectiveDeployment(); currentDeploymentVersion != nil && - (currentDeploymentVersion.BuildId != targetDeploymentVersion.BuildId || - currentDeploymentVersion.SeriesName != targetDeploymentVersion.DeploymentName) { + m.ms.GetEffectiveVersioningBehavior() != enumspb.VERSIONING_BEHAVIOR_UNSPECIFIED { + + // effectiveDeploymentVersion may be nil if the workflow is on an unversioned build; + // in that case proto getters return zero values and we correctly fall through to signal. + effectiveDeploymentVersion := worker_versioning.ExternalWorkerDeploymentVersionFromDeployment(m.ms.GetEffectiveDeployment()) + + switch { + // 1. Override active — operator controls version, don't signal. Clear any stale declined/notified state so that + // when/if the operator removes the override, we re-calculate the declined/notified state and appropriately fire the + // signal to upgrade to the version. + case m.ms.executionInfo.GetVersioningInfo().GetVersioningOverride() != nil: + m.ms.executionInfo.DeclinedTargetVersionUpgrade = nil + m.ms.executionInfo.LastNotifiedTargetVersion = nil + // 2. AutoUpgrade — will transition naturally, no CaN needed. + case m.ms.GetEffectiveVersioningBehavior() == enumspb.VERSIONING_BEHAVIOR_AUTO_UPGRADE: + // Rest of the checks are guaranteed to have the Workflow's Effective Versioning Behavior to be Pinned in nature. + // 3. Already on target — nothing changed. Clear any stale declined/notified state. + case effectiveDeploymentVersion.GetBuildId() == targetDeploymentVersion.GetBuildId() && + effectiveDeploymentVersion.GetDeploymentName() == targetDeploymentVersion.GetDeploymentName(): + // TODO (Shivam): Revision number mechanics to strengthen this check + m.ms.executionInfo.DeclinedTargetVersionUpgrade = nil + m.ms.executionInfo.LastNotifiedTargetVersion = nil + // 4. Previously declined upgrade — target unchanged since the decline. + case m.ms.executionInfo.GetDeclinedTargetVersionUpgrade() != nil && + m.ms.executionInfo.GetDeclinedTargetVersionUpgrade().GetDeploymentVersion().GetBuildId() == targetDeploymentVersion.GetBuildId() && + m.ms.executionInfo.GetDeclinedTargetVersionUpgrade().GetDeploymentVersion().GetDeploymentName() == targetDeploymentVersion.GetDeploymentName(): + default: + // Otherwise — target changed + did not decline to upgrade on CaN/retry. Signal the SDK. targetDeploymentVersionChanged = true + m.ms.executionInfo.LastNotifiedTargetVersion = &persistencespb.LastNotifiedTargetVersion{ + DeploymentVersion: targetDeploymentVersion, + } + m.ms.executionInfo.DeclinedTargetVersionUpgrade = nil } } // emit metric @@ -750,6 +774,14 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskCompletedEvent( ) workflowTask.ScheduledEventID = scheduledEvent.GetEventId() + //nolint:staticcheck // SA1019 + versioningStamp := request.GetWorkerVersionStamp() + if versioningStamp.GetUseVersioning() && m.ms.GetAssignedBuildId() == "" { + // WV2 is not used. making sure the versioning stamp does not go through otherwise the + // workflow will start using WV2 which can cause issues. + // TODO: remove this block after deleting old wv [cleanup-old-wv] + versioningStamp = nil + } startedEvent := m.ms.hBuilder.AddWorkflowTaskStartedEvent( workflowTask.ScheduledEventID, workflowTask.RequestID, @@ -757,7 +789,7 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskCompletedEvent( workflowTask.StartedTime, workflowTask.SuggestContinueAsNew, workflowTask.HistorySizeBytes, - request.WorkerVersionStamp, + versioningStamp, workflowTask.BuildIdRedirectCounter, workflowTask.SuggestContinueAsNewReasons, workflowTask.TargetWorkerDeploymentVersionChanged, @@ -1159,7 +1191,7 @@ func (m *workflowTaskStateMachine) GetTransientWorkflowTaskInfo( identity string, ) *historyspb.TransientWorkflowTaskInfo { - // Create scheduled and started events which are not written to the history yet. + // Create scheduled event which is not written to the history yet. scheduledEvent := &historypb.HistoryEvent{ EventId: workflowTask.ScheduledEventID, EventTime: timestamppb.New(workflowTask.ScheduledTime), @@ -1174,6 +1206,15 @@ func (m *workflowTaskStateMachine) GetTransientWorkflowTaskInfo( }, } + // Check if WFT is started + if workflowTask.StartedEventID == common.EmptyEventID { + // WFT only scheduled, not started yet + return &historyspb.TransientWorkflowTaskInfo{ + HistorySuffix: []*historypb.HistoryEvent{scheduledEvent}, + } + } + + // WFT both scheduled and started var versioningStamp *commonpb.WorkerVersionStamp if workflowTask.BuildId != "" { // fill out the stamp value of the transient WFT based on MS data diff --git a/service/history/workflow_rebuilder.go b/service/history/workflow_rebuilder.go index 2f16284679c..60b1d77f958 100644 --- a/service/history/workflow_rebuilder.go +++ b/service/history/workflow_rebuilder.go @@ -28,7 +28,6 @@ type ( branchToken []byte stateTransitionCount int64 dbRecordVersion int64 - requestID string mutableState *persistencespb.WorkflowMutableState } workflowRebuilder interface { @@ -96,7 +95,6 @@ func (r *workflowRebuilderImpl) rebuild( rebuildSpec.branchToken, rebuildSpec.stateTransitionCount, rebuildSpec.dbRecordVersion, - rebuildSpec.requestID, rebuildSpec.mutableState, ) if err != nil { @@ -196,7 +194,6 @@ func (r *workflowRebuilderImpl) getRebuildSpecFromMutableState( branchToken: currentVersionHistory.BranchToken, stateTransitionCount: mutableState.ExecutionInfo.StateTransitionCount, dbRecordVersion: resp.DBRecordVersion, - requestID: mutableState.ExecutionState.CreateRequestId, mutableState: resp.State, }, nil } @@ -207,7 +204,6 @@ func (r *workflowRebuilderImpl) replayResetWorkflow( branchToken []byte, stateTransitionCount int64, dbRecordVersion int64, - requestID string, mutableState *persistencespb.WorkflowMutableState, ) (historyi.MutableState, error) { rebuildMutableState, rebuildStats, err := ndc.NewStateRebuilder(r.shard, r.logger).RebuildWithCurrentMutableState( @@ -219,7 +215,6 @@ func (r *workflowRebuilderImpl) replayResetWorkflow( nil, // skip event ID & version check workflowKey, branchToken, - requestID, mutableState, ) if err != nil { diff --git a/service/matching/ack_manager_test.go b/service/matching/ack_manager_test.go index 64cdcd999a3..d22a8438f2a 100644 --- a/service/matching/ack_manager_test.go +++ b/service/matching/ack_manager_test.go @@ -180,7 +180,7 @@ func BenchmarkAckManager_AddTask(b *testing.B) { ackMgr := newTestAckMgr(log.NewTestLogger()) tasks := make([]int, 1000) - for i := 0; i < len(tasks); i++ { + for i := range tasks { tasks[i] = i } b.ResetTimer() @@ -192,7 +192,7 @@ func BenchmarkAckManager_AddTask(b *testing.B) { tasks[i], tasks[j] = tasks[j], tasks[i] }) b.StartTimer() - for i := 0; i < len(tasks); i++ { + for i := range tasks { tasks[i] = i ackMgr.addTask(int64(i)) } @@ -208,7 +208,7 @@ func BenchmarkAckManager_CompleteTask(b *testing.B) { // Add 1000 tasks in order and complete them in a random order. // This will cause our ack level to jump as we complete them b.StopTimer() - for i := 0; i < len(tasks); i++ { + for i := range tasks { tasks[i] = i ackMgr.addTask(int64(i)) ackMgr.db.updateBacklogStats(1, time.Time{}) // Increment the backlog so that we don't under-count @@ -218,7 +218,7 @@ func BenchmarkAckManager_CompleteTask(b *testing.B) { }) b.StartTimer() - for i := 0; i < len(tasks); i++ { + for i := range tasks { ackMgr.completeTask(int64(i)) } } diff --git a/service/matching/backlog_manager_test.go b/service/matching/backlog_manager_test.go index 0ffa852d8b6..63f3c2c1012 100644 --- a/service/matching/backlog_manager_test.go +++ b/service/matching/backlog_manager_test.go @@ -7,6 +7,7 @@ import ( "maps" "math" "math/rand" + "slices" "sync" "sync/atomic" "testing" @@ -18,6 +19,7 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/primitives/timestamp" testutil "go.temporal.io/server/common/testing" "go.temporal.io/server/common/testing/testlogger" @@ -41,6 +43,9 @@ type BacklogManagerTestSuite struct { cancelCtx context.CancelFunc taskMgr *testTaskManager ptqMgr *MockphysicalTaskQueueManager + + capturedTasksLock sync.Mutex + capturedTasksSlice []*internalTask } func TestBacklogManager_Classic_Suite(t *testing.T) { @@ -123,6 +128,27 @@ func (s *BacklogManagerTestSuite) SetupTest() { } } +func (s *BacklogManagerTestSuite) setupToCaptureTasks() { + s.ptqMgr.EXPECT().AddSpooledTask(gomock.Any()).DoAndReturn(func(t *internalTask) error { + s.capturedTasksLock.Lock() + defer s.capturedTasksLock.Unlock() + s.capturedTasksSlice = append(s.capturedTasksSlice, t) + return nil + }).AnyTimes() +} + +func (s *BacklogManagerTestSuite) capturedTasksLen() int { + s.capturedTasksLock.Lock() + defer s.capturedTasksLock.Unlock() + return len(s.capturedTasksSlice) +} + +func (s *BacklogManagerTestSuite) capturedTasks() []*internalTask { + s.capturedTasksLock.Lock() + defer s.capturedTasksLock.Unlock() + return slices.Clone(s.capturedTasksSlice) +} + func (s *BacklogManagerTestSuite) TestReadLevelForAllExpiredTasksInBatch() { if s.newMatcher { s.T().Skip("not compatible with new backlog manager") @@ -291,7 +317,7 @@ func (s *BacklogManagerTestSuite) TestApproximateBacklogCount_IncrementedBySpool taskCount := 10 s.ptqMgr.EXPECT().AddSpooledTask(gomock.Any()).Return(nil).AnyTimes() - for i := 0; i < taskCount; i++ { + for range taskCount { s.NoError(s.blm.SpoolTask(&persistencespb.TaskInfo{ ExpiryTime: timestamp.TimeNowPtrUtcAddSeconds(3000), CreateTime: timestamp.TimeNowPtrUtc(), @@ -318,7 +344,7 @@ func (s *BacklogManagerTestSuite) TestApproximateBacklogCount_IncrementedBySpool taskCount := 10 s.ptqMgr.EXPECT().AddSpooledTask(gomock.Any()).Return(nil).AnyTimes() - for i := 0; i < taskCount; i++ { + for range taskCount { s.Error(s.blm.SpoolTask(&persistencespb.TaskInfo{ ExpiryTime: timestamp.TimeNowPtrUtcAddSeconds(3000), CreateTime: timestamp.TimeNowPtrUtc(), @@ -355,6 +381,168 @@ func (s *BacklogManagerTestSuite) TestApproximateBacklogCount_NotIncrementedBySp "backlog count should not be incremented") } +func (s *BacklogManagerTestSuite) TestApproximateBacklogCount_ResetOnDrained() { + if !s.newMatcher || s.fairness { + s.T().Skip("only for priority backlog manager") + } + + blm := s.blm.(*priBacklogManagerImpl) + db := blm.db + + s.setupToCaptureTasks() + + s.blm.Start() + defer s.blm.Stop() + s.Require().NoError(s.blm.WaitUntilInitialized(context.Background())) + + // Spool 3 tasks through the real writer path. + for range 3 { + s.Require().NoError(s.blm.SpoolTask(&persistencespb.TaskInfo{ + ExpiryTime: timestamp.TimeNowPtrUtcAddSeconds(3000), + CreateTime: timestamp.TimeNowPtrUtc(), + })) + } + + // Wait for all tasks to reach the matcher via signalNewTasks/direct-add. + s.Eventually(func() bool { return s.capturedTasksLen() == 3 }, 5*time.Second, 10*time.Millisecond) + + s.EqualValues(3, totalApproximateBacklogCount(s.blm)) + + // Inject backlog count divergence (simulating accumulated drift). + db.updateBacklogStats(2, time.Time{}) + s.EqualValues(5, totalApproximateBacklogCount(s.blm)) + + // Advance maxReadLevel past all task IDs to simulate a range renewal. + // After direct-add, readLevel == old maxReadLevel == last task ID. + maxRL := db.GetMaxReadLevel(subqueueZero) + 100 + db.setMaxReadLevelForTesting(subqueueZero, maxRL) + + // Signal the reader pump to scan through the empty range up to the + // new maxReadLevel, advancing readLevel. + blm.subqueues[subqueueZero].SignalTaskLoading() + + // Wait for the reader pump to scan through the gap. + s.Eventually(func() bool { + rl, _ := blm.subqueues[subqueueZero].getLevels() + return rl >= maxRL + }, 5*time.Second, 10*time.Millisecond) + + // Complete all tasks. On the last completion: + // - outstandingTasks is empty + // - readLevel >= maxReadLevel (pump already scanned) + // - isDrainedLocked() returns true + // - ackLevel gets set to maxReadLevel + // - backlog counts reset to 0 + for _, t := range s.capturedTasks() { + t.finish(nil, true) + } + + _, ackLevel := blm.subqueues[subqueueZero].getLevels() + s.Equal(ackLevel, maxRL) + + s.Zero(totalApproximateBacklogCount(s.blm)) +} + +func (s *BacklogManagerTestSuite) TestSkipExpiredTasks_AllExpiredThenValid() { + s.testSkipExpiredTasks(10, 0, 33, 3) +} + +func (s *BacklogManagerTestSuite) TestSkipExpiredTasks_ValidExpiredValid() { + s.testSkipExpiredTasks(10, 3, 33, 3) +} + +// testSkipExpiredTasks verifies that the task reader correctly skips over expired tasks +// in the DB and advances the ack level past them. +// expiredPattern is: # valid, # expired, # valid, # expired, ... +func (s *BacklogManagerTestSuite) testSkipExpiredTasks(batchSize int, numValidExpired ...int) { + if !s.newMatcher { + s.T().Skip("not compatible with classic backlog manager") + } + + s.cfgcli.OverrideValue(dynamicconfig.MatchingGetTasksBatchSize.Key(), batchSize) + + // expand 1, 3, 2 -> {false, true, true, true, false, false} + var expiredPattern []bool + var isExpired bool + for _, num := range numValidExpired { + expiredPattern = append(expiredPattern, slices.Repeat([]bool{isExpired}, num)...) + isExpired = !isExpired + } + + // Pre-populate the DB with tasks before starting the backlog manager. + // This simulates tasks that were written and then expired before reading. + ctx := context.Background() + queue := s.ptqMgr.QueueKey() + queueInfo := &persistencespb.TaskQueueInfo{ + NamespaceId: queue.NamespaceId(), + Name: queue.PersistenceName(), + TaskType: queue.TaskType(), + // start with ack level at zero + } + _, err := s.taskMgr.CreateTaskQueue(ctx, &persistence.CreateTaskQueueRequest{ + RangeID: 1, + TaskQueueInfo: queueInfo, + }) + s.Require().NoError(err) + + var dbTasks []*persistencespb.AllocatedTaskInfo + numValid := 0 + for i, expired := range expiredPattern { + id := int64(i + 1) + task := &persistencespb.AllocatedTaskInfo{ + TaskId: id, + Data: &persistencespb.TaskInfo{ + CreateTime: timestamp.TimeNowPtrUtcAddSeconds(-3600), + }, + } + if expired { + task.Data.ExpiryTime = timestamp.TimeNowPtrUtcAddSeconds(-60) + } else { + task.Data.ExpiryTime = timestamp.TimeNowPtrUtcAddSeconds(3600) + numValid++ + } + if s.fairness { + task.TaskPass = id * 1000 // spread out pass numbers + } + dbTasks = append(dbTasks, task) + } + _, err = s.taskMgr.CreateTasks(ctx, &persistence.CreateTasksRequest{ + TaskQueueInfo: &persistence.PersistedTaskQueueInfo{Data: queueInfo, RangeID: 1}, + Tasks: dbTasks, + }) + s.Require().NoError(err) + + s.setupToCaptureTasks() + + // Start backlog manager. + s.blm.Start() + defer s.blm.Stop() + s.Require().NoError(s.blm.WaitUntilInitialized(context.Background())) + + // Wait for all valid tasks to be delivered. + s.Require().Eventually(func() bool { + return s.capturedTasksLen() >= numValid + }, 2*time.Second, 10*time.Millisecond, "timed out waiting for valid tasks to be delivered") + + // Complete the delivered tasks. + for _, t := range s.capturedTasks() { + t.finish(nil, true) + } + + // Verify the ack level advances past all tasks (expired + valid). + lastID := int64(len(expiredPattern)) + s.Eventually(func() bool { + db := s.blm.getDB() + db.Lock() + defer db.Unlock() + if s.fairness { + ackLevel := fairLevelFromProto(db.subqueues[subqueueZero].FairAckLevel) + return !ackLevel.less(fairLevel{pass: lastID * 1000, id: lastID}) + } + return db.subqueues[subqueueZero].AckLevel >= lastID + }, 2*time.Second, 10*time.Millisecond, "ack level did not advance past all tasks") +} + func totalApproximateBacklogCount(c backlogManager) (total int64) { for _, stats := range c.BacklogStatsByPriority() { total += stats.ApproximateBacklogCount @@ -362,6 +550,55 @@ func totalApproximateBacklogCount(c backlogManager) (total int64) { return total } +func (s *BacklogManagerTestSuite) TestBypassReader() { + if !s.newMatcher { + s.T().Skip("bypass only applies to pri/fair") + } + + s.setupToCaptureTasks() + + // set up initial qkey in db so that we always read one range on load + qkey := s.ptqMgr.QueueKey() + _, err := s.taskMgr.CreateTaskQueue(context.Background(), &persistence.CreateTaskQueueRequest{ + RangeID: 1, + TaskQueueInfo: &persistencespb.TaskQueueInfo{ + NamespaceId: qkey.NamespaceId(), + Name: qkey.PersistenceName(), + TaskType: qkey.TaskType(), + }, + }) + s.Require().NoError(err) + + s.blm.Start() + defer s.blm.Stop() + s.Require().NoError(s.blm.WaitUntilInitialized(context.Background())) + + // wait for the initial read to complete so we're at the end + s.Eventually(func() bool { + return s.taskMgr.getGetTasksCount(qkey) == 1 + }, 5*time.Second, 10*time.Millisecond) + + for range 3 { + prevCreateCount := s.taskMgr.getCreateTaskCount(qkey) + prevCaptureCount := s.capturedTasksLen() + + // write a task + s.Require().NoError(s.blm.SpoolTask(&persistencespb.TaskInfo{ + ExpiryTime: timestamp.TimeNowPtrUtcAddSeconds(3000), + CreateTime: timestamp.TimeNowPtrUtc(), + })) + + // we have written one batch of tasks + s.Equal(prevCreateCount+1, s.taskMgr.getCreateTaskCount(qkey)) + + // wait for the task to arrive at the matcher bypassing the read path + s.Eventually(func() bool { return s.capturedTasksLen() == prevCaptureCount+1 }, 5*time.Second, 10*time.Millisecond) + + // we should have passed the task in memory without any more GetTasks calls + s.Equal(1, s.taskMgr.getGetTasksCount(qkey)) + } +} + type standingBacklogParams struct { lower, upper int64 // range of standing backlog gap int64 // add/finish tasks as long as we're within gap of the target @@ -598,6 +835,8 @@ func (s *BacklogManagerTestSuite) testStandingBacklog(p standingBacklogParams) { }) } + qkey := s.ptqMgr.QueueKey() + s.T().Logf("reads %d, writes %d", s.taskMgr.getGetTasksCount(qkey), s.taskMgr.getCreateTaskBatchCount(qkey)) elapsed := time.Since(start) s.T().Logf("processed %d tasks, %.3f/s", processed.Load(), float64(processed.Load())/elapsed.Seconds()) } diff --git a/service/matching/config.go b/service/matching/config.go index 0e400dd8f2a..b3549ef4027 100644 --- a/service/matching/config.go +++ b/service/matching/config.go @@ -56,6 +56,8 @@ type ( BreakdownMetricsByPartition dynamicconfig.BoolPropertyFnWithTaskQueueFilter BreakdownMetricsByBuildID dynamicconfig.BoolPropertyFnWithTaskQueueFilter EnableWorkerPluginMetrics dynamicconfig.BoolPropertyFn + EnablePollerAutoscalingMetrics dynamicconfig.BoolPropertyFn + ExternalPayloadsEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter WorkerRegistryNumBuckets dynamicconfig.IntPropertyFn WorkerRegistryEntryTTL dynamicconfig.DurationPropertyFn WorkerRegistryMinEvictAge dynamicconfig.DurationPropertyFn @@ -84,6 +86,7 @@ type ( BacklogNegligibleAge dynamicconfig.DurationPropertyFnWithTaskQueueFilter MaxWaitForPollerBeforeFwd dynamicconfig.DurationPropertyFnWithTaskQueueFilter QueryPollerUnavailableWindow dynamicconfig.DurationPropertyFn + EmitTaskDispatchLatencyAtPoll dynamicconfig.BoolPropertyFnWithTaskQueueFilter QueryWorkflowTaskTimeoutLogRate dynamicconfig.FloatPropertyFnWithTaskQueueFilter MembershipUnloadDelay dynamicconfig.DurationPropertyFn TaskQueueInfoByBuildIdTTL dynamicconfig.DurationPropertyFnWithTaskQueueFilter @@ -142,13 +145,14 @@ type ( taskQueueConfig struct { forwarderConfig - SyncMatchWaitDuration func() time.Duration - EphemeralDataUpdateInterval func() time.Duration - BacklogMetricsEmitInterval func() time.Duration - PriorityBacklogForwarding func() bool - BacklogNegligibleAge func() time.Duration - MaxWaitForPollerBeforeFwd func() time.Duration - QueryPollerUnavailableWindow func() time.Duration + SyncMatchWaitDuration func() time.Duration + EphemeralDataUpdateInterval func() time.Duration + BacklogMetricsEmitInterval func() time.Duration + PriorityBacklogForwarding func() bool + BacklogNegligibleAge func() time.Duration + MaxWaitForPollerBeforeFwd func() time.Duration + QueryPollerUnavailableWindow func() time.Duration + EmitTaskDispatchLatencyAtPoll func() bool // Time to hold a poll request before returning an empty response if there are no tasks LongPollExpirationInterval func() time.Duration BacklogTaskForwardTimeout func() time.Duration @@ -292,6 +296,8 @@ func NewConfig( BreakdownMetricsByPartition: dynamicconfig.MetricsBreakdownByPartition.Get(dc), BreakdownMetricsByBuildID: dynamicconfig.MetricsBreakdownByBuildID.Get(dc), EnableWorkerPluginMetrics: dynamicconfig.MatchingEnableWorkerPluginMetrics.Get(dc), + EnablePollerAutoscalingMetrics: dynamicconfig.MatchingEnablePollerAutoscalingMetrics.Get(dc), + ExternalPayloadsEnabled: dynamicconfig.ExternalPayloadsEnabled.Get(dc), WorkerRegistryNumBuckets: dynamicconfig.MatchingWorkerRegistryNumBuckets.Get(dc), WorkerRegistryEntryTTL: dynamicconfig.MatchingWorkerRegistryEntryTTL.Get(dc), WorkerRegistryMinEvictAge: dynamicconfig.MatchingWorkerRegistryMinEvictAge.Get(dc), @@ -322,6 +328,7 @@ func NewConfig( BacklogNegligibleAge: dynamicconfig.MatchingBacklogNegligibleAge.Get(dc), MaxWaitForPollerBeforeFwd: dynamicconfig.MatchingMaxWaitForPollerBeforeFwd.Get(dc), QueryPollerUnavailableWindow: dynamicconfig.QueryPollerUnavailableWindow.Get(dc), + EmitTaskDispatchLatencyAtPoll: dynamicconfig.MatchingEmitTaskDispatchLatencyAtPoll.Get(dc), QueryWorkflowTaskTimeoutLogRate: dynamicconfig.MatchingQueryWorkflowTaskTimeoutLogRate.Get(dc), MembershipUnloadDelay: dynamicconfig.MatchingMembershipUnloadDelay.Get(dc), TaskQueueInfoByBuildIdTTL: dynamicconfig.TaskQueueInfoByBuildIdTTL.Get(dc), @@ -414,6 +421,9 @@ func newTaskQueueConfig(tq *tqid.TaskQueue, config *Config, ns namespace.Name) * return config.MaxWaitForPollerBeforeFwd(ns.String(), taskQueueName, taskType) }, QueryPollerUnavailableWindow: config.QueryPollerUnavailableWindow, + EmitTaskDispatchLatencyAtPoll: func() bool { + return config.EmitTaskDispatchLatencyAtPoll(ns.String(), taskQueueName, taskType) + }, LongPollExpirationInterval: func() time.Duration { return config.LongPollExpirationInterval(ns.String(), taskQueueName, taskType) }, diff --git a/service/matching/configs/quotas_test.go b/service/matching/configs/quotas_test.go index 57765306184..2d44ccdc7d3 100644 --- a/service/matching/configs/quotas_test.go +++ b/service/matching/configs/quotas_test.go @@ -86,7 +86,7 @@ func (s *quotasSuite) TestOperatorPrioritized() { requestTime := time.Now() limitCount := 0 - for i := 0; i < 12; i++ { + for range 12 { if !limiter.Allow(requestTime, apiRequest) { limitCount++ s.True(limiter.Allow(requestTime, operatorRequest)) diff --git a/service/matching/counter/cmsketch.go b/service/matching/counter/cmsketch.go index 3f1532f3f69..388772a415d 100644 --- a/service/matching/counter/cmsketch.go +++ b/service/matching/counter/cmsketch.go @@ -9,9 +9,10 @@ import ( type ( CMSketchParams struct { - W int // width of sketch - D int // depth of sketch - Grow CMSGrowParams + W int // width of sketch + D int // depth of sketch + Grow CMSGrowParams + Reseed CMSReseedParams } CMSGrowParams struct { @@ -21,6 +22,10 @@ type ( MaxW int // cap for W } + CMSReseedParams struct { + Interval int // reseed every N operations (0 = disabled) + } + // topKFunc is a callback that returns the top-K entries to preserve during resize. topKFunc func() []TopKEntry @@ -29,27 +34,42 @@ type ( cmSketch struct { params CMSketchParams seed0 maphash.Seed - seeds []uint64 - // TODO: use uint32 with a sliding window - cells []int64 + seeds []uint64 // length D+1 (D active rows + 1 shadow row) + // cells stores offsets from base. The actual value for a cell is base + int64(cells[i]). + // This allows us to use uint32 for storage while supporting the full int64 range. + base int64 + cells []uint32 // length W*(D+1) + + // shadowRow is the row index (0 to D) currently acting as the shadow. + // The shadow row receives writes but is excluded from min calculations. + // On reseed, shadowRow rotates and the new shadow is zeroed with a new seed. + shadowRow int skips, incs int // used to calculate skip rate + reseedOps int // operations since last reseed - topKProvider topKFunc // callback to get top-K entries on resize + src rand.Source // for generating new seeds on reseed + topKProvider topKFunc // callback to get top-K entries on resize } ) +// slideHeadroom is how much space to leave in the sliding window when we need to slide +const slideHeadroom = math.MaxUint32 / 2 + var _ Counter = (*cmSketch)(nil) func NewCMSketchCounter(params CMSketchParams, src rand.Source, topKProvider topKFunc) *cmSketch { params.D = max(1, params.D) params.W = max(1, params.W) params.Grow.SkipRateDecay = max(1_000, params.Grow.SkipRateDecay) + numRows := params.D + 1 // + 1 for shadow row return &cmSketch{ params: params, seed0: maphash.MakeSeed(), - seeds: makeSeeds(params.D, src), - cells: make([]int64, params.W*params.D), + seeds: makeSeeds(numRows, src), + cells: make([]uint32, params.W*numRows), + shadowRow: 0, + src: src, topKProvider: topKProvider, } } @@ -59,7 +79,8 @@ func (s *cmSketch) GetPass(key string, base, inc int64) int64 { return base // we don't handle negatives here } - indexes := make([]int, s.params.D) + numRows := s.params.D + 1 + indexes := make([]int, numRows) s.fillIndexes(key, indexes) current := s.getByIndexes(indexes) @@ -72,6 +93,11 @@ func (s *cmSketch) GetPass(key string, base, inc int64) int64 { s.incs >>= 1 } + if s.reseedOps++; s.params.Reseed.Interval > 0 && s.reseedOps >= s.params.Reseed.Interval { + s.reseed() + s.reseedOps = 0 + } + return int64(pass) } @@ -80,10 +106,11 @@ func (s *cmSketch) SkipRate() float64 { } func (s *cmSketch) EstimateDistinctKeys() int { + // this is not very accurate at all, especially with the shadow row. // TODO: improve this estimate with more math count := 0 - for _, d := range s.cells { - if d > 0 { + for _, c := range s.cells { + if c > 0 { count++ } } @@ -94,6 +121,8 @@ func (s *cmSketch) TopK() []TopKEntry { return s.topKProvider() } +// fillIndexes computes cell indexes for all D+1 rows (D active + 1 shadow). +// len(indexes) must == len(s.seeds) == D+1 func (s *cmSketch) fillIndexes(k string, indexes []int) { w := s.params.W // get 64 bits of hash @@ -122,46 +151,103 @@ func (s *cmSketch) maybeGrow() { topK = s.topKProvider() } + numRows := s.params.D + 1 s.params.W = min(int(float64(s.params.W)*s.params.Grow.Ratio), s.params.Grow.MaxW) s.seed0 = maphash.MakeSeed() - s.cells = make([]int64, s.params.W*s.params.D) - s.skips, s.incs = 0, 0 + // we're resetting everything so might as well reseed now too + s.seeds = makeSeeds(numRows, s.src) + s.base = 0 + s.cells = make([]uint32, s.params.W*numRows) + s.shadowRow = s.params.D // reset shadow to last row + s.skips, s.incs, s.reseedOps = 0, 0, 0 if len(topK) > 0 { // restore top entries after resize. GetPass can in theory call back into maybeGrow, - // but only if called more than SkipRateDecay times, so just limit to that. - topK = topK[:min(len(topK), s.params.Grow.SkipRateDecay)] + // but we can just reset the counters each time to prevent that. for _, entry := range topK { _ = s.GetPass(entry.Key, entry.Count, 0) + s.skips, s.incs, s.reseedOps = 0, 0, 0 } } +} + +// reseed rotates the shadow row to break persistent hash collisions over time. +// The current shadow becomes active, and the next row becomes the new shadow +// (zeroed with a fresh seed). The shadow row has been accumulating writes for the +// entire previous interval, so it already has accurate counts for active keys. +func (s *cmSketch) reseed() { + numRows := s.params.D + 1 + s.shadowRow = (s.shadowRow + 1) % numRows + s.seeds[s.shadowRow] = s.src.Uint64() - // reset these again - s.skips, s.incs = 0, 0 + // clear the new shadow row + rowStart := s.shadowRow * s.params.W + for i := range s.params.W { + s.cells[rowStart+i] = 0 + } } func (s *cmSketch) getByIndexes(indexes []int) int64 { // TODO: consider using better estimator: https://dl.acm.org/doi/pdf/10.1145/3219819.3219975 - minVal := int64(math.MaxInt64) - for _, idx := range indexes { + minVal := uint32(math.MaxUint32) + for i, idx := range indexes { + if i == s.shadowRow { + continue // skip shadow row for reads + } minVal = min(minVal, s.cells[idx]) } - return minVal + return s.base + int64(minVal) } func (s *cmSketch) ensureByIndexes(indexes []int, target int64) (skips int) { - for _, idx := range indexes { - if s.cells[idx] < target { - s.cells[idx] = target - } else { - skips++ + offset := target - s.base + if offset < 0 { + // target is below our window floor, all cells are already high enough + return s.params.D // only count active rows for skips + } + if offset > math.MaxUint32 { + // would overflow uint32, need to slide the base up first + s.slideBase(offset + slideHeadroom - math.MaxUint32) + offset = math.MaxUint32 - slideHeadroom + } + + uoffset := uint32(offset) + for i, idx := range indexes { + if s.cells[idx] < uoffset { + s.cells[idx] = uoffset + } else if i != s.shadowRow { + skips++ // only count skips for active rows, not shadow } } return } -func makeSeeds(d int, src rand.Source) []uint64 { - out := make([]uint64, d) +// slideBase increases the base by delta, subtracting delta from all cells. +// Cells that would go negative are clamped to 0 (those keys are "dragged up"). +func (s *cmSketch) slideBase(delta int64) { + if delta <= 0 { + return + } + s.base += delta + if delta >= math.MaxUint32 { + for i := range s.cells { + s.cells[i] = 0 + } + return + } + // delta fits in uint32 + udelta := uint32(delta) + for i := range s.cells { + if s.cells[i] > udelta { + s.cells[i] -= udelta + } else { + s.cells[i] = 0 + } + } +} + +func makeSeeds(rows int, src rand.Source) []uint64 { + out := make([]uint64, rows) for i := range out { out[i] = src.Uint64() } diff --git a/service/matching/counter/cmsketch_test.go b/service/matching/counter/cmsketch_test.go index 5e6d83ce7f7..a926d5c64c9 100644 --- a/service/matching/counter/cmsketch_test.go +++ b/service/matching/counter/cmsketch_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "math/rand/v2" + "slices" "testing" "github.com/stretchr/testify/assert" @@ -97,3 +98,177 @@ func TestCMSketch_Grow_PreservedOnResize(t *testing.T) { assert.GreaterOrEqual(t, cms.GetPass("topkey1", 0, 1), int64(9999)) assert.GreaterOrEqual(t, cms.GetPass("topkey2", 0, 1), int64(99999)) } + +func TestCMSketch_SlideBase(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + cms := NewCMSketchCounter(CMSketchParams{W: 10, D: 3}, src, nil) + + // start with a value that will require sliding when we add more + startBase := int64(math.MaxUint32) - 100 + p1 := cms.GetPass("hot", startBase, 0) + assert.Equal(t, startBase, p1) + assert.Equal(t, int64(0), cms.base, "base should still be 0, value fits in uint32") + + // push past MaxUint32, which should trigger a slide + p2 := cms.GetPass("hot", 0, 200) + assert.Equal(t, startBase+200, p2) + assert.Positive(t, cms.base, "base should have slid up") + + // the value should still be correct after sliding + p3 := cms.GetPass("hot", 0, 0) + assert.Equal(t, startBase+200, p3) +} + +func TestCMSketch_SlideBase_DragUp(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + cms := NewCMSketchCounter(CMSketchParams{W: 10, D: 3}, src, nil) + + // set up a cold key at a low value + coldPass := cms.GetPass("cold", 0, 100) + assert.Equal(t, int64(100), coldPass) + + // set up a hot key near the uint32 boundary + hotStart := int64(math.MaxUint32) - 50 + cms.GetPass("hot", hotStart, 0) + + // push hot key past the boundary, triggering a slide + cms.GetPass("hot", 0, 200) + + // cold key should have been dragged up to the new base + coldPassAfter := cms.GetPass("cold", 0, 0) + assert.Greater(t, coldPassAfter, coldPass, "cold key should have been dragged up") + assert.Equal(t, cms.base, coldPassAfter, "cold key should be at or above base") +} + +func TestCMSketch_SlideBase_Headroom(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + cms := NewCMSketchCounter(CMSketchParams{W: 10, D: 3}, src, nil) + + // push a key just past MaxUint32 to trigger a slide + startBase := int64(math.MaxUint32) + 1 + cms.GetPass("key", startBase, 0) + + // after sliding, there should be some headroom so we can do many more increments + // without another slide + baseBeforeIncs := cms.base + for range 1000 { + cms.GetPass("key", 0, 1000) + } + assert.Equal(t, baseBeforeIncs, cms.base, "base should not have changed after moderate increments") +} + +func TestCMSketch_SlideBase_TargetBelowBase(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + cms := NewCMSketchCounter(CMSketchParams{W: 10, D: 3}, src, nil) + + // push past MaxUint32 to establish a high base + highValue := int64(math.MaxUint32) + 1000 + cms.GetPass("key", highValue, 0) + assert.Positive(t, cms.base) + + // try to set a value below the base, should be a no-op + result := cms.GetPass("key", 100, 0) // base arg of 100 is below cms.base + assert.Equal(t, highValue, result, "should return current value, not the low base") + + // new key with a low base should get dragged up to the current base + newKeyResult := cms.GetPass("newkey", 100, 0) + assert.Equal(t, cms.base, newKeyResult, "new key should be at base") +} + +func TestCMSketch_SlideBase_MultipleSlides(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + cms := NewCMSketchCounter(CMSketchParams{W: 10, D: 3}, src, nil) + + // do multiple slides by repeatedly jumping past the uint32 boundary + const jump = int64(math.MaxUint32 * 3 / 4) + var lastPass int64 + for range 5 { + lastPass = cms.GetPass("key", lastPass+jump, 0) + } + + // values should still be tracked correctly + assert.Equal(t, 5*jump, lastPass) +} + +func TestCMSketch_Reseed(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + interval := 20 + cms := NewCMSketchCounter(CMSketchParams{ + W: 100, + D: 3, + Reseed: CMSReseedParams{Interval: interval}, + }, src, nil) + + // do interval-1 operations + for i := range interval - 1 { + _ = cms.GetPass(fmt.Sprintf("k%d", i), 0, 1) + } + assert.Equal(t, 0, cms.shadowRow, "shadow should not have rotated yet") + + // trigger reseed + prevShadow := cms.shadowRow + prevSeeds := slices.Clone(cms.seeds) + _ = cms.GetPass("trigger", 0, 1) + assert.NotEqual(t, prevShadow, cms.shadowRow, "shadow should have rotated") + assert.NotEqual(t, prevSeeds, cms.seeds[1], "some seed should have changed") + + // counts for existing keys should not be reset + for i := range interval - 1 { + assert.GreaterOrEqual(t, cms.GetPass(fmt.Sprintf("k%d", i), 0, 0), int64(1)) + } + assert.GreaterOrEqual(t, cms.GetPass("trigger", 0, 0), int64(1)) +} + +func TestCMSketch_Reseed_BreaksCollision(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + cms := NewCMSketchCounter(CMSketchParams{ + W: 5, // small to force collision + D: 3, + }, src, nil) + + // set up a "hot" key + hotCount := int64(100) + _ = cms.GetPass("hot", hotCount, 0) + + // find a key that collides with "hot" on all rows + var collider string + // should succeed within several hundred iterations + for i := 0; ; i++ { + key := fmt.Sprintf("probe%d", i) + if cms.GetPass(key, 0, 0) >= hotCount { + collider = key + break + } + } + t.Log("found colliding key", collider) + + for i := range 15 { + // try reseed to break the collision. usually succeeds in 1-2 reseeds but could take + // more if we get very unlucky. + t.Log("reseeding", i+1) + cms.reseed() + hotCount++ + _ = cms.GetPass("hot", hotCount, 0) + if cms.GetPass(collider, 0, 0) < hotCount { + return + } + } + assert.Fail(t, "couldn't break collision after multiple tries") +} + +func TestCMSketch_SlideBase_LargeDelta(t *testing.T) { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + cms := NewCMSketchCounter(CMSketchParams{W: 10, D: 3}, src, nil) + + // set a cell to some value + cms.GetPass("key1", 1000, 0) + + const delta = int64(math.MaxUint32) + 1 + cms.slideBase(delta) + + // all cells should be zero + for _, val := range cms.cells { + assert.Zero(t, val) + } + assert.Equal(t, delta, cms.base) +} diff --git a/service/matching/counter/hybrid.go b/service/matching/counter/hybrid.go index 51abf945f2f..1fcde4fce8b 100644 --- a/service/matching/counter/hybrid.go +++ b/service/matching/counter/hybrid.go @@ -35,6 +35,9 @@ var DefaultCounterParams = CounterParams{ Ratio: 1.5, MaxW: 10_000, }, + Reseed: CMSReseedParams{ + Interval: 250_000, + }, }, } diff --git a/service/matching/counter/map.go b/service/matching/counter/map.go index 05a50f79399..5cf19a6212b 100644 --- a/service/matching/counter/map.go +++ b/service/matching/counter/map.go @@ -10,7 +10,7 @@ import ( // mapCounter is not safe for concurrent use. type mapCounter struct { m map[string]int - heap topKHeap + heap []TopKEntry limit int } @@ -35,7 +35,7 @@ func (m *mapCounter) getPassWithOverflow(key string, base, inc int64) (int64, bo count := max(base, prev+inc) // inline simple case of updateHeap m.heap[idx].Count = count - heap.Fix(&m.heap, idx) + heap.Fix(m, idx) return count, false } // not present, fall back to full updateHeap @@ -56,44 +56,47 @@ func (m *mapCounter) updateHeap(key string, count int64) bool { if idx, ok := m.m[key]; ok { // already in heap - update count and fix m.heap[idx].Count = count - heap.Fix(&m.heap, idx) + heap.Fix(m, idx) return false } if len(m.heap) < m.limit { // heap not full - add m.m[key] = len(m.heap) - heap.Push(&m.heap, TopKEntry{Key: key, Count: count}) + heap.Push(m, TopKEntry{Key: key, Count: count}) return false } // heap is full - only add if count > min if count > m.heap[0].Count { // evict min - evicted := heap.Pop(&m.heap).(TopKEntry) // nolint:revive // unchecked-type-assertion + evicted := heap.Pop(m).(TopKEntry) // nolint:revive // unchecked-type-assertion delete(m.m, evicted.Key) // add new m.m[key] = len(m.heap) - heap.Push(&m.heap, TopKEntry{Key: key, Count: count}) + heap.Push(m, TopKEntry{Key: key, Count: count}) } return true } -// topKHeap implements heap.Interface as a min-heap (smallest count at root) -type topKHeap []TopKEntry - -func (h topKHeap) Len() int { return len(h) } -func (h topKHeap) Less(i, j int) bool { return h[i].Count < h[j].Count } -func (h topKHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +// implements heap.Interface using m.heap +func (m *mapCounter) Len() int { return len(m.heap) } +func (m *mapCounter) Less(i, j int) bool { return m.heap[i].Count < m.heap[j].Count } +func (m *mapCounter) Swap(i, j int) { + m.heap[i], m.heap[j] = m.heap[j], m.heap[i] + // don't forget to fix the map: + m.m[m.heap[i].Key] = i + m.m[m.heap[j].Key] = j +} -func (h *topKHeap) Push(x any) { - *h = append(*h, x.(TopKEntry)) +func (m *mapCounter) Push(x any) { + m.heap = append(m.heap, x.(TopKEntry)) } -func (h *topKHeap) Pop() any { - n := len(*h) - entry := (*h)[n-1] - (*h)[n-1] = TopKEntry{} - *h = (*h)[0 : n-1] +func (m *mapCounter) Pop() any { + n := len(m.heap) + entry := m.heap[n-1] + m.heap[n-1] = TopKEntry{} + m.heap = m.heap[0 : n-1] return entry } diff --git a/service/matching/counter/map_test.go b/service/matching/counter/map_test.go index 3eb423a87f0..4e593b48de5 100644 --- a/service/matching/counter/map_test.go +++ b/service/matching/counter/map_test.go @@ -2,6 +2,7 @@ package counter import ( "fmt" + "math/rand" "testing" "github.com/stretchr/testify/assert" @@ -87,3 +88,43 @@ func TestMapCounter_TopK_ManyEntries(t *testing.T) { TopKEntry{Key: "key95", Count: 95}, }, m.TopK()) } + +func TestMapCounter_HeapCorrectness(t *testing.T) { + m := NewMapCounter(10) + + // add three keys + m.GetPass("key1", 0, 100) + m.GetPass("key2", 0, 200) + m.GetPass("key3", 0, 50) + + // key3 (50) should be at root (index 0) + assert.Equal(t, "key3", m.heap[0].Key) + + // update key3 to be the largest + m.GetPass("key3", 0, 300) + + // key3 count is now 350. it should have moved down. + // key1 (100) should now be the new root. + assert.Equal(t, "key1", m.heap[0].Key) + assert.Equal(t, int64(100), m.heap[0].Count) + + // verify map and heap are in sync + for key, idx := range m.m { + assert.Equal(t, key, m.heap[idx].Key) + } +} + +func TestMapCounter_HeapIndexTracking(t *testing.T) { + m := NewMapCounter(5) + + for i := range 1000 { + // update 10 keys with random increments + key := fmt.Sprintf("k%d", i%10) + m.GetPass(key, 0, int64(rand.Intn(100))) + + // check after each operation + for k, idx := range m.m { + assert.Equal(t, k, m.heap[idx].Key) + } + } +} diff --git a/service/matching/db.go b/service/matching/db.go index ef2d30c5a81..e6e0689bc46 100644 --- a/service/matching/db.go +++ b/service/matching/db.go @@ -17,6 +17,7 @@ import ( "go.temporal.io/server/common/persistence" "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/softassert" + "go.temporal.io/server/service/matching/counter" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -387,6 +388,34 @@ func (db *taskQueueDB) updateBacklogStatsLocked(subqueue subqueueIndex, countDel db.subqueues[subqueue].oldestTime = oldestTime } +func (db *taskQueueDB) persistTopKFairnessKeys(subqueue subqueueIndex, entries []counter.TopKEntry) { + db.Lock() + defer db.Unlock() + + counts := make([]*persistencespb.FairnessKeyCount, len(entries)) + for i, entry := range entries { + counts[i] = &persistencespb.FairnessKeyCount{Key: entry.Key, Count: entry.Count} + } + + db.subqueues[subqueue].TopKFairnessCounts = counts + db.lastChange = time.Now() +} + +func (db *taskQueueDB) getTopKFairnessKeys(subqueue subqueueIndex) []counter.TopKEntry { + db.Lock() + defer db.Unlock() + + if subqueue >= subqueueIndex(len(db.subqueues)) { + return nil + } + counts := db.subqueues[subqueue].TopKFairnessCounts + entries := make([]counter.TopKEntry, len(counts)) + for i, count := range counts { + entries[i] = counter.TopKEntry{Key: count.Key, Count: count.Count} + } + return entries +} + // getApproximateBacklogCountsBySubqueue return the approximate backlog count for each subqueue. // The index corresponds to the subqueue id. func (db *taskQueueDB) getApproximateBacklogCountsBySubqueue() []int64 { diff --git a/service/matching/fair_backlog_manager.go b/service/matching/fair_backlog_manager.go index 4feb6a42d0a..e7b0247ae51 100644 --- a/service/matching/fair_backlog_manager.go +++ b/service/matching/fair_backlog_manager.go @@ -38,10 +38,12 @@ type ( subqueuesByPriority map[priorityKey]subqueueIndex priorityBySubqueue map[subqueueIndex]priorityKey - logger log.Logger - throttledLogger log.ThrottledLogger - matchingClient matchingservice.MatchingServiceClient - metricsHandler metrics.Handler + logger log.Logger + throttledLogger log.ThrottledLogger + matchingClient matchingservice.MatchingServiceClient + metricsHandler metrics.Handler + counterFactory func() counter.Counter + initializedError *future.FutureImpl[struct{}] // skipFinalUpdate controls behavior on Stop: if it's false, we try to write one final // update before unloading @@ -77,11 +79,12 @@ func newFairBacklogManager( priorityBySubqueue: make(map[subqueueIndex]priorityKey), matchingClient: matchingClient, metricsHandler: metricsHandler, + counterFactory: counterFactory, logger: logger, throttledLogger: throttledLogger, initializedError: future.NewFuture[struct{}](), } - bmg.taskWriter = newFairTaskWriter(bmg, counterFactory) + bmg.taskWriter = newFairTaskWriter(bmg, bmg.newCounterForSubqueue) return bmg } @@ -349,6 +352,7 @@ func (c *fairBacklogManagerImpl) InternalStatus() []*taskqueuespb.InternalTaskQu LoadedTasks: int64(r.getLoadedTasks()), FairMaxReadLevel: maxReadLevel.toProto(), ApproximateBacklogCount: count, + BacklogDrained: r.isDrained(), } } return status @@ -392,6 +396,15 @@ func (c *fairBacklogManagerImpl) getDB() *taskQueueDB { return c.db } +func (c *fairBacklogManagerImpl) newCounterForSubqueue(subqueue subqueueIndex) counter.Counter { + cntr := c.counterFactory() + // restore persisted keys + for _, entry := range c.db.getTopKFairnessKeys(subqueue) { + _ = cntr.GetPass(entry.Key, entry.Count, 0) + } + return cntr +} + // hasFinishedDraining returns true if this is a draining backlog manager and all tasks have // been fully drained (read and acked). func (c *fairBacklogManagerImpl) hasFinishedDraining() bool { diff --git a/service/matching/fair_task_reader.go b/service/matching/fair_task_reader.go index d881ada961c..a7e6480f82e 100644 --- a/service/matching/fair_task_reader.go +++ b/service/matching/fair_task_reader.go @@ -3,7 +3,6 @@ package matching import ( "context" "errors" - "slices" "sync" "time" @@ -32,10 +31,11 @@ type ( lock sync.Mutex - readPending bool - backoffTimer *time.Timer - retrier backoff.Retrier - addRetries *semaphore.Weighted + readPending bool + backoffTimer *time.Timer + retrier backoff.Retrier + throttleRetrier backoff.Retrier + addRetries *semaphore.Weighted backlogAge backlogAgeTracker outstandingTasks treemap.Map // fairLevel -> *internalTask if unacked, or nil if acked @@ -90,7 +90,15 @@ func newFairTaskReader( subqueue: subqueue, logger: backlogMgr.logger, retrier: backoff.NewRetrier( - common.CreateReadTaskRetryPolicy(), + backoff.NewExponentialRetryPolicy(50*time.Millisecond). + WithMaximumInterval(10*time.Second). + WithExpirationInterval(backoff.NoInterval), + clock.NewRealTimeSource(), + ), + throttleRetrier: backoff.NewRetrier( + backoff.NewExponentialRetryPolicy(2*time.Second). + WithMaximumInterval(30*time.Second). + WithExpirationInterval(backoff.NoInterval), clock.NewRealTimeSource(), ), backlogAge: newBacklogAgeTracker(), @@ -237,6 +245,10 @@ func (tr *fairTaskReader) readTasksImpl() { tr.advanceAckLevelLocked() } + // If a backoff timer fired while readPending was still true, its maybeReadTasksLocked call + // was a no-op. Re-check now that readPending is false to avoid getting stuck. + tr.maybeReadTasksLocked() + // unlock before calling addTaskToMatcher tr.lock.Unlock() @@ -254,13 +266,14 @@ func (tr *fairTaskReader) readTaskBatch(readLevel fairLevel, loadedTasks int) er if tr.backlogMgr.signalIfFatal(err) || common.IsContextCanceledErr(err) { // don't retry } else if common.IsResourceExhausted(err) { - tr.retryReadAfter(taskReaderThrottleRetryDelay) + tr.retryReadAfter(tr.throttleRetrier.NextBackOff(err)) } else { tr.retryReadAfter(tr.retrier.NextBackOff(err)) } return err } tr.retrier.Reset() + tr.throttleRetrier.Reset() // If we got less than we asked for, we know we hit the end. // If there was a concurrent write such that we incorrectly think we hit the end here, @@ -270,20 +283,11 @@ func (tr *fairTaskReader) readTaskBatch(readLevel fairLevel, loadedTasks int) er mode = mergeReadToEnd } - // filter out expired - // TODO(fairness): if we have _only_ expired tasks, and we filter them out here, we won't move - // the ack level and delete them. maybe we should put them in outstandingTasks as pre-acked. - tasks := slices.DeleteFunc(res.Tasks, func(t *persistencespb.AllocatedTaskInfo) bool { - if IsTaskExpired(t) { - metrics.ExpiredTasksPerTaskQueueCounter.With(tr.backlogMgr.metricsHandler).Record(1, metrics.TaskExpireStageReadTag) - return true - } - return false - }) - // Note: even if (especially if) len(tasks) == 0, we should go through the mergeTasks logic - // to update atEnd and the backlog size estimate. - tr.mergeTasks(tasks, mode) + // to update atEnd and the backlog size estimate. Expired tasks are passed through to + // mergeTasksLocked where they'll be added as pre-acked (nil) entries so they advance the + // ack level and get GC'd. + tr.mergeTasks(res.Tasks, mode) return nil } @@ -477,16 +481,31 @@ func (tr *fairTaskReader) mergeTasksLocked(tasks []*persistencespb.AllocatedTask tr.evictedAcks.PopMax() } - internalTasks := make([]*internalTask, len(tasks)) - for i, t := range tasks { + var hasExpired bool + internalTasks := make([]*internalTask, 0, len(tasks)) + for _, t := range tasks { level := fairLevelFromAllocatedTask(t) - internalTasks[i] = newInternalTaskFromBacklog(t, tr.completeTask) - tr.backlogMgr.setPriority(internalTasks[i]) + if IsTaskExpired(t) { + // Expired tasks are added as pre-acked (nil) so they participate in + // readLevel calculation above and advance ackLevel + get GC'd below. + tr.outstandingTasks.Put(level, nil) + metrics.ExpiredTasksPerTaskQueueCounter.With(tr.backlogMgr.metricsHandler).Record(1, metrics.TaskExpireStageReadTag) + hasExpired = true + continue + } + task := newInternalTaskFromBacklog(t, tr.completeTask) + tr.backlogMgr.setPriority(task) // After we get to this point, we must eventually call task.finish or // task.finishForwarded, which will call tr.completeTask. - tr.outstandingTasks.Put(level, internalTasks[i]) + tr.outstandingTasks.Put(level, task) tr.loadedTasks++ tr.backlogAge.record(t.Data.CreateTime, 1) + internalTasks = append(internalTasks, task) + } + + if hasExpired { + // Advance ack level past any expired tasks we just added as pre-acked. + tr.advanceAckLevelLocked() } // Update atEnd: @@ -680,5 +699,8 @@ func (tr *fairTaskReader) finalGC() { tr.lock.Lock() ackLevel := tr.ackLevel tr.lock.Unlock() + if ackLevel.pass == 0 { + return + } _, _ = tr.doGCAt(ackLevel) } diff --git a/service/matching/fair_task_writer.go b/service/matching/fair_task_writer.go index 4ec026320a0..dd2104624ed 100644 --- a/service/matching/fair_task_writer.go +++ b/service/matching/fair_task_writer.go @@ -24,7 +24,7 @@ type ( config *taskQueueConfig db *taskQueueDB logger log.Logger - counterFactory func() counter.Counter + counterFactory func(subqueueIndex) counter.Counter appendCh chan *writeTaskRequest // state: @@ -36,7 +36,7 @@ type ( func newFairTaskWriter( backlogMgr *fairBacklogManagerImpl, - counterFactory func() counter.Counter, + counterFactory func(subqueueIndex) counter.Counter, ) *fairTaskWriter { return &fairTaskWriter{ backlogMgr: backlogMgr, @@ -124,7 +124,7 @@ func (w *fairTaskWriter) pickPasses(tasks []*writeTaskRequest, bases []fairLevel base := bases[task.subqueue].pass cntr := w.counters[task.subqueue] if cntr == nil { - cntr = w.counterFactory() + cntr = w.counterFactory(task.subqueue) w.counters[task.subqueue] = cntr } pass := cntr.GetPass(key, base, inc) @@ -150,6 +150,10 @@ func (w *fairTaskWriter) taskWriterLoop() { return } + // TODO: this will be out of phase with the timer in fairBacklogManagerImpl.periodicSync. + // can we align them better? + persistFairnessKeys := time.NewTicker(w.config.UpdateAckInterval()).C + var reqs []*writeTaskRequest for { atomic.StoreInt64(&w.currentTaskIDBlock.start, w.taskIDBlock.start) @@ -176,6 +180,15 @@ func (w *fairTaskWriter) taskWriterLoop() { for _, req := range reqs { req.responseCh <- err } + + // maybe persist fairness key counts if it's time + select { + case <-persistFairnessKeys: + for subqueue, cntr := range w.counters { + w.db.persistTopKFairnessKeys(subqueue, cntr.TopK()) + } + default: + } } } diff --git a/service/matching/forwarder.go b/service/matching/forwarder.go index 6b893db2aa0..27e67ce8aad 100644 --- a/service/matching/forwarder.go +++ b/service/matching/forwarder.go @@ -169,8 +169,10 @@ func (fwdr *Forwarder) getForwardInfo(task *internalTask) *taskqueuespb.TaskForw } // task is forwarded for the first time forwardInfo := &taskqueuespb.TaskForwardInfo{ + CreateTime: task.getCreateTime(), TaskSource: task.source, SourcePartition: fwdr.partition.RpcName(), + OriginPartition: fwdr.partition.RpcName(), DispatchBuildId: fwdr.queue.Version().BuildId(), DispatchVersionSet: fwdr.queue.Version().VersionSet(), RedirectInfo: task.redirectInfo, @@ -348,7 +350,7 @@ func (fwdr *Forwarder) handleErr(err error) error { func newForwarderReqToken(maxOutstanding int) *ForwarderReqToken { reqToken := &ForwarderReqToken{ch: make(chan *ForwarderReqToken, maxOutstanding)} - for i := 0; i < maxOutstanding; i++ { + for range maxOutstanding { reqToken.ch <- reqToken } return reqToken diff --git a/service/matching/forwarder_test.go b/service/matching/forwarder_test.go index 3d545431a11..4602b8a0be5 100644 --- a/service/matching/forwarder_test.go +++ b/service/matching/forwarder_test.go @@ -17,6 +17,7 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/convert" + "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/tqid" "go.uber.org/mock/gomock" ) @@ -64,7 +65,7 @@ func (t *ForwarderTestSuite) SetupTest() { t.partition = tqFam.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition() if t.newFwdr { - t.fwdr, err = newPriForwarder(t.cfg, UnversionedQueueKey(t.partition), t.client) + t.fwdr, err = newPriForwarder(t.cfg, UnversionedQueueKey(t.partition), t.client, testhooks.TestHooks{}) t.NoError(err) } else { t.fwdr, err = newForwarder(t.cfg, UnversionedQueueKey(t.partition), t.client) @@ -88,7 +89,7 @@ func (t *ForwarderTestSuite) TestForwardWorkflowTask() { var request *matchingservice.AddWorkflowTaskRequest t.client.EXPECT().AddWorkflowTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...any) { request = arg1 }, ).Return(&matchingservice.AddWorkflowTaskResponse{}, nil) @@ -117,7 +118,7 @@ func (t *ForwarderTestSuite) TestForwardWorkflowTask_WithBuildId() { var request *matchingservice.AddWorkflowTaskRequest t.client.EXPECT().AddWorkflowTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...any) { request = arg1 t.Equal(bld, request.GetForwardInfo().GetDispatchBuildId()) }, @@ -146,7 +147,7 @@ func (t *ForwarderTestSuite) TestForwardActivityTask() { var request *matchingservice.AddActivityTaskRequest t.client.EXPECT().AddActivityTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddActivityTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddActivityTaskRequest, arg2 ...any) { request = arg1 }, ).Return(&matchingservice.AddActivityTaskResponse{}, nil) @@ -173,7 +174,7 @@ func (t *ForwarderTestSuite) TestForwardActivityTask_WithBuildId() { var request *matchingservice.AddActivityTaskRequest t.client.EXPECT().AddActivityTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddActivityTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddActivityTaskRequest, arg2 ...any) { request = arg1 t.Equal(bld, request.ForwardInfo.GetDispatchBuildId()) }, @@ -202,7 +203,7 @@ func (t *ForwarderTestSuite) TestForwardTaskRateExceeded() { t.client.EXPECT().AddActivityTask(gomock.Any(), gomock.Any(), gomock.Any()).Return(&matchingservice.AddActivityTaskResponse{}, nil).Times(rps) taskInfo := randomTaskInfo() task := newInternalTaskFromBacklog(taskInfo, nil) - for i := 0; i < rps; i++ { + for range rps { t.NoError(t.fwdr.ForwardTask(context.Background(), task)) } t.Equal(errForwarderSlowDown, t.fwdr.ForwardTask(context.Background(), task)) @@ -220,7 +221,7 @@ func (t *ForwarderTestSuite) TestForwardQueryTask() { resp := &matchingservice.QueryWorkflowResponse{} var request *matchingservice.QueryWorkflowRequest t.client.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.QueryWorkflowRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.QueryWorkflowRequest, arg2 ...any) { request = arg1 }, ).Return(resp, nil) @@ -240,7 +241,7 @@ func (t *ForwarderTestSuite) TestForwardQueryTaskRateNotEnforced() { resp := &matchingservice.QueryWorkflowResponse{} rps := 2 t.client.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any()).Return(resp, nil).Times(rps + 1) - for i := 0; i < rps; i++ { + for range rps { _, err := t.fwdr.ForwardQueryTask(context.Background(), task) t.NoError(err) } @@ -265,7 +266,7 @@ func (t *ForwarderTestSuite) TestForwardPollWorkflowTaskQueue() { var request *matchingservice.PollWorkflowTaskQueueRequest t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { request = arg1 }, ).Return(resp, nil) @@ -296,7 +297,7 @@ func (t *ForwarderTestSuite) TestForwardPollWorkflowTaskQueuePreservesWorkerInst var request *matchingservice.PollWorkflowTaskQueueRequest t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { request = arg1 }, ).Return(resp, nil) @@ -323,7 +324,7 @@ func (t *ForwarderTestSuite) TestForwardPollForActivity() { var request *matchingservice.PollActivityTaskQueueRequest t.client.EXPECT().PollActivityTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollActivityTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollActivityTaskQueueRequest, arg2 ...any) { request = arg1 }, ).Return(resp, nil) @@ -354,7 +355,7 @@ func (t *ForwarderTestSuite) TestForwardPollForActivityPreservesWorkerInstanceKe var request *matchingservice.PollActivityTaskQueueRequest t.client.EXPECT().PollActivityTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollActivityTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollActivityTaskQueueRequest, arg2 ...any) { request = arg1 }, ).Return(resp, nil) @@ -384,7 +385,7 @@ func (t *ForwarderTestSuite) TestForwardPollForNexusPreservesWorkerInstanceKey() var request *matchingservice.PollNexusTaskQueueRequest t.client.EXPECT().PollNexusTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollNexusTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollNexusTaskQueueRequest, arg2 ...any) { request = arg1 }, ).Return(resp, nil) @@ -424,7 +425,7 @@ func (t *ForwarderTestSuite) TestMaxOutstandingConcurrency() { adds = 0 polls = 0 t.Run(tc.name, func() { - for i := 0; i < concurrency; i++ { + for range concurrency { wg.Add(1) go func() { timer := time.NewTimer(time.Millisecond * 200) @@ -472,7 +473,7 @@ func (t *ForwarderTestSuite) TestMaxOutstandingConfigUpdate() { startC := make(chan struct{}) doneWG := sync.WaitGroup{} - for i := 0; i < 10; i++ { + for range 10 { doneWG.Add(1) go func() { <-startC diff --git a/service/matching/fx.go b/service/matching/fx.go index 82c528e7dea..17473f7b270 100644 --- a/service/matching/fx.go +++ b/service/matching/fx.go @@ -199,12 +199,17 @@ func WorkersRegistryProvider( serviceConfig *Config, ) workers.Registry { return workers.NewRegistry(lc, workers.RegistryParams{ - NumBuckets: serviceConfig.WorkerRegistryNumBuckets, - TTL: serviceConfig.WorkerRegistryEntryTTL, - MinEvictAge: serviceConfig.WorkerRegistryMinEvictAge, - MaxItems: serviceConfig.WorkerRegistryMaxEntries, - EvictionInterval: serviceConfig.WorkerRegistryEvictionInterval, - MetricsHandler: metricsHandler, - EnablePluginMetrics: serviceConfig.EnableWorkerPluginMetrics, + NumBuckets: serviceConfig.WorkerRegistryNumBuckets, + TTL: serviceConfig.WorkerRegistryEntryTTL, + MinEvictAge: serviceConfig.WorkerRegistryMinEvictAge, + MaxItems: serviceConfig.WorkerRegistryMaxEntries, + EvictionInterval: serviceConfig.WorkerRegistryEvictionInterval, + MetricsHandler: metricsHandler, + MetricsConfig: workers.WorkerMetricsConfig{ + EnablePluginMetrics: serviceConfig.EnableWorkerPluginMetrics, + EnablePollerAutoscalingMetrics: serviceConfig.EnablePollerAutoscalingMetrics, + BreakdownMetricsByTaskQueue: serviceConfig.BreakdownMetricsByTaskQueue, + ExternalPayloadsEnabled: serviceConfig.ExternalPayloadsEnabled, + }, }) } diff --git a/service/matching/handler.go b/service/matching/handler.go index 07cf75560ab..8359f88baad 100644 --- a/service/matching/handler.go +++ b/service/matching/handler.go @@ -2,6 +2,7 @@ package matching import ( "context" + "strings" "sync" "time" @@ -12,6 +13,7 @@ import ( "go.temporal.io/server/api/matchingservice/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" + "go.temporal.io/server/common/headers" "go.temporal.io/server/common/log" "go.temporal.io/server/common/membership" "go.temporal.io/server/common/metrics" @@ -23,6 +25,7 @@ import ( "go.temporal.io/server/common/searchattribute" "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/tqid" + "go.temporal.io/server/service/matching/hooks" "go.temporal.io/server/service/matching/workers" "go.temporal.io/server/service/worker/workerdeployment" "go.uber.org/fx" @@ -70,6 +73,7 @@ type ( RateLimiter TaskDispatchRateLimiter `optional:"true"` WorkersRegistry workers.Registry Serializer serialization.Serializer + TaskHookFactories []hooks.TaskHookFactory `group:"TaskHookFactories"` } ) @@ -112,6 +116,7 @@ func NewHandler( params.SearchAttributeMapperProvider, params.RateLimiter, params.Serializer, + params.TaskHookFactories, ), namespaceRegistry: params.NamespaceRegistry, workersRegistry: params.WorkersRegistry, @@ -151,6 +156,26 @@ func (h *Handler) opMetricsHandler( ) } +// internalTaskQueuePrefix identifies server-internal task queues +// (e.g. /temporal-sys/worker-commands/{namespace}/{worker_grouping_key}). +// Note: BreakdownMetricsByTaskQueue should NOT be enabled for these queues as +// they are per-worker and will cause cardinality explosion. +const internalTaskQueuePrefix = "/temporal-sys/" + +// recordNexusTaskRequest emits the nexus_task_requests metric with namespace, +// operation, client_name, and is_internal tags. +func (h *Handler) recordNexusTaskRequest(ctx context.Context, namespaceID string, taskQueueName string, operation string) { + nsName := h.namespaceName(namespace.ID(namespaceID)) + clientName, _ := headers.GetClientNameAndVersion(ctx) + isInternal := strings.HasPrefix(taskQueueName, internalTaskQueuePrefix) + metrics.NexusTaskRequests.With(h.metricsHandler).Record(1, + metrics.NamespaceTag(nsName.String()), + metrics.OperationTag(operation), + metrics.ClientNameTag(clientName), + metrics.IsInternalTag(isInternal), + ) +} + // AddActivityTask - adds an activity task. func (h *Handler) AddActivityTask( ctx context.Context, @@ -503,6 +528,11 @@ func (h *Handler) PollNexusTaskQueue(ctx context.Context, request *matchingservi enumspb.TASK_QUEUE_TYPE_NEXUS, metrics.MatchingPollWorkflowTaskQueueScope, ) + // Only record on the initial handler call (ForwardedSource == ""), not on + // the forwarded call to the root partition, to avoid double-counting. + if request.GetForwardedSource() == "" { + h.recordNexusTaskRequest(ctx, request.GetNamespaceId(), request.GetRequest().GetTaskQueue().GetName(), "PollNexusTaskQueue") + } if request.GetForwardedSource() != "" { h.reportForwardedPerTaskQueueCounter(opMetrics, namespace.ID(request.GetNamespaceId())) @@ -526,6 +556,7 @@ func (h *Handler) RespondNexusTaskCompleted(ctx context.Context, request *matchi enumspb.TASK_QUEUE_TYPE_NEXUS, metrics.MatchingRespondNexusTaskCompletedScope, ) + h.recordNexusTaskRequest(ctx, request.GetNamespaceId(), request.GetTaskQueue().GetName(), "RespondNexusTaskCompleted") return h.engine.RespondNexusTaskCompleted(ctx, request, opMetrics) } @@ -538,6 +569,7 @@ func (h *Handler) RespondNexusTaskFailed(ctx context.Context, request *matchings enumspb.TASK_QUEUE_TYPE_NEXUS, metrics.MatchingRespondNexusTaskFailedScope, ) + h.recordNexusTaskRequest(ctx, request.GetNamespaceId(), request.GetTaskQueue().GetName(), "RespondNexusTaskFailed") return h.engine.RespondNexusTaskFailed(ctx, request, opMetrics) } @@ -587,18 +619,41 @@ func (h *Handler) ListWorkers( if err != nil { return nil, err } - var workersInfo []*workerpb.WorkerInfo - for _, heartbeat := range resp.Workers { - workersInfo = append(workersInfo, &workerpb.WorkerInfo{ + // TODO: Stop populating workersInfo once all callers migrate to the Workers field. + workersInfo := make([]*workerpb.WorkerInfo, len(resp.Workers)) + workersList := make([]*workerpb.WorkerListInfo, len(resp.Workers)) + for i, heartbeat := range resp.Workers { + workersInfo[i] = &workerpb.WorkerInfo{ WorkerHeartbeat: heartbeat, - }) + } + workersList[i] = workerHeartbeatToListInfo(heartbeat) } return &matchingservice.ListWorkersResponse{ WorkersInfo: workersInfo, + Workers: workersList, NextPageToken: resp.NextPageToken, }, nil } +func workerHeartbeatToListInfo(hb *workerpb.WorkerHeartbeat) *workerpb.WorkerListInfo { + hostInfo := hb.GetHostInfo() + return &workerpb.WorkerListInfo{ + WorkerInstanceKey: hb.GetWorkerInstanceKey(), + WorkerIdentity: hb.GetWorkerIdentity(), + TaskQueue: hb.GetTaskQueue(), + DeploymentVersion: hb.GetDeploymentVersion(), + SdkName: hb.GetSdkName(), + SdkVersion: hb.GetSdkVersion(), + Status: hb.GetStatus(), + StartTime: hb.GetStartTime(), + HostName: hostInfo.GetHostName(), + WorkerGroupingKey: hostInfo.GetWorkerGroupingKey(), + ProcessId: hostInfo.GetProcessId(), + Plugins: hb.GetPlugins(), + Drivers: hb.GetDrivers(), + } +} + func (h *Handler) UpdateFairnessState( ctx context.Context, request *matchingservice.UpdateFairnessStateRequest, ) (*matchingservice.UpdateFairnessStateResponse, error) { diff --git a/service/matching/handler_test.go b/service/matching/handler_test.go new file mode 100644 index 00000000000..bbe64cc0591 --- /dev/null +++ b/service/matching/handler_test.go @@ -0,0 +1,249 @@ +package matching + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + deploymentpb "go.temporal.io/api/deployment/v1" + enumspb "go.temporal.io/api/enums/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + workerpb "go.temporal.io/api/worker/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/api/matchingservice/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/metrics/metricstest" + "go.temporal.io/server/common/namespace" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// nexusMetricsCaptureEngine is a stub Engine for Nexus handler tests. It embeds +// the Engine interface so only the methods under test need to be implemented. +type nexusMetricsCaptureEngine struct { + Engine // embedded to satisfy all other interface methods (will panic if called) +} + +func (e *nexusMetricsCaptureEngine) Start() {} +func (e *nexusMetricsCaptureEngine) Stop() {} + +func (e *nexusMetricsCaptureEngine) PollNexusTaskQueue( + _ context.Context, + _ *matchingservice.PollNexusTaskQueueRequest, + _ metrics.Handler, +) (*matchingservice.PollNexusTaskQueueResponse, error) { + return &matchingservice.PollNexusTaskQueueResponse{}, nil +} + +func (e *nexusMetricsCaptureEngine) RespondNexusTaskCompleted( + _ context.Context, + _ *matchingservice.RespondNexusTaskCompletedRequest, + _ metrics.Handler, +) (*matchingservice.RespondNexusTaskCompletedResponse, error) { + return &matchingservice.RespondNexusTaskCompletedResponse{}, nil +} + +// ctxWithClientName creates a context with the given client-name set in incoming +// gRPC metadata and a deadline (required by PollNexusTaskQueue). +func ctxWithClientName(t *testing.T, clientName string) context.Context { + t.Helper() + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + t.Cleanup(cancel) + md := metadata.New(map[string]string{ + headers.ClientNameHeaderName: clientName, + }) + return metadata.NewIncomingContext(ctx, md) +} + +// newTestHandler creates a Handler wired with the given capture handler, a stub +// engine, and a mock namespace registry that returns a fixed name. +func newTestHandler(t *testing.T, captureHandler *metricstest.CaptureHandler) (*Handler, *nexusMetricsCaptureEngine) { + t.Helper() + ctrl := gomock.NewController(t) + nsRegistry := namespace.NewMockRegistry(ctrl) + nsRegistry.EXPECT().GetNamespaceByID(gomock.Any()).Return(nil, assert.AnError).AnyTimes() + + stubEngine := &nexusMetricsCaptureEngine{} + + h := &Handler{ + engine: stubEngine, + config: NewConfig(dynamicconfig.NewNoopCollection()), + metricsHandler: captureHandler, + logger: log.NewNoopLogger(), + throttledLogger: log.NewNoopLogger(), + namespaceRegistry: nsRegistry, + } + // Mark handler as started so requests are served. + h.startWG.Add(1) + h.startWG.Done() + + return h, stubEngine +} + +// findMetricWithTag searches captured metrics for a recording of the named metric +// that contains the specified tag key/value pair. +func findMetricWithTag(snap metricstest.CaptureSnapshot, metricName, tagKey, tagValue string) bool { + for _, rec := range snap[metricName] { + if v, ok := rec.Tags[tagKey]; ok && v == tagValue { + return true + } + } + return false +} + +const nexusTaskRequestsMetric = "nexus_task_requests" + +func TestNexusHandlersEmitClientNameMetric(t *testing.T) { + const expectedClientName = "temporal-go" + taskQueue := &taskqueuepb.TaskQueue{Name: "test-tq", Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + t.Run("PollNexusTaskQueue", func(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + h, _ := newTestHandler(t, captureHandler) + ctx := ctxWithClientName(t, expectedClientName) + + _, err := h.PollNexusTaskQueue(ctx, &matchingservice.PollNexusTaskQueueRequest{ + NamespaceId: "test-ns-id", + Request: &workflowservice.PollNexusTaskQueueRequest{ + TaskQueue: taskQueue, + }, + }) + require.NoError(t, err) + + snap := capture.Snapshot() + require.NotEmpty(t, snap[nexusTaskRequestsMetric]) + require.True(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "client_name", expectedClientName), + "should have client_name tag, got: %v", snap) + require.True(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "operation", "PollNexusTaskQueue"), + "should have operation tag, got: %v", snap) + require.True(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "is_internal", "false"), + "should have is_internal=false, got: %v", snap) + }) + + t.Run("RespondNexusTaskCompleted", func(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + h, _ := newTestHandler(t, captureHandler) + ctx := ctxWithClientName(t, expectedClientName) + + _, err := h.RespondNexusTaskCompleted(ctx, &matchingservice.RespondNexusTaskCompletedRequest{ + NamespaceId: "test-ns-id", + TaskQueue: taskQueue, + }) + require.NoError(t, err) + + snap := capture.Snapshot() + require.NotEmpty(t, snap[nexusTaskRequestsMetric]) + require.True(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "client_name", expectedClientName), + "should have client_name tag, got: %v", snap) + require.True(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "operation", "RespondNexusTaskCompleted"), + "should have operation tag, got: %v", snap) + require.True(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "is_internal", "false"), + "should have is_internal=false, got: %v", snap) + }) + + t.Run("no client_name when header absent", func(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + h, _ := newTestHandler(t, captureHandler) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err := h.PollNexusTaskQueue(ctx, &matchingservice.PollNexusTaskQueueRequest{ + NamespaceId: "test-ns-id", + Request: &workflowservice.PollNexusTaskQueueRequest{ + TaskQueue: taskQueue, + }, + }) + require.NoError(t, err) + + snap := capture.Snapshot() + require.NotEmpty(t, snap[nexusTaskRequestsMetric], + "nexus_task_requests should still be emitted without client_name header") + assert.False(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "client_name", expectedClientName), + "should not have client_name tag when header is absent, got: %v", snap) + }) + + t.Run("is_internal for /temporal-sys/ task queue", func(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + h, _ := newTestHandler(t, captureHandler) + ctx := ctxWithClientName(t, expectedClientName) + internalTQ := &taskqueuepb.TaskQueue{ + Name: "/temporal-sys/worker-commands/ns/grouping-key", + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + } + + _, err := h.PollNexusTaskQueue(ctx, &matchingservice.PollNexusTaskQueueRequest{ + NamespaceId: "test-ns-id", + Request: &workflowservice.PollNexusTaskQueueRequest{ + TaskQueue: internalTQ, + }, + }) + require.NoError(t, err) + + snap := capture.Snapshot() + require.NotEmpty(t, snap[nexusTaskRequestsMetric]) + require.True(t, findMetricWithTag(snap, nexusTaskRequestsMetric, "is_internal", "true"), + "should have is_internal=true for /temporal-sys/ task queue, got: %v", snap) + }) +} + +func TestWorkerHeartbeatToListInfo_AllFieldsSet(t *testing.T) { + hb := &workerpb.WorkerHeartbeat{ + WorkerInstanceKey: "instance-key-1", + WorkerIdentity: "worker-identity-1", + HostInfo: &workerpb.WorkerHostInfo{ + HostName: "host-1", + WorkerGroupingKey: "grouping-key-1", + ProcessId: "pid-123", + }, + TaskQueue: "my-task-queue", + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "dep-1", BuildId: "build-1"}, + SdkName: "temporal-go", + SdkVersion: "1.0.0", + Status: enumspb.WORKER_STATUS_RUNNING, + StartTime: timestamppb.Now(), + Plugins: []*workerpb.PluginInfo{{Name: "plugin-1"}}, + Drivers: []*workerpb.StorageDriverInfo{{Type: "driver-1"}}, + } + + result := workerHeartbeatToListInfo(hb) + require.NotNil(t, result) + + resultReflect := result.ProtoReflect() + fields := resultReflect.Descriptor().Fields() + for i := range fields.Len() { + fd := fields.Get(i) + val := resultReflect.Get(fd) + if fd.IsList() { + assert.NotEqualf(t, 0, val.List().Len(), + "WorkerListInfo repeated field %q is empty — "+ + "if you added a new field to WorkerListInfo, make sure to populate it from WorkerHeartbeat", + fd.Name(), + ) + } else { + assert.Truef(t, resultReflect.Has(fd), + "WorkerListInfo field %q is not set by workerHeartbeatToListInfo — "+ + "if you added a new field to WorkerListInfo, make sure to populate it from WorkerHeartbeat", + fd.Name(), + ) + } + } +} diff --git a/service/matching/hooks/task_lifecycle_hooks.go b/service/matching/hooks/task_lifecycle_hooks.go new file mode 100644 index 00000000000..7f7d3b7a96f --- /dev/null +++ b/service/matching/hooks/task_lifecycle_hooks.go @@ -0,0 +1,46 @@ +package hooks + +import ( + "context" + + deploymentpb "go.temporal.io/api/deployment/v1" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/tqid" +) + +type ( + // TaskQueuePartition is a simplified version of tqid.Partition that removes details + // the hooks should not concern themselves with + TaskQueuePartition interface { + NamespaceId() string + TaskQueue() *tqid.TaskQueue + TaskType() enumspb.TaskQueueType + Kind() enumspb.TaskQueueKind + } + + TaskHookFactoryCreateDetails struct { + Namespace *namespace.Namespace + Partition TaskQueuePartition + } + TaskAddHookDetails struct { + DeploymentVersion *deploymentpb.WorkerDeploymentVersion + IsSyncMatch bool + } + + TaskHookFactory interface { + // Create returns a TaskHook instance that will be leveraged as part + // of the specific task queue partition (as specified in the details). + // This might also return nil, if no hooking into that task queue + // partition is desired. + Create(details *TaskHookFactoryCreateDetails) TaskHook + } + TaskHook interface { + // Start is called when the task queue partition manager for the hooks partition is started + Start() + // Stop is called when the task queue partition manager for the hooks partition is stopped + Stop() + // ProcessTaskAdd is called for each Task addition (whether sync or async matching) + ProcessTaskAdd(ctx context.Context, event *TaskAddHookDetails) + } +) diff --git a/service/matching/matcher.go b/service/matching/matcher.go index 7af77302705..7ef49b4f9e7 100644 --- a/service/matching/matcher.go +++ b/service/matching/matcher.go @@ -373,6 +373,9 @@ forLoop: } func (tm *TaskMatcher) emitDispatchLatency(task *internalTask, forwarded bool) { + if tm.config.EmitTaskDispatchLatencyAtPoll() { + return // metric will be emitted at poll response + } if task.event.Data.CreateTime == nil { return // should not happen but for safety } diff --git a/service/matching/matcher_test.go b/service/matching/matcher_test.go index 61b748bb5c9..fc0756b96ff 100644 --- a/service/matching/matcher_test.go +++ b/service/matching/matcher_test.go @@ -158,7 +158,7 @@ func (t *MatcherTestSuite) testRemoteSyncMatch(taskSource enumsspb.TaskSource) { var remotePollErr error var remotePollResp matchingservice.PollWorkflowTaskQueueResponse t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { task, err := t.rootMatcher.Poll(arg0, &pollMetadata{}) if err != nil { remotePollErr = err @@ -185,7 +185,7 @@ func (t *MatcherTestSuite) testRemoteSyncMatch(taskSource enumsspb.TaskSource) { var remoteSyncMatch bool var req *matchingservice.AddWorkflowTaskRequest t.client.EXPECT().AddWorkflowTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...any) { req = arg1 task.forwardInfo = req.GetForwardInfo() close(pollSigC) @@ -269,7 +269,7 @@ func (t *MatcherTestSuite) TestForwardingWhenBacklogIsYoung() { wg.Add(1) t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { wg.Done() }, ).Return(nil, errMatchingHostThrottleTest) @@ -299,7 +299,7 @@ func (t *MatcherTestSuite) TestForwardingWhenBacklogIsYoung() { wg.Add(1) t.client.EXPECT().AddWorkflowTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...any) { // Offer forwarding has occured wg.Done() }, @@ -462,7 +462,7 @@ func (t *MatcherTestSuite) TestSyncMatchFailure() { var req *matchingservice.AddWorkflowTaskRequest t.client.EXPECT().AddWorkflowTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...any) { req = arg1 }, ).Return(&matchingservice.AddWorkflowTaskResponse{}, errMatchingHostThrottleTest) @@ -476,7 +476,7 @@ func (t *MatcherTestSuite) TestSyncMatchFailure() { func (t *MatcherTestSuite) TestQueryNoCurrentPollersButRecentPollers() { t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { _, err := t.rootMatcher.PollForQuery(arg0, &pollMetadata{}) t.Assert().ErrorIs(err, errNoTasks) }, @@ -491,7 +491,7 @@ func (t *MatcherTestSuite) TestQueryNoCurrentPollersButRecentPollers() { // send query and expect generic DeadlineExceeded error task = newInternalQueryTask(uuid.NewString(), &matchingservice.QueryWorkflowRequest{}) t.client.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(ctx context.Context, req *matchingservice.QueryWorkflowRequest, arg2 ...interface{}) { + func(ctx context.Context, req *matchingservice.QueryWorkflowRequest, arg2 ...any) { task.forwardInfo = req.GetForwardInfo() resp, err := t.rootMatcher.OfferQuery(ctx, task) t.Nil(resp) @@ -507,7 +507,7 @@ func (t *MatcherTestSuite) TestQueryNoCurrentPollersButRecentPollers() { func (t *MatcherTestSuite) TestQueryNoRecentPoller() { t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { _, err := t.rootMatcher.PollForQuery(arg0, &pollMetadata{}) t.Assert().ErrorIs(err, errNoTasks) }, @@ -529,7 +529,7 @@ func (t *MatcherTestSuite) TestQueryNoRecentPoller() { // make the query and expect errNoRecentPoller task = newInternalQueryTask(uuid.NewString(), &matchingservice.QueryWorkflowRequest{}) t.client.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(ctx context.Context, req *matchingservice.QueryWorkflowRequest, arg2 ...interface{}) { + func(ctx context.Context, req *matchingservice.QueryWorkflowRequest, arg2 ...any) { task.forwardInfo = req.GetForwardInfo() resp, err := t.rootMatcher.OfferQuery(ctx, task) t.Nil(resp) @@ -547,7 +547,7 @@ func (t *MatcherTestSuite) TestQueryNoPollerAtAll() { task := newInternalQueryTask(uuid.NewString(), &matchingservice.QueryWorkflowRequest{}) t.client.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(ctx context.Context, req *matchingservice.QueryWorkflowRequest, arg2 ...interface{}) { + func(ctx context.Context, req *matchingservice.QueryWorkflowRequest, arg2 ...any) { task.forwardInfo = req.GetForwardInfo() resp, err := t.rootMatcher.OfferQuery(ctx, task) t.Nil(resp) @@ -624,7 +624,7 @@ func (t *MatcherTestSuite) TestQueryRemoteSyncMatch() { var req *matchingservice.QueryWorkflowRequest t.client.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.QueryWorkflowRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.QueryWorkflowRequest, arg2 ...any) { req = arg1 close(pollSigC) time.Sleep(10 * time.Millisecond) @@ -670,7 +670,7 @@ func (t *MatcherTestSuite) TestQueryRemoteSyncMatchError() { var req *matchingservice.QueryWorkflowRequest t.client.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.QueryWorkflowRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.QueryWorkflowRequest, arg2 ...any) { req = arg1 close(pollSigC) time.Sleep(10 * time.Millisecond) @@ -756,7 +756,7 @@ func (t *MatcherTestSuite) TestMustOfferRemoteMatch() { var req *matchingservice.AddWorkflowTaskRequest t.client.EXPECT().AddWorkflowTask(gomock.Any(), gomock.Any(), gomock.Any()).Return(&matchingservice.AddWorkflowTaskResponse{}, errMatchingHostThrottleTest) t.client.EXPECT().AddWorkflowTask(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.AddWorkflowTaskRequest, arg2 ...any) { req = arg1 task := newInternalTaskForSyncMatch(task.event.AllocatedTaskInfo.Data, req.ForwardInfo, 0, nil) close(pollSigC) @@ -785,7 +785,7 @@ func (t *MatcherTestSuite) TestRemotePoll() { var req *matchingservice.PollWorkflowTaskQueueRequest t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { req = arg1 }, ).Return(&matchingservice.PollWorkflowTaskQueueResponse{ @@ -812,7 +812,7 @@ func (t *MatcherTestSuite) TestRemotePollForQuery() { var req *matchingservice.PollWorkflowTaskQueueRequest t.client.EXPECT().PollWorkflowTaskQueue(gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...interface{}) { + func(arg0 context.Context, arg1 *matchingservice.PollWorkflowTaskQueueRequest, arg2 ...any) { req = arg1 }, ).Return(&matchingservice.PollWorkflowTaskQueueResponse{ diff --git a/service/matching/matching_engine.go b/service/matching/matching_engine.go index 0195923910d..bf99f34b6e9 100644 --- a/service/matching/matching_engine.go +++ b/service/matching/matching_engine.go @@ -33,6 +33,7 @@ import ( "go.temporal.io/server/client/matching" "go.temporal.io/server/common" "go.temporal.io/server/common/backoff" + "go.temporal.io/server/common/cache" "go.temporal.io/server/common/clock" hlc "go.temporal.io/server/common/clock/hybrid_logical_clock" "go.temporal.io/server/common/cluster" @@ -54,12 +55,14 @@ import ( "go.temporal.io/server/common/searchattribute" serviceerrors "go.temporal.io/server/common/serviceerror" "go.temporal.io/server/common/stream_batcher" + "go.temporal.io/server/common/taskqueue" "go.temporal.io/server/common/tasktoken" "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/tqid" "go.temporal.io/server/common/util" "go.temporal.io/server/common/worker_versioning" "go.temporal.io/server/service/history/api" + "go.temporal.io/server/service/matching/hooks" "go.temporal.io/server/service/worker/workerdeployment" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -68,6 +71,14 @@ const ( // If sticky poller is not seem in last 10s, we treat it as sticky worker unavailable // This seems aggressive, but the default sticky schedule_to_start timeout is 5s, so 10s seems reasonable. stickyPollerUnavailableWindow = 10 * time.Second + + // shutdownWorkersCacheMaxSize is generous: each entry is a UUID string (~36 bytes), + // entries auto-expire after shutdownWorkersCacheTTL, and the cache only grows when + // workers shut down. Even with aggressive autoscaling, a single matching node is + // unlikely to see more than a few hundred worker shutdowns within the TTL window. + // LRU eviction ensures the oldest entries (least likely to re-poll) are evicted first. + shutdownWorkersCacheMaxSize = 10000 + shutdownWorkersCacheTTL = 30 * time.Second // If a compatible poller hasn't been seen for this time, we fail the CommitBuildId // Set to 70s so that it's a little over the max time a poller should be kept waiting. versioningPollerSeenWindow = 70 * time.Second @@ -139,7 +150,7 @@ type ( timeSource clock.TimeSource visibilityManager manager.VisibilityManager nexusEndpointClient *nexusEndpointClient - nexusEndpointsOwnershipLostCh chan struct{} + nexusEndpointsOwnershipLostCh atomic.Value // stores chan struct{} saMapperProvider searchattribute.MapperProvider saProvider searchattribute.Provider metricsHandler metrics.Handler @@ -166,6 +177,10 @@ type ( outstandingPollers collection.SyncMap[string, context.CancelFunc] // workerInstancePollers tracks pollers by worker instance key for bulk cancellation during shutdown. workerInstancePollers workerPollerTracker + // shutdownWorkers is a TTL cache of recently-shutdown worker instance keys. + // Polls from workers in this cache are rejected immediately to prevent + // zombie re-polls from stealing tasks after ShutdownWorker. + shutdownWorkers cache.Cache // Only set if global namespaces are enabled on the cluster. namespaceReplicationQueue persistence.NamespaceReplicationQueue // Lock to serialize replication queue updates. @@ -176,6 +191,8 @@ type ( reachabilityCache reachabilityCache // Rate limiter to limit the task dispatch rateLimiter TaskDispatchRateLimiter + + taskHookFactories []hooks.TaskHookFactory } ) @@ -255,32 +272,33 @@ func NewEngine( saMapperProvider searchattribute.MapperProvider, rateLimiter TaskDispatchRateLimiter, historySerializer serialization.Serializer, + taskHookFactories []hooks.TaskHookFactory, ) Engine { scopedMetricsHandler := metricsHandler.WithTags(metrics.OperationTag(metrics.MatchingEngineScope)) e := &matchingEngineImpl{ - status: common.DaemonStatusInitialized, - taskManager: taskManager, - fairTaskManager: fairTaskManager, - historyClient: historyClient, - matchingRawClient: matchingRawClient, - tokenSerializer: tasktoken.NewSerializer(), - workerDeploymentClient: workerDeploymentClient, - historySerializer: historySerializer, - logger: log.With(logger, tag.ComponentMatchingEngine), - throttledLogger: log.With(throttledLogger, tag.ComponentMatchingEngine), - namespaceRegistry: namespaceRegistry, - hostInfoProvider: hostInfoProvider, - serviceResolver: resolver, - membershipChangedCh: make(chan *membership.ChangedEvent, 1), // allow one signal to be buffered while we're working - clusterMeta: clusterMeta, - timeSource: clock.NewRealTimeSource(), // No need to mock this at the moment - visibilityManager: visibilityManager, - nexusEndpointClient: newEndpointClient(config.NexusEndpointsRefreshInterval, nexusEndpointManager), - nexusEndpointsOwnershipLostCh: make(chan struct{}), - saProvider: saProvider, - saMapperProvider: saMapperProvider, - metricsHandler: scopedMetricsHandler, - partitions: make(map[tqid.PartitionKey]taskQueuePartitionManager), + status: common.DaemonStatusInitialized, + taskManager: taskManager, + fairTaskManager: fairTaskManager, + historyClient: historyClient, + matchingRawClient: matchingRawClient, + tokenSerializer: tasktoken.NewSerializer(), + workerDeploymentClient: workerDeploymentClient, + historySerializer: historySerializer, + logger: log.With(logger, tag.ComponentMatchingEngine), + throttledLogger: log.With(throttledLogger, tag.ComponentMatchingEngine), + namespaceRegistry: namespaceRegistry, + hostInfoProvider: hostInfoProvider, + serviceResolver: resolver, + membershipChangedCh: make(chan *membership.ChangedEvent, 1), // allow one signal to be buffered while we're working + clusterMeta: clusterMeta, + timeSource: clock.NewRealTimeSource(), // No need to mock this at the moment + visibilityManager: visibilityManager, + nexusEndpointClient: newEndpointClient(config.NexusEndpointsRefreshInterval, nexusEndpointManager), + // nexusEndpointsOwnershipLostCh initialized below + saProvider: saProvider, + saMapperProvider: saMapperProvider, + metricsHandler: scopedMetricsHandler, + partitions: make(map[tqid.PartitionKey]taskQueuePartitionManager), gaugeMetrics: gaugeMetrics{ loadedTaskQueueFamilyCount: make(map[taskQueueCounterKey]int), loadedTaskQueueCount: make(map[taskQueueCounterKey]int), @@ -294,10 +312,13 @@ func NewEngine( nexusResults: collection.NewSyncMap[string, chan *nexusResult](), outstandingPollers: collection.NewSyncMap[string, context.CancelFunc](), workerInstancePollers: workerPollerTracker{pollers: make(map[string]map[string]context.CancelFunc)}, + shutdownWorkers: cache.New(shutdownWorkersCacheMaxSize, &cache.Options{TTL: shutdownWorkersCacheTTL}), namespaceReplicationQueue: namespaceReplicationQueue, userDataUpdateBatchers: collection.NewSyncMap[namespace.ID, *stream_batcher.Batcher[*userDataUpdate, error]](), rateLimiter: rateLimiter, + taskHookFactories: taskHookFactories, } + e.nexusEndpointsOwnershipLostCh.Store(make(chan struct{})) e.reachabilityCache = newReachabilityCache( metrics.NoopMetricsHandler, visibilityManager, @@ -432,26 +453,12 @@ func (e *matchingEngineImpl) getTaskQueuePartitionManager( create bool, loadCause loadCause, ) (retPM taskQueuePartitionManager, retCreated bool, retErr error) { - var newPM *taskQueuePartitionManagerImpl - defer func() { if retErr != nil || retPM == nil { return } - if retErr = retPM.WaitUntilInitialized(ctx); retErr != nil { e.unloadTaskQueuePartition(retPM, unloadCauseInitError) - return - } - - if retCreated { - // Whenever a root partition is loaded, we need to force all child partitions to load. - // If there is a backlog of tasks on any child partitions, force loading will ensure - // that they can forward their tasks the poller which caused the root partition to be - // loaded. These partitions could be managed by this matchingEngineImpl, but are most - // likely not. We skip checking and just make gRPC requests to force loading them all. - // Note that if retCreated is true, retPM must be newPM, so we can use newPM here. - newPM.ForceLoadAllChildPartitions() } }() @@ -472,6 +479,7 @@ func (e *matchingEngineImpl) getTaskQueuePartitionManager( return nil, false, err } + var newPM *taskQueuePartitionManagerImpl tqConfig := newTaskQueueConfig(partition.TaskQueue(), e.config, namespaceEntry.Name()) tqConfig.loadCause = loadCause logger, throttledLogger, metricsHandler := e.loggerAndMetricsForPartition(namespaceEntry, partition, tqConfig) @@ -691,6 +699,7 @@ pollLoop: } if task.isStarted() { // tasks received from remote are already started. So, simply forward the response + // no need to emit task dispatch latency metric because the parent partition already did it. return e.convertPollWorkflowTaskQueueResponse(task.pollWorkflowTaskQueueResponse(), task.namespace) } @@ -739,6 +748,8 @@ pollLoop: NextPageToken: nextPageToken, } + // Local query match. Emit the dispatch latency metric. This metric does not include the query response time. + e.emitTaskDispatchLatency(task, partition, req.GetNamespaceId(), request.Namespace, pollMetadata) return e.createPollWorkflowTaskQueueResponse(task, resp, opMetrics), nil } @@ -824,6 +835,7 @@ pollLoop: } task.finish(nil, true) + e.emitTaskDispatchLatency(task, partition, req.GetNamespaceId(), request.Namespace, pollMetadata) return e.createPollWorkflowTaskQueueResponse(task, resp, opMetrics), nil } } @@ -1098,6 +1110,7 @@ pollLoop: continue pollLoop } task.finish(nil, true) + e.emitTaskDispatchLatency(task, partition, req.GetNamespaceId(), request.Namespace, pollMetadata) return e.createPollActivityTaskQueueResponse(task, resp, opMetrics), nil } } @@ -1215,6 +1228,9 @@ func (e *matchingEngineImpl) CancelOutstandingWorkerPolls( ctx context.Context, request *matchingservice.CancelOutstandingWorkerPollsRequest, ) (*matchingservice.CancelOutstandingWorkerPollsResponse, error) { + if request.WorkerInstanceKey != "" { + e.shutdownWorkers.Put(request.WorkerInstanceKey, struct{}{}) + } cancelledCount := e.workerInstancePollers.CancelAll(request.WorkerInstanceKey) e.removePollerFromHistory(ctx, request) return &matchingservice.CancelOutstandingWorkerPollsResponse{CancelledCount: cancelledCount}, nil @@ -1324,7 +1340,7 @@ func (e *matchingEngineImpl) DescribeTaskQueue( } numPartitions := max(tqConfig.NumWritePartitions(), tqConfig.NumReadPartitions()) for _, taskQueueType := range req.TaskQueueTypes { - for i := 0; i < numPartitions; i++ { + for i := range numPartitions { partitionResp, err := e.matchingRawClient.DescribeTaskQueuePartition(ctx, &matchingservice.DescribeTaskQueuePartitionRequest{ NamespaceId: request.GetNamespaceId(), TaskQueuePartition: &taskqueuespb.TaskQueuePartition{ @@ -1356,16 +1372,12 @@ func (e *matchingEngineImpl) DescribeTaskQueue( if req.GetReportStats() { totalStats := physicalTqInfos[buildId][taskQueueType].TaskQueueStats partitionStats := vii.PhysicalTaskQueueInfo.TaskQueueStats - mergedStats = &taskqueuepb.TaskQueueStats{ - ApproximateBacklogCount: totalStats.ApproximateBacklogCount + partitionStats.ApproximateBacklogCount, - ApproximateBacklogAge: oldestBacklogAge(totalStats.ApproximateBacklogAge, partitionStats.ApproximateBacklogAge), - TasksAddRate: totalStats.TasksAddRate + partitionStats.TasksAddRate, - TasksDispatchRate: totalStats.TasksDispatchRate + partitionStats.TasksDispatchRate, - } + mergedStats = cloneTaskQueueStats(totalStats) + taskqueue.MergeStats(mergedStats, partitionStats) } physicalTqInfos[buildId][taskQueueType] = &taskqueuespb.PhysicalTaskQueueInfo{ - Pollers: dedupPollers(append(physInfo.GetPollers(), vii.PhysicalTaskQueueInfo.GetPollers()...)), + Pollers: taskqueue.DedupPollers(append(physInfo.GetPollers(), vii.PhysicalTaskQueueInfo.GetPollers()...)), TaskQueueStats: mergedStats, } } @@ -1522,8 +1534,8 @@ func (e *matchingEngineImpl) DescribeTaskQueue( if _, ok := taskQueueStatsByPriority[pri]; !ok { taskQueueStatsByPriority[pri] = &taskqueuepb.TaskQueueStats{} } - mergeStats(taskQueueStats, priorityStats) - mergeStats(taskQueueStatsByPriority[pri], priorityStats) + taskqueue.MergeStats(taskQueueStats, priorityStats) + taskqueue.MergeStats(taskQueueStatsByPriority[pri], priorityStats) } } } @@ -1604,18 +1616,6 @@ func (e *matchingEngineImpl) DescribeVersionedTaskQueues( return resp, nil } -func dedupPollers(pollerInfos []*taskqueuepb.PollerInfo) []*taskqueuepb.PollerInfo { - allKeys := make(map[string]bool) - var list []*taskqueuepb.PollerInfo - for _, item := range pollerInfos { - if _, value := allKeys[item.GetIdentity()]; !value { - allKeys[item.GetIdentity()] = true - list = append(list, item) - } - } - return list -} - func (e *matchingEngineImpl) DescribeTaskQueuePartition( ctx context.Context, request *matchingservice.DescribeTaskQueuePartitionRequest, @@ -2109,7 +2109,7 @@ func (e *matchingEngineImpl) SyncDeploymentUserData( deploymentData.UnversionedRampData = vd } - } else if idx := worker_versioning.FindDeploymentVersion(deploymentData, vd.GetVersion()); idx >= 0 { + } else if idx := worker_versioning.FindOldDeploymentVersion(deploymentData, vd.GetVersion()); idx >= 0 { old := deploymentData.Versions[idx] if old.GetRoutingUpdateTime().AsTime().After(vd.GetRoutingUpdateTime().AsTime()) { continue @@ -2131,19 +2131,17 @@ func (e *matchingEngineImpl) SyncDeploymentUserData( clearVersionFromRoutingConfig(workerDeploymentData, nil, vd) } } else if v := req.GetForgetVersion(); v != nil { - if idx := worker_versioning.FindDeploymentVersion(deploymentData, v); idx >= 0 { + // Go through the new and old deployment data format for this deployment and remove the version if present. + workerDeploymentData := deploymentData.GetDeploymentsData()[v.GetDeploymentName()] + deleted := removeDeploymentVersions( + deploymentData, + v.GetDeploymentName(), + workerDeploymentData, + []string{v.GetBuildId()}, + /* removeOldFormat */ true, + ) + if deleted { changed = true - deploymentData.Versions = append(deploymentData.Versions[:idx], deploymentData.Versions[idx+1:]...) - - // Go through the new deployment data format for this deployment and remove the version if present. - workerDeploymentData := deploymentData.GetDeploymentsData()[v.GetDeploymentName()] - _ = removeDeploymentVersions( - deploymentData, - v.GetDeploymentName(), - workerDeploymentData, - []string{v.GetBuildId()}, - /* removeOldFormat */ false, - ) } } else { @@ -2158,7 +2156,7 @@ func (e *matchingEngineImpl) SyncDeploymentUserData( rc := req.GetUpdateRoutingConfig() tqWorkerDeploymentData := deploymentData.GetDeploymentsData()[req.GetDeploymentName()] - ignoreRevCheck, _ := testhooks.Get[bool](e.testHooks, testhooks.MatchingIgnoreRoutingConfigRevisionCheck) + ignoreRevCheck, _ := testhooks.Get(e.testHooks, testhooks.MatchingIgnoreRoutingConfigRevisionCheck, namespace.ID(req.NamespaceId)) if ignoreRevCheck || rc.GetRevisionNumber() > tqWorkerDeploymentData.GetRoutingConfig().GetRevisionNumber() { changed = true // Update routing config when newer or equal revision is provided @@ -2629,6 +2627,7 @@ pollLoop: nexusReq.Header[nexus.HeaderOperationTimeout] = commonnexus.FormatDuration(time.Until(task.nexus.operationDeadline)) } + e.emitTaskDispatchLatency(task, partition, req.GetNamespaceId(), request.Namespace, pollMetadata) return &matchingservice.PollNexusTaskQueueResponse{ Response: &workflowservice.PollNexusTaskQueueResponse{ TaskToken: serializedToken, @@ -2759,7 +2758,7 @@ func (e *matchingEngineImpl) ListNexusEndpoints(ctx context.Context, request *ma func (e *matchingEngineImpl) checkNexusEndpointsOwnership() (bool, <-chan struct{}, error) { // Get the channel before checking the condition to prevent the channel from being closed while we're running this // check. - ch := e.nexusEndpointsOwnershipLostCh + ch := e.nexusEndpointsOwnershipLostCh.Load().(chan struct{}) //nolint:revive // type is always chan struct{} self := e.hostInfoProvider.HostInfo().Identity() owner, err := e.serviceResolver.Lookup(nexusEndpointsTablePartitionRoutingKey) if err != nil { @@ -2777,8 +2776,7 @@ func (e *matchingEngineImpl) notifyNexusEndpointsOwnershipChange() { return } if !isOwner { - close(e.nexusEndpointsOwnershipLostCh) - e.nexusEndpointsOwnershipLostCh = make(chan struct{}) + close(e.nexusEndpointsOwnershipLostCh.Swap(make(chan struct{})).(chan struct{})) //nolint:revive // type is always chan struct{} } e.nexusEndpointClient.notifyOwnershipChanged(isOwner) } @@ -2840,7 +2838,7 @@ func (e *matchingEngineImpl) getAllPartitionRpcNames( } n := e.config.NumTaskqueueWritePartitions(ns.String(), taskQueueFamily.Name(), taskQueueType) - for i := 0; i < n; i++ { + for i := range n { partitionKeys = append(partitionKeys, taskQueueFamily.TaskQueue(taskQueueType).NormalPartition(i).RpcName()) } return partitionKeys, nil @@ -2862,6 +2860,17 @@ func (e *matchingEngineImpl) pollTask( // reached, instead of emptyTask, context timeout error is returned to the frontend by the rpc stack, // which counts against our SLO. By shortening the timeout by a very small amount, the emptyTask can be // returned to the handler before a context timeout error is generated. + workerInstanceKey := pollMetadata.workerInstanceKey + if workerInstanceKey != "" && e.shutdownWorkers.Get(workerInstanceKey) != nil { + e.logger.Info("Rejecting poll from recently-shutdown worker", + tag.WorkflowNamespaceID(partition.NamespaceId()), + tag.WorkflowTaskQueueName(partition.TaskQueue().Name()), + tag.WorkflowTaskQueueType(partition.TaskType()), + tag.NewStringTag("worker-instance-key", workerInstanceKey), + ) + return nil, false, errNoTasks + } + ctx, cancel := contextutil.WithDeadlineBuffer(ctx, pm.LongPollExpirationInterval(), returnEmptyTaskTimeBudget) defer cancel() @@ -2870,7 +2879,6 @@ func (e *matchingEngineImpl) pollTask( // Also track by worker instance key for bulk cancellation during shutdown. // Use UUID (not pollerID) because pollerID is reused when forwarded. - workerInstanceKey := pollMetadata.workerInstanceKey pollerTrackerKey := uuid.NewString() if workerInstanceKey != "" { e.workerInstancePollers.Add(workerInstanceKey, pollerTrackerKey, cancel) @@ -2886,6 +2894,70 @@ func (e *matchingEngineImpl) pollTask( return pm.PollTask(ctx, pollMetadata) } +// emitTaskDispatchLatency emits latency metrics for a task dispatched to a worker. +// Here is what task_dispatch_latency measures vs schedule_to_start_latency: +// +// Latency | task_dispatch | schedule_to_start +// +// --------------------------------------------------+------------------+------------------ +// +// transfer task processing | excluded | included +// record*TaskStarted latency | included | partial +// task forward latency | included | included +// poll forward latency | excluded for now | excluded +// backlog delay | included | included +// sync match delay | included | included +// rescheduling of the same task attempt by History | resets latency | does not reset +// +// ---------------------------------------------------------------------------------------- +func (e *matchingEngineImpl) emitTaskDispatchLatency( + task *internalTask, + partition tqid.Partition, + namespaceID string, + namespaceName string, + pollMetadata *pollMetadata, +) { + tqName := partition.TaskQueue().Name() + taskType := partition.TaskType() + + if !e.config.EmitTaskDispatchLatencyAtPoll(namespaceName, tqName, taskType) { + return + } + + taskCreateTime := task.getCreateTime() + if taskCreateTime == nil { + return + } + + // Determine origin partition: for forwarded tasks use the origin partition from + // forward info; for local tasks use the current partition. + originPartition := partition + if task.isForwarded() && task.forwardInfo.GetOriginPartition() != "" { + o, err := tqid.NormalPartitionFromRpcName(task.forwardInfo.GetOriginPartition(), namespaceID, taskType) + if err == nil { + originPartition = o + } // else ignore the error and use the current partition + } + + workerVersion := worker_versioning.WorkerDeploymentVersionToStringV32(worker_versioning.DeploymentVersionFromOptions(pollMetadata.deploymentOptions)) + + handler := metrics.GetPerTaskQueuePartitionIDScope( + e.metricsHandler, + namespaceName, + originPartition, + e.config.BreakdownMetricsByTaskQueue(namespaceName, tqName, taskType), + e.config.BreakdownMetricsByPartition(namespaceName, tqName, taskType), + ) + + metrics.TaskDispatchLatencyPerTaskQueue.With(handler).Record( + time.Since(timestamp.TimeValue(taskCreateTime)), + metrics.TaskSourceTag(task.source), + metrics.ForwardedTag(task.isForwarded()), + metrics.MatchingTaskPriorityTag(task.getPriority().GetPriorityKey()), + metrics.WorkerVersionTag(workerVersion, e.config.BreakdownMetricsByBuildID(namespaceName, tqName, taskType)), + ) +} + // Unloads the given task queue partition. If it has already been unloaded (i.e. it's not present in the loaded // partitions map), then does nothing. // partitions map), unloadPM.Stop(...) is still called. @@ -3215,14 +3287,7 @@ func (e *matchingEngineImpl) recordWorkflowTaskStarted( ctx, cancel := newRecordTaskStartedContext(ctx, task) defer cancel() - // Only send the target Deployment Version if it is different from the poller's version. - // If the poller is not versioned, we still send the target version because the workflow may be moving from an - // unversioned worker to a versioned worker. - var sentTargetVersion *deploymentpb.WorkerDeploymentVersion - if task.targetWorkerDeploymentVersion.GetBuildId() != pollReq.DeploymentOptions.GetBuildId() || - task.targetWorkerDeploymentVersion.GetDeploymentName() != pollReq.DeploymentOptions.GetDeploymentName() { - sentTargetVersion = worker_versioning.ExternalWorkerDeploymentVersionFromVersion(task.targetWorkerDeploymentVersion) - } + sentTargetVersion := worker_versioning.ExternalWorkerDeploymentVersionFromVersion(task.targetWorkerDeploymentVersion) recordStartedRequest := &historyservice.RecordWorkflowTaskStartedRequest{ NamespaceId: task.event.Data.GetNamespaceId(), @@ -3553,6 +3618,10 @@ func (e *matchingEngineImpl) UpdateFairnessState( return &matchingservice.UpdateFairnessStateResponse{}, nil } +func (e *matchingEngineImpl) newTaskTracker() *taskTracker { + return newTaskTracker(e.timeSource, 5*time.Second, 30*time.Second) +} + // migrateOldFormatVersions moves versions present in the given deployment from the // deprecated old-format slice into the new per-deployment map. // @@ -3593,15 +3662,15 @@ func removeDeploymentVersions( buildIDs []string, removeOldFormat bool, ) bool { - if workerDeploymentData == nil { + if workerDeploymentData == nil && !removeOldFormat { return false } changed := false deletedInNew := false for _, buildID := range buildIDs { - if _, exists := workerDeploymentData.Versions[buildID]; exists { - delete(workerDeploymentData.Versions, buildID) + if _, exists := workerDeploymentData.GetVersions()[buildID]; exists { + delete(workerDeploymentData.GetVersions(), buildID) deletedInNew = true changed = true } @@ -3620,7 +3689,7 @@ func removeDeploymentVersions( } // Only remove the deployment entry if versions were actually deleted from the new-format map. - if deletedInNew && len(workerDeploymentData.Versions) == 0 { + if workerDeploymentData != nil && deletedInNew && len(workerDeploymentData.GetVersions()) == 0 { delete(deploymentData.GetDeploymentsData(), deploymentName) } return changed diff --git a/service/matching/matching_engine_test.go b/service/matching/matching_engine_test.go index 688987a7365..aba5f873dd0 100644 --- a/service/matching/matching_engine_test.go +++ b/service/matching/matching_engine_test.go @@ -42,6 +42,7 @@ import ( taskqueuespb "go.temporal.io/server/api/taskqueue/v1" tokenspb "go.temporal.io/server/api/token/v1" "go.temporal.io/server/common" + "go.temporal.io/server/common/cache" "go.temporal.io/server/common/clock" hlc "go.temporal.io/server/common/clock/hybrid_logical_clock" "go.temporal.io/server/common/cluster" @@ -213,8 +214,8 @@ func (s *matchingEngineSuite) newConfig() *Config { res := defaultTestConfig() if s.fairness { useFairness(res) - } else if s.newMatcher { - useNewMatcher(res) + } else if !s.newMatcher { + useClassicMatcher(res) } return res } @@ -239,7 +240,7 @@ func newMatchingEngine( mockVisibilityManager manager.VisibilityManager, mockHostInfoProvider membership.HostInfoProvider, mockServiceResolver membership.ServiceResolver, nexusEndpointManager persistence.NexusEndpointManager, ) *matchingEngineImpl { - return &matchingEngineImpl{ + e := &matchingEngineImpl{ taskManager: taskMgr, fairTaskManager: fairTaskMgr, historyClient: mockHistoryClient, @@ -250,23 +251,24 @@ func newMatchingEngine( loadedTaskQueuePartitionCount: make(map[taskQueueCounterKey]int), loadedPhysicalTaskQueueCount: make(map[taskQueueCounterKey]int), }, - queryResults: collection.NewSyncMap[string, chan *queryResult](), - logger: logger, - throttledLogger: log.ThrottledLogger(logger), - metricsHandler: metrics.NoopMetricsHandler, - matchingRawClient: mockMatchingClient, - tokenSerializer: tasktoken.NewSerializer(), - config: config, - namespaceRegistry: mockNamespaceCache, - hostInfoProvider: mockHostInfoProvider, - serviceResolver: mockServiceResolver, - membershipChangedCh: make(chan *membership.ChangedEvent, 1), - clusterMeta: clustertest.NewMetadataForTest(cluster.NewTestClusterMetadataConfig(false, true)), - timeSource: clock.NewRealTimeSource(), - visibilityManager: mockVisibilityManager, - nexusEndpointClient: newEndpointClient(config.NexusEndpointsRefreshInterval, nexusEndpointManager), - nexusEndpointsOwnershipLostCh: make(chan struct{}), - } + queryResults: collection.NewSyncMap[string, chan *queryResult](), + logger: logger, + throttledLogger: log.ThrottledLogger(logger), + metricsHandler: metrics.NoopMetricsHandler, + matchingRawClient: mockMatchingClient, + tokenSerializer: tasktoken.NewSerializer(), + config: config, + namespaceRegistry: mockNamespaceCache, + hostInfoProvider: mockHostInfoProvider, + serviceResolver: mockServiceResolver, + membershipChangedCh: make(chan *membership.ChangedEvent, 1), + clusterMeta: clustertest.NewMetadataForTest(cluster.NewTestClusterMetadataConfig(false, true)), + timeSource: clock.NewRealTimeSource(), + visibilityManager: mockVisibilityManager, + nexusEndpointClient: newEndpointClient(config.NexusEndpointsRefreshInterval, nexusEndpointManager), + } + e.nexusEndpointsOwnershipLostCh.Store(make(chan struct{})) + return e } func (s *matchingEngineSuite) newPartitionManager(prtn tqid.Partition, config *Config) taskQueuePartitionManager { @@ -316,7 +318,7 @@ func (s *matchingEngineSuite) PollForTasksEmptyResultTest(callContext context.Co var taskQueueType enumspb.TaskQueueType tlID := newUnversionedRootQueueKey(namespaceID, tl, taskType) const pollCount = 10 - for i := 0; i < pollCount; i++ { + for range pollCount { if taskType == enumspb.TASK_QUEUE_TYPE_ACTIVITY { pollResp, err := s.matchingEngine.PollActivityTaskQueue(callContext, &matchingservice.PollActivityTaskQueueRequest{ NamespaceId: namespaceID, @@ -466,7 +468,7 @@ func (s *matchingEngineSuite) testFailAddTaskWithHistoryError( s.mockHistoryClient.EXPECT(). RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ context.Context, _ *historyservice.RecordWorkflowTaskStartedRequest, _ ...interface{}) (*historyservice.RecordWorkflowTaskStartedResponse, error) { + DoAndReturn(func(_ context.Context, _ *historyservice.RecordWorkflowTaskStartedRequest, _ ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) { s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(10 * time.Millisecond) return nil, recordError }) @@ -508,7 +510,7 @@ func (s *matchingEngineSuite) TestPollWorkflowTaskQueues() { // History service is using mock s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordWorkflowTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) { s.logger.Debug("Mock Received RecordWorkflowTaskStartedRequest") response := &historyservice.RecordWorkflowTaskStartedResponse{ WorkflowType: workflowType, @@ -883,7 +885,7 @@ func (s *matchingEngineSuite) AddTasksTest(taskType enumspb.TaskQueueType, isFor workflowID := "workflow1" execution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID} - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { scheduledEventID := i * 3 var err error if taskType == enumspb.TASK_QUEUE_TYPE_ACTIVITY { @@ -987,7 +989,7 @@ func (s *matchingEngineSuite) TestAddThenConsumeActivities() { Kind: enumspb.TASK_QUEUE_KIND_NORMAL, } - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { scheduledEventID := i * 3 addRequest := matchingservice.AddActivityTaskRequest{ NamespaceId: namespaceID, @@ -1011,7 +1013,7 @@ func (s *matchingEngineSuite) TestAddThenConsumeActivities() { // History service is using mock s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordActivityTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) { s.logger.Debug("Mock Received RecordActivityTaskStartedRequest") resp := &historyservice.RecordActivityTaskStartedResponse{ Attempt: 1, @@ -1123,7 +1125,7 @@ func (s *matchingEngineSuite) TestSyncMatchActivities() { identity := "nobody" s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordActivityTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) { s.logger.Debug("Mock Received RecordActivityTaskStartedRequest") return &historyservice.RecordActivityTaskStartedResponse{ Attempt: 1, @@ -1158,7 +1160,7 @@ func (s *matchingEngineSuite) TestSyncMatchActivities() { }, }, metrics.NoopMetricsHandler) } - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { scheduledEventID := i * 3 var wg sync.WaitGroup @@ -1229,7 +1231,7 @@ func (s *matchingEngineSuite) TestSyncMatchActivities() { assert.EqualValues(collect, 0, s.taskManager.getTaskCount(dbq)) }, 2*time.Second, 100*time.Millisecond) - syncCtr := scope.Snapshot().Counters()["test.sync_throttle_count+namespace="+matchingTestNamespace+",namespace_state=active,operation=TaskQueueMgr,partition=0,service_name=matching,task_type=Activity,taskqueue=makeToast,worker_version=__unversioned__"] + syncCtr := scope.Snapshot().Counters()["test.sync_throttle_count+namespace="+matchingTestNamespace+",namespace_state=active,operation=TaskQueueMgr,partition=0,service_name=matching,task_type=Activity,taskqueue=makeToast,worker_build_id=,worker_deployment_name=,worker_version=__unversioned__"] s.Equal(1, int(syncCtr.Value())) // Check times zero rps is set = throttle counter expectedRange := int64((taskCount + 1) / 30) // Due to conflicts some ids are skipped and more real ranges are used. @@ -1310,7 +1312,7 @@ func (s *matchingEngineSuite) TestRateLimiterAcrossVersionedQueues() { identity := "nobody" s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordActivityTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) { s.logger.Debug("Mock Received RecordActivityTaskStartedRequest") return &historyservice.RecordActivityTaskStartedResponse{ Attempt: 1, @@ -1350,7 +1352,7 @@ func (s *matchingEngineSuite) TestRateLimiterAcrossVersionedQueues() { const taskCount = 2 resultChan := make(chan *matchingservice.PollActivityTaskQueueResponse) - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { maxDispatch := defaultTaskDispatchRPS if i == 1 { maxDispatch = 0 // second poller overrides the dispatch rate to 0 @@ -1400,7 +1402,7 @@ func (s *matchingEngineSuite) TestRateLimiterAcrossVersionedQueues() { }) s.NoError(err) - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { scheduledEventID := i * 3 addRequest := matchingservice.AddActivityTaskRequest{ NamespaceId: namespaceID, @@ -1422,14 +1424,14 @@ func (s *matchingEngineSuite) TestRateLimiterAcrossVersionedQueues() { } // Verifying that both the pollers don't receive any tasks since the overall dispatch rate has been set to 0 - for i := int64(0); i < taskCount; i++ { + for range int64(taskCount) { receivedResult := <-resultChan s.Nil(receivedResult.TaskToken) } // Restart the pollers with maxTasksPerSecond = defaultTaskDispatchRPS so that they can receive tasks maxDispatch := float64(defaultTaskDispatchRPS) - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { go func() { result, _ := pollFunc(maxDispatch, strconv.FormatInt(int64(i), 10)) resultChan <- result @@ -1437,7 +1439,7 @@ func (s *matchingEngineSuite) TestRateLimiterAcrossVersionedQueues() { } // Verifying that both the pollers receive the tasks which were added previously - for i := int64(0); i < taskCount; i++ { + for range int64(taskCount) { receivedResult := <-resultChan s.NotNil(receivedResult) @@ -1508,7 +1510,7 @@ func (s *matchingEngineSuite) concurrentPublishConsumeActivities( var wg sync.WaitGroup wg.Add(2 * workerCount) - for p := 0; p < workerCount; p++ { + for range workerCount { go func() { defer wg.Done() for i := int64(0); i < taskCount; i++ { @@ -1541,7 +1543,7 @@ func (s *matchingEngineSuite) concurrentPublishConsumeActivities( // History service is using mock s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordActivityTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) { s.logger.Debug("Mock Received RecordActivityTaskStartedRequest") return &historyservice.RecordActivityTaskStartedResponse{ Attempt: 1, @@ -1563,7 +1565,7 @@ func (s *matchingEngineSuite) concurrentPublishConsumeActivities( }, nil }).AnyTimes() - for p := 0; p < workerCount; p++ { + for p := range workerCount { go func(wNum int) { defer wg.Done() for i := int64(0); i < taskCount; { @@ -1651,9 +1653,9 @@ func (s *matchingEngineSuite) TestConcurrentPublishConsumeWorkflowTasks() { var wg sync.WaitGroup wg.Add(2 * workerCount) - for p := 0; p < workerCount; p++ { + for range workerCount { go func() { - for i := int64(0); i < taskCount; i++ { + for range int64(taskCount) { addRequest := matchingservice.AddWorkflowTaskRequest{ NamespaceId: namespaceID, Execution: workflowExecution, @@ -1677,7 +1679,7 @@ func (s *matchingEngineSuite) TestConcurrentPublishConsumeWorkflowTasks() { // History service is using mock s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordWorkflowTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) { s.logger.Debug("Mock Received RecordWorkflowTaskStartedRequest") return &historyservice.RecordWorkflowTaskStartedResponse{ PreviousStartedEventId: startedEventID, @@ -1690,7 +1692,7 @@ func (s *matchingEngineSuite) TestConcurrentPublishConsumeWorkflowTasks() { }, nil }).AnyTimes() - for p := 0; p < workerCount; p++ { + for range workerCount { go func() { for i := int64(0); i < taskCount; { result, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{ @@ -1852,15 +1854,15 @@ func (s *matchingEngineSuite) TestMultipleEnginesActivitiesRangeStealing() { } engines := make([]*matchingEngineImpl, engineCount) - for p := 0; p < engineCount; p++ { + for p := range engineCount { e := s.newMatchingEngine(s.newConfig(), s.classicTaskManager, s.fairTaskManager) e.config.RangeSize = rangeSize engines[p] = e e.Start() } - for j := 0; j < iterations; j++ { - for p := 0; p < engineCount; p++ { + for range iterations { + for p := range engineCount { engine := engines[p] for i := int64(0); i < taskCount; i++ { addRequest := matchingservice.AddActivityTaskRequest{ @@ -1899,7 +1901,7 @@ func (s *matchingEngineSuite) TestMultipleEnginesActivitiesRangeStealing() { // History service is using mock s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordActivityTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) { if _, ok := startedTasks[taskRequest.GetScheduledEventId()]; ok { s.logger.Debug("From error function Mock Received DUPLICATED RecordActivityTaskStartedRequest", tag.Int64("scheduled-event-id", taskRequest.GetScheduledEventId())) return nil, serviceerror.NewNotFound("already started") @@ -1925,8 +1927,8 @@ func (s *matchingEngineSuite) TestMultipleEnginesActivitiesRangeStealing() { }), }, nil }).AnyTimes() - for j := 0; j < iterations; j++ { - for p := 0; p < engineCount; p++ { + for range iterations { + for p := range engineCount { engine := engines[p] for i := int64(0); i < taskCount; /* incremented explicitly to skip empty polls */ { result, err := engine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{ @@ -2008,15 +2010,15 @@ func (s *matchingEngineSuite) TestMultipleEnginesWorkflowTasksRangeStealing() { } engines := make([]*matchingEngineImpl, engineCount) - for p := 0; p < engineCount; p++ { + for p := range engineCount { e := s.newMatchingEngine(s.newConfig(), s.classicTaskManager, s.fairTaskManager) e.config.RangeSize = rangeSize engines[p] = e e.Start() } - for j := 0; j < iterations; j++ { - for p := 0; p < engineCount; p++ { + for range iterations { + for p := range engineCount { engine := engines[p] for i := int64(0); i < taskCount; i++ { addRequest := matchingservice.AddWorkflowTaskRequest{ @@ -2051,7 +2053,7 @@ func (s *matchingEngineSuite) TestMultipleEnginesWorkflowTasksRangeStealing() { // History service is using mock s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordWorkflowTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) { if _, ok := startedTasks[taskRequest.GetScheduledEventId()]; ok { s.logger.Debug("From error function Mock Received DUPLICATED RecordWorkflowTaskStartedRequest", tag.Int64("scheduled-event-id", taskRequest.GetScheduledEventId())) return nil, serviceerrors.NewTaskAlreadyStarted("Workflow") @@ -2070,8 +2072,8 @@ func (s *matchingEngineSuite) TestMultipleEnginesWorkflowTasksRangeStealing() { }, nil }).AnyTimes() - for j := 0; j < iterations; j++ { - for p := 0; p < engineCount; p++ { + for range iterations { + for p := range engineCount { engine := engines[p] for i := int64(0); i < taskCount; /* incremented explicitly to skip empty polls */ { result, err := engine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{ @@ -2192,7 +2194,7 @@ func (s *matchingEngineSuite) TestTaskQueueManagerGetTaskBatch() { s.matchingEngine.config.RangeSize = rangeSize // add taskCount tasks - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { scheduledEventID := i * 3 addRequest := matchingservice.AddActivityTaskRequest{ NamespaceId: namespaceID, @@ -2244,7 +2246,7 @@ func (s *matchingEngineSuite) TestTaskQueueManagerGetTaskBatch() { blm.taskAckManager.setReadLevel(int64(expectedBufSize)) // complete rangeSize events - for i := int64(0); i < rangeSize; i++ { + for range int64(rangeSize) { identity := "nobody" result, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{ NamespaceId: namespaceID, @@ -2273,7 +2275,7 @@ func (s *matchingEngineSuite) TestTaskQueueManager_CyclingBehavior() { config := s.newConfig() dbq := newUnversionedRootQueueKey(uuid.NewString(), "makeToast", enumspb.TASK_QUEUE_TYPE_ACTIVITY) - for i := 0; i < 4; i++ { + for range 4 { prevGetTasksCount := s.taskManager.getGetTasksCount(dbq) mgr := s.newPartitionManager(dbq.partition, config) @@ -2319,7 +2321,7 @@ func (s *matchingEngineSuite) TestTaskExpiryAndCompletion() { } for _, tc := range testCases { - for i := int64(0); i < taskCount; i++ { + for i := range int64(taskCount) { scheduledEventID := i * 3 addRequest := matchingservice.AddActivityTaskRequest{ NamespaceId: namespaceID, @@ -2363,9 +2365,9 @@ func (s *matchingEngineSuite) TestTaskExpiryAndCompletion() { } remaining := taskCount - for i := 0; i < 2; i++ { + for range 2 { // verify that (1) expired tasks are not returned in poll result (2) taskCleaner deletes tasks correctly - for i := int64(0); i < taskCount/4; i++ { + for range int64(taskCount / 4) { result, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), pollReq, metrics.NoopMetricsHandler) s.NoError(err) s.NotNil(result) @@ -2403,7 +2405,7 @@ func (s *matchingEngineSuite) TestGetVersioningData() { s.NotNil(res) // Set a long list of versions - for i := 0; i < 10; i++ { + for i := range 10 { id := fmt.Sprintf("%d", i) res, err := s.matchingEngine.UpdateWorkerBuildIdCompatibility(context.Background(), &matchingservice.UpdateWorkerBuildIdCompatibilityRequest{ NamespaceId: namespaceID.String(), @@ -2424,7 +2426,7 @@ func (s *matchingEngineSuite) TestGetVersioningData() { s.NotNil(res) } // Make a long compat-versions chain - for i := 0; i < 80; i++ { + for i := range 80 { id := fmt.Sprintf("9.%d", i) prevCompat := fmt.Sprintf("9.%d", i-1) if i == 0 { @@ -3214,7 +3216,7 @@ func (s *matchingEngineSuite) generateWorkflowExecution() (*commonpb.WorkflowTyp func (s *matchingEngineSuite) mockHistoryWhilePolling(workflowType *commonpb.WorkflowType) { s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordWorkflowTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) { return &historyservice.RecordWorkflowTaskStartedResponse{ PreviousStartedEventId: 1, StartedEventId: 1, @@ -3285,13 +3287,11 @@ func (s *matchingEngineSuite) addWorkflowTasksConcurrently( taskQueue *taskqueuepb.TaskQueue, workflowExecution *commonpb.WorkflowExecution, ) { for range numWorkers { - wg.Add(1) - go func() { + wg.Go(func() { for range taskCount { s.addWorkflowTask(workflowExecution, taskQueue) } - wg.Done() - }() + }) } } @@ -3322,13 +3322,11 @@ func (s *matchingEngineSuite) pollWorkflowTasksConcurrently( ) { s.mockHistoryWhilePolling(workflowType) for range numPollers { - wg.Add(1) - go func() { + wg.Go(func() { for range taskCount { s.createPollWorkflowTaskRequestAndPoll(taskQueue) } - wg.Done() - }() + }) } } @@ -3571,16 +3569,6 @@ func (s *matchingEngineSuite) TestMultipleWorkersLesserNumberOfPollersThanTasksD s.concurrentPublishAndConsumeValidateBacklogCounter(5, 500, 200) } -func (s *matchingEngineSuite) TestOldestBacklogAge() { - firstAge := durationpb.New(100 * time.Second) - secondAge := durationpb.New(1 * time.Millisecond) - s.Same(firstAge, oldestBacklogAge(firstAge, secondAge)) - - thirdAge := durationpb.New(5 * time.Minute) - s.Same(thirdAge, oldestBacklogAge(firstAge, thirdAge)) - s.Same(thirdAge, oldestBacklogAge(secondAge, thirdAge)) -} - func (s *matchingEngineSuite) TestCheckNexusEndpointsOwnership() { isOwner, _, err := s.matchingEngine.checkNexusEndpointsOwnership() s.NoError(err) @@ -3592,7 +3580,7 @@ func (s *matchingEngineSuite) TestCheckNexusEndpointsOwnership() { } func (s *matchingEngineSuite) TestNotifyNexusEndpointsOwnershipLost() { - ch := s.matchingEngine.nexusEndpointsOwnershipLostCh + ch := s.matchingEngine.nexusEndpointsOwnershipLostCh.Load().(chan struct{}) //nolint:revive // type is always chan struct{} s.matchingEngine.notifyNexusEndpointsOwnershipChange() select { case <-ch: @@ -4275,7 +4263,7 @@ func (s *matchingEngineSuite) setupRecordActivityTaskStartedMock(tlName string) // History service is using mock s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...interface{}) (*historyservice.RecordActivityTaskStartedResponse, error) { + func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) { s.logger.Debug("Mock Received RecordActivityTaskStartedRequest") return &historyservice.RecordActivityTaskStartedResponse{ Attempt: 1, @@ -4838,11 +4826,12 @@ type testQueueData struct { } type testQueuePersistenceStats struct { - createTaskCount int - getTasksCount int - getUserDataCount int - createCount int - updateCount int + createTaskCount int + createTaskBatchCount int + getTasksCount int + getUserDataCount int + createCount int + updateCount int } func newTestQueueData() *testQueueData { @@ -5082,6 +5071,7 @@ func (m *testTaskManager) CreateTasks( tlm.tasks.Put(fairLevelFromAllocatedTask(task), common.CloneProto(task)) tlm.createTaskCount++ } + tlm.createTaskBatchCount++ resp := &persistence.CreateTasksResponse{} if m.updateMetadataOnCreateTasks { @@ -5144,7 +5134,7 @@ func (m *testTaskManager) getTaskCount(q *PhysicalTaskQueueKey) int { return tlm.tasks.Size() } -// getCreateTaskCount returns how many times CreateTask was called +// getCreateTaskCount returns how many tasks were added func (m *testTaskManager) getCreateTaskCount(q *PhysicalTaskQueueKey) int { tlm := m.getQueueDataByKey(q) tlm.Lock() @@ -5152,6 +5142,14 @@ func (m *testTaskManager) getCreateTaskCount(q *PhysicalTaskQueueKey) int { return tlm.createTaskCount } +// getCreateTaskBatchCount returns how many times CreateTask was called +func (m *testTaskManager) getCreateTaskBatchCount(q *PhysicalTaskQueueKey) int { + tlm := m.getQueueDataByKey(q) + tlm.Lock() + defer tlm.Unlock() + return tlm.createTaskBatchCount +} + // getGetTasksCount returns how many times GetTasks was called func (m *testTaskManager) getGetTasksCount(q *PhysicalTaskQueueKey) int { tlm := m.getQueueDataByKey(q) @@ -5681,8 +5679,8 @@ func (d *dynamicRateBurstWrapper) Burst() int { } // TODO(pri): cleanup; delete this -func useNewMatcher(config *Config) { - config.NewMatcherSub = staticTrueChange +func useClassicMatcher(config *Config) { + config.NewMatcherSub = staticFalseChange } func useFairness(config *Config) { @@ -5693,6 +5691,10 @@ func staticTrueChange(_, _ string, _ enumspb.TaskQueueType, _ func(dynamicconfig return dynamicconfig.StaticGradualChange(true), func() {} } +func staticFalseChange(_, _ string, _ enumspb.TaskQueueType, _ func(dynamicconfig.GradualChange[bool])) (dynamicconfig.GradualChange[bool], func()) { + return dynamicconfig.StaticGradualChange(false), func() {} +} + func TestCancelOutstandingWorkerPolls(t *testing.T) { t.Parallel() @@ -5700,6 +5702,7 @@ func TestCancelOutstandingWorkerPolls(t *testing.T) { t.Parallel() engine := &matchingEngineImpl{ workerInstancePollers: workerPollerTracker{pollers: make(map[string]map[string]context.CancelFunc)}, + shutdownWorkers: cache.New(shutdownWorkersCacheMaxSize, &cache.Options{TTL: shutdownWorkersCacheTTL}), } resp, err := engine.CancelOutstandingWorkerPolls(context.Background(), @@ -5715,6 +5718,7 @@ func TestCancelOutstandingWorkerPolls(t *testing.T) { t.Parallel() engine := &matchingEngineImpl{ workerInstancePollers: workerPollerTracker{pollers: make(map[string]map[string]context.CancelFunc)}, + shutdownWorkers: cache.New(shutdownWorkersCacheMaxSize, &cache.Options{TTL: shutdownWorkersCacheTTL}), } workerKey := "test-worker" @@ -5741,6 +5745,7 @@ func TestCancelOutstandingWorkerPolls(t *testing.T) { worker2Cancelled := false engine := &matchingEngineImpl{ workerInstancePollers: workerPollerTracker{pollers: make(map[string]map[string]context.CancelFunc)}, + shutdownWorkers: cache.New(shutdownWorkersCacheMaxSize, &cache.Options{TTL: shutdownWorkersCacheTTL}), } // Set up pollers for worker1 and worker2 @@ -5763,6 +5768,7 @@ func TestCancelOutstandingWorkerPolls(t *testing.T) { t.Parallel() engine := &matchingEngineImpl{ workerInstancePollers: workerPollerTracker{pollers: make(map[string]map[string]context.CancelFunc)}, + shutdownWorkers: cache.New(shutdownWorkersCacheMaxSize, &cache.Options{TTL: shutdownWorkersCacheTTL}), } workerKey := "test-worker" @@ -5785,4 +5791,38 @@ func TestCancelOutstandingWorkerPolls(t *testing.T) { require.True(t, childCancelled, "child partition poll should be cancelled") require.True(t, parentCancelled, "parent partition poll should be cancelled") }) + + t.Run("adds worker to shutdown cache", func(t *testing.T) { + t.Parallel() + engine := &matchingEngineImpl{ + workerInstancePollers: workerPollerTracker{pollers: make(map[string]map[string]context.CancelFunc)}, + shutdownWorkers: cache.New(shutdownWorkersCacheMaxSize, &cache.Options{TTL: shutdownWorkersCacheTTL}), + } + + workerKey := "test-worker" + + _, err := engine.CancelOutstandingWorkerPolls(context.Background(), + &matchingservice.CancelOutstandingWorkerPollsRequest{ + WorkerInstanceKey: workerKey, + }) + + require.NoError(t, err) + require.NotNil(t, engine.shutdownWorkers.Get(workerKey), "worker should be in shutdown cache") + }) + + t.Run("empty worker key does not populate shutdown cache", func(t *testing.T) { + t.Parallel() + engine := &matchingEngineImpl{ + workerInstancePollers: workerPollerTracker{pollers: make(map[string]map[string]context.CancelFunc)}, + shutdownWorkers: cache.New(shutdownWorkersCacheMaxSize, &cache.Options{TTL: shutdownWorkersCacheTTL}), + } + + _, err := engine.CancelOutstandingWorkerPolls(context.Background(), + &matchingservice.CancelOutstandingWorkerPollsRequest{ + WorkerInstanceKey: "", + }) + + require.NoError(t, err) + require.Equal(t, 0, engine.shutdownWorkers.Size()) + }) } diff --git a/service/matching/physical_task_queue_manager.go b/service/matching/physical_task_queue_manager.go index 1b46eb61f46..173b5b1257c 100644 --- a/service/matching/physical_task_queue_manager.go +++ b/service/matching/physical_task_queue_manager.go @@ -9,7 +9,6 @@ import ( "sync/atomic" "time" - "github.com/nexus-rpc/sdk-go/nexus" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" @@ -28,6 +27,7 @@ import ( "go.temporal.io/server/common/namespace" "go.temporal.io/server/common/quotas" "go.temporal.io/server/common/softassert" + "go.temporal.io/server/common/taskqueue" "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/util" "go.temporal.io/server/common/worker_versioning" @@ -85,12 +85,11 @@ type ( clusterMeta cluster.Metadata metricsHandler metrics.Handler // namespace/taskqueue tagged metric scope // pollerHistory stores poller which poll from this taskqueue in last few minutes - pollerHistory *pollerHistory - currentPolls atomic.Int64 - taskValidator taskValidator - deploymentRegistrationCh chan struct{} - deploymentVersionRegistered bool - pollerScalingRateLimiter quotas.RateLimiter + pollerHistory *pollerHistory + currentPolls atomic.Int64 + taskValidator taskValidator + deploymentRegistrationCh chan struct{} + pollerScalingRateLimiter quotas.RateLimiter taskTrackerLock sync.RWMutex tasksAdded map[priorityKey]*taskTracker @@ -134,7 +133,10 @@ func newPhysicalTaskQueueManager( buildIDTag := tag.WorkerVersion(versionTagValue) taggedMetricsHandler := partitionMgr.metricsHandler.WithTags( metrics.OperationTag(metrics.MatchingTaskQueueMgrScope), - metrics.WorkerVersionTag(versionTagValue, config.BreakdownMetricsByBuildID())) + metrics.WorkerVersionTag(versionTagValue, config.BreakdownMetricsByBuildID()), + metrics.WorkerDeploymentNameTag(queue.Version().Deployment().GetSeriesName(), config.BreakdownMetricsByBuildID()), + metrics.WorkerDeploymentBuildIDTag(queue.Version().Deployment().GetBuildId(), config.BreakdownMetricsByBuildID()), + ) tqCtx, tqCancel := context.WithCancel(partitionMgr.callerInfoContext(context.Background())) @@ -197,7 +199,7 @@ func newPhysicalTaskQueueManager( var err error if queue.Partition().IsChild() { // Every DB Queue needs its own forwarder so that the throttles do not interfere - fwdr, err = newPriForwarder(&config.forwarderConfig, queue, e.matchingRawClient) + fwdr, err = newPriForwarder(&config.forwarderConfig, queue, e.matchingRawClient, e.testHooks) if err != nil { return nil, err } @@ -236,7 +238,7 @@ func newPhysicalTaskQueueManager( var err error if queue.Partition().IsChild() { // Every DB Queue needs its own forwarder so that the throttles do not interfere - fwdr, err = newPriForwarder(&config.forwarderConfig, queue, e.matchingRawClient) + fwdr, err = newPriForwarder(&config.forwarderConfig, queue, e.matchingRawClient, e.testHooks) if err != nil { return nil, err } @@ -500,7 +502,7 @@ func (c *physicalTaskQueueManagerImpl) PollTask( task.backlogCountHint = c.backlogCountHint if pollMetadata.forwardedFrom == "" { // track the task on the child, not where a poll was forwarded to - c.getOrCreateTaskTracker(c.tasksDispatched, priorityKey(task.getPriority().GetPriorityKey())).incrementTaskCount() + c.getOrCreateTaskTracker(c.tasksDispatched, priorityKey(task.getPriority().GetPriorityKey())).inc(1) } return task, nil } @@ -573,39 +575,20 @@ func (c *physicalTaskQueueManagerImpl) UserDataChanged() { // if dispatched to local poller then nil and nil is returned. func (c *physicalTaskQueueManagerImpl) DispatchQueryTask( ctx context.Context, - taskId string, - request *matchingservice.QueryWorkflowRequest, + task *internalTask, ) (*matchingservice.QueryWorkflowResponse, error) { - task := newInternalQueryTask(taskId, request) - c.config.setDefaultPriority(task) if !task.isForwarded() { - c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(request.GetPriority().GetPriorityKey())).incrementTaskCount() + c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(task.getPriority().GetPriorityKey())).inc(1) } return c.matcher.OfferQuery(ctx, task) } func (c *physicalTaskQueueManagerImpl) DispatchNexusTask( ctx context.Context, - taskId string, - request *matchingservice.DispatchNexusTaskRequest, + task *internalTask, ) (*matchingservice.DispatchNexusTaskResponse, error) { - deadline, _ := ctx.Deadline() // If not set by user, our client will set a default. - var opDeadline time.Time - if header := nexus.Header(request.GetRequest().GetHeader()); header != nil { - if opTimeoutHeader := header.Get(nexus.HeaderOperationTimeout); opTimeoutHeader != "" { - opTimeout, err := time.ParseDuration(opTimeoutHeader) - if err != nil { - // Operation-Timeout header is not required so don't fail request on parsing errors. - c.logger.Warn(fmt.Sprintf("unable to parse %v header: %v", nexus.HeaderOperationTimeout, opTimeoutHeader), tag.Error(err), tag.WorkflowNamespaceID(request.NamespaceId)) - } else { - opDeadline = time.Now().Add(opTimeout) - } - } - } - task := newInternalNexusTask(taskId, deadline, opDeadline, request) - c.config.setDefaultPriority(task) if !task.isForwarded() { - c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(0)).incrementTaskCount() // Nexus has no priorities + c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(0)).inc(1) // Nexus has no priorities } return c.matcher.OfferNexusTask(ctx, task) } @@ -666,7 +649,7 @@ func (c *physicalTaskQueueManagerImpl) GetStatsByPriority(includeRates bool) map if m := c.getDrainBacklogMgr(); m != nil { drainStats := m.BacklogStatsByPriority() for pri, tqs := range drainStats { - mergeStats(util.GetOrSetNew(stats, pri), tqs) + taskqueue.MergeStats(util.GetOrSetNew(stats, pri), tqs) } } @@ -700,8 +683,8 @@ func (c *physicalTaskQueueManagerImpl) TrySyncMatch(ctx context.Context, task *i if !task.isForwarded() { // request sent by history service c.liveness.markAlive() - c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(task.getPriority().GetPriorityKey())).incrementTaskCount() - if disable, _ := testhooks.Get[bool](c.partitionMgr.engine.testHooks, testhooks.MatchingDisableSyncMatch); disable { + c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(task.getPriority().GetPriorityKey())).inc(1) + if disable, _ := testhooks.Get(c.partitionMgr.engine.testHooks, testhooks.MatchingDisableSyncMatch, c.partitionMgr.ns.ID()); disable { return false, nil } } @@ -732,6 +715,19 @@ func (c *physicalTaskQueueManagerImpl) ensureRegisteredInDeploymentVersion( return errMissingDeploymentVersion } + userData, _, err := c.partitionMgr.getPerTypeUserData() + if err != nil { + return err + } + + deploymentData := userData.GetDeploymentData() + if worker_versioning.HasDeploymentVersion(deploymentData, worker_versioning.DeploymentVersionFromDeployment(workerDeployment)) { + // already registered in user data, we can assume the workflow is running. + // TODO: consider replication scenarios where user data is replicated before + // the deployment workflow. + return nil + } + select { case <-ctx.Done(): return ctx.Err() @@ -749,17 +745,13 @@ func (c *physicalTaskQueueManagerImpl) ensureRegisteredInDeploymentVersion( } }() - if c.deploymentVersionRegistered { - // deployment version already registered - return nil - } - - userData, _, err := c.partitionMgr.GetUserDataManager().GetUserData() + // Recheck user data in case it was updated in the meantime while we were waiting for the lock. + userData, _, err = c.partitionMgr.getPerTypeUserData() if err != nil { return err } - deploymentData := userData.GetData().GetPerType()[int32(c.queue.TaskType())].GetDeploymentData() + deploymentData = userData.GetDeploymentData() if worker_versioning.HasDeploymentVersion(deploymentData, worker_versioning.DeploymentVersionFromDeployment(workerDeployment)) { // already registered in user data, we can assume the workflow is running. // TODO: consider replication scenarios where user data is replicated before @@ -768,7 +760,7 @@ func (c *physicalTaskQueueManagerImpl) ensureRegisteredInDeploymentVersion( } backoff := deploymentRegisterErrorBackoff - if testBackoff, ok := testhooks.Get[time.Duration](c.partitionMgr.engine.testHooks, testhooks.MatchingDeploymentRegisterErrorBackoff); ok { + if testBackoff, ok := testhooks.Get(c.partitionMgr.engine.testHooks, testhooks.MatchingDeploymentRegisterErrorBackoff, c.partitionMgr.ns.ID()); ok { backoff = testBackoff } @@ -809,11 +801,11 @@ func (c *physicalTaskQueueManagerImpl) ensureRegisteredInDeploymentVersion( // the deployment workflow will register itself in this task queue's user data. // wait for it to propagate here. for { - userData, userDataChanged, err := c.partitionMgr.GetUserDataManager().GetUserData() + userData, userDataChanged, err := c.partitionMgr.getPerTypeUserData() if err != nil { return err } - deploymentData := userData.GetData().GetPerType()[int32(c.queue.TaskType())].GetDeploymentData() + deploymentData := userData.GetDeploymentData() if worker_versioning.HasDeploymentVersion(deploymentData, worker_versioning.DeploymentVersionFromDeployment(workerDeployment)) { break } @@ -825,7 +817,6 @@ func (c *physicalTaskQueueManagerImpl) ensureRegisteredInDeploymentVersion( } } - c.deploymentVersionRegistered = true return nil } @@ -939,8 +930,8 @@ func (c *physicalTaskQueueManagerImpl) getOrCreateTaskTracker( } // Initalize all task trackers together; or the timeframes won't line up. - c.tasksAdded[priorityKey] = newTaskTracker(c.partitionMgr.engine.timeSource) - c.tasksDispatched[priorityKey] = newTaskTracker(c.partitionMgr.engine.timeSource) + c.tasksAdded[priorityKey] = c.partitionMgr.engine.newTaskTracker() + c.tasksDispatched[priorityKey] = c.partitionMgr.engine.newTaskTracker() return intervals[priorityKey] } @@ -948,29 +939,7 @@ func (c *physicalTaskQueueManagerImpl) getOrCreateTaskTracker( func aggregateStats(stats map[int32]*taskqueuepb.TaskQueueStats) *taskqueuepb.TaskQueueStats { result := &taskqueuepb.TaskQueueStats{ApproximateBacklogAge: durationpb.New(0)} for _, s := range stats { - mergeStats(result, s) + taskqueue.MergeStats(result, s) } return result } - -func mergeStats(into, from *taskqueuepb.TaskQueueStats) { - into.ApproximateBacklogCount += from.ApproximateBacklogCount - into.ApproximateBacklogAge = oldestBacklogAge(into.ApproximateBacklogAge, from.ApproximateBacklogAge) - into.TasksAddRate += from.TasksAddRate - into.TasksDispatchRate += from.TasksDispatchRate -} - -func oldestBacklogAge(left, right *durationpb.Duration) *durationpb.Duration { - // Treat nil as zero to keep stats aggregation defensive. It is okay here to reassign the pointer values when - // they are nil since a nil Duration proto is equivalent to a zero duration. - if left == nil { - left = durationpb.New(0) - } - if right == nil { - right = durationpb.New(0) - } - if left.AsDuration() > right.AsDuration() { - return left - } - return right -} diff --git a/service/matching/physical_task_queue_manager_interface.go b/service/matching/physical_task_queue_manager_interface.go index ca487f34fdd..cab1ef067fb 100644 --- a/service/matching/physical_task_queue_manager_interface.go +++ b/service/matching/physical_task_queue_manager_interface.go @@ -44,10 +44,10 @@ type ( UserDataChanged() // DispatchQueryTask will dispatch query to local or remote poller. If forwarded then result or error is returned, // if dispatched to local poller then nil and nil is returned. - DispatchQueryTask(ctx context.Context, taskId string, request *matchingservice.QueryWorkflowRequest) (*matchingservice.QueryWorkflowResponse, error) + DispatchQueryTask(ctx context.Context, task *internalTask) (*matchingservice.QueryWorkflowResponse, error) // DispatchNexusTask dispatches a nexus task to a local or remote poller. If forwarded then result or // error is returned, if dispatched to local poller then nil and nil is returned. - DispatchNexusTask(ctx context.Context, taskId string, request *matchingservice.DispatchNexusTaskRequest) (*matchingservice.DispatchNexusTaskResponse, error) + DispatchNexusTask(ctx context.Context, task *internalTask) (*matchingservice.DispatchNexusTaskResponse, error) UpdatePollerInfo(pollerIdentity, *pollMetadata) RemovePoller(pollerIdentity) GetAllPollerInfo() []*taskqueuepb.PollerInfo diff --git a/service/matching/physical_task_queue_manager_mock.go b/service/matching/physical_task_queue_manager_mock.go index dcb64f89477..cfc8c369b09 100644 --- a/service/matching/physical_task_queue_manager_mock.go +++ b/service/matching/physical_task_queue_manager_mock.go @@ -74,33 +74,33 @@ func (mr *MockphysicalTaskQueueManagerMockRecorder) AddSpooledTaskToMatcher(task } // DispatchNexusTask mocks base method. -func (m *MockphysicalTaskQueueManager) DispatchNexusTask(ctx context.Context, taskId string, request *matchingservice.DispatchNexusTaskRequest) (*matchingservice.DispatchNexusTaskResponse, error) { +func (m *MockphysicalTaskQueueManager) DispatchNexusTask(ctx context.Context, task *internalTask) (*matchingservice.DispatchNexusTaskResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DispatchNexusTask", ctx, taskId, request) + ret := m.ctrl.Call(m, "DispatchNexusTask", ctx, task) ret0, _ := ret[0].(*matchingservice.DispatchNexusTaskResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // DispatchNexusTask indicates an expected call of DispatchNexusTask. -func (mr *MockphysicalTaskQueueManagerMockRecorder) DispatchNexusTask(ctx, taskId, request any) *gomock.Call { +func (mr *MockphysicalTaskQueueManagerMockRecorder) DispatchNexusTask(ctx, task any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DispatchNexusTask", reflect.TypeOf((*MockphysicalTaskQueueManager)(nil).DispatchNexusTask), ctx, taskId, request) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DispatchNexusTask", reflect.TypeOf((*MockphysicalTaskQueueManager)(nil).DispatchNexusTask), ctx, task) } // DispatchQueryTask mocks base method. -func (m *MockphysicalTaskQueueManager) DispatchQueryTask(ctx context.Context, taskId string, request *matchingservice.QueryWorkflowRequest) (*matchingservice.QueryWorkflowResponse, error) { +func (m *MockphysicalTaskQueueManager) DispatchQueryTask(ctx context.Context, task *internalTask) (*matchingservice.QueryWorkflowResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DispatchQueryTask", ctx, taskId, request) + ret := m.ctrl.Call(m, "DispatchQueryTask", ctx, task) ret0, _ := ret[0].(*matchingservice.QueryWorkflowResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // DispatchQueryTask indicates an expected call of DispatchQueryTask. -func (mr *MockphysicalTaskQueueManagerMockRecorder) DispatchQueryTask(ctx, taskId, request any) *gomock.Call { +func (mr *MockphysicalTaskQueueManagerMockRecorder) DispatchQueryTask(ctx, task any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DispatchQueryTask", reflect.TypeOf((*MockphysicalTaskQueueManager)(nil).DispatchQueryTask), ctx, taskId, request) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DispatchQueryTask", reflect.TypeOf((*MockphysicalTaskQueueManager)(nil).DispatchQueryTask), ctx, task) } // DispatchSpooledTask mocks base method. diff --git a/service/matching/physical_task_queue_manager_test.go b/service/matching/physical_task_queue_manager_test.go index dce4d0c5079..37e0471b61e 100644 --- a/service/matching/physical_task_queue_manager_test.go +++ b/service/matching/physical_task_queue_manager_test.go @@ -87,10 +87,9 @@ func (s *PhysicalTaskQueueManagerTestSuite) SetupTest() { engine.partitions[prtn.Key()] = prtnMgr if s.fairness { - prtnMgr.config.NewMatcher = true prtnMgr.config.EnableFairness = true - } else if s.newMatcher { - prtnMgr.config.NewMatcher = true + } else if !s.newMatcher { + prtnMgr.config.NewMatcher = false } s.tqMgr, err = newPhysicalTaskQueueManager(prtnMgr, s.physicalTaskQueueKey) @@ -153,8 +152,8 @@ func makePollMetadata(rps float64) *pollMetadata { // runOneShotPoller spawns a goroutine to call tqMgr.PollTask on the provided tqMgr. // The second return value is a channel of either error or *internalTask. -func runOneShotPoller(ctx context.Context, tqm physicalTaskQueueManager) (*goro.Handle, chan interface{}) { - out := make(chan interface{}, 1) +func runOneShotPoller(ctx context.Context, tqm physicalTaskQueueManager) (*goro.Handle, chan any) { + out := make(chan any, 1) handle := goro.NewHandle(ctx).Go(func(ctx context.Context) error { task, err := tqm.PollTask(ctx, makePollMetadata(rpsInf)) if task == nil { @@ -175,6 +174,15 @@ func defaultTqId() *PhysicalTaskQueueKey { return newTestUnversionedPhysicalQueueKey(defaultNamespaceId, defaultRootTqID, enumspb.TASK_QUEUE_TYPE_WORKFLOW, 0) } +// getTaskManager returns the underlying testTaskManager (which is accessed differently +// depending on mode) +func (s *PhysicalTaskQueueManagerTestSuite) getTaskManager() *testTaskManager { + if s.fairness { + return s.tqMgr.partitionMgr.engine.fairTaskManager.(*testTaskManager) + } + return s.tqMgr.partitionMgr.engine.taskManager.(*testTaskManager) +} + // TODO(pri): old matcher cleanup func (s *PhysicalTaskQueueManagerTestSuite) TestReaderBacklogAge() { if s.newMatcher { @@ -192,21 +200,21 @@ func (s *PhysicalTaskQueueManagerTestSuite) TestReaderBacklogAge() { go blm.taskReader.dispatchBufferedTasks() s.EventuallyWithT(func(collect *assert.CollectT) { - require.InDelta(s.T(), time.Minute, blm.taskReader.getBacklogHeadAge(), float64(time.Second)) + assert.InDelta(collect, time.Minute, blm.taskReader.getBacklogHeadAge(), float64(time.Second)) }, time.Second, 10*time.Millisecond) _, err := blm.pqMgr.PollTask(context.Background(), makePollMetadata(rpsInf)) s.NoError(err) s.EventuallyWithT(func(collect *assert.CollectT) { - require.InDelta(s.T(), 10*time.Second, blm.taskReader.getBacklogHeadAge(), float64(500*time.Millisecond)) + assert.InDelta(collect, 10*time.Second, blm.taskReader.getBacklogHeadAge(), float64(500*time.Millisecond)) }, time.Second, 10*time.Millisecond) _, err = blm.pqMgr.PollTask(context.Background(), makePollMetadata(rpsInf)) s.NoError(err) s.EventuallyWithT(func(collect *assert.CollectT) { - require.Equalf(s.T(), time.Duration(0), blm.taskReader.getBacklogHeadAge(), "backlog age being reset because of no tasks in the buffer") + assert.Equalf(collect, time.Duration(0), blm.taskReader.getBacklogHeadAge(), "backlog age being reset because of no tasks in the buffer") }, time.Second, 10*time.Millisecond) } @@ -240,7 +248,7 @@ func (s *PhysicalTaskQueueManagerTestSuite) TestLegacyDescribeTaskQueue() { startTaskID := int64(1) taskCount := int64(3) - for i := int64(0); i < taskCount; i++ { + for i := range taskCount { blm.taskAckManager.addTask(startTaskID + i) } @@ -266,7 +274,7 @@ func (s *PhysicalTaskQueueManagerTestSuite) TestLegacyDescribeTaskQueue() { // Add a poller and complete all tasks pollerIdent := pollerIdentity("test-poll") s.tqMgr.pollerHistory.updatePollerInfo(pollerIdent, &pollMetadata{}) - for i := int64(0); i < taskCount; i++ { + for i := range taskCount { _, numAcked := blm.taskAckManager.completeTask(startTaskID + i) blm.db.updateBacklogStats(-numAcked, time.Time{}) } @@ -353,15 +361,12 @@ func (s *PhysicalTaskQueueManagerTestSuite) TestAddTaskStandby() { } func (s *PhysicalTaskQueueManagerTestSuite) TestTQMDoesFinalUpdateOnIdleUnload() { - if s.newMatcher { - s.T().Skip("not supported by new matcher") - } - s.config.MaxTaskQueueIdleTime = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(1 * time.Second) + s.config.EnableMigration = dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(false) s.tqMgr.Start() defer s.tqMgr.Stop(unloadCauseShuttingDown) - tm, _ := s.tqMgr.partitionMgr.engine.taskManager.(*testTaskManager) + tm := s.getTaskManager() s.EventuallyWithT(func(collect *assert.CollectT) { // will unload due to idleness require.Equal(collect, 1, tm.getUpdateCount(s.physicalTaskQueueKey)) @@ -371,17 +376,13 @@ func (s *PhysicalTaskQueueManagerTestSuite) TestTQMDoesFinalUpdateOnIdleUnload() func (s *PhysicalTaskQueueManagerTestSuite) TestTQMDoesNotDoFinalUpdateOnOwnershipLost() { // TODO: use mocks instead of testTaskManager so we can do synchronization better instead of sleeps s.config.UpdateAckInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(100 * time.Millisecond) + s.config.EnableMigration = dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(false) s.tqMgr.Start() // wait for goroutines to start and to acquire rangeid lock time.Sleep(10 * time.Millisecond) // nolint:forbidigo - var tm *testTaskManager - if s.fairness { - tm = s.tqMgr.partitionMgr.engine.fairTaskManager.(*testTaskManager) // nolint:revive - } else { - tm = s.tqMgr.partitionMgr.engine.taskManager.(*testTaskManager) // nolint:revive - } + tm := s.getTaskManager() s.Equal(0, tm.getUpdateCount(s.physicalTaskQueueKey)) // simulate stolen lock diff --git a/service/matching/pri_backlog_manager.go b/service/matching/pri_backlog_manager.go index 0ed53a67258..6093b76151f 100644 --- a/service/matching/pri_backlog_manager.go +++ b/service/matching/pri_backlog_manager.go @@ -346,6 +346,7 @@ func (c *priBacklogManagerImpl) InternalStatus() []*taskqueuespb.InternalTaskQue LoadedTasks: int64(r.getLoadedTasks()), MaxReadLevel: c.db.GetMaxReadLevel(subqueueIndex(i)), ApproximateBacklogCount: backlogCountsBySubqueue[i], + BacklogDrained: r.isDrained(), } } return status diff --git a/service/matching/pri_forwarder.go b/service/matching/pri_forwarder.go index dd2ab8c8c38..e8f305b5d51 100644 --- a/service/matching/pri_forwarder.go +++ b/service/matching/pri_forwarder.go @@ -11,6 +11,8 @@ import ( "go.temporal.io/server/api/matchingservice/v1" taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/common" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/tqid" "google.golang.org/protobuf/types/known/durationpb" ) @@ -23,6 +25,7 @@ type ( queue *PhysicalTaskQueueKey partition *tqid.NormalPartition client matchingservice.MatchingServiceClient + testHooks testhooks.TestHooks } ) @@ -40,6 +43,7 @@ func newPriForwarder( cfg *forwarderConfig, queue *PhysicalTaskQueueKey, client matchingservice.MatchingServiceClient, + testHooks testhooks.TestHooks, ) (*priForwarder, error) { partition, ok := queue.Partition().(*tqid.NormalPartition) if !ok { @@ -50,11 +54,16 @@ func newPriForwarder( client: client, partition: partition, queue: queue, + testHooks: testHooks, }, nil } // ForwardTask forwards an activity or workflow task to the parent task queue partition if it exists func (f *priForwarder) ForwardTask(ctx context.Context, task *internalTask) error { + if delay, ok := testhooks.Get(f.testHooks, testhooks.MatchingForwardTaskDelay, namespace.ID(task.event.Data.GetNamespaceId())); ok { + time.Sleep(delay) + } + degree := f.cfg.ForwarderMaxChildrenPerNode() target, err := f.partition.ParentPartition(degree) if err != nil { @@ -127,8 +136,10 @@ func (f *priForwarder) getForwardInfo(task *internalTask) *taskqueuespb.TaskForw } // task is forwarded for the first time return &taskqueuespb.TaskForwardInfo{ + CreateTime: task.getCreateTime(), TaskSource: task.source, SourcePartition: f.partition.RpcName(), + OriginPartition: f.partition.RpcName(), DispatchBuildId: f.queue.Version().BuildId(), DispatchVersionSet: f.queue.Version().VersionSet(), RedirectInfo: task.redirectInfo, @@ -140,6 +151,10 @@ func (f *priForwarder) ForwardQueryTask( ctx context.Context, task *internalTask, ) (*matchingservice.QueryWorkflowResponse, error) { + if delay, ok := testhooks.Get(f.testHooks, testhooks.MatchingForwardTaskDelay, namespace.ID(task.query.request.GetNamespaceId())); ok { + time.Sleep(delay) + } + degree := f.cfg.ForwarderMaxChildrenPerNode() target, err := f.partition.ParentPartition(degree) if err != nil { @@ -155,6 +170,7 @@ func (f *priForwarder) ForwardQueryTask( QueryRequest: task.query.request.QueryRequest, VersionDirective: task.query.request.VersionDirective, ForwardInfo: f.getForwardInfo(task), + Priority: task.query.request.GetPriority(), }) return resp, err @@ -162,6 +178,10 @@ func (f *priForwarder) ForwardQueryTask( // ForwardNexusTask forwards a nexus task to parent task queue partition, if it exists. func (f *priForwarder) ForwardNexusTask(ctx context.Context, task *internalTask) (*matchingservice.DispatchNexusTaskResponse, error) { + if delay, ok := testhooks.Get(f.testHooks, testhooks.MatchingForwardTaskDelay, namespace.ID(task.nexus.request.GetNamespaceId())); ok { + time.Sleep(delay) + } + degree := f.cfg.ForwarderMaxChildrenPerNode() target, err := f.partition.ParentPartition(degree) if err != nil { diff --git a/service/matching/pri_matcher.go b/service/matching/pri_matcher.go index cc6b1656fb3..63b074b2fff 100644 --- a/service/matching/pri_matcher.go +++ b/service/matching/pri_matcher.go @@ -506,6 +506,9 @@ func (tm *priTaskMatcher) AddTask(task *internalTask) error { } func (tm *priTaskMatcher) emitDispatchLatency(task *internalTask, forwarded bool) { + if tm.config.EmitTaskDispatchLatencyAtPoll() { + return // metric will be emitted at poll response + } if task.event.Data.CreateTime == nil { return // should not happen but for safety } diff --git a/service/matching/pri_task_reader.go b/service/matching/pri_task_reader.go index 5ddc083974d..e15ff7c1e66 100644 --- a/service/matching/pri_task_reader.go +++ b/service/matching/pri_task_reader.go @@ -378,7 +378,8 @@ func (tr *priTaskReader) signalNewTasks(resp subqueueCreateTasksResponse) { // Because we checked readLevel, we know that getTasksPump can't have beat us to // adding these tasks to outstandingTasks. So they should definitely not be there. _, found := tr.outstandingTasks.Get(t.TaskId) - return softassert.That(tr.logger, !found, "newly-written task already present in outstanding tasks") + softassert.That(tr.logger, !found, "newly-written task already present in outstanding tasks") + return found }) if !canAddDirect { @@ -426,8 +427,11 @@ func (tr *priTaskReader) getLoadedTasks() int { func (tr *priTaskReader) isDrained() bool { tr.lock.Lock() defer tr.lock.Unlock() - maxReadLevel := tr.backlogMgr.db.GetMaxReadLevel(tr.subqueue) - return tr.readLevel >= maxReadLevel && tr.outstandingTasks.Empty() + return tr.isDrainedLocked() +} + +func (tr *priTaskReader) isDrainedLocked() bool { + return tr.outstandingTasks.Empty() && tr.readLevel >= tr.backlogMgr.db.GetMaxReadLevel(tr.subqueue) } func (tr *priTaskReader) ackTaskLocked(taskId int64) int64 { @@ -453,6 +457,12 @@ func (tr *priTaskReader) ackTaskLocked(taskId int64) int64 { tr.outstandingTasks.Remove(minId) numAcked += 1 } + + // Also if we're completely drained, we can move the ack level up to the read level. + if tr.isDrainedLocked() { + tr.ackLevel = tr.readLevel + } + return numAcked } @@ -543,5 +553,8 @@ func (tr *priTaskReader) finalGC() { tr.lock.Lock() ackLevel := tr.ackLevel tr.lock.Unlock() + if ackLevel == 0 { + return + } _, _ = tr.doGCAt(ackLevel) } diff --git a/service/matching/reachability.go b/service/matching/reachability.go index 6cfb162dcee..32021000889 100644 --- a/service/matching/reachability.go +++ b/service/matching/reachability.go @@ -317,7 +317,7 @@ func newReachabilityCache( // Get retrieves the Workflow Count existence value based on the query-string key. func (c *reachabilityCache) Get(ctx context.Context, countRequest manager.CountWorkflowExecutionsRequest, open bool) (exists, hit bool, err error) { // try cache - var result interface{} + var result any if open { result = c.openWFCache.Get(countRequest) } else { diff --git a/service/matching/task.go b/service/matching/task.go index 273d6476c70..3259870ab01 100644 --- a/service/matching/task.go +++ b/service/matching/task.go @@ -13,6 +13,7 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/common/namespace" + "google.golang.org/protobuf/types/known/timestamppb" ) type ( @@ -23,12 +24,14 @@ type ( } // queryTaskInfo contains the info for a query task queryTaskInfo struct { - taskID string - request *matchingservice.QueryWorkflowRequest + taskID string + createTime *timestamppb.Timestamp + request *matchingservice.QueryWorkflowRequest } // nexusTaskInfo contains the info for a nexus task nexusTaskInfo struct { taskID string + createTime *timestamppb.Timestamp deadline time.Time operationDeadline time.Time request *matchingservice.DispatchNexusTaskRequest @@ -165,8 +168,9 @@ func newInternalQueryTask( ) *internalTask { return &internalTask{ query: &queryTaskInfo{ - taskID: taskID, - request: request, + taskID: taskID, + createTime: getCreateTime(request.GetForwardInfo()), + request: request, }, forwardInfo: request.GetForwardInfo(), responseC: make(chan taskResponse, 1), @@ -175,6 +179,13 @@ func newInternalQueryTask( } } +func getCreateTime(f *taskqueuespb.TaskForwardInfo) *timestamppb.Timestamp { + if t := f.GetCreateTime(); t != nil { + return t + } + return timestamppb.Now() +} + func newInternalNexusTask( taskID string, deadline time.Time, @@ -184,6 +195,7 @@ func newInternalNexusTask( return &internalTask{ nexus: &nexusTaskInfo{ taskID: taskID, + createTime: getCreateTime(request.GetForwardInfo()), deadline: deadline, operationDeadline: operationDeadline, request: request, @@ -231,6 +243,20 @@ func (task *internalTask) isSyncMatchTask() bool { return task.responseC != nil } +func (task *internalTask) getCreateTime() *timestamppb.Timestamp { + if task.forwardInfo != nil && task.forwardInfo.GetCreateTime() != nil { + return task.forwardInfo.GetCreateTime() + } else if task.event != nil { + return task.event.Data.GetCreateTime() + } else if task.query != nil { + return task.query.createTime + } else if task.nexus != nil { + return task.nexus.createTime + } + + return timestamppb.Now() +} + func (task *internalTask) workflowExecution() *commonpb.WorkflowExecution { switch { case task.event != nil: diff --git a/service/matching/task_queue_partition_manager.go b/service/matching/task_queue_partition_manager.go index 34ad2b85dd3..431c3ba070e 100644 --- a/service/matching/task_queue_partition_manager.go +++ b/service/matching/task_queue_partition_manager.go @@ -3,12 +3,15 @@ package matching import ( "context" "errors" + "fmt" "maps" "math" "math/bits" + "strings" "sync" "time" + "github.com/nexus-rpc/sdk-go/nexus" commonpb "go.temporal.io/api/common/v1" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" @@ -33,9 +36,11 @@ import ( "go.temporal.io/server/common/quotas" serviceerrors "go.temporal.io/server/common/serviceerror" "go.temporal.io/server/common/softassert" + "go.temporal.io/server/common/taskqueue" "go.temporal.io/server/common/tqid" "go.temporal.io/server/common/util" "go.temporal.io/server/common/worker_versioning" + "go.temporal.io/server/service/matching/hooks" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -74,6 +79,8 @@ type ( // TODO(stephanos): move cache out of partition manager cache cache.Cache // non-nil for root-partition + taskHooks []hooks.TaskHook + goroGroup goro.Group autoEnableRateLimiter quotas.RateLimiter @@ -118,6 +125,17 @@ func newTaskQueuePartitionManager( userDataManager, tqConfig, partition.TaskQueue().TaskType()) + var taskHooks []hooks.TaskHook + for _, hookFactory := range e.taskHookFactories { + taskHook := hookFactory.Create(&hooks.TaskHookFactoryCreateDetails{ + Namespace: ns, + Partition: partition, + }) + if taskHook != nil { + taskHooks = append(taskHooks, taskHook) + } + } + pm := &taskQueuePartitionManagerImpl{ engine: e, partition: partition, @@ -132,6 +150,7 @@ func newTaskQueuePartitionManager( rateLimitManager: rateLimitManager, defaultQueueFuture: future.NewFuture[physicalTaskQueueManager](), autoEnableRateLimiter: quotas.NewRateLimiter(1.0/60, 1), + taskHooks: taskHooks, } pm.initCtx, pm.initCancel = context.WithCancel(context.Background()) @@ -145,7 +164,9 @@ func newTaskQueuePartitionManager( } func (pm *taskQueuePartitionManagerImpl) initialize() (retErr error) { + defer pm.initCancel() defer func() { pm.defaultQueueFuture.SetIfNotReady(nil, retErr) }() + unload := func(bool) { pm.unloadFromEngine(unloadCauseConfigChange) } @@ -199,6 +220,15 @@ func (pm *taskQueuePartitionManagerImpl) initialize() (retErr error) { defaultQ.Start() pm.goroGroup.Go(pm.updateEphemeralData) pm.goroGroup.Go(pm.emitLogicalBacklogMetrics) + + // Whenever a root partition is loaded, we need to force all child partitions to load. + // If there is a backlog of tasks on any child partitions, force loading will ensure + // that they can forward their tasks the poller which caused the root partition to be + // loaded. We're in a separate goroutine in initialize() so we can do it here. + if defaultQ.WaitUntilInitialized(pm.initCtx) == nil { + pm.ForceLoadAllChildPartitions() + } + return nil } @@ -214,6 +244,10 @@ func (pm *taskQueuePartitionManagerImpl) Start() { pm.loadTime = time.Now() pm.engine.updateTaskQueuePartitionGauge(pm.Namespace(), pm.partition, 1) pm.userDataManager.Start() + for _, hook := range pm.taskHooks { + hook.Start() + } + //nolint:errcheck go pm.initialize() } @@ -242,6 +276,10 @@ func (pm *taskQueuePartitionManagerImpl) Stop(unloadCause unloadCause) { } pm.versionedQueuesLock.Unlock() + for _, hook := range pm.taskHooks { + hook.Stop() + } + // Then, stop user data manager to wrap up any reads/writes. pm.userDataManager.Stop() @@ -359,6 +397,7 @@ reredirectTask: if isActive { syncMatched, err = syncMatchQueue.TrySyncMatch(ctx, syncMatchTask) if syncMatched && !pm.shouldBacklogSyncMatchTaskOnError(err) { + pm.processTaskAddHooks(ctx, targetVersion, syncMatched) // Build ID is not returned for sync match. The returned build ID is used by History to update // mutable state (and visibility) when the first workflow task is spooled. @@ -384,7 +423,21 @@ reredirectTask: assignedBuildId = spoolQueue.QueueKey().Version().BuildId() } - return assignedBuildId, false, spoolQueue.SpoolTask(params.taskInfo) + err = spoolQueue.SpoolTask(params.taskInfo) + if err == nil { + pm.processTaskAddHooks(ctx, targetVersion, false) + } + + return assignedBuildId, false, err +} + +func (pm *taskQueuePartitionManagerImpl) processTaskAddHooks(ctx context.Context, targetVersion *deploymentspb.WorkerDeploymentVersion, syncMatched bool) { + for _, l := range pm.taskHooks { + l.ProcessTaskAdd(ctx, &hooks.TaskAddHookDetails{ + DeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromVersion(targetVersion), + IsSyncMatch: syncMatched, + }) + } } func (pm *taskQueuePartitionManagerImpl) shouldBacklogSyncMatchTaskOnError(err error) bool { @@ -685,6 +738,9 @@ func (pm *taskQueuePartitionManagerImpl) DispatchQueryTask( taskID string, request *matchingservice.QueryWorkflowRequest, ) (*matchingservice.QueryWorkflowResponse, error) { + task := newInternalQueryTask(taskID, request) + pm.config.setDefaultPriority(task) + reredirectTask: _, syncMatchQueue, _, _, _, err := pm.getPhysicalQueuesForAdd(ctx, request.VersionDirective, @@ -711,7 +767,7 @@ reredirectTask: dbq.MarkAlive() } - res, err := syncMatchQueue.DispatchQueryTask(ctx, taskID, request) + res, err := syncMatchQueue.DispatchQueryTask(ctx, task) if errors.Is(err, errReprocessTask) { // We get this if userdata changed while the task was blocked in DispatchQueryTask goto reredirectTask @@ -724,6 +780,23 @@ func (pm *taskQueuePartitionManagerImpl) DispatchNexusTask( taskId string, request *matchingservice.DispatchNexusTaskRequest, ) (*matchingservice.DispatchNexusTaskResponse, error) { + deadline, _ := ctx.Deadline() // If not set by user, our client will set a default. + var opDeadline time.Time + if header := nexus.Header(request.GetRequest().GetHeader()); header != nil { + if opTimeoutHeader := header.Get(nexus.HeaderOperationTimeout); opTimeoutHeader != "" { + opTimeout, err := time.ParseDuration(opTimeoutHeader) + if err != nil { + // Operation-Timeout header is not required so don't fail request on parsing errors. + pm.logger.Warn(fmt.Sprintf("unable to parse %v header: %v", nexus.HeaderOperationTimeout, opTimeoutHeader), tag.Error(err), tag.WorkflowNamespaceID(request.NamespaceId)) + } else { + opDeadline = time.Now().Add(opTimeout) + } + } + } + + task := newInternalNexusTask(taskId, deadline, opDeadline, request) + pm.config.setDefaultPriority(task) + reredirectTask: _, syncMatchQueue, _, _, _, err := pm.getPhysicalQueuesForAdd(ctx, worker_versioning.MakeUseAssignmentRulesDirective(), @@ -749,7 +822,7 @@ reredirectTask: dbq.MarkAlive() } - res, err := syncMatchQueue.DispatchNexusTask(ctx, taskId, request) + res, err := syncMatchQueue.DispatchNexusTask(ctx, task) if errors.Is(err, errReprocessTask) { // We get this if userdata changed while the task was blocked in DispatchNexusTask goto reredirectTask @@ -996,7 +1069,7 @@ func (pm *taskQueuePartitionManagerImpl) describe( unversionedCurrentShareByPriority, unversionedRampingShareByPriority = splitStatsByPriorityByRampPercentage(unversionedStatsByPriority, rampPercentage) } else if currentExists { - // If there exist no ramping version, weattribute the entire unversioned backlog to the current version. + // If there exist no ramping version, we attribute the entire unversioned backlog to the current version. unversionedCurrentShareByPriority = cloneStatsByPriority(unversionedStatsByPriority) } } @@ -1174,7 +1247,7 @@ func (pm *taskQueuePartitionManagerImpl) emitLogicalBacklogMetrics(ctx context.C select { case <-ctx.Done(): return ctx.Err() - case <-time.After(interval): + case <-time.After(backoff.Jitter(interval, 0.05)): pm.fetchAndEmitLogicalBacklogMetrics(ctx) } } @@ -1207,8 +1280,11 @@ func (pm *taskQueuePartitionManagerImpl) fetchAndEmitLogicalBacklogMetrics(ctx c pqInfo := vInfo.GetPhysicalTaskQueueInfo() + deploymentName, buildID := parseDeploymentFromVersionKey(versionKey) versionHandler := pm.metricsHandler.WithTags( metrics.WorkerVersionTag(versionKey, pm.config.BreakdownMetricsByBuildID()), + metrics.WorkerDeploymentNameTag(deploymentName, pm.config.BreakdownMetricsByBuildID()), + metrics.WorkerDeploymentBuildIDTag(buildID, pm.config.BreakdownMetricsByBuildID()), ) // Per-priority backlog count @@ -1244,8 +1320,11 @@ func (pm *taskQueuePartitionManagerImpl) emitZeroLogicalBacklogForQueue(version if !pm.config.BreakdownMetricsByTaskQueue() || !pm.config.BreakdownMetricsByPartition() { return } + deploymentName, buildID := parseDeploymentFromVersionKey(version.MetricsTagValue()) handler := pm.metricsHandler.WithTags( metrics.WorkerVersionTag(version.MetricsTagValue(), pm.config.BreakdownMetricsByBuildID()), + metrics.WorkerDeploymentNameTag(deploymentName, pm.config.BreakdownMetricsByBuildID()), + metrics.WorkerDeploymentBuildIDTag(buildID, pm.config.BreakdownMetricsByBuildID()), ) for pri := range pq.GetStatsByPriority(false) { metrics.ApproximateBacklogCount.With(handler).Record(0, metrics.MatchingTaskPriorityTag(pri)) @@ -1253,6 +1332,18 @@ func (pm *taskQueuePartitionManagerImpl) emitZeroLogicalBacklogForQueue(version metrics.ApproximateBacklogAgeSeconds.With(handler).Record(0) } +// parseDeploymentFromVersionKey extracts the deployment name and build ID from a version key +// string used as the map key in DescribeTaskQueuePartitionResponse.VersionsInfoInternal. +// The key format is "deploymentName:buildId" for V3 deployment-based versions, or empty for +// unversioned queues. Returns empty strings when the delimiter is not found (unversioned or +// V2 version-set keys). +func parseDeploymentFromVersionKey(versionKey string) (deploymentName, buildID string) { + if name, id, found := strings.Cut(versionKey, worker_versioning.WorkerDeploymentVersionDelimiter); found { + return name, id + } + return "", "" +} + func (pm *taskQueuePartitionManagerImpl) ephemeralDataChanged(data *taskqueuespb.EphemeralData) { // for now, only sticky partitions act on ephemeral data, normal partitions ignore it. if pm.partition.Kind() != enumspb.TASK_QUEUE_KIND_STICKY { @@ -1416,9 +1507,9 @@ func mergeStatsByPriority(into, from map[int32]*taskqueuepb.TaskQueueStats) { continue } ensureStatsWithAge(into, pri) - // mergeStats requires non-nil ApproximateBacklogAge on both inputs. + // MergeStats requires non-nil ApproximateBacklogAge on both inputs. s = cloneTaskQueueStats(s) - mergeStats(into[pri], s) + taskqueue.MergeStats(into[pri], s) } } @@ -1469,38 +1560,33 @@ func (pm *taskQueuePartitionManagerImpl) callerInfoContext(ctx context.Context) return headers.SetCallerInfo(ctx, headers.NewBackgroundHighCallerInfo(pm.ns.Name().String())) } -// ForceLoadAllChildPartitions spins off go routines which make RPC calls to all the +// ForceLoadAllChildPartitions force-loads known child (read) partitions in new goroutines. func (pm *taskQueuePartitionManagerImpl) ForceLoadAllChildPartitions() { if !pm.partition.IsRoot() { return } - partition := pm.partition - taskQueue := partition.TaskQueue() - - namespaceId := partition.NamespaceId() - taskQueueName := taskQueue.Name() - taskQueueType := taskQueue.TaskType() - partitionTotal := pm.config.NumReadPartitions() - - // record total - 1 as we won't try to forceLoad the Root partition. - pm.metricsHandler.Counter(metrics.ForceLoadedTaskQueuePartitions.Name()).Record(int64(partitionTotal) - 1) + partitions := int32(pm.config.NumReadPartitions()) + if partitions <= 1 { + return + } - for partitionId := 1; partitionId < partitionTotal; partitionId++ { + // record total-1 as we won't try to load the root partition. + pm.metricsHandler.Counter(metrics.ForceLoadedTaskQueuePartitions.Name()).Record(int64(partitions) - 1) + for id := int32(1); id < partitions; id++ { go func() { ctx := pm.callerInfoContext(context.Background()) resp, err := pm.matchingClient.ForceLoadTaskQueuePartition(ctx, &matchingservice.ForceLoadTaskQueuePartitionRequest{ - NamespaceId: namespaceId, + NamespaceId: pm.partition.NamespaceId(), TaskQueuePartition: &taskqueuespb.TaskQueuePartition{ - TaskQueue: taskQueueName, - TaskQueueType: taskQueueType, - PartitionId: &taskqueuespb.TaskQueuePartition_NormalPartitionId{NormalPartitionId: int32(partitionId)}, + TaskQueue: pm.partition.TaskQueue().Name(), + TaskQueueType: pm.partition.TaskQueue().TaskType(), + PartitionId: &taskqueuespb.TaskQueuePartition_NormalPartitionId{NormalPartitionId: id}, }, }) if err != nil { - pm.logger.Error("Failed to force load non-root partition after root partition was loaded", - tag.Error(err)) + pm.logger.Error("failed to load child partition after root partition was loaded", tag.Error(err)) return } diff --git a/service/matching/task_queue_partition_manager_test.go b/service/matching/task_queue_partition_manager_test.go index 787f158fc1a..43b9ae77e98 100644 --- a/service/matching/task_queue_partition_manager_test.go +++ b/service/matching/task_queue_partition_manager_test.go @@ -21,6 +21,7 @@ import ( persistencespb "go.temporal.io/server/api/persistence/v1" taskqueuespb "go.temporal.io/server/api/taskqueue/v1" hlc "go.temporal.io/server/common/clock/hybrid_logical_clock" + "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/metrics/metricstest" "go.temporal.io/server/common/namespace" @@ -28,6 +29,7 @@ import ( "go.temporal.io/server/common/testing/testlogger" "go.temporal.io/server/common/tqid" "go.temporal.io/server/common/worker_versioning" + "go.temporal.io/server/service/matching/hooks" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -77,13 +79,16 @@ func (s *PartitionManagerTestSuite) SetupTest() { ns, registry := createMockNamespaceCache(s.controller, namespace.Name(namespaceName)) s.ns = ns config := defaultTestConfig() + config.EnableMigration = dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(false) if s.fairness { useFairness(config) - } else if s.newMatcher { - useNewMatcher(config) + } else if !s.newMatcher { + useClassicMatcher(config) } s.matchingClient = matchingservicemock.NewMockMatchingServiceClient(s.controller) + s.matchingClient.EXPECT().ForceLoadTaskQueuePartition(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&matchingservice.ForceLoadTaskQueuePartitionResponse{}, nil).AnyTimes() engine := createTestMatchingEngine(logger, s.controller, config, s.matchingClient, registry) f, err := tqid.NewTaskQueueFamily(namespaceID, taskQueueName) @@ -169,7 +174,7 @@ func (s *PartitionManagerTestSuite) TestDescribeTaskQueuePartition_MultipleBuild ApproximateBacklogCount: 1, }, TaskQueueStatsByPriorityKey: map[int32]*taskqueuepb.TaskQueueStats{ - 3: &taskqueuepb.TaskQueueStats{ + 3: { ApproximateBacklogAge: durationpb.New(0), ApproximateBacklogCount: 1, }, @@ -290,7 +295,7 @@ func (s *PartitionManagerTestSuite) TestDescribeTaskQueuePartition_CurrentAndRam s.Require().NoError(err) dQueue := s.partitionMgr.defaultQueue() // Backlog 10 tasks in the unversioned/default queue. - for i := 0; i < 10; i++ { + for i := range 10 { err := dQueue.SpoolTask(&persistencespb.TaskInfo{ NamespaceId: namespaceID, RunId: "run", @@ -471,7 +476,7 @@ func (s *PartitionManagerTestSuite) TestDescribeTaskQueuePartition_OnlyCurrentNo s.Require().NoError(err) dQueue := s.partitionMgr.defaultQueue() // Backlog 10 tasks in the unversioned/default queue. - for i := 0; i < 10; i++ { + for i := range 10 { err = dQueue.SpoolTask(&persistencespb.TaskInfo{ NamespaceId: namespaceID, RunId: "run", @@ -543,7 +548,7 @@ func (s *PartitionManagerTestSuite) TestDescribeTaskQueuePartition_OnlyRampingNo s.Require().NoError(err) dQueue := s.partitionMgr.defaultQueue() // Backlog 10 tasks in the unversioned/default queue. - for i := 0; i < 10; i++ { + for i := range 10 { err = dQueue.SpoolTask(&persistencespb.TaskInfo{ NamespaceId: namespaceID, RunId: "run", @@ -596,7 +601,7 @@ func (s *PartitionManagerTestSuite) TestDescribeTaskQueuePartition_UnversionedDo s.Require().NoError(err) dQueue := s.partitionMgr.defaultQueue() // Backlog 5 tasks in the unversioned/default queue. - for i := 0; i < 5; i++ { + for i := range 5 { err = dQueue.SpoolTask(&persistencespb.TaskInfo{ NamespaceId: namespaceID, RunId: "run", @@ -670,7 +675,7 @@ func (s *PartitionManagerTestSuite) addRoutingConfigUserData(deploymentName, cur // spoolDefaultTasks spools n tasks to the partition manager's default queue. func (s *PartitionManagerTestSuite) spoolDefaultTasks(pm *taskQueuePartitionManagerImpl, n int) { dQueue := pm.defaultQueue() - for i := 0; i < n; i++ { + for i := range n { err := dQueue.SpoolTask(&persistencespb.TaskInfo{ NamespaceId: namespaceID, RunId: "run", @@ -724,7 +729,7 @@ func (s *PartitionManagerTestSuite) TestLogicalBacklogMetrics_CurrentOnly() { BuildId: currentBuildID, }, true) s.Require().NoError(err) - for i := 0; i < 2; i++ { + for i := range 2 { err = currentQ.SpoolTask(&persistencespb.TaskInfo{ NamespaceId: namespaceID, RunId: "run", @@ -1334,6 +1339,76 @@ type testPartitionManagerConfig struct { withRecentPoller bool // Whether to register a poller to simulate recent poller activity } +// capturingTaskMatchHook records ProcessTaskMatch calls for test assertions. +type capturingTaskMatchHook struct { + mu sync.Mutex + taskQueueName string + taskQueueType enumspb.TaskQueueType + calls []capturedTaskMatchDetails +} + +type capturedTaskMatchDetails struct { + TaskQueueName string + TaskQueueType enumspb.TaskQueueType + IsSyncMatch bool + DeploymentVersion *deploymentpb.WorkerDeploymentVersion +} + +func (h *capturingTaskMatchHook) Create(details *hooks.TaskHookFactoryCreateDetails) hooks.TaskHook { + h.taskQueueName = details.Partition.TaskQueue().Name() + h.taskQueueType = details.Partition.TaskQueue().TaskType() + return h +} + +func (h *capturingTaskMatchHook) Start() { +} + +func (h *capturingTaskMatchHook) Stop() { +} + +func (h *capturingTaskMatchHook) ProcessTaskAdd(ctx context.Context, event *hooks.TaskAddHookDetails) { + h.mu.Lock() + defer h.mu.Unlock() + details := capturedTaskMatchDetails{ + TaskQueueName: h.taskQueueName, + TaskQueueType: h.taskQueueType, + IsSyncMatch: event.IsSyncMatch, + } + if event.DeploymentVersion != nil { + details.DeploymentVersion = &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: event.DeploymentVersion.DeploymentName, + BuildId: event.DeploymentVersion.BuildId, + } + } + h.calls = append(h.calls, details) +} + +func (h *capturingTaskMatchHook) getCalls() []capturedTaskMatchDetails { + h.mu.Lock() + defer h.mu.Unlock() + return append([]capturedTaskMatchDetails(nil), h.calls...) +} + +// setupPartitionManagerWithTaskHookFactories creates a partition manager with the given task match hooks. +func (s *PartitionManagerTestSuite) setupPartitionManagerWithTaskHookFactories(taskHookFactories []hooks.TaskHookFactory) (*taskQueuePartitionManagerImpl, func()) { + f, err := tqid.NewTaskQueueFamily(namespaceID, taskQueueName) + s.Require().NoError(err) + partition := f.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition() + tqConfig := newTaskQueueConfig(partition.TaskQueue(), s.partitionMgr.engine.config, s.partitionMgr.ns.Name()) + s.partitionMgr.engine.taskHookFactories = taskHookFactories + + pm, err := newTaskQueuePartitionManager(s.partitionMgr.engine, s.partitionMgr.ns, partition, tqConfig, s.partitionMgr.logger, s.partitionMgr.throttledLogger, metrics.NoopMetricsHandler, s.userDataMgr) + s.Require().NoError(err) + pm.Start() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + err = pm.WaitUntilInitialized(ctx) + cancel() + s.Require().NoError(err) + + return pm, func() { pm.Stop(unloadCauseUnspecified) } +} + // setupPartitionManagerWithCapture creates a partition manager with a capturing metrics handler // and returns the manager, capture, and a cleanup function func (s *PartitionManagerTestSuite) setupPartitionManagerWithCapture( @@ -1480,6 +1555,117 @@ func (s *PartitionManagerTestSuite) TestNoRecentPollerMetric_OldPartitionWithRec s.Empty(recordings, "Metric should not be emitted when there are recent pollers") } +func (s *PartitionManagerTestSuite) TestTaskAddHooks_AddHookSyncMatch() { + hook := &capturingTaskMatchHook{} + pm, cleanup := s.setupPartitionManagerWithTaskHookFactories([]hooks.TaskHookFactory{hook}) + defer cleanup() + + type pollResult struct { + task *internalTask + err error + } + pollDone := make(chan pollResult, 1) + pollStarted := make(chan struct{}) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + close(pollStarted) + task, _, err := pm.PollTask(ctx, &pollMetadata{ + workerVersionCapabilities: &commonpb.WorkerVersionCapabilities{ + BuildId: "", + UseVersioning: false, + }, + }) + pollDone <- pollResult{task: task, err: err} + if task != nil && task.responseC != nil { + close(task.responseC) + } + }() + s.Require().Eventually(func() bool { + select { + case <-pollStarted: + return true + default: + return false + } + }, 2*time.Second, 1*time.Millisecond) + + _, syncMatched, err := pm.AddTask(context.Background(), addTaskParams{ + taskInfo: &persistencespb.TaskInfo{ + NamespaceId: namespaceID, + RunId: "run", + WorkflowId: "wf", + }, + }) + s.Require().NoError(err) + s.Require().True(syncMatched) + + var pr pollResult + s.Require().Eventually(func() bool { + select { + case pr = <-pollDone: + return true + default: + return false + } + }, 2*time.Second, 10*time.Millisecond) + s.Require().NoError(pr.err) + s.Require().NotNil(pr.task) + s.Require().NotNil(pr.task.responseC) + + s.Require().Eventually(func() bool { return len(hook.getCalls()) >= 1 }, 2*time.Second, 10*time.Millisecond) + calls := hook.getCalls() + s.Require().Len(calls, 1) + s.Equal(taskQueueName, calls[0].TaskQueueName) + s.Equal(enumspb.TASK_QUEUE_TYPE_WORKFLOW, calls[0].TaskQueueType) + s.True(calls[0].IsSyncMatch) + s.Nil(calls[0].DeploymentVersion) +} + +func (s *PartitionManagerTestSuite) TestTaskAddHooks_AddHookNoSyncMatch() { + hook := &capturingTaskMatchHook{} + pm, cleanup := s.setupPartitionManagerWithTaskHookFactories([]hooks.TaskHookFactory{hook}) + defer cleanup() + + _, syncMatched, err := pm.AddTask(context.Background(), addTaskParams{ + taskInfo: &persistencespb.TaskInfo{ + NamespaceId: namespaceID, + RunId: "run", + WorkflowId: "wf", + VersionDirective: worker_versioning.MakeBuildIdDirective("buildXYZ"), + }, + }) + s.Require().NoError(err) + s.Require().False(syncMatched) + + calls := hook.getCalls() + s.Require().Len(calls, 1) + s.Equal(taskQueueName, calls[0].TaskQueueName) + s.Equal(enumspb.TASK_QUEUE_TYPE_WORKFLOW, calls[0].TaskQueueType) + s.False(calls[0].IsSyncMatch) +} + +func (s *PartitionManagerTestSuite) TestTaskAddHooks_MultipleHooksInvoked() { + hook1 := &capturingTaskMatchHook{} + hook2 := &capturingTaskMatchHook{} + pm, cleanup := s.setupPartitionManagerWithTaskHookFactories([]hooks.TaskHookFactory{hook1, hook2}) + defer cleanup() + + _, _, err := pm.AddTask(context.Background(), addTaskParams{ + taskInfo: &persistencespb.TaskInfo{ + NamespaceId: namespaceID, + RunId: "run", + WorkflowId: "wf", + }, + }) + s.Require().NoError(err) + + s.Len(hook1.getCalls(), 1) + s.Len(hook2.getCalls(), 1) + s.False(hook1.getCalls()[0].IsSyncMatch) + s.False(hook2.getCalls()[0].IsSyncMatch) +} + type mockUserDataManager struct { sync.Mutex data *persistencespb.VersionedTaskQueueUserData diff --git a/service/matching/task_tracker.go b/service/matching/task_tracker.go index a4c5f6889b1..e88a49954fa 100644 --- a/service/matching/task_tracker.go +++ b/service/matching/task_tracker.go @@ -7,27 +7,20 @@ import ( "go.temporal.io/server/common/clock" ) -const ( - // The duration of each mini-bucket in the circularTaskBuffer - intervalSize = 5 - // The total duration which is used to calculate the rate of tasks added/dispatched - totalIntervalSize = 30 -) - // a circular array of a fixed size for tracking tasks type circularTaskBuffer struct { - buffer []int + buffer []int32 currentPos int } func newCircularTaskBuffer(size int) circularTaskBuffer { return circularTaskBuffer{ - buffer: make([]int, size), + buffer: make([]int32, size), } } -func (cb *circularTaskBuffer) incrementTaskCount() { - cb.buffer[cb.currentPos]++ +func (cb *circularTaskBuffer) inc(n int) { + cb.buffer[cb.currentPos] += int32(n) } func (cb *circularTaskBuffer) advance() { @@ -39,56 +32,62 @@ func (cb *circularTaskBuffer) advance() { func (cb *circularTaskBuffer) totalTasks() int { totalTasks := 0 for _, count := range cb.buffer { - totalTasks += count + totalTasks += int(count) } return totalTasks } type taskTracker struct { - lock sync.Mutex - clock clock.TimeSource - startTime time.Time // time when taskTracker was initialized - bucketStartTime time.Time // the starting time of a bucket in the buffer - bucketSize time.Duration // the duration of each bucket in the buffer - numberOfBuckets int // the total number of buckets in the buffer - totalIntervalSize time.Duration // the number of seconds over which rate of tasks are added/dispatched - tasksInInterval circularTaskBuffer + lock sync.Mutex + clock clock.TimeSource + startTime time.Time // time when taskTracker was initialized + bucketStartTime time.Time // the starting time of a bucket in the buffer + bucketSize time.Duration // duration of each bucket in the buffer + buckets int // number of buckets in the buffer + totalInterval time.Duration // duration over which rate of tasks is measured + tasks circularTaskBuffer } -func newTaskTracker(timeSource clock.TimeSource) *taskTracker { +func newTaskTracker( + timeSource clock.TimeSource, + bucketSize time.Duration, + totalInterval time.Duration, +) *taskTracker { + bucketSize = max(bucketSize, time.Millisecond) + buckets := int(totalInterval/bucketSize) + 1 return &taskTracker{ - clock: timeSource, - startTime: timeSource.Now(), - bucketStartTime: timeSource.Now(), - bucketSize: time.Duration(intervalSize) * time.Second, - numberOfBuckets: (totalIntervalSize / intervalSize) + 1, - totalIntervalSize: time.Duration(totalIntervalSize) * time.Second, - tasksInInterval: newCircularTaskBuffer((totalIntervalSize / intervalSize) + 1), + clock: timeSource, + startTime: timeSource.Now(), + bucketStartTime: timeSource.Now(), + bucketSize: bucketSize, + buckets: buckets, + totalInterval: totalInterval, + tasks: newCircularTaskBuffer(buckets), } } -// advanceAndResetTracker advances the trackers position and clears out any expired intervals +// advanceAndResetLocked advances the trackers position and clears out any expired intervals // This method must be called with taskTracker's lock held. -func (s *taskTracker) advanceAndResetTracker(elapsed time.Duration) { +func (s *taskTracker) advanceAndResetLocked(elapsed time.Duration) { // Calculate the number of intervals elapsed since the start interval time intervalsElapsed := int(elapsed / s.bucketSize) - for i := 0; i < min(intervalsElapsed, s.numberOfBuckets); i++ { - s.tasksInInterval.advance() // advancing our circular buffer's position until we land on the right interval + for range min(intervalsElapsed, s.buckets) { + s.tasks.advance() // advancing our circular buffer's position until we land on the right interval } s.bucketStartTime = s.bucketStartTime.Add(time.Duration(intervalsElapsed) * s.bucketSize) } -// incrementTaskCount adds/removes tasks from the current time that falls in the appropriate interval -func (s *taskTracker) incrementTaskCount() { +// inc increments the count of tasks by n at the current time +func (s *taskTracker) inc(n int) { s.lock.Lock() defer s.lock.Unlock() currentTime := s.clock.Now() // Calculate elapsed time from the latest start interval time elapsed := currentTime.Sub(s.bucketStartTime) - s.advanceAndResetTracker(elapsed) - s.tasksInInterval.incrementTaskCount() + s.advanceAndResetLocked(elapsed) + s.tasks.inc(n) } // rate returns the rate of tasks added/dispatched in a given interval @@ -99,10 +98,10 @@ func (s *taskTracker) rate() float32 { // Calculate elapsed time from the latest start interval time elapsed := currentTime.Sub(s.bucketStartTime) - s.advanceAndResetTracker(elapsed) - totalTasks := s.tasksInInterval.totalTasks() + s.advanceAndResetLocked(elapsed) + totalTasks := s.tasks.totalTasks() - elapsedTime := min(currentTime.Sub(s.bucketStartTime)+s.totalIntervalSize, + elapsedTime := min(currentTime.Sub(s.bucketStartTime)+s.totalInterval, currentTime.Sub(s.startTime)) if elapsedTime <= 0 { diff --git a/service/matching/task_tracker_test.go b/service/matching/task_tracker_test.go index 4fcb2431d8e..2083f3688bf 100644 --- a/service/matching/task_tracker_test.go +++ b/service/matching/task_tracker_test.go @@ -8,28 +8,20 @@ import ( "go.temporal.io/server/common/clock" ) -// addTasks is a helper which adds numberOfTasks to a taskTracker -func trackTasksHelper(tr *taskTracker, numberOfTasks int) { - for i := 0; i < numberOfTasks; i++ { - // adding a bunch of tasks - tr.incrementTaskCount() - } -} - func TestAddTasksRate(t *testing.T) { // define a fake clock and it's time for testing timeSource := clock.NewEventTimeSource() currentTime := time.Now() timeSource.Update(currentTime) - tr := newTaskTracker(timeSource) + tr := newTaskTracker(timeSource, 5*time.Second, 30*time.Second) // mini windows will have the following format : (start time, end time) // (0 - 4), (5 - 9), (10 - 14), (15 - 19), (20 - 24), (25 - 29), (30 - 34), ... // rate should be zero when no time is passed require.Equal(t, float32(0), tr.rate()) // time: 0 - trackTasksHelper(tr, 100) + tr.inc(100) require.Equal(t, float32(0), tr.rate()) // still zero because no time is passed // tasks should be placed in the first mini-window @@ -37,22 +29,25 @@ func TestAddTasksRate(t *testing.T) { require.InEpsilon(t, float32(100), tr.rate(), 0.001) // 100 tasks added in 1 second = 100 / 1 = 100 // tasks should be placed in the second mini-window with 6 total seconds elapsed - timeSource.Advance(5 * time.Second) - trackTasksHelper(tr, 200) // time: 6 second + timeSource.Advance(5 * time.Second) // time: 6 second + tr.inc(100) + tr.inc(100) require.InEpsilon(t, float32(50), tr.rate(), 0.001) // (100 + 200) tasks added in 6 seconds = 300/6 = 50 timeSource.Advance(24 * time.Second) // time: 30 second - trackTasksHelper(tr, 300) + tr.inc(100) + tr.inc(100) + tr.inc(100) require.InEpsilon(t, float32(20), tr.rate(), 0.001) // (100 + 200 + 300) tasks added in (30 + 0 (current window)) seconds = 600/30 = 20 // this should clear out the first mini-window of 100 tasks timeSource.Advance(5 * time.Second) // time: 35 second - trackTasksHelper(tr, 10) + tr.inc(10) require.InEpsilon(t, float32(17), tr.rate(), 0.001) // (10 + 200 + 300) tasks added in (30 + 0 (current window)) seconds = 510/30 = 17 // this should clear out the second and third mini-windows timeSource.Advance(15 * time.Second) // time: 50 second - trackTasksHelper(tr, 10) + tr.inc(10) require.InEpsilon(t, float32(10.666667), tr.rate(), 0.001) // (10 + 10 + 300) tasks added in (30 + 0 (current window)) seconds = 320/30 = 10.66 // a minute passes and no tasks are added diff --git a/service/matching/user_data_manager.go b/service/matching/user_data_manager.go index bb3c629a6a2..f4979225834 100644 --- a/service/matching/user_data_manager.go +++ b/service/matching/user_data_manager.go @@ -39,6 +39,10 @@ const ( const maxFastUserDataFetches = 5 +// noEphemeralDataVersion as a value for LastKnownEphemeralDataVersion means +// the caller does not want to receive ephemeral data. +const noEphemeralDataVersion = -1 + type ( userDataManager interface { Start() @@ -594,9 +598,10 @@ func (m *userDataManagerImpl) HandleGetUserDataRequest( return nil, err } newUserData := userData.GetVersion() > lastVersion - newEphData := ephData.GetVersion() > lastEphVersion + // noEphemeralDataVersion means the caller does not want ephemeral data + newEphData := lastEphVersion != noEphemeralDataVersion && ephData.GetVersion() > lastEphVersion if newUserData || newEphData { - m.logger.Info("returning user data", + m.logger.Debug("returning user data", tag.Bool("long-poll", req.WaitNewData), tag.Int64("request-known-version", lastVersion), tag.UserDataVersion(userData.GetVersion()), @@ -709,7 +714,7 @@ func (m *userDataManagerImpl) CheckTaskQueueUserDataPropagation( for i := 1; i < wfPartitions; i++ { go check(i, enumspb.TASK_QUEUE_TYPE_WORKFLOW) } - for i := 0; i < actPartitions; i++ { + for i := range actPartitions { go check(i, enumspb.TASK_QUEUE_TYPE_ACTIVITY) } @@ -721,6 +726,7 @@ func (m *userDataManagerImpl) CheckTaskQueueUserDataPropagation( } } +// LocalBacklogPriorityChanged can be called on any normal partition. func (m *userDataManagerImpl) LocalBacklogPriorityChanged(backlogPriority map[PhysicalTaskQueueVersion]int64) { // TODO: later, we'll send this data to the root to propagate instead of just keeping it // locally and merging. @@ -738,30 +744,49 @@ func (m *userDataManagerImpl) LocalBacklogPriorityChanged(backlogPriority map[Ph }) } - newEph := &taskqueuespb.VersionedEphemeralData{ - Data: &taskqueuespb.EphemeralData{ - Partition: []*taskqueuespb.EphemeralData_ByPartition{ - &taskqueuespb.EphemeralData_ByPartition{ - Partition: int32(normal.PartitionId()), - Version: byVersion, - }, - }, + newPartition := []*taskqueuespb.EphemeralData_ByPartition{ + &taskqueuespb.EphemeralData_ByPartition{ + Partition: int32(normal.PartitionId()), + Version: byVersion, }, - Version: time.Now().UnixNano(), + } + + m.updateEphemeralData(func(newData *taskqueuespb.EphemeralData) { + newData.Partition = newPartition + }) +} + +func (m *userDataManagerImpl) gotIncomingEphemeralData(eph *taskqueuespb.VersionedEphemeralData) { + if m.partition.IsRoot() { + // Root activity/nexus partition should not get ephemeral data from its fetch source + // (root workflow partition). This is done by setting LastKnownEphemeralDataVersion to + // -1 on root non-wf partitions and having HandleGetUserDataRequest not send ephemeral + // data in that case. But if we do get it (e.g. during deployment), ignore it. + return } m.lock.Lock() defer m.lock.Unlock() - m.myEphemeralData = newEph + m.incomingEphemeralData = eph m.mergeEphemeralDataLocked() } -func (m *userDataManagerImpl) gotIncomingEphemeralData(eph *taskqueuespb.VersionedEphemeralData) { +// updateEphemeralData updates the ephemeral data owned by this partition. The update function +// will be given a non-nil clone of the current data, which it should mutate. +func (m *userDataManagerImpl) updateEphemeralData(update func(*taskqueuespb.EphemeralData)) { m.lock.Lock() defer m.lock.Unlock() - m.incomingEphemeralData = eph + newData := common.CloneProto(m.myEphemeralData.GetData()) + if newData == nil { + newData = &taskqueuespb.EphemeralData{} + } + update(newData) + m.myEphemeralData = &taskqueuespb.VersionedEphemeralData{ + Data: newData, + Version: time.Now().UnixNano(), + } m.mergeEphemeralDataLocked() } @@ -773,6 +798,12 @@ func (m *userDataManagerImpl) getMergedEphemeralData() (*taskqueuespb.VersionedE } func (m *userDataManagerImpl) getIncomingEphemeralDataVersion() int64 { + if m.partition.IsRoot() { + // The root activity/nexus partition should not fetch ephemeral data from the root + // workflow partition. + return noEphemeralDataVersion + } + m.lock.Lock() defer m.lock.Unlock() diff --git a/service/matching/user_data_manager_test.go b/service/matching/user_data_manager_test.go index f6758c23bdf..e13d5f9a981 100644 --- a/service/matching/user_data_manager_test.go +++ b/service/matching/user_data_manager_test.go @@ -18,6 +18,7 @@ import ( "go.temporal.io/server/api/matchingservice/v1" "go.temporal.io/server/api/matchingservicemock/v1" persistencespb "go.temporal.io/server/api/persistence/v1" + taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/backoff" "go.temporal.io/server/common/dynamicconfig" @@ -656,28 +657,39 @@ func TestUserData_FetchesActivityToWorkflow(t *testing.T) { Version: 1, Data: mkUserData(1), } + eph1 := &taskqueuespb.VersionedEphemeralData{ + Version: 123456789, + Data: &taskqueuespb.EphemeralData{ + Partition: []*taskqueuespb.EphemeralData_ByPartition{ + &taskqueuespb.EphemeralData_ByPartition{Partition: 1}, + }, + }, + } tqCfg.matchingClientMock.EXPECT().GetTaskQueueUserData( gomock.Any(), &matchingservice.GetTaskQueueUserDataRequest{ - NamespaceId: defaultNamespaceId, - TaskQueue: defaultRootTqID, - TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, - LastKnownUserDataVersion: 0, - WaitNewData: false, + NamespaceId: defaultNamespaceId, + TaskQueue: defaultRootTqID, + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + LastKnownUserDataVersion: 0, + LastKnownEphemeralDataVersion: -1, + WaitNewData: false, }). Return(&matchingservice.GetTaskQueueUserDataResponse{ - UserData: data1, + UserData: data1, + EphemeralData: eph1, // return some data, expect it to be ignored }, nil) tqCfg.matchingClientMock.EXPECT().GetTaskQueueUserData( gomock.Any(), &matchingservice.GetTaskQueueUserDataRequest{ - NamespaceId: defaultNamespaceId, - TaskQueue: defaultRootTqID, - TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, - LastKnownUserDataVersion: 1, - WaitNewData: true, // after first successful poll, there would be long polls + NamespaceId: defaultNamespaceId, + TaskQueue: defaultRootTqID, + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + LastKnownUserDataVersion: 1, + LastKnownEphemeralDataVersion: -1, + WaitNewData: true, // after first successful poll, there would be long polls }). Return(&matchingservice.GetTaskQueueUserDataResponse{ UserData: data1, @@ -690,6 +702,58 @@ func TestUserData_FetchesActivityToWorkflow(t *testing.T) { userData, _, err := m.GetUserData() require.NoError(t, err) require.Equal(t, data1, userData) + ephMerged, _ := m.getMergedEphemeralData() + require.Nil(t, ephMerged, "activity root should not process ephemeral data from workflow root") + m.Stop() + m.goroGroup.Wait() // ensure gomock doesn't complain about calls after the test returns +} + +func TestUserData_GetEphemeralData(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + ctx := context.Background() + dbq := newTestUnversionedPhysicalQueueKey(defaultNamespaceId, defaultRootTqID, enumspb.TASK_QUEUE_TYPE_WORKFLOW, 0) + tqCfg := defaultTqmTestOpts(controller) + tqCfg.dbq = dbq + + m := createUserDataManager(t, controller, tqCfg) + m.Start() + require.NoError(t, m.WaitUntilInitialized(ctx)) + + // set some ephemeral data + m.LocalBacklogPriorityChanged(map[PhysicalTaskQueueVersion]int64{ + PhysicalTaskQueueVersion{}: 10, + }) + + // get it + res, err := m.HandleGetUserDataRequest(context.Background(), + &matchingservice.GetTaskQueueUserDataRequest{ + NamespaceId: defaultNamespaceId, + TaskQueue: defaultRootTqID, + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + LastKnownUserDataVersion: 0, + LastKnownEphemeralDataVersion: 0, + WaitNewData: false, + }) + require.NoError(t, err) + require.Nil(t, res.UserData) + require.NotNil(t, res.EphemeralData) + + // don't return it when not requested + res, err = m.HandleGetUserDataRequest(context.Background(), + &matchingservice.GetTaskQueueUserDataRequest{ + NamespaceId: defaultNamespaceId, + TaskQueue: defaultRootTqID, + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + LastKnownUserDataVersion: 0, + LastKnownEphemeralDataVersion: -1, + WaitNewData: false, + }) + require.NoError(t, err) + require.Nil(t, res.UserData) + require.Nil(t, res.EphemeralData) + m.Stop() m.goroGroup.Wait() // ensure gomock doesn't complain about calls after the test returns } @@ -871,7 +935,7 @@ func TestUserData_Propagation(t *testing.T) { } const iters = 5 - for iter := 0; iter < iters; iter++ { + for iter := range iters { newVersion, err := managers[0].UpdateUserData(ctx, UserDataUpdateOptions{}, func(data *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) { return data, false, nil }) diff --git a/service/matching/version_rule_helper_test.go b/service/matching/version_rule_helper_test.go index 72995386d5a..a32cc128f81 100644 --- a/service/matching/version_rule_helper_test.go +++ b/service/matching/version_rule_helper_test.go @@ -50,7 +50,7 @@ func TestFindAssignmentBuildId_WithRamp(t *testing.T) { histogram := make(map[string]int) runs := 1000000 - for i := 0; i < runs; i++ { + for i := range runs { b := FindAssignmentBuildId(rules, "run-"+strconv.Itoa(i)) histogram[b]++ } @@ -66,12 +66,12 @@ func TestFindAssignmentBuildId_WithRamp(t *testing.T) { func TestCalcRampThresholdUniform(t *testing.T) { buildPref := "bldXYZ-" histogram := [100]int{} - for i := 0; i < 1000000; i++ { + for i := range 1000000 { v := calcRampThreshold(buildPref + strconv.Itoa(i)) histogram[int32(v)]++ } - for i := 0; i < 100; i++ { + for i := range 100 { assert.InEpsilon(t, 10000, histogram[i], 0.1) } } diff --git a/service/matching/version_rule_test.go b/service/matching/version_rule_test.go index 80ecea132b9..ad9444bf249 100644 --- a/service/matching/version_rule_test.go +++ b/service/matching/version_rule_test.go @@ -237,7 +237,7 @@ func TestInsertAssignmentRuleMaxRules(t *testing.T) { var err error // insert 3x --> success - for i := 0; i < 3; i++ { + for range 3 { data, err = insertAssignmentRule(mkAssignmentRuleWithoutRamp("1"), data, clock, 0, maxRules) assert.NoError(t, err) } @@ -584,7 +584,7 @@ func TestAddRedirectRuleMaxRules(t *testing.T) { var err error // insert 3x --> success - for i := 0; i < 3; i++ { + for i := range 3 { src := fmt.Sprintf("%d", i) dst := fmt.Sprintf("%d", i+1) data, err = insertRedirectRule(mkRedirectRule(src, dst), data, clock, maxRules, ignoreMaxUpstreamBuildIDs) diff --git a/service/matching/version_sets_test.go b/service/matching/version_sets_test.go index 21546868def..092e316d450 100644 --- a/service/matching/version_sets_test.go +++ b/service/matching/version_sets_test.go @@ -25,7 +25,7 @@ func mkNewSet(id string, clock *clockspb.HybridLogicalClock) *persistencespb.Com func mkInitialData(numSets int, clock *clockspb.HybridLogicalClock) *persistencespb.VersioningData { sets := make([]*persistencespb.CompatibleVersionSet, numSets) - for i := 0; i < numSets; i++ { + for i := range numSets { sets[i] = mkNewSet(fmt.Sprintf("%v", i), clock) } return &persistencespb.VersioningData{ diff --git a/service/matching/workers/registry_impl.go b/service/matching/workers/registry_impl.go index 183f6dd3eff..8ad8c175be3 100644 --- a/service/matching/workers/registry_impl.go +++ b/service/matching/workers/registry_impl.go @@ -46,27 +46,27 @@ type ( // It partitions the keyspace into buckets and enforces TTL and capacity. // Eviction runs in the background. registryImpl struct { - buckets []*bucket // buckets for partitioning the keyspace - maxItemsFn dynamicconfig.IntPropertyFn // dynamic config for maximum entries - ttlFn dynamicconfig.DurationPropertyFn // dynamic config for entry TTL - minEvictAgeFn dynamicconfig.DurationPropertyFn // dynamic config for minimum evict age - evictionIntervalFn dynamicconfig.DurationPropertyFn // dynamic config for eviction interval - total atomic.Int64 // atomic counter of total entries - quit chan struct{} // channel to signal shutdown of the eviction loop - seed maphash.Seed // seed for the hasher, used to ensure consistent hashing - metricsHandler metrics.Handler // metrics handler for recording registry metrics - enableWorkerPluginMetrics dynamicconfig.BoolPropertyFn // dynamic config function to control plugin metrics export + buckets []*bucket // buckets for partitioning the keyspace + maxItemsFn dynamicconfig.IntPropertyFn // dynamic config for maximum entries + ttlFn dynamicconfig.DurationPropertyFn // dynamic config for entry TTL + minEvictAgeFn dynamicconfig.DurationPropertyFn // dynamic config for minimum evict age + evictionIntervalFn dynamicconfig.DurationPropertyFn // dynamic config for eviction interval + total atomic.Int64 // atomic counter of total entries + quit chan struct{} // channel to signal shutdown of the eviction loop + seed maphash.Seed // seed for the hasher, used to ensure consistent hashing + metricsHandler metrics.Handler // metrics handler for recording registry metrics + metricsEmitter *workerMetricsEmitter // emitter for heartbeat-derived metrics } // RegistryParams contains all parameters for creating a worker registry. RegistryParams struct { - NumBuckets dynamicconfig.IntPropertyFn - TTL dynamicconfig.DurationPropertyFn - MinEvictAge dynamicconfig.DurationPropertyFn - MaxItems dynamicconfig.IntPropertyFn - EvictionInterval dynamicconfig.DurationPropertyFn - MetricsHandler metrics.Handler - EnablePluginMetrics dynamicconfig.BoolPropertyFn + NumBuckets dynamicconfig.IntPropertyFn + TTL dynamicconfig.DurationPropertyFn + MinEvictAge dynamicconfig.DurationPropertyFn + MaxItems dynamicconfig.IntPropertyFn + EvictionInterval dynamicconfig.DurationPropertyFn + MetricsHandler metrics.Handler + MetricsConfig WorkerMetricsConfig } ) @@ -213,15 +213,18 @@ func NewRegistry(lc fx.Lifecycle, params RegistryParams) Registry { func newRegistryImpl(params RegistryParams) *registryImpl { m := ®istryImpl{ - buckets: make([]*bucket, params.NumBuckets()), - maxItemsFn: params.MaxItems, - ttlFn: params.TTL, - minEvictAgeFn: params.MinEvictAge, - evictionIntervalFn: params.EvictionInterval, - seed: maphash.MakeSeed(), - quit: make(chan struct{}), - metricsHandler: params.MetricsHandler, - enableWorkerPluginMetrics: params.EnablePluginMetrics, + buckets: make([]*bucket, params.NumBuckets()), + maxItemsFn: params.MaxItems, + ttlFn: params.TTL, + minEvictAgeFn: params.MinEvictAge, + evictionIntervalFn: params.EvictionInterval, + seed: maphash.MakeSeed(), + quit: make(chan struct{}), + metricsHandler: params.MetricsHandler, + metricsEmitter: &workerMetricsEmitter{ + handler: params.MetricsHandler, + config: params.MetricsConfig, + }, } for i := range m.buckets { @@ -276,33 +279,6 @@ func (m *registryImpl) recordEvictionMetric() { } } -// recordPluginMetric sets a value of 1 for each unique plugin name present in the heartbeats. -func (m *registryImpl) recordPluginMetric(nsName namespace.Name, heartbeats []*workerpb.WorkerHeartbeat) { - // Check if plugin metrics are enabled via dynamic config - if !m.enableWorkerPluginMetrics() { - return - } - - // Track which plugins we've already recorded - recordedPlugins := make(map[string]bool) - - for _, hb := range heartbeats { - for _, pluginInfo := range hb.Plugins { - pluginName := pluginInfo.Name - if !recordedPlugins[pluginName] { - metrics.WorkerPluginNameMetric. - With(m.metricsHandler). - Record( - 1, - metrics.NamespaceIDTag(nsName.String()), - metrics.WorkerPluginNameTag(pluginName), - ) - recordedPlugins[pluginName] = true - } - } - } -} - // filterWorkers returns all WorkerHeartbeats in a namespace // for which predicate(hb) returns true. func (m *registryImpl) filterWorkers( @@ -388,17 +364,7 @@ func (m *registryImpl) Stop() { func (m *registryImpl) RecordWorkerHeartbeats(nsID namespace.ID, nsName namespace.Name, workerHeartbeat []*workerpb.WorkerHeartbeat) { m.upsertHeartbeats(nsID, workerHeartbeat) - m.recordPluginMetric(nsName, workerHeartbeat) - m.recordActivitySlotsMetric(workerHeartbeat) -} - -// recordActivitySlotsMetric records the distribution of activity slots in use across workers. -func (m *registryImpl) recordActivitySlotsMetric(heartbeats []*workerpb.WorkerHeartbeat) { - for _, hb := range heartbeats { - if hb.ActivityTaskSlotsInfo != nil { - metrics.WorkerRegistryActivitySlotsUsed.With(m.metricsHandler).Record(int64(hb.ActivityTaskSlotsInfo.CurrentUsedSlots)) - } - } + m.metricsEmitter.emit(nsID, nsName, workerHeartbeat) } func (m *registryImpl) ListWorkers(nsID namespace.ID, params ListWorkersParams) (ListWorkersResponse, error) { diff --git a/service/matching/workers/registry_impl_test.go b/service/matching/workers/registry_impl_test.go index eee0948b4d6..0829d6722df 100644 --- a/service/matching/workers/registry_impl_test.go +++ b/service/matching/workers/registry_impl_test.go @@ -27,13 +27,15 @@ func TestUpdateAndListNamespace(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(2), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(2), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -74,13 +76,15 @@ func TestShutdownStatusRemovesWorker(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -120,13 +124,15 @@ func TestShutdownStatusRemovesWorker(t *testing.T) { func TestShutdownStatusForNonExistentWorker(t *testing.T) { m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: metrics.NoopMetricsHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -142,13 +148,15 @@ func TestShutdownStatusForNonExistentWorker(t *testing.T) { func TestListNamespacePredicate(t *testing.T) { m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: metrics.NoopMetricsHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -185,13 +193,15 @@ func TestEvictByTTL(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(1 * time.Second), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(1 * time.Second), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -228,13 +238,15 @@ func TestEvictByCapacity(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -284,13 +296,15 @@ func TestEvictByCapacityWithMinAgeProtection(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(minEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(minEvictAge), + MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -334,13 +348,15 @@ func TestEvictByCapacityAfterMinAge(t *testing.T) { // Uses real time.NewTicker - synctest provides virtual time control m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(minEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(minEvictAge), + MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -380,13 +396,15 @@ func TestMultipleNamespaces(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(2), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(2), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -446,13 +464,15 @@ func TestEvictLoopRecordsUtilizationMetric(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(evictionInterval), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(maxItems), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(evictionInterval), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) // Add some entries to create utilization @@ -489,13 +509,15 @@ func TestEvictLoopRecordsUtilizationMetric(t *testing.T) { func BenchmarkUpdate(b *testing.B) { m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(16), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(time.Minute), - MaxItems: dynamicconfig.GetIntPropertyFn(b.N), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(16), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(time.Minute), + MaxItems: dynamicconfig.GetIntPropertyFn(b.N), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: metrics.NoopMetricsHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() hb := &workerpb.WorkerHeartbeat{WorkerInstanceKey: "benchWorker"} @@ -507,17 +529,19 @@ func BenchmarkUpdate(b *testing.B) { func BenchmarkListNamespace(b *testing.B) { m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(16), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(time.Minute), - MaxItems: dynamicconfig.GetIntPropertyFn(1000), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(16), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(time.Minute), + MaxItems: dynamicconfig.GetIntPropertyFn(1000), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: metrics.NoopMetricsHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() // Pre-populate with entries - for i := 0; i < 1000; i++ { + for i := range 1000 { key := fmt.Sprintf("worker%d", i) hb := &workerpb.WorkerHeartbeat{WorkerInstanceKey: key} m.upsertHeartbeats("benchNs", []*workerpb.WorkerHeartbeat{hb}) @@ -533,13 +557,15 @@ func BenchmarkRandomUpdate(b *testing.B) { namespaces := []namespace.ID{"ns1", "ns2", "ns3"} totalHeartbeats := 30 // Total heartbeats per namespace m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(len(namespaces)), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(time.Minute), - MaxItems: dynamicconfig.GetIntPropertyFn(b.N), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(len(namespaces)), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(time.Minute), + MaxItems: dynamicconfig.GetIntPropertyFn(b.N), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: metrics.NoopMetricsHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -550,7 +576,7 @@ func BenchmarkRandomUpdate(b *testing.B) { } var pairs []pair for _, ns := range namespaces { - for i := 0; i < totalHeartbeats; i++ { + for i := range totalHeartbeats { key := fmt.Sprintf("%s-worker%d", ns, i) hb := &workerpb.WorkerHeartbeat{WorkerInstanceKey: key, CurrentStickyCacheSize: int32(i)} m.upsertHeartbeats(ns, []*workerpb.WorkerHeartbeat{hb}) @@ -576,13 +602,15 @@ func TestActivitySlotsMetric(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(1), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(1), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -643,13 +671,15 @@ func TestPluginMetricsExported(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(2), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + NumBuckets: dynamicconfig.GetIntPropertyFn(2), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, }) defer m.Stop() @@ -691,10 +721,10 @@ func TestPluginMetricsExported(t *testing.T) { pluginMetrics := snapshot[metrics.WorkerPluginNameMetric.Name()] assert.Len(t, pluginMetrics, 3, "plugin-a from both workers should be deduplicated") - // Helper function to find metric by namespace and plugin name + // Helper function to find metric by namespace name and plugin name findMetric := func(namespaceName namespace.Name, pluginName string) *metricstest.CapturedRecording { for _, metric := range pluginMetrics { - if metric.Tags["namespace_id"] == namespaceName.String() && metric.Tags[metrics.WorkerPluginNameTagName] == pluginName { + if metric.Tags["namespace"] == namespaceName.String() && metric.Tags[metrics.WorkerPluginNameTagName] == pluginName { return metric } } @@ -721,13 +751,15 @@ func TestPluginMetricsDisabled(t *testing.T) { defer captureHandler.StopCapture(capture) m := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(2), - TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), - MaxItems: dynamicconfig.GetIntPropertyFn(10), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), - MetricsHandler: captureHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(false), + NumBuckets: dynamicconfig.GetIntPropertyFn(2), + TTL: dynamicconfig.GetDurationPropertyFn(time.Hour), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(0), + MaxItems: dynamicconfig.GetIntPropertyFn(10), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(time.Hour), + MetricsHandler: captureHandler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(false), + }, }) defer m.Stop() diff --git a/service/matching/workers/registry_test.go b/service/matching/workers/registry_test.go index ab18970b973..93b874d4b7f 100644 --- a/service/matching/workers/registry_test.go +++ b/service/matching/workers/registry_test.go @@ -10,6 +10,7 @@ import ( workerpb "go.temporal.io/api/worker/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/metrics/metricstest" "go.temporal.io/server/common/namespace" ) @@ -21,6 +22,21 @@ const ( testDefaultEvictionInterval = 10 * time.Minute ) +func testDefaultRegistryParams(handler metrics.Handler) RegistryParams { + return RegistryParams{ + NumBuckets: dynamicconfig.GetIntPropertyFn(10), + TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), + MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), + MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), + EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), + MetricsHandler: handler, + MetricsConfig: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), + ExternalPayloadsEnabled: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), + }, + } +} + func TestRegistryImpl_RecordWorkerHeartbeat(t *testing.T) { tests := []struct { name string @@ -78,15 +94,7 @@ func TestRegistryImpl_RecordWorkerHeartbeat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(10), - TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), - }) + r := newRegistryImpl(testDefaultRegistryParams(metrics.NoopMetricsHandler)) tt.setup(r) r.RecordWorkerHeartbeats(tt.nsID, namespace.Name(tt.nsID+"_name"), []*workerpb.WorkerHeartbeat{tt.workerHeartbeat}) @@ -182,15 +190,7 @@ func TestRegistryImpl_ListWorkers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(10), - TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), - }) + r := newRegistryImpl(testDefaultRegistryParams(metrics.NoopMetricsHandler)) tt.setup(r) resp, err := r.ListWorkers(tt.nsID, ListWorkersParams{}) @@ -312,15 +312,7 @@ func TestRegistryImpl_ListWorkersWithQuery(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(10), - TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), - }) + r := newRegistryImpl(testDefaultRegistryParams(metrics.NoopMetricsHandler)) tt.setup(r) resp, err := r.ListWorkers(tt.nsID, ListWorkersParams{Query: tt.query}) @@ -422,15 +414,7 @@ func TestRegistryImpl_DescribeWorker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(10), - TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), - }) + r := newRegistryImpl(testDefaultRegistryParams(metrics.NoopMetricsHandler)) tt.setup(r) result, err := r.DescribeWorker(tt.nsID, tt.workerInstanceKey) @@ -447,15 +431,7 @@ func TestRegistryImpl_DescribeWorker(t *testing.T) { } func TestRegistryImpl_ListWorkersPagination(t *testing.T) { - r := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(10), - TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), - }) + r := newRegistryImpl(testDefaultRegistryParams(metrics.NoopMetricsHandler)) // Add 5 workers in non-sorted order to verify sorting works r.upsertHeartbeats("ns1", []*workerpb.WorkerHeartbeat{ @@ -551,15 +527,7 @@ func TestRegistryImpl_ListWorkersPaginationWithDeletedCursor(t *testing.T) { } func TestRegistryImpl_ListWorkersNoPagination(t *testing.T) { - r := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(10), - TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), - }) + r := newRegistryImpl(testDefaultRegistryParams(metrics.NoopMetricsHandler)) r.upsertHeartbeats("ns1", []*workerpb.WorkerHeartbeat{ {WorkerInstanceKey: "worker-a"}, @@ -575,15 +543,7 @@ func TestRegistryImpl_ListWorkersNoPagination(t *testing.T) { } func TestRegistryImpl_ListWorkersInvalidPageToken(t *testing.T) { - r := newRegistryImpl(RegistryParams{ - NumBuckets: dynamicconfig.GetIntPropertyFn(10), - TTL: dynamicconfig.GetDurationPropertyFn(testDefaultEntryTTL), - MinEvictAge: dynamicconfig.GetDurationPropertyFn(testDefaultMinEvictAge), - MaxItems: dynamicconfig.GetIntPropertyFn(testDefaultMaxEntries), - EvictionInterval: dynamicconfig.GetDurationPropertyFn(testDefaultEvictionInterval), - MetricsHandler: metrics.NoopMetricsHandler, - EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(true), - }) + r := newRegistryImpl(testDefaultRegistryParams(metrics.NoopMetricsHandler)) r.upsertHeartbeats("ns1", []*workerpb.WorkerHeartbeat{ {WorkerInstanceKey: "worker-a"}, @@ -593,3 +553,73 @@ func TestRegistryImpl_ListWorkersInvalidPageToken(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "invalid next_page_token") } + +func TestRegistryImpl_RecordStorageDriverMetric(t *testing.T) { + t.Run("disabled when ExternalPayloadsEnabled is false", func(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + params := testDefaultRegistryParams(captureHandler) + r := newRegistryImpl(params) + + r.metricsEmitter.emit(namespace.ID("test-ns-id"), namespace.Name("test-ns"), []*workerpb.WorkerHeartbeat{ + { + WorkerInstanceKey: "worker1", + Drivers: []*workerpb.StorageDriverInfo{{Type: "s3"}}, + }, + }) + + snap := capture.Snapshot() + assert.Empty(t, snap["worker_storage_driver_type"], "no metrics should be emitted when external payloads is disabled") + }) + + t.Run("emits storage driver type when enabled", func(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + params := testDefaultRegistryParams(captureHandler) + params.MetricsConfig.ExternalPayloadsEnabled = dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true) + r := newRegistryImpl(params) + + r.metricsEmitter.emit(namespace.ID("test-ns-id"), namespace.Name("test-ns"), []*workerpb.WorkerHeartbeat{ + { + WorkerInstanceKey: "worker1", + Drivers: []*workerpb.StorageDriverInfo{{Type: "s3"}}, + }, + }) + + snap := capture.Snapshot() + recordings := snap["worker_storage_driver_type"] + require.Len(t, recordings, 1) + assert.Equal(t, "s3", recordings[0].Tags["worker_storage_driver_type"]) + assert.Equal(t, "test-ns", recordings[0].Tags["namespace"]) + }) + + t.Run("deduplication across heartbeats", func(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + params := testDefaultRegistryParams(captureHandler) + params.MetricsConfig.ExternalPayloadsEnabled = dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true) + r := newRegistryImpl(params) + + r.metricsEmitter.emit(namespace.ID("test-ns-id"), namespace.Name("test-ns"), []*workerpb.WorkerHeartbeat{ + { + WorkerInstanceKey: "worker1", + Drivers: []*workerpb.StorageDriverInfo{{Type: "s3"}}, + }, + { + WorkerInstanceKey: "worker2", + Drivers: []*workerpb.StorageDriverInfo{{Type: "s3"}}, + }, + }) + + snap := capture.Snapshot() + recordings := snap["worker_storage_driver_type"] + require.Len(t, recordings, 1, "same driver type from multiple heartbeats should produce a single metric") + assert.Equal(t, "s3", recordings[0].Tags["worker_storage_driver_type"]) + }) +} diff --git a/service/matching/workers/worker_metrics_emitter.go b/service/matching/workers/worker_metrics_emitter.go new file mode 100644 index 00000000000..e6dbaa908d1 --- /dev/null +++ b/service/matching/workers/worker_metrics_emitter.go @@ -0,0 +1,96 @@ +package workers + +import ( + enumspb "go.temporal.io/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/tqid" +) + +// WorkerMetricsConfig contains dynamic config flags for worker-related metrics. +type WorkerMetricsConfig struct { + EnablePluginMetrics dynamicconfig.BoolPropertyFn + EnablePollerAutoscalingMetrics dynamicconfig.BoolPropertyFn + BreakdownMetricsByTaskQueue dynamicconfig.BoolPropertyFnWithTaskQueueFilter + ExternalPayloadsEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter +} + +// workerMetricsEmitter encapsulates logic for emitting metrics derived from worker heartbeats. +type workerMetricsEmitter struct { + handler metrics.Handler + config WorkerMetricsConfig +} + +func (e *workerMetricsEmitter) emit(nsID namespace.ID, nsName namespace.Name, heartbeats []*workerpb.WorkerHeartbeat) { + enablePluginMetrics := e.config.EnablePluginMetrics != nil && e.config.EnablePluginMetrics() + enablePollerAutoscalingMetrics := e.config.EnablePollerAutoscalingMetrics != nil && e.config.EnablePollerAutoscalingMetrics() + enableStorageDriverMetrics := e.config.ExternalPayloadsEnabled != nil && e.config.ExternalPayloadsEnabled(nsName.String()) + + recordedPlugins := make(map[string]bool) + recordedDrivers := make(map[string]bool) + + for _, hb := range heartbeats { + // Activity slots metric (always enabled) + if hb.ActivityTaskSlotsInfo != nil { + metrics.WorkerRegistryActivitySlotsUsed.With(e.handler).Record(int64(hb.ActivityTaskSlotsInfo.CurrentUsedSlots)) + } + + // Plugin metrics (if enabled) + if enablePluginMetrics { + for _, pluginInfo := range hb.Plugins { + pluginName := pluginInfo.Name + if !recordedPlugins[pluginName] { + metrics.WorkerPluginNameMetric. + With(e.handler). + Record(1, metrics.NamespaceTag(nsName.String()), metrics.WorkerPluginNameTag(pluginName)) + recordedPlugins[pluginName] = true + } + } + } + + // Poller autoscaling metrics (if enabled) + if enablePollerAutoscalingMetrics { + e.emitPollerAutoscaling(nsID, nsName, hb) + } + + // Storage driver metrics (if external payloads enabled) + if enableStorageDriverMetrics { + for _, driver := range hb.GetDrivers() { + driverType := driver.GetType() + if !recordedDrivers[driverType] { + metrics.WorkerStorageDriverTypeMetric. + With(e.handler). + Record(1, metrics.NamespaceTag(nsName.String()), metrics.WorkerStorageDriverTypeTag(driverType)) + recordedDrivers[driverType] = true + } + } + } + } +} + +func (e *workerMetricsEmitter) emitPollerAutoscaling(nsID namespace.ID, nsName namespace.Name, hb *workerpb.WorkerHeartbeat) { + family, err := tqid.NewTaskQueueFamily(nsID.String(), hb.GetTaskQueue()) + if err != nil { + return + } + + recordAutoscaling := func(taskType enumspb.TaskQueueType) { + tq := family.TaskQueue(taskType) + breakdownByTQ := e.config.BreakdownMetricsByTaskQueue != nil && + e.config.BreakdownMetricsByTaskQueue(nsName.String(), hb.GetTaskQueue(), taskType) + handler := metrics.GetPerTaskQueueScope(e.handler, nsName.String(), tq, breakdownByTQ) + metrics.PollerAutoscalingHeartbeatCount.With(handler).Record(1) + } + + if hb.WorkflowPollerInfo.GetIsAutoscaling() { + recordAutoscaling(enumspb.TASK_QUEUE_TYPE_WORKFLOW) + } + if hb.ActivityPollerInfo.GetIsAutoscaling() { + recordAutoscaling(enumspb.TASK_QUEUE_TYPE_ACTIVITY) + } + if hb.NexusPollerInfo.GetIsAutoscaling() { + recordAutoscaling(enumspb.TASK_QUEUE_TYPE_NEXUS) + } +} diff --git a/service/matching/workers/worker_metrics_emitter_test.go b/service/matching/workers/worker_metrics_emitter_test.go new file mode 100644 index 00000000000..527c1c087d7 --- /dev/null +++ b/service/matching/workers/worker_metrics_emitter_test.go @@ -0,0 +1,115 @@ +package workers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + enumspb "go.temporal.io/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/metrics/metricstest" + "go.temporal.io/server/common/namespace" +) + +func TestPollerAutoscalingMetrics(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + emitter := &workerMetricsEmitter{ + handler: captureHandler, + config: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(false), + EnablePollerAutoscalingMetrics: dynamicconfig.GetBoolPropertyFn(true), + }, + } + + testNamespaceID := namespace.ID("test-namespace-id") + testNamespaceName := namespace.Name("test-namespace") + testTaskQueue := "test-task-queue" + + // Worker 1: workflow autoscaling enabled, activity disabled + worker1 := &workerpb.WorkerHeartbeat{ + WorkerInstanceKey: "worker_1", + TaskQueue: testTaskQueue, + WorkflowPollerInfo: &workerpb.WorkerPollerInfo{ + IsAutoscaling: true, + }, + ActivityPollerInfo: &workerpb.WorkerPollerInfo{ + IsAutoscaling: false, + }, + } + + // Worker 2: workflow, activity, and nexus all enabled + worker2 := &workerpb.WorkerHeartbeat{ + WorkerInstanceKey: "worker_2", + TaskQueue: testTaskQueue, + WorkflowPollerInfo: &workerpb.WorkerPollerInfo{ + IsAutoscaling: true, + }, + ActivityPollerInfo: &workerpb.WorkerPollerInfo{ + IsAutoscaling: true, + }, + NexusPollerInfo: &workerpb.WorkerPollerInfo{ + IsAutoscaling: true, + }, + } + + // Worker 3: workflow only + worker3 := &workerpb.WorkerHeartbeat{ + WorkerInstanceKey: "worker_3", + TaskQueue: testTaskQueue, + WorkflowPollerInfo: &workerpb.WorkerPollerInfo{ + IsAutoscaling: true, + }, + } + + emitter.emit(testNamespaceID, testNamespaceName, []*workerpb.WorkerHeartbeat{worker1, worker2, worker3}) + + snapshot := capture.Snapshot() + autoscalingMetrics := snapshot[metrics.PollerAutoscalingHeartbeatCount.Name()] + + // Counter increments per heartbeat: workflow x3, activity x1, nexus x1 = 5 total recordings + assert.Len(t, autoscalingMetrics, 5, "expected 5 counter increments") + + taskTypeCounts := make(map[string]int) + for _, m := range autoscalingMetrics { + taskTypeCounts[m.Tags[metrics.TaskTypeTagName]]++ + assert.Equal(t, string(testNamespaceName), m.Tags["namespace"]) + assert.Equal(t, "__omitted__", m.Tags["taskqueue"]) + } + assert.Equal(t, 3, taskTypeCounts[enumspb.TASK_QUEUE_TYPE_WORKFLOW.String()], "workflow should have 3 increments") + assert.Equal(t, 1, taskTypeCounts[enumspb.TASK_QUEUE_TYPE_ACTIVITY.String()], "activity should have 1 increment") + assert.Equal(t, 1, taskTypeCounts[enumspb.TASK_QUEUE_TYPE_NEXUS.String()], "nexus should have 1 increment") +} + +func TestPollerAutoscalingMetricsDisabled(t *testing.T) { + captureHandler := metricstest.NewCaptureHandler() + capture := captureHandler.StartCapture() + defer captureHandler.StopCapture(capture) + + emitter := &workerMetricsEmitter{ + handler: captureHandler, + config: WorkerMetricsConfig{ + EnablePluginMetrics: dynamicconfig.GetBoolPropertyFn(false), + EnablePollerAutoscalingMetrics: dynamicconfig.GetBoolPropertyFn(false), + }, + } + + worker1 := &workerpb.WorkerHeartbeat{ + WorkerInstanceKey: "worker_1", + TaskQueue: "test-task-queue", + WorkflowPollerInfo: &workerpb.WorkerPollerInfo{ + IsAutoscaling: true, + }, + } + + testNamespaceID := namespace.ID("test-namespace-id") + testNamespaceName := namespace.Name("test-namespace") + emitter.emit(testNamespaceID, testNamespaceName, []*workerpb.WorkerHeartbeat{worker1}) + + snapshot := capture.Snapshot() + autoscalingMetrics := snapshot[metrics.PollerAutoscalingHeartbeatCount.Name()] + assert.Empty(t, autoscalingMetrics, "should not record autoscaling metrics when disabled") +} diff --git a/service/matching/workers/worker_query_engine.go b/service/matching/workers/worker_query_engine.go index 0bde529dfa6..ec23a929a91 100644 --- a/service/matching/workers/worker_query_engine.go +++ b/service/matching/workers/worker_query_engine.go @@ -17,11 +17,15 @@ const ( workerHostNameColName = "HostName" workerTaskQueueColName = "TaskQueue" workerDeploymentNameColName = "DeploymentName" + workerBuildIDColName = "BuildId" workerSdkNameColName = "SdkName" workerSdkVersionColName = "SdkVersion" workerStartTimeColName = "StartTime" workerHeartbeatTimeColName = "HeartbeatTime" workerStatusColName = "WorkerStatus" + // "Status" is a SQL reserved word, so the parser lowercases and backtick-quotes it. + // After stripping backticks we get "status". + workerStatusColNameAlias = "status" ) const ( @@ -35,7 +39,7 @@ FilterWorkers filters the list of per-namespace worker heartbeats against the pr The query should be a valid SQL query without WHERE clause. Query is used to filter workers based on worker heartbeat info. -The following worker status attributes are expected are supported as part of the query: +The following worker attributes are supported as part of the query: * WorkerInstanceKey * WorkerIdentity * HostName @@ -45,8 +49,8 @@ The following worker status attributes are expected are supported as part of the * SdkName * SdkVersion * StartTime -* LastHeartbeatTime -* Status +* HeartbeatTime +* WorkerStatus (or Status) Currently metrics are not supported as a part of ListWorkers query. Field names are case-sensitive. @@ -56,16 +60,20 @@ The query can have conditions on multiple fields. Date time fields should be in RFC3339 format. Example query: - "TaskQueue = 'my_task_queue' AND LastHeartbeatTime < '2023-10-27T10:30:00Z' " + "TaskQueue = 'my_task_queue' AND HeartbeatTime < '2023-10-27T10:30:00Z' " Different fields can support different operators. - - string fields (e.g., WorkerIdentity, HostName, TaskQueue, DeploymentName, SdkName, SdkVersion): - starts_with, not starts_with - - time fields (e.g., StartTime, LastHeartbeatTime): - =, !=, >, >=, <, <=, between + - string fields (e.g., WorkerIdentity, HostName, TaskQueue, DeploymentName, BuildId, SdkName, SdkVersion): + =, !=, starts_with, not starts_with, IS NULL, IS NOT NULL + - time fields (e.g., StartTime, HeartbeatTime): + =, !=, >, >=, <, <=, between, IS NULL, IS NOT NULL - metric fields (e.g., total_sticky_cache_hit): =, !=, >, >=, <, <= +For string fields, IS NULL matches workers where the field is empty, and IS NOT NULL matches +workers where the field is non-empty. For time fields, IS NULL matches workers where the +timestamp is not set. + Returns the list of workers for which the query matches the worker heartbeat, or an error, Errors are: - the query is invalid. @@ -111,6 +119,12 @@ var ( } return hb.DeploymentVersion.DeploymentName }, + workerBuildIDColName: func(hb *workerpb.WorkerHeartbeat) string { + if hb.DeploymentVersion == nil { + return "" + } + return hb.DeploymentVersion.BuildId + }, workerSdkNameColName: func(hb *workerpb.WorkerHeartbeat) string { return hb.SdkName }, @@ -120,6 +134,9 @@ var ( workerStatusColName: func(hb *workerpb.WorkerHeartbeat) string { return hb.Status.String() }, + workerStatusColNameAlias: func(hb *workerpb.WorkerHeartbeat) string { + return hb.Status.String() + }, } ) @@ -206,7 +223,7 @@ func (w *workerQueryEngine) evaluateExpression(expr sqlparser.Expr) (bool, error case *sqlparser.RangeCond: return w.evaluateRange(e) case *sqlparser.IsExpr: - return false, serviceerror.NewInvalidArgumentf("%s: 'is' expression", notSupportedErrMessage) + return w.evaluateIsExpr(e) case *sqlparser.NotExpr: return false, serviceerror.NewInvalidArgumentf("%s: 'not' expression", notSupportedErrMessage) case *sqlparser.FuncExpr: @@ -241,6 +258,42 @@ func (w *workerQueryEngine) evaluateOr(expr *sqlparser.OrExpr) (bool, error) { return w.evaluateExpression(expr.Right) } +func (w *workerQueryEngine) evaluateIsExpr(expr *sqlparser.IsExpr) (bool, error) { + if expr == nil { + return false, serviceerror.NewInvalidArgumentf("IsExpr input expression cannot be nil") + } + + colNameExpr, ok := expr.Expr.(*sqlparser.ColName) + if !ok { + return false, serviceerror.NewInvalidArgumentf("invalid filter name: %s", sqlparser.String(expr.Expr)) + } + colName := strings.ReplaceAll(sqlparser.String(colNameExpr), "`", "") + + if expr.Operator != sqlparser.IsNullStr && expr.Operator != sqlparser.IsNotNullStr { + return false, serviceerror.NewInvalidArgumentf( + "%s: 'is' operator %q is not supported; only IS NULL and IS NOT NULL are supported", + notSupportedErrMessage, expr.Operator) + } + + isNull := expr.Operator == sqlparser.IsNullStr + + if propertyFunc, ok := propertyMapFuncs[colName]; ok { + isEmpty := propertyFunc(w.currentWorker) == "" + return isEmpty == isNull, nil + } + + switch colName { + case workerStartTimeColName, workerHeartbeatTimeColName: + timeValue, err := w.getTimeValue(colName) + if err != nil { + return false, err + } + return timeValue.IsZero() == isNull, nil + default: + return false, serviceerror.NewInvalidArgumentf("unknown or unsupported worker heartbeat search field: %s", colName) + } +} + func (w *workerQueryEngine) evaluateComparison(expr *sqlparser.ComparisonExpr) (bool, error) { if expr == nil { return false, serviceerror.NewInvalidArgumentf("ComparisonExpr input expression cannot be nil") @@ -250,32 +303,26 @@ func (w *workerQueryEngine) evaluateComparison(expr *sqlparser.ComparisonExpr) ( if !ok { return false, serviceerror.NewInvalidArgumentf("invalid filter name: %s", sqlparser.String(expr.Left)) } - colName := sqlparser.String(colNameExpr) + // Strip backticks added by the SQL parser for reserved words (e.g., "Status" → "`status`" → "status"). + colName := strings.ReplaceAll(sqlparser.String(colNameExpr), "`", "") valExpr, ok := expr.Right.(*sqlparser.SQLVal) if !ok { return false, serviceerror.NewInvalidArgumentf("invalid value: %s", sqlparser.String(expr.Right)) } valStr := sqlparser.String(valExpr) - switch colName { - case workerInstanceKeyColName, - workerIdentityColName, - workerHostNameColName, - workerTaskQueueColName, - workerDeploymentNameColName, - workerSdkNameColName, - workerSdkVersionColName, - workerStatusColName: - propertyFunc, ok := propertyMapFuncs[colName] - if !ok { - return false, serviceerror.NewInvalidArgumentf("unknown or unsupported worker heartbeat search field: %s", colName) - } + // First check if the column name is a valid property function. + if propertyFunc, ok := propertyMapFuncs[colName]; ok { val, err := sqlquery.ExtractStringValue(valStr) if err != nil { return false, serviceerror.NewInvalidArgumentf("invalid value for %s: %v", colName, err) } existingVal := propertyFunc(w.currentWorker) return compareQueryString(val, existingVal, expr.Operator, colName) + } + + // If not, then check if the column name is a valid time column. + switch colName { case workerStartTimeColName: expectedTime, err := sqlquery.ConvertToTime(valStr) if err != nil { @@ -303,7 +350,8 @@ func (w *workerQueryEngine) evaluateRange(expr *sqlparser.RangeCond) (bool, erro if !ok { return false, serviceerror.NewInvalidArgumentf("unknown or unsupported column name: %s", sqlparser.String(expr.Left)) } - colNameStr := sqlparser.String(colName) + // Strip backticks added by the SQL parser for reserved words (e.g., "Status" → "`status`" → "status"). + colNameStr := strings.ReplaceAll(sqlparser.String(colName), "`", "") switch colNameStr { case workerStartTimeColName, workerHeartbeatTimeColName: diff --git a/service/matching/workers/worker_query_engine_helpers_test.go b/service/matching/workers/worker_query_engine_helpers_test.go index 93cb8a64654..f23367e2052 100644 --- a/service/matching/workers/worker_query_engine_helpers_test.go +++ b/service/matching/workers/worker_query_engine_helpers_test.go @@ -111,7 +111,7 @@ func TestGetWhereCause_EdgeCases(t *testing.T) { t.Run("Very long query", func(t *testing.T) { // Test with a very long but valid query var conditions []string - for i := 0; i < 100; i++ { + for i := range 100 { conditions = append(conditions, fmt.Sprintf("WorkerInstanceKey = 'worker%d'", i)) } query := fmt.Sprintf("SELECT * FROM table1 WHERE %s", strings.Join(conditions, " OR ")) diff --git a/service/matching/workers/worker_query_engine_test.go b/service/matching/workers/worker_query_engine_test.go index 4e25bb35d96..2c0ea7cffc7 100644 --- a/service/matching/workers/worker_query_engine_test.go +++ b/service/matching/workers/worker_query_engine_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" workerpb "go.temporal.io/api/worker/v1" @@ -140,6 +141,16 @@ func TestActivityInfoMatchEvaluator_SupportedFields(t *testing.T) { query: fmt.Sprintf("%s = 'deployment_name_unknown'", workerDeploymentNameColName), expectedMatch: false, }, + { + name: "BuildId, true", + query: fmt.Sprintf("%s = 'build_id'", workerBuildIDColName), + expectedMatch: true, + }, + { + name: "BuildId, false", + query: fmt.Sprintf("%s = 'build_id_unknown'", workerBuildIDColName), + expectedMatch: false, + }, { name: "SdkName, true", query: fmt.Sprintf("%s = 'sdk_name'", workerSdkNameColName), @@ -170,6 +181,16 @@ func TestActivityInfoMatchEvaluator_SupportedFields(t *testing.T) { query: fmt.Sprintf("%s = 'status_unknown'", workerStatusColName), expectedMatch: false, }, + { + name: "Status (alias), true", + query: "Status = 'Running'", + expectedMatch: true, + }, + { + name: "Status (alias), false", + query: "Status = 'status_unknown'", + expectedMatch: false, + }, } hb := &workerpb.WorkerHeartbeat{ @@ -328,3 +349,176 @@ func TestActivityInfoMatchEvaluator_SupportedTimeFields(t *testing.T) { }) } } + +func TestWorkerQueryEngine_IsNullString(t *testing.T) { + hbWithDeployment := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: "my-deployment", + }, + } + hbWithoutDeployment := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + } + + engine, err := newWorkerQueryEngine("nsID", fmt.Sprintf("%s IS NULL", workerDeploymentNameColName)) + require.NoError(t, err) + + match, err := engine.EvaluateWorker(hbWithoutDeployment) + require.NoError(t, err) + assert.True(t, match, "IS NULL should match when DeploymentName is empty") + + match, err = engine.EvaluateWorker(hbWithDeployment) + require.NoError(t, err) + assert.False(t, match, "IS NULL should not match when DeploymentName is set") +} + +func TestWorkerQueryEngine_IsNotNullString(t *testing.T) { + hbWithDeployment := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: "my-deployment", + }, + } + hbWithoutDeployment := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + } + + engine, err := newWorkerQueryEngine("nsID", fmt.Sprintf("%s IS NOT NULL", workerDeploymentNameColName)) + require.NoError(t, err) + + match, err := engine.EvaluateWorker(hbWithDeployment) + require.NoError(t, err) + assert.True(t, match, "IS NOT NULL should match when DeploymentName is set") + + match, err = engine.EvaluateWorker(hbWithoutDeployment) + require.NoError(t, err) + assert.False(t, match, "IS NOT NULL should not match when DeploymentName is empty") +} + +func TestWorkerQueryEngine_IsNullTime(t *testing.T) { + startTimeStr := "2023-10-26T14:30:00Z" + startTime, err := sqlquery.ConvertToTime(fmt.Sprintf("'%s'", startTimeStr)) + require.NoError(t, err) + + hbWithTime := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + StartTime: timestamppb.New(startTime), + HeartbeatTime: timestamppb.New(startTime), + } + hbWithoutTime := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + } + + for _, colName := range []string{workerStartTimeColName, workerHeartbeatTimeColName} { + engine, err := newWorkerQueryEngine("nsID", fmt.Sprintf("%s IS NULL", colName)) + require.NoError(t, err) + + t.Run(fmt.Sprintf("%s matches when not set", colName), func(t *testing.T) { + match, err := engine.EvaluateWorker(hbWithoutTime) + require.NoError(t, err) + assert.True(t, match) + }) + + t.Run(fmt.Sprintf("%s does not match when set", colName), func(t *testing.T) { + match, err := engine.EvaluateWorker(hbWithTime) + require.NoError(t, err) + assert.False(t, match) + }) + } +} + +func TestWorkerQueryEngine_IsNotNullTime(t *testing.T) { + startTimeStr := "2023-10-26T14:30:00Z" + startTime, err := sqlquery.ConvertToTime(fmt.Sprintf("'%s'", startTimeStr)) + require.NoError(t, err) + + hbWithTime := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + StartTime: timestamppb.New(startTime), + HeartbeatTime: timestamppb.New(startTime), + } + hbWithoutTime := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + } + + for _, colName := range []string{workerStartTimeColName, workerHeartbeatTimeColName} { + engine, err := newWorkerQueryEngine("nsID", fmt.Sprintf("%s IS NOT NULL", colName)) + require.NoError(t, err) + + t.Run(fmt.Sprintf("%s matches when set", colName), func(t *testing.T) { + match, err := engine.EvaluateWorker(hbWithTime) + require.NoError(t, err) + assert.True(t, match) + }) + + t.Run(fmt.Sprintf("%s does not match when not set", colName), func(t *testing.T) { + match, err := engine.EvaluateWorker(hbWithoutTime) + require.NoError(t, err) + assert.False(t, match) + }) + } +} + +func TestWorkerQueryEngine_IsNullComposite(t *testing.T) { + hb := &workerpb.WorkerHeartbeat{ + TaskQueue: "task_queue", + SdkName: "temporal-go", + } + + tests := []struct { + name string + query string + expectedMatch bool + }{ + { + name: "IS NOT NULL AND equality, both match", + query: fmt.Sprintf("%s IS NOT NULL AND %s = 'task_queue'", workerSdkNameColName, workerTaskQueueColName), + expectedMatch: true, + }, + { + name: "IS NULL OR equality, right matches", + query: fmt.Sprintf("%s IS NULL OR %s = 'task_queue'", workerSdkNameColName, workerTaskQueueColName), + expectedMatch: true, + }, + { + name: "IS NULL AND equality, left fails", + query: fmt.Sprintf("%s IS NULL AND %s = 'task_queue'", workerSdkNameColName, workerTaskQueueColName), + expectedMatch: false, + }, + { + name: "IS NULL with empty field in composite", + query: fmt.Sprintf("%s IS NULL AND %s IS NOT NULL", workerDeploymentNameColName, workerSdkNameColName), + expectedMatch: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine, err := newWorkerQueryEngine("nsID", tt.query) + require.NoError(t, err) + match, err := engine.EvaluateWorker(hb) + require.NoError(t, err) + assert.Equal(t, tt.expectedMatch, match) + }) + } +} + +func TestWorkerQueryEngine_IsNullRejectsUnknownColumn(t *testing.T) { + hb := &workerpb.WorkerHeartbeat{TaskQueue: "task_queue"} + engine, err := newWorkerQueryEngine("nsID", "UnknownField IS NULL") + require.NoError(t, err) + _, err = engine.EvaluateWorker(hb) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown or unsupported") +} + +// Only IS NULL and IS NOT NULL are supported; other IS operators (e.g., IS TRUE) should be rejected. +func TestWorkerQueryEngine_IsExprRejectsUnsupportedOperators(t *testing.T) { + hb := &workerpb.WorkerHeartbeat{TaskQueue: "task_queue"} + engine, err := newWorkerQueryEngine("nsID", fmt.Sprintf("%s IS TRUE", workerTaskQueueColName)) + require.NoError(t, err) + _, err = engine.EvaluateWorker(hb) + require.Error(t, err) + assert.Contains(t, err.Error(), "not supported") +} diff --git a/service/worker/batcher/activities.go b/service/worker/batcher/activities.go index 7cd1f5445e1..87fe0f2daf6 100644 --- a/service/worker/batcher/activities.go +++ b/service/worker/batcher/activities.go @@ -133,6 +133,10 @@ func fetchPage( Query: config.adjustedQuery, }) if err != nil { + var invalidArgErr *serviceerror.InvalidArgument + if errors.As(err, &invalidArgErr) { + return nil, temporal.NewNonRetryableApplicationError(err.Error(), "InvalidArgument", err) + } return nil, err } @@ -266,8 +270,19 @@ type activities struct { concurrency dynamicconfig.IntPropertyFnWithNamespaceFilter } -func (a *activities) checkNamespaceID(namespaceID string) error { - if namespaceID != a.namespaceID.String() { +// checkNamespace validates that batchParams targets the worker's own namespace. +// The NamespaceId, Request.Namespace (if set), and AdminRequest.Namespace (if set) +// must all agree with the worker's bound namespace. This prevents cross-namespace +// escalation via the privileged internal-frontend connection (NoopClaimMapper → RoleAdmin). +func (a *activities) checkNamespace(batchParams *batchspb.BatchOperationInput) error { + if batchParams.NamespaceId != a.namespaceID.String() { + return errNamespaceMismatch + } + ns := a.namespace.String() + if req := batchParams.GetRequest(); req != nil && req.GetNamespace() != ns { + return errNamespaceMismatch + } + if req := batchParams.GetAdminRequest(); req != nil && req.GetNamespace() != ns { return errNamespaceMismatch } return nil @@ -280,14 +295,15 @@ func (a *activities) BatchActivityWithProtobuf(ctx context.Context, batchParams hbd := HeartBeatDetails{} metricsHandler := a.MetricsHandler.WithTags(metrics.OperationTag(metrics.BatcherScope), metrics.NamespaceIDTag(batchParams.NamespaceId)) - if err := a.checkNamespaceID(batchParams.NamespaceId); err != nil { + if err := a.checkNamespace(batchParams); err != nil { metrics.BatcherOperationFailures.With(metricsHandler).Record(1) logger.Error("Failed to run batch operation due to namespace mismatch", tag.Error(err)) return hbd, err } + ns := a.namespace.String() sdkClient := a.ClientFactory.NewClient(sdkclient.Options{ - Namespace: a.namespace.String(), + Namespace: ns, DataConverter: sdk.PreferProtoDataConverter, }) startOver := true @@ -299,19 +315,16 @@ func (a *activities) BatchActivityWithProtobuf(ctx context.Context, batchParams } } - // Get namespace and query based on request type (public vs admin) - var ns string + // Get executions based on request type (public vs admin). var visibilityQuery string var executions []*commonpb.WorkflowExecution if batchParams.AdminRequest != nil { ctx = headers.SetCallerType(ctx, headers.CallerTypePreemptable) adminReq := batchParams.AdminRequest - ns = adminReq.Namespace visibilityQuery = adminReq.GetVisibilityQuery() executions = adminReq.GetExecutions() } else { - ns = batchParams.Request.Namespace visibilityQuery = a.adjustQueryBatchTypeEnum(batchParams.Request.VisibilityQuery, batchParams.BatchType) executions = batchParams.Request.Executions } @@ -325,6 +338,10 @@ func (a *activities) BatchActivityWithProtobuf(ctx context.Context, batchParams if err != nil { metrics.BatcherOperationFailures.With(metricsHandler).Record(1) logger.Error("Failed to get estimate workflow count", tag.Error(err)) + var invalidArgErr *serviceerror.InvalidArgument + if errors.As(err, &invalidArgErr) { + return HeartBeatDetails{}, temporal.NewNonRetryableApplicationError(err.Error(), "InvalidArgument", err) + } return HeartBeatDetails{}, err } estimateCount = resp.GetCount() @@ -482,7 +499,7 @@ func (a *activities) startTaskProcessor( } else { // Old fields //nolint:staticcheck // SA1019: worker versioning v0.31 - eventId, err = getResetEventIDByType(ctx, operation.ResetOperation.ResetType, batchOperation.Request.Namespace, executionInfo.Execution, frontendClient, logger) + eventId, err = getResetEventIDByType(ctx, operation.ResetOperation.ResetType, namespace, executionInfo.Execution, frontendClient, logger) //nolint:staticcheck // SA1019: worker versioning v0.31 resetReapplyType = operation.ResetOperation.ResetReapplyType } diff --git a/service/worker/batcher/activities_namespace_test.go b/service/worker/batcher/activities_namespace_test.go new file mode 100644 index 00000000000..833d9bc6538 --- /dev/null +++ b/service/worker/batcher/activities_namespace_test.go @@ -0,0 +1,156 @@ +package batcher + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + batchpb "go.temporal.io/api/batch/v1" + commonpb "go.temporal.io/api/common/v1" + workflowpb "go.temporal.io/api/workflow/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/testsuite" + "go.temporal.io/server/api/adminservice/v1" + batchspb "go.temporal.io/server/api/batch/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/testing/mockapi/workflowservicemock/v1" + "go.uber.org/mock/gomock" + "golang.org/x/time/rate" +) + +// These tests guard against cross-namespace escalation via the batcher activity. +// The per-NS worker's frontendClient dials internal-frontend (NoopClaimMapper → +// RoleAdmin), so the activity MUST NOT forward a user-controlled namespace string +// to frontendClient calls. The namespace for all downstream operations must be +// the worker's bound namespace. + +const ( + boundNSName = "bound-ns" + boundNSID = "bound-ns-id" + otherNSName = "other-ns" +) + +func newBoundActivities(frontend workflowservice.WorkflowServiceClient) *activities { + return &activities{ + activityDeps: activityDeps{ + MetricsHandler: metrics.NoopMetricsHandler, + Logger: log.NewTestLogger(), + FrontendClient: frontend, + }, + namespace: namespace.Name(boundNSName), + namespaceID: namespace.ID(boundNSID), + rps: dynamicconfig.GetIntPropertyFnFilteredByNamespace(50), + concurrency: dynamicconfig.GetIntPropertyFnFilteredByNamespace(1), + } +} + +// TestBatchActivityWithProtobuf_RejectsMismatchedRequestNamespace verifies that +// BatchActivityWithProtobuf rejects a request whose Request.Namespace differs +// from the worker's bound namespace, even when NamespaceId is valid. +// This blocks the cross-namespace attack where an attacker submits a valid +// NamespaceId for their own namespace but sets Request.Namespace to a victim's. +func TestBatchActivityWithProtobuf_RejectsMismatchedRequestNamespace(t *testing.T) { + ts := testsuite.WorkflowTestSuite{} + env := ts.NewTestActivityEnvironment() + a := newBoundActivities(nil) + env.RegisterActivity(a.BatchActivityWithProtobuf) + + input := &batchspb.BatchOperationInput{ + NamespaceId: boundNSID, // ID check passes; name check must catch the mismatch + Request: &workflowservice.StartBatchOperationRequest{ + Namespace: otherNSName, // mismatched — must be rejected + Operation: &workflowservice.StartBatchOperationRequest_SignalOperation{ + SignalOperation: &batchpb.BatchOperationSignal{Signal: "s"}, + }, + Executions: []*commonpb.WorkflowExecution{{WorkflowId: "w"}}, + }, + } + + _, err := env.ExecuteActivity(a.BatchActivityWithProtobuf, input) + require.Error(t, err) + require.ErrorContains(t, err, errNamespaceMismatch.Error()) +} + +// TestBatchActivityWithProtobuf_RejectsMismatchedAdminRequestNamespace verifies +// that the same namespace mismatch check applies to admin batch requests. +func TestBatchActivityWithProtobuf_RejectsMismatchedAdminRequestNamespace(t *testing.T) { + ts := testsuite.WorkflowTestSuite{} + env := ts.NewTestActivityEnvironment() + a := newBoundActivities(nil) + env.RegisterActivity(a.BatchActivityWithProtobuf) + + input := &batchspb.BatchOperationInput{ + NamespaceId: boundNSID, + AdminRequest: &adminservice.StartAdminBatchOperationRequest{ + Namespace: otherNSName, // mismatched — must be rejected + Executions: []*commonpb.WorkflowExecution{{WorkflowId: "w"}}, + }, + } + + _, err := env.ExecuteActivity(a.BatchActivityWithProtobuf, input) + require.Error(t, err) + require.ErrorContains(t, err, errNamespaceMismatch.Error()) +} + +// TestStartTaskProcessor_UsesWorkerBoundNamespaceForSignal verifies that when +// BatchActivityWithProtobuf dispatches via startTaskProcessor, the namespace +// delivered to frontendClient.SignalWorkflowExecution is the worker's bound +// namespace. This is a belt-and-suspenders check: even if the early validation +// above is ever relaxed, the namespace used in operations stays worker-bound. +func TestStartTaskProcessor_UsesWorkerBoundNamespaceForSignal(t *testing.T) { + r := require.New(t) + ctrl := gomock.NewController(t) + mockFE := workflowservicemock.NewMockWorkflowServiceClient(ctrl) + + var captured *workflowservice.SignalWorkflowExecutionRequest + mockFE.EXPECT(). + SignalWorkflowExecution(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, req *workflowservice.SignalWorkflowExecutionRequest, _ ...any) (*workflowservice.SignalWorkflowExecutionResponse, error) { + captured = req + return &workflowservice.SignalWorkflowExecutionResponse{}, nil + }) + + a := newBoundActivities(mockFE) + + // Simulate the namespace that BatchActivityWithProtobuf derives — + // with the fix this is always a.namespace.String(). + ns := a.namespace.String() + batchOp := &batchspb.BatchOperationInput{ + NamespaceId: boundNSID, + Request: &workflowservice.StartBatchOperationRequest{ + Namespace: ns, + Operation: &workflowservice.StartBatchOperationRequest_SignalOperation{ + SignalOperation: &batchpb.BatchOperationSignal{Signal: "s"}, + }, + }, + } + + taskCh := make(chan task, 1) + respCh := make(chan taskResponse, 1) + taskCh <- task{ + executionInfo: &workflowpb.WorkflowExecutionInfo{ + Execution: &commonpb.WorkflowExecution{WorkflowId: "w"}, + }, + page: &page{}, + } + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + defer close(done) + a.startTaskProcessor(ctx, batchOp, ns, taskCh, respCh, + rate.NewLimiter(rate.Inf, 1), nil, mockFE, + metrics.NoopMetricsHandler, log.NewTestLogger()) + }() + + <-respCh + cancel() + <-done + + r.NotNil(captured) + r.Equal(boundNSName, captured.Namespace, + "frontendClient.SignalWorkflowExecution must receive the worker's bound namespace") +} diff --git a/service/worker/batcher/activities_test.go b/service/worker/batcher/activities_test.go index 1e3e8081101..65ea595503f 100644 --- a/service/worker/batcher/activities_test.go +++ b/service/worker/batcher/activities_test.go @@ -10,6 +10,7 @@ import ( "unicode" "github.com/stretchr/testify/suite" + batchpb "go.temporal.io/api/batch/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" historypb "go.temporal.io/api/history/v1" @@ -22,6 +23,7 @@ import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/api/historyservicemock/v1" "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/testing/mockapi/workflowservicemock/v1" "go.uber.org/mock/gomock" @@ -453,7 +455,7 @@ func (s *activitiesSuite) TestProcessAdminTask_RefreshWorkflowTasks() { // Expect RefreshWorkflowTasks to be called with correct parameters mockHistoryClient.EXPECT().RefreshWorkflowTasks(gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, req *historyservice.RefreshWorkflowTasksRequest, _ ...interface{}) (*historyservice.RefreshWorkflowTasksResponse, error) { + func(_ context.Context, req *historyservice.RefreshWorkflowTasksRequest, _ ...any) (*historyservice.RefreshWorkflowTasksResponse, error) { s.Equal(namespaceID, req.NamespaceId) s.NotZero(req.ArchetypeId) // WorkflowArchetypeID is computed dynamically s.Equal(workflowID, req.Request.Execution.WorkflowId) @@ -565,6 +567,74 @@ func (s *activitiesSuite) TestIsNonRetryableError() { } } +// TestStartTaskProcessor_SignalUsesWorkerNamespace verifies that startTaskProcessor uses +// the worker's authoritative namespace (passed as the namespace argument) for operations, +// not the user-controlled namespace from batchOperation.Request.Namespace. +// This guards against a regression introduced in PR #8144 where batchParams.Request.Namespace +// (user-controlled) was used instead of a.namespace.String() (server-trusted). +func (s *activitiesSuite) TestStartTaskProcessor_SignalUsesWorkerNamespace() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + a := &activities{ + activityDeps: activityDeps{ + FrontendClient: s.mockFrontendClient, + Logger: log.NewTestLogger(), + MetricsHandler: metrics.NoopMetricsHandler, + }, + } + + workerNamespace := "trusted-namespace" + requestNamespace := "untrusted-namespace" // intentionally different + + batchOperation := &batchspb.BatchOperationInput{ + NamespaceId: "some-namespace-id", + Request: &workflowservice.StartBatchOperationRequest{ + Namespace: requestNamespace, + Operation: &workflowservice.StartBatchOperationRequest_SignalOperation{ + SignalOperation: &batchpb.BatchOperationSignal{ + Signal: "test-signal", + }, + }, + }, + } + + testPage := &page{ + executionInfos: []*workflowpb.WorkflowExecutionInfo{ + { + Execution: &commonpb.WorkflowExecution{ + WorkflowId: "test-workflow-id", + RunId: "test-run-id", + }, + }, + }, + } + testTask := task{ + executionInfo: testPage.executionInfos[0], + attempts: 1, + page: testPage, + } + + taskCh := make(chan task, 1) + respCh := make(chan taskResponse, 1) + limiter := rate.NewLimiter(rate.Limit(100), 1) + + // The signal must be executed with the worker's trusted namespace, not the user-supplied one. + s.mockFrontendClient.EXPECT(). + SignalWorkflowExecution(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, req *workflowservice.SignalWorkflowExecutionRequest, _ ...any) (*workflowservice.SignalWorkflowExecutionResponse, error) { + s.Equal(workerNamespace, req.Namespace, "must use worker namespace, not request namespace") + return &workflowservice.SignalWorkflowExecutionResponse{}, nil + }) + + taskCh <- testTask + + go a.startTaskProcessor(ctx, batchOperation, workerNamespace, taskCh, respCh, limiter, nil, s.mockFrontendClient, metrics.NoopMetricsHandler, log.NewTestLogger()) + + resp := <-respCh + s.NoError(resp.err) +} + func (s *activitiesSuite) TestProcessAdminTask_UnknownOperation() { ctx := context.Background() diff --git a/service/worker/batcher/workflow.go b/service/worker/batcher/workflow.go index 2a2c335e4e5..f573c947c94 100644 --- a/service/worker/batcher/workflow.go +++ b/service/worker/batcher/workflow.go @@ -142,7 +142,7 @@ type BatchOperationStats struct { // 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{}{ + memo := map[string]any{ BatchOperationStatsMemo: BatchOperationStats{ NumSuccess: result.SuccessCount, NumFailure: result.ErrorCount, diff --git a/service/worker/batcher/workflow_test.go b/service/worker/batcher/workflow_test.go index 6667894831b..1104d28224b 100644 --- a/service/worker/batcher/workflow_test.go +++ b/service/worker/batcher/workflow_test.go @@ -44,9 +44,9 @@ func (s *batcherSuite) TestBatchWorkflow_ValidParams_Query_Protobuf() { ErrorCount: 27, }, nil) s.env.OnUpsertMemo(mock.Anything).Run(func(args mock.Arguments) { - memo, ok := args.Get(0).(map[string]interface{}) + memo, ok := args.Get(0).(map[string]any) s.Require().True(ok) - s.Equal(map[string]interface{}{ + s.Equal(map[string]any{ "batch_operation_stats": BatchOperationStats{ NumSuccess: 42, NumFailure: 27, @@ -76,9 +76,9 @@ func (s *batcherSuite) TestBatchWorkflow_ValidParams_Executions_Protobuf() { ErrorCount: 27, }, nil) s.env.OnUpsertMemo(mock.Anything).Run(func(args mock.Arguments) { - memo, ok := args.Get(0).(map[string]interface{}) + memo, ok := args.Get(0).(map[string]any) s.Require().True(ok) - s.Equal(map[string]interface{}{ + s.Equal(map[string]any{ "batch_operation_stats": BatchOperationStats{ NumSuccess: 42, NumFailure: 27, diff --git a/service/worker/dummy/workflow.go b/service/worker/dummy/workflow.go index 5b0512a8c98..7fd5512e182 100644 --- a/service/worker/dummy/workflow.go +++ b/service/worker/dummy/workflow.go @@ -10,8 +10,8 @@ const ( DummyWFTypeName = "temporal-sys-dummy-workflow" // dummyDuration only needs to account for the duration in which the key needs - // to stay reserved. - dummyDuration = 1 * time.Hour + // to stay reserved. Matches sentinelIdleTime in the CHASM scheduler package. + dummyDuration = 15 * time.Minute ) // DummyWorkflow is a sentinel workflow that hangs open for a fixed duration. diff --git a/service/worker/fx.go b/service/worker/fx.go index eaa1769913d..1231e11918d 100644 --- a/service/worker/fx.go +++ b/service/worker/fx.go @@ -4,8 +4,10 @@ import ( "context" "os" + wcicomponent "go.temporal.io/auto-scaled-workers/wci/workercomponent" "go.temporal.io/server/api/adminservice/v1" "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/client" "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" @@ -24,6 +26,7 @@ import ( "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/resolver" "go.temporal.io/server/common/resource" + "go.temporal.io/server/common/sdk" "go.temporal.io/server/common/searchattribute" "go.temporal.io/server/service" "go.temporal.io/server/service/worker/batcher" @@ -45,8 +48,10 @@ var Module = fx.Options( scheduler.Module, batcher.Module, workerdeployment.Module, + wcicomponent.Module, dlq.Module, dummy.Module, + fx.Provide(schedulerpb.NewSchedulerServiceLayeredClient), fx.Provide( func(c resource.HistoryClient) dlq.HistoryClient { return c @@ -79,18 +84,21 @@ var Module = fx.Options( fx.Provide(func( clusterMetadata cluster.Metadata, metadataManager persistence.MetadataManager, + dataMerger nsreplication.NamespaceDataMerger, logger log.Logger, ) nsreplication.TaskExecutor { return nsreplication.NewTaskExecutor( clusterMetadata.GetCurrentClusterName(), metadataManager, + dataMerger, logger, ) }), + fx.Provide(nsreplication.NewNoopDataMerger), fx.Provide(ServerProvider), fx.Provide(NewService), fx.Provide(fx.Annotate(NewWorkerManager, fx.ParamTags(workercommon.WorkerComponentTag))), - fx.Provide(NewPerNamespaceWorkerManager), + fx.Provide(PerNamespaceWorkerManagerProvider), fx.Invoke(ServiceLifetimeHooks), ) @@ -180,6 +188,30 @@ func ServiceLifetimeHooks(lc fx.Lifecycle, svc *Service) { lc.Append(fx.StartStopHook(svc.Start, svc.Stop)) } +type perNamespaceWorkerManagerInitParams struct { + fx.In + Logger log.Logger + SdkClientFactory sdk.ClientFactory + NamespaceRegistry namespace.Registry + HostName resource.HostName + Config *Config + ClusterMetadata cluster.Metadata + Components []workercommon.PerNSWorkerComponent `group:"perNamespaceWorkerComponent"` +} + +func PerNamespaceWorkerManagerProvider(params perNamespaceWorkerManagerInitParams) *PerNamespaceWorkerManager { + return NewPerNamespaceWorkerManager( + params.Logger, + params.SdkClientFactory, + params.NamespaceRegistry, + params.HostName, + params.Config, + params.ClusterMetadata, + params.Components, + primitives.PerNSWorkerTaskQueue, + ) +} + func ServerProvider(rpcFactory common.RPCFactory, logger log.Logger) *grpc.Server { opts, err := rpcFactory.GetInternodeGRPCServerOptions() if err != nil { diff --git a/service/worker/migration/activities.go b/service/worker/migration/activities.go index 62fff92dabe..862fe31bedf 100644 --- a/service/worker/migration/activities.go +++ b/service/worker/migration/activities.go @@ -7,7 +7,6 @@ import ( "sort" "time" - "github.com/pkg/errors" commonpb "go.temporal.io/api/common/v1" replicationpb "go.temporal.io/api/replication/v1" "go.temporal.io/api/serviceerror" @@ -118,6 +117,13 @@ type ( chasmRegistry *chasm.Registry } + shardStatus struct { + shardID int32 + laggingTasks int64 + timeLag time.Duration + isReady bool + } + WorkflowVerifier func( ctx context.Context, request *verifyReplicationTasksRequest, @@ -199,64 +205,80 @@ func (a *activities) WaitReplication(ctx context.Context, waitRequest waitReplic // Check if remote cluster has caught up on all shards on replication tasks func (a *activities) checkReplicationOnce(ctx context.Context, waitRequest waitReplicationRequest) (bool, error) { - resp, err := a.historyClient.GetReplicationStatus(ctx, &historyservice.GetReplicationStatusRequest{ - RemoteClusters: []string{waitRequest.RemoteCluster}, + RemoteClusters: []string{waitRequest.RemoteCluster}, // only the specified remote cluster }) if err != nil { return false, err } - if int(waitRequest.ShardCount) != len(resp.Shards) { + + localShards := resp.Shards + + if int(waitRequest.ShardCount) != len(localShards) { return false, fmt.Errorf("GetReplicationStatus returns %d shards, expecting %d", len(resp.Shards), waitRequest.ShardCount) } - // check that every shard has caught up - readyShardCount := 0 - logged := false - - sort.SliceStable(resp.Shards, func(i, j int) bool { - return resp.Shards[i].ShardId < resp.Shards[j].ShardId + sort.SliceStable(localShards, func(i, j int) bool { + return localShards[i].ShardId < localShards[j].ShardId }) - for _, shard := range resp.Shards { - clusterInfo, hasClusterInfo := shard.RemoteClusters[waitRequest.RemoteCluster] - if hasClusterInfo { - // WE are all caught up - if shard.MaxReplicationTaskId == clusterInfo.AckedTaskId { - readyShardCount++ - continue - } + // this is the minimum task ID each shard must reach before catchup is considered complete + requiredMinTaskIDPerShard := waitRequest.WaitForTaskIds - // Caught up to the last checked IDs, and within allowed lagging range - if clusterInfo.AckedTaskId >= waitRequest.WaitForTaskIds[shard.ShardId] && - (shard.MaxReplicationTaskId-clusterInfo.AckedTaskId <= waitRequest.AllowedLaggingTasks || - shard.MaxReplicationTaskVisibilityTime.AsTime().Sub(clusterInfo.AckedTaskVisibilityTime.AsTime()) <= waitRequest.AllowedLagging) { - readyShardCount++ - continue - } + shardStatuses := make([]shardStatus, 0, len(localShards)) + + for _, localShard := range localShards { + remoteShardProgress, hasRemoteShardProgress := localShard.RemoteClusters[waitRequest.RemoteCluster] + if !hasRemoteShardProgress { + a.logger.Info("GetReplicationStatus response missing expected remote cluster for shard during replication catchup", tag.ShardID(localShard.ShardId), tag.ClusterName(waitRequest.RemoteCluster)) + + // this is not expected, so fail activity to surface the error, but retryPolicy will keep retrying. + return false, fmt.Errorf("GetReplicationStatus response for shard %d does not contains remote cluster %s", localShard.ShardId, waitRequest.RemoteCluster) } - // shard is not ready, log first non-ready shard - if !logged { - logged = true - if !hasClusterInfo { - a.logger.Info("Wait catchup missing remote cluster info", tag.ShardID(shard.ShardId), tag.ClusterName(waitRequest.RemoteCluster)) - // this is not expected, so fail activity to surface the error, but retryPolicy will keep retrying. - return false, fmt.Errorf("GetReplicationStatus response for shard %d does not contains remote cluster %s", shard.ShardId, waitRequest.RemoteCluster) - } - a.logger.Info("Wait catchup not ready", - tag.Int32("ShardId", shard.ShardId), - tag.String("RemoteCluster", waitRequest.RemoteCluster), - tag.Int64("AckedTaskId", clusterInfo.AckedTaskId), - tag.Int64("WaitForTaskId", waitRequest.WaitForTaskIds[shard.ShardId]), - tag.Duration("AllowedLagging", waitRequest.AllowedLagging), - tag.Duration("ActualLagging", shard.MaxReplicationTaskVisibilityTime.AsTime().Sub(clusterInfo.AckedTaskVisibilityTime.AsTime())), - tag.Int64("MaxReplicationTaskId", shard.MaxReplicationTaskId), - tag.Time("MaxReplicationTaskVisibilityTime", shard.MaxReplicationTaskVisibilityTime.AsTime()), - tag.Time("AckedTaskVisibilityTime", clusterInfo.AckedTaskVisibilityTime.AsTime()), - tag.Int64("AllowedLaggingTasks", waitRequest.AllowedLaggingTasks), - tag.Int64("ActualLaggingTasks", shard.MaxReplicationTaskId-clusterInfo.AckedTaskId), - ) + laggingTasks := localShard.MaxReplicationTaskId - remoteShardProgress.AckedTaskId + timeLag := localShard.MaxReplicationTaskVisibilityTime.AsTime().Sub(remoteShardProgress.AckedTaskVisibilityTime.AsTime()) + + fullyCaughtUp := localShard.MaxReplicationTaskId == remoteShardProgress.AckedTaskId + passedRequiredMinimum := remoteShardProgress.AckedTaskId >= requiredMinTaskIDPerShard[localShard.ShardId] + withinLagTolerance := laggingTasks <= waitRequest.AllowedLaggingTasks || timeLag <= waitRequest.AllowedLagging + + status := shardStatus{ + shardID: localShard.GetShardId(), + laggingTasks: laggingTasks, + timeLag: timeLag, + isReady: fullyCaughtUp || (passedRequiredMinimum && withinLagTolerance), + } + + shardStatuses = append(shardStatuses, status) + } + + var ( + readyShardCount int + notReadyShardCount int + + maxLaggingTasksShardID int32 + maxLaggingTasks int64 + + maxTimeLagShardID int32 + maxTimeLag time.Duration + ) + + for _, status := range shardStatuses { + if status.isReady { + readyShardCount++ + } else { + notReadyShardCount++ + } + + if status.laggingTasks > maxLaggingTasks { + maxLaggingTasks = status.laggingTasks + maxLaggingTasksShardID = status.shardID + } + + if status.timeLag > maxTimeLag { + maxTimeLag = status.timeLag + maxTimeLagShardID = status.shardID } } @@ -266,7 +288,24 @@ func (a *activities) checkReplicationOnce(ctx context.Context, waitRequest waitR metrics.OperationTag(metrics.MigrationWorkflowScope), metrics.TargetClusterTag(waitRequest.RemoteCluster)) - return readyShardCount == len(resp.Shards), nil + isReady := notReadyShardCount == 0 + + if !isReady { + a.logger.Info("Wait catchup not ready", + tag.String("RemoteCluster", waitRequest.RemoteCluster), + tag.Int("TotalShards", len(localShards)), + tag.Int("ReadyShards", readyShardCount), + tag.Int("NotReadyShards", len(localShards)-readyShardCount), + tag.Duration("AllowedLagging", waitRequest.AllowedLagging), + tag.Int64("AllowedLaggingTasks", waitRequest.AllowedLaggingTasks), + tag.Int32("MaxLaggingTasksShardID", maxLaggingTasksShardID), + tag.Int64("MaxLaggingTasks", maxLaggingTasks), + tag.Int32("MaxTimeLagShardID", maxTimeLagShardID), + tag.Duration("MaxTimeLag", maxTimeLag), + ) + } + + return isReady, nil } func (a *activities) WaitHandover(ctx context.Context, waitRequest waitHandoverRequest) error { @@ -290,51 +329,74 @@ func (a *activities) WaitHandover(ctx context.Context, waitRequest waitHandoverR // Check if remote cluster has caught up on all shards on replication tasks func (a *activities) checkHandoverOnce(ctx context.Context, waitRequest waitHandoverRequest) (bool, error) { - resp, err := a.historyClient.GetReplicationStatus(ctx, &historyservice.GetReplicationStatusRequest{ - RemoteClusters: []string{waitRequest.RemoteCluster}, + RemoteClusters: []string{waitRequest.RemoteCluster}, // only the specified remote cluster }) if err != nil { return false, err } - if int(waitRequest.ShardCount) != len(resp.Shards) { - return false, fmt.Errorf("GetReplicationStatus returns %d shards, expecting %d", len(resp.Shards), waitRequest.ShardCount) + + localShards := resp.Shards + + if int(waitRequest.ShardCount) != len(localShards) { + return false, fmt.Errorf("GetReplicationStatus returns %d shards, expecting %d", len(localShards), waitRequest.ShardCount) } - readyShardCount := 0 - logged := false + shardStatuses := make([]shardStatus, 0, len(localShards)) + var handoverInfosMissingCount int + // check that every shard is ready to handover - for _, shard := range resp.Shards { - clusterInfo, hasClusterInfo := shard.RemoteClusters[waitRequest.RemoteCluster] - handoverInfo, hasHandoverInfo := shard.HandoverNamespaces[waitRequest.Namespace] - if hasClusterInfo && hasHandoverInfo { - if clusterInfo.AckedTaskId >= handoverInfo.HandoverReplicationTaskId { - readyShardCount++ - continue - } + for _, localShard := range localShards { + remoteShardProgress, hasRemoteShardProgress := localShard.RemoteClusters[waitRequest.RemoteCluster] + if !hasRemoteShardProgress { + a.logger.Info("GetReplicationStatus response missing expected remote cluster for shard during handover", tag.ShardID(localShard.ShardId), tag.ClusterName(waitRequest.RemoteCluster)) + + // this is not expected, so fail activity to surface the error, but retryPolicy will keep retrying. + return false, fmt.Errorf("GetReplicationStatus response for shard %d does not contains remote cluster %s", localShard.ShardId, waitRequest.RemoteCluster) } - // shard is not ready, log first non-ready shard - if !logged { - logged = true - if !hasClusterInfo { - a.logger.Info("Wait handover missing remote cluster info", tag.ShardID(shard.ShardId), tag.ClusterName(waitRequest.RemoteCluster)) - // this is not expected, so fail activity to surface the error, but retryPolicy will keep retrying. - return false, fmt.Errorf("GetReplicationStatus response for shard %d does not contains remote cluster %s", shard.ShardId, waitRequest.RemoteCluster) - } - if !hasHandoverInfo { - // this could happen before namespace cache refresh - a.logger.Info("Wait handover missing handover namespace info", tag.ShardID(shard.ShardId), tag.ClusterName(waitRequest.RemoteCluster), tag.WorkflowNamespace(waitRequest.Namespace)) - } else { - a.logger.Info("Wait handover not ready", - tag.Int32("ShardId", shard.ShardId), - tag.Int64("AckedTaskId", clusterInfo.AckedTaskId), - tag.Int64("HandoverTaskId", handoverInfo.HandoverReplicationTaskId), - tag.String("Namespace", waitRequest.Namespace), - tag.String("RemoteCluster", waitRequest.RemoteCluster), - tag.Int64("MaxReplicationTaskId", shard.MaxReplicationTaskId), - ) - } + handoverInfo, hasHandoverInfo := localShard.HandoverNamespaces[waitRequest.Namespace] + if !hasHandoverInfo { + // this could happen before namespace cache refresh + a.logger.Info("Wait handover missing handover namespace info", tag.ShardID(localShard.ShardId), tag.ClusterName(waitRequest.RemoteCluster), tag.WorkflowNamespace(waitRequest.Namespace)) + + handoverInfosMissingCount++ + + shardStatuses = append(shardStatuses, shardStatus{ + shardID: localShard.GetShardId(), + isReady: false, + }) + + continue + } + + laggingTasks := handoverInfo.HandoverReplicationTaskId - remoteShardProgress.AckedTaskId + + shardStatuses = append(shardStatuses, shardStatus{ + shardID: localShard.GetShardId(), + laggingTasks: laggingTasks, + isReady: laggingTasks <= 0, + }) + } + + var ( + readyShardCount int + notReadyShardCount int + + maxHandoverLagShardID int32 + maxHandoverLag int64 + ) + + for _, status := range shardStatuses { + if status.isReady { + readyShardCount++ + } else { + notReadyShardCount++ + } + + if status.laggingTasks > maxHandoverLag { + maxHandoverLag = status.laggingTasks + maxHandoverLagShardID = status.shardID } } @@ -344,12 +406,23 @@ func (a *activities) checkHandoverOnce(ctx context.Context, waitRequest waitHand metrics.OperationTag(metrics.MigrationWorkflowScope), metrics.TargetClusterTag(waitRequest.RemoteCluster), metrics.NamespaceTag(waitRequest.Namespace)) - a.logger.Info("Wait handover ready shard count.", - tag.Int("ReadyShards", readyShardCount), - tag.String("Namespace", waitRequest.Namespace), - tag.String("RemoteCluster", waitRequest.RemoteCluster)) - return readyShardCount == len(resp.Shards), nil + isReady := notReadyShardCount == 0 + + if !isReady { + a.logger.Info("Wait handover not ready", + tag.String("RemoteCluster", waitRequest.RemoteCluster), + tag.String("Namespace", waitRequest.Namespace), + tag.Int("TotalShards", len(localShards)), + tag.Int("ReadyShards", readyShardCount), + tag.Int("NotReadyShards", notReadyShardCount), + tag.Int("HandoverInfosMissing", handoverInfosMissingCount), + tag.Int32("MaxHandoverLagShardID", maxHandoverLagShardID), + tag.Int64("MaxHandoverLag", maxHandoverLag), + ) + } + + return isReady, nil } func (a *activities) generateWorkflowReplicationTask( @@ -383,6 +456,7 @@ func (a *activities) generateWorkflowReplicationTask( RunId: execution.RunID, }, Archetype: archetype, + ArchetypeId: execution.ArchetypeID, TargetClusters: targetClusters, }) if err != nil { @@ -712,7 +786,6 @@ func (a *activities) checkSkipWorkflowExecution( ArchetypeId: execution.ArchetypeID, SkipForceReload: true, }) - if err != nil { if common.IsNotFoundError(err) { // The outstanding workflow execution may be deleted (due to retention) on source cluster after replication tasks were generated. @@ -781,6 +854,7 @@ func (a *activities) verifySingleReplicationTask( RunId: execution.RunID, }, Archetype: archetype, + ArchetypeId: execution.ArchetypeID, SkipForceReload: true, }) a.forceReplicationMetricsHandler.Timer(metrics.VerifyDescribeMutableStateLatency.Name()).Record(time.Since(s)) @@ -810,7 +884,7 @@ func (a *activities) verifySingleReplicationTask( return verifyResult{ status: notVerified, - }, errors.WithMessage(err, "failed to describe workflow from the remote cluster") + }, fmt.Errorf("failed to describe workflow from the remote cluster: %w", err) } } @@ -972,7 +1046,7 @@ func (a *activities) getTargetClusterReplicationStatus(ctx context.Context, wait targetAckIDOnShard := make(map[int32]int64) resp, err := a.historyClient.GetReplicationStatus(ctx, &historyservice.GetReplicationStatusRequest{ - RemoteClusters: []string{waitRequest.TargetCluster}, + RemoteClusters: []string{waitRequest.TargetCluster}, // only the specified remote cluster }) if err != nil { return targetAckIDOnShard, err @@ -990,69 +1064,83 @@ func (a *activities) getTargetClusterReplicationStatus(ctx context.Context, wait } // Check if remote cluster has caught up on all shards on replication tasks from target replica. -func (a *activities) checkReplicationOnRemoteCluster(ctx context.Context, waitRequest waitCatchupRequest, targetAckIDOnShard map[int32]int64) (bool, error) { - +func (a *activities) checkReplicationOnRemoteCluster(ctx context.Context, waitRequest waitCatchupRequest, requiredMinTaskIDPerShard map[int32]int64) (bool, error) { resp, err := a.historyClient.GetReplicationStatus(ctx, &historyservice.GetReplicationStatusRequest{ - RemoteClusters: []string{waitRequest.CatchupCluster}, + RemoteClusters: []string{waitRequest.CatchupCluster}, // only the specified remote cluster }) if err != nil { return false, err } - expectedShardCount := len(targetAckIDOnShard) + localShards := resp.Shards + + shardStatuses := make([]shardStatus, 0, len(localShards)) - readyShardCount := 0 - logged := false // check that on every shard, all source clusters have caught up with target cluster - for _, shard := range resp.Shards { - clusterInfo, hasClusterInfo := shard.RemoteClusters[waitRequest.CatchupCluster] - if hasClusterInfo { - value, exists := targetAckIDOnShard[shard.ShardId] - // If the target acked task ID is not found, the shard is considered ready, as the remote ack level - // is assumed to be more up-to-date than the active ack level. - if !exists { - readyShardCount++ - continue - } - // WE are all caught up - if clusterInfo.AckedTaskId >= shard.MaxReplicationTaskId { - readyShardCount++ - continue - } - if clusterInfo.AckedTaskId >= value { - readyShardCount++ - continue - } + for _, localShard := range localShards { + remoteShardProgress, hasRemoteShardProgress := localShard.RemoteClusters[waitRequest.CatchupCluster] + if !hasRemoteShardProgress { + a.logger.Info("GetReplicationStatus response missing expected remote cluster for shard during remote cluster replication catchup", tag.ShardID(localShard.ShardId), tag.ClusterName(waitRequest.CatchupCluster)) + // this is not expected, so fail activity to surface the error, but retryPolicy will keep retrying. + return false, temporal.NewNonRetryableApplicationError(fmt.Sprintf("GetReplicationStatus response for shard %d does not contains remote cluster %s", localShard.ShardId, waitRequest.CatchupCluster), "", nil) } - // shard is not ready, log first non-ready shard - if !logged { - logged = true - if !hasClusterInfo { - a.logger.Info("Wait catchup missing remote cluster info", tag.ShardID(shard.ShardId), tag.ClusterName(waitRequest.CatchupCluster)) - // this is not expected, so fail activity to surface the error, but retryPolicy will keep retrying. - return false, temporal.NewNonRetryableApplicationError(fmt.Sprintf("GetReplicationStatus response for shard %d does not contains remote cluster %s", shard.ShardId, waitRequest.CatchupCluster), "", nil) - } + laggingTasks := localShard.MaxReplicationTaskId - remoteShardProgress.AckedTaskId + + requiredMinTaskID, exists := requiredMinTaskIDPerShard[localShard.ShardId] + + // If the target acked task ID is NOT found, the shard is considered ready, as the remote ack level + // is assumed to be more up-to-date than the active ack level. + noTargetAckID := !exists + fullyCaughtUp := laggingTasks <= 0 + reachedTarget := remoteShardProgress.AckedTaskId >= requiredMinTaskID - a.logger.Info("Wait catchup not ready", - tag.Int32("ShardId", shard.ShardId), - tag.Int64("AckedTaskId", clusterInfo.AckedTaskId), - tag.String("Namespace", waitRequest.Namespace), - tag.String("CatchupCluster", waitRequest.CatchupCluster), - tag.String("TargetCluster", waitRequest.TargetCluster), - tag.Int64("targetAckIDOnShard", targetAckIDOnShard[shard.ShardId]), - tag.Int64("MaxReplicationTaskId", shard.MaxReplicationTaskId), - tag.Duration("ActualLagging", shard.MaxReplicationTaskVisibilityTime.AsTime().Sub(clusterInfo.AckedTaskVisibilityTime.AsTime())), - tag.Time("MaxReplicationTaskVisibilityTime", shard.MaxReplicationTaskVisibilityTime.AsTime()), - tag.Time("AckedTaskVisibilityTime", clusterInfo.AckedTaskVisibilityTime.AsTime()), - tag.Int64("ActualLaggingTasks", shard.MaxReplicationTaskId-clusterInfo.AckedTaskId), - ) + status := shardStatus{ + shardID: localShard.GetShardId(), + laggingTasks: laggingTasks, + isReady: noTargetAckID || fullyCaughtUp || reachedTarget, + } + + shardStatuses = append(shardStatuses, status) + } + + var ( + readyShardCount int + notReadyShardCount int + + maxLaggingTasksShardID int32 + maxLaggingTasks int64 + ) + for _, status := range shardStatuses { + if status.isReady { + readyShardCount++ + } else { + notReadyShardCount++ } + if status.laggingTasks > maxLaggingTasks { + maxLaggingTasks = status.laggingTasks + maxLaggingTasksShardID = status.shardID + } + } + + isReady := notReadyShardCount == 0 + + if !isReady { + a.logger.Info("Wait catchup not ready", + tag.String("CatchupCluster", waitRequest.CatchupCluster), + tag.String("TargetCluster", waitRequest.TargetCluster), + tag.String("Namespace", waitRequest.Namespace), + tag.Int("TotalShards", len(localShards)), + tag.Int("ReadyShards", readyShardCount), + tag.Int("NotReadyShards", notReadyShardCount), + tag.Int32("MaxLaggingTasksShardID", maxLaggingTasksShardID), + tag.Int64("MaxLaggingTasks", maxLaggingTasks), + ) } - return readyShardCount == expectedShardCount, nil + return isReady, nil } func (a *activities) archetypeIDToName(ctx context.Context, archetypeID chasm.ArchetypeID) (chasm.Archetype, error) { diff --git a/service/worker/migration/activities_test.go b/service/worker/migration/activities_test.go index 0423717d5fd..df474eeabce 100644 --- a/service/worker/migration/activities_test.go +++ b/service/worker/migration/activities_test.go @@ -181,6 +181,7 @@ func (s *activitiesSuite) TestVerifyReplicationTasks_Success() { RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(&adminservice.DescribeMutableStateResponse{}, nil).Times(1) @@ -202,6 +203,7 @@ func (s *activitiesSuite) TestVerifyReplicationTasks_Success() { RunId: execution2.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution2.ArchetypeID, SkipForceReload: true, })).Return(r.resp, r.err).Times(1) } @@ -272,6 +274,7 @@ func (s *activitiesSuite) TestVerifyReplicationTasks_SkipWorkflowExecution() { RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(nil, serviceerror.NewNotFound("")).Times(1) @@ -328,6 +331,7 @@ func (s *activitiesSuite) TestVerifyReplicationTasks_FailedNotFound() { RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(nil, serviceerror.NewNotFound("")).AnyTimes() @@ -384,6 +388,7 @@ func (s *activitiesSuite) Test_verifySingleReplicationTask() { RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(&adminservice.DescribeMutableStateResponse{}, nil).Times(1) result, err := s.a.verifySingleReplicationTask(ctx, &request, s.mockRemoteAdminClient, &testNamespace, request.Executions[0]) @@ -398,6 +403,7 @@ func (s *activitiesSuite) Test_verifySingleReplicationTask() { RunId: execution2.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution2.ArchetypeID, SkipForceReload: true, })).Return(&adminservice.DescribeMutableStateResponse{}, serviceerror.NewNotFound("")).Times(1) @@ -431,7 +437,7 @@ func createExecutions( ) []*ExecutionInfo { var executions []*ExecutionInfo - for i := 0; i < len(states); i++ { + for range states { executions = append(executions, execution1) } @@ -446,6 +452,7 @@ Loop: RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(&adminservice.DescribeMutableStateResponse{}, nil).Times(1) case executionNotfound: @@ -456,6 +463,7 @@ Loop: RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(nil, serviceerror.NewNotFound("")).Times(1) break Loop @@ -467,6 +475,7 @@ Loop: RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(nil, serviceerror.NewInternal("")).Times(1) } @@ -616,6 +625,7 @@ func (s *activitiesSuite) Test_verifyReplicationTasksNoProgress() { RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(nil, serviceerror.NewNotFound("")).Times(1) @@ -661,6 +671,7 @@ func (s *activitiesSuite) Test_verifyReplicationTasksSkipRetention() { RunId: execution1.RunID, }, Archetype: chasm.WorkflowArchetype, + ArchetypeId: execution1.ArchetypeID, SkipForceReload: true, })).Return(nil, serviceerror.NewNotFound("")).Times(1) diff --git a/service/worker/migration/force_replication_workflow_test.go b/service/worker/migration/force_replication_workflow_test.go index 438a96556e5..154ccbb7053 100644 --- a/service/worker/migration/force_replication_workflow_test.go +++ b/service/worker/migration/force_replication_workflow_test.go @@ -32,7 +32,7 @@ import ( type ( ForceReplicationWorkflowTestSuite struct { suite.Suite - forceReplicationWorkflowFn interface{} + forceReplicationWorkflowFn any } ) @@ -40,7 +40,7 @@ func TestForceReplicationWorkflowTestSuite(t *testing.T) { t.Parallel() for _, tc := range []struct { name string - forceReplicationWorkflowFn interface{} + forceReplicationWorkflowFn any }{ { name: "ForceReplicationWorkflow", @@ -718,7 +718,7 @@ func (i *heartbeatRecordingInterceptor) Init(outbound interceptor.ActivityOutbou return i.ActivityInboundInterceptorBase.Init(i) } -func (i *heartbeatRecordingInterceptor) RecordHeartbeat(ctx context.Context, details ...interface{}) { +func (i *heartbeatRecordingInterceptor) RecordHeartbeat(ctx context.Context, details ...any) { if d, ok := details[0].(seedReplicationQueueWithUserDataEntriesHeartbeatDetails); ok { i.seedRecordedHeartbeats = append(i.seedRecordedHeartbeats, d) } else if d, ok := details[0].(replicationTasksHeartbeatDetails); ok { diff --git a/service/worker/pernamespaceworker.go b/service/worker/pernamespaceworker.go index bdca012e745..2f6cf20e65b 100644 --- a/service/worker/pernamespaceworker.go +++ b/service/worker/pernamespaceworker.go @@ -24,19 +24,15 @@ import ( "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/membership" "go.temporal.io/server/common/namespace" - "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/quotas" "go.temporal.io/server/common/resource" "go.temporal.io/server/common/sdk" "go.temporal.io/server/common/util" workercommon "go.temporal.io/server/service/worker/common" - "go.uber.org/fx" expmaps "golang.org/x/exp/maps" ) const ( - perNamespaceWorkerManagerListenerKey = "perNamespaceWorkerManager" - // Always refresh workers after this time even if there were no membership or namespace // state changes. This is to pick up dynamic config changes in component enabled status, // and heal anything that may have gotten into a bad state. @@ -44,18 +40,7 @@ const ( ) type ( - perNamespaceWorkerManagerInitParams struct { - fx.In - Logger log.Logger - SdkClientFactory sdk.ClientFactory - NamespaceRegistry namespace.Registry - HostName resource.HostName - Config *Config - ClusterMetadata cluster.Metadata - Components []workercommon.PerNSWorkerComponent `group:"perNamespaceWorkerComponent"` - } - - perNamespaceWorkerManager struct { + PerNamespaceWorkerManager struct { status int32 // from init params or Start @@ -65,6 +50,7 @@ type ( self membership.HostInfo hostName resource.HostName config *Config + taskQueueName string serviceResolver membership.ServiceResolver components []workercommon.PerNSWorkerComponent initialRetry time.Duration @@ -78,7 +64,7 @@ type ( } perNamespaceWorker struct { - wm *perNamespaceWorkerManager + wm *PerNamespaceWorkerManager logger log.Logger cancel func() @@ -114,27 +100,37 @@ var ( errInvalidConfiguration = errors.New("invalid dynamic configuration") ) -func NewPerNamespaceWorkerManager(params perNamespaceWorkerManagerInitParams) *perNamespaceWorkerManager { - return &perNamespaceWorkerManager{ - logger: log.With(params.Logger, tag.ComponentPerNSWorkerManager), - sdkClientFactory: params.SdkClientFactory, - namespaceRegistry: params.NamespaceRegistry, - hostName: params.HostName, - config: params.Config, - components: params.Components, +func NewPerNamespaceWorkerManager( + logger log.Logger, + sdkClientFactory sdk.ClientFactory, + namespaceRegistry namespace.Registry, + hostName resource.HostName, + config *Config, + clusterMetadata cluster.Metadata, + components []workercommon.PerNSWorkerComponent, + taskQueueName string, +) *PerNamespaceWorkerManager { + return &PerNamespaceWorkerManager{ + logger: log.With(logger, tag.ComponentPerNSWorkerManager), + sdkClientFactory: sdkClientFactory, + namespaceRegistry: namespaceRegistry, + hostName: hostName, + taskQueueName: taskQueueName, + config: config, + components: components, initialRetry: 1 * time.Second, - thisClusterName: params.ClusterMetadata.GetCurrentClusterName(), - startLimiter: quotas.NewDefaultOutgoingRateLimiter(quotas.RateFn(params.Config.PerNamespaceWorkerStartRate)), + thisClusterName: clusterMetadata.GetCurrentClusterName(), + startLimiter: quotas.NewDefaultOutgoingRateLimiter(quotas.RateFn(config.PerNamespaceWorkerStartRate)), membershipChangedCh: make(chan *membership.ChangedEvent), workers: make(map[namespace.ID]*perNamespaceWorker), } } -func (wm *perNamespaceWorkerManager) Running() bool { +func (wm *PerNamespaceWorkerManager) Running() bool { return atomic.LoadInt32(&wm.status) == common.DaemonStatusStarted } -func (wm *perNamespaceWorkerManager) Start( +func (wm *PerNamespaceWorkerManager) Start( self membership.HostInfo, serviceResolver membership.ServiceResolver, ) { @@ -154,7 +150,7 @@ func (wm *perNamespaceWorkerManager) Start( // this will call namespaceCallback with current namespaces wm.namespaceRegistry.RegisterStateChangeCallback(wm, wm.namespaceCallback) - err := wm.serviceResolver.AddListener(perNamespaceWorkerManagerListenerKey, wm.membershipChangedCh) + err := wm.serviceResolver.AddListener(fmt.Sprintf("%p", wm), wm.membershipChangedCh) if err != nil { wm.logger.Fatal("Unable to register membership listener", tag.Error(err)) } @@ -164,7 +160,7 @@ func (wm *perNamespaceWorkerManager) Start( wm.logger.Info("", tag.LifeCycleStarted) } -func (wm *perNamespaceWorkerManager) Stop() { +func (wm *PerNamespaceWorkerManager) Stop() { if !atomic.CompareAndSwapInt32( &wm.status, common.DaemonStatusStarted, @@ -176,7 +172,7 @@ func (wm *perNamespaceWorkerManager) Stop() { wm.logger.Info("", tag.LifeCycleStopping) wm.namespaceRegistry.UnregisterStateChangeCallback(wm) - err := wm.serviceResolver.RemoveListener(perNamespaceWorkerManagerListenerKey) + err := wm.serviceResolver.RemoveListener(fmt.Sprintf("%p", wm)) if err != nil { wm.logger.Error("Unable to unregister membership listener", tag.Error(err)) } @@ -195,11 +191,11 @@ func (wm *perNamespaceWorkerManager) Stop() { wm.logger.Info("", tag.LifeCycleStopped) } -func (wm *perNamespaceWorkerManager) namespaceCallback(ns *namespace.Namespace, nsDeleted bool) { +func (wm *PerNamespaceWorkerManager) namespaceCallback(ns *namespace.Namespace, nsDeleted bool) { go wm.getWorkerByNamespace(ns).update(ns, nsDeleted, nil, nil) } -func (wm *perNamespaceWorkerManager) refreshAll() { +func (wm *PerNamespaceWorkerManager) refreshAll() { wm.lock.Lock() defer wm.lock.Unlock() for _, worker := range wm.workers { @@ -207,13 +203,13 @@ func (wm *perNamespaceWorkerManager) refreshAll() { } } -func (wm *perNamespaceWorkerManager) membershipChangedListener() { +func (wm *PerNamespaceWorkerManager) membershipChangedListener() { for range wm.membershipChangedCh { wm.refreshAll() } } -func (wm *perNamespaceWorkerManager) periodicRefresh() { +func (wm *PerNamespaceWorkerManager) periodicRefresh() { for range time.NewTicker(refreshInterval).C { if atomic.LoadInt32(&wm.status) != common.DaemonStatusStarted { return @@ -222,7 +218,7 @@ func (wm *perNamespaceWorkerManager) periodicRefresh() { } } -func (wm *perNamespaceWorkerManager) getWorkerByNamespace(ns *namespace.Namespace) *perNamespaceWorker { +func (wm *PerNamespaceWorkerManager) getWorkerByNamespace(ns *namespace.Namespace) *perNamespaceWorker { wm.lock.Lock() defer wm.lock.Unlock() @@ -246,7 +242,7 @@ func (wm *perNamespaceWorkerManager) getWorkerByNamespace(ns *namespace.Namespac return worker } -func (wm *perNamespaceWorkerManager) removeWorker(ns *namespace.Namespace) { +func (wm *PerNamespaceWorkerManager) removeWorker(ns *namespace.Namespace) { wm.lock.Lock() defer wm.lock.Unlock() prev := wm.workers[ns.ID()] @@ -490,7 +486,7 @@ func (w *perNamespaceWorker) startWorker( sdkoptions.OnFatalError = w.onFatalError // this should not block because the client already has server capabilities - worker := w.wm.sdkClientFactory.NewWorker(client, primitives.PerNSWorkerTaskQueue, sdkoptions) + worker := w.wm.sdkClientFactory.NewWorker(client, w.wm.taskQueueName, sdkoptions) details := workercommon.RegistrationDetails{ TotalWorkers: allocation.total, Multiplicity: allocation.local, diff --git a/service/worker/pernamespaceworker_test.go b/service/worker/pernamespaceworker_test.go index 622488028b0..e3e64b96362 100644 --- a/service/worker/pernamespaceworker_test.go +++ b/service/worker/pernamespaceworker_test.go @@ -39,7 +39,7 @@ type perNsWorkerManagerSuite struct { cmp1 *workercommon.MockPerNSWorkerComponent cmp2 *workercommon.MockPerNSWorkerComponent - manager *perNamespaceWorkerManager + manager *PerNamespaceWorkerManager } func TestPerNsWorkerManager(t *testing.T) { @@ -57,7 +57,7 @@ func (s *perNsWorkerManagerSuite) SetupTest() { s.cmp1 = workercommon.NewMockPerNSWorkerComponent(s.controller) s.cmp2 = workercommon.NewMockPerNSWorkerComponent(s.controller) - s.manager = NewPerNamespaceWorkerManager(perNamespaceWorkerManagerInitParams{ + s.manager = PerNamespaceWorkerManagerProvider(perNamespaceWorkerManagerInitParams{ Logger: s.logger, SdkClientFactory: s.cfactory, NamespaceRegistry: s.registry, @@ -484,7 +484,7 @@ func (s *perNsWorkerManagerSuite) TestRateLimit() { // try to start 100 workers // rate limiter will allow 10, then 10 more after a short delay, then no more for 10s delay := 50 * time.Millisecond - for i := 0; i < 100; i++ { + for i := range 100 { res := quotas.NewMockReservation(s.controller) mockLimiter.EXPECT().Reserve().Return(res) switch i % 10 { @@ -508,13 +508,13 @@ func (s *perNsWorkerManagerSuite) TestRateLimit() { starts := make(chan struct{}, 100) wkr.EXPECT().Start().Do(func() { starts <- struct{}{} }).AnyTimes() - for i := 0; i < 100; i++ { + for i := range 100 { ns := testns(fmt.Sprintf("test-%d", i), enumspb.NAMESPACE_STATE_REGISTERED) s.manager.namespaceCallback(ns, false) } start := time.Now() - for i := 0; i < 10; i++ { + for range 10 { <-starts } select { @@ -524,7 +524,7 @@ func (s *perNsWorkerManagerSuite) TestRateLimit() { } s.Less(time.Since(start), delay, "should be 10 started immediately") - for i := 0; i < 10; i++ { + for range 10 { <-starts } select { @@ -558,7 +558,7 @@ func TestPerNsWorkerManagerSubscription(t *testing.T) { t.Cleanup(dc.Stop) config := NewConfig(dc, nil) - manager := NewPerNamespaceWorkerManager(perNamespaceWorkerManagerInitParams{ + manager := PerNamespaceWorkerManagerProvider(perNamespaceWorkerManagerInitParams{ Logger: logger, SdkClientFactory: cfactory, NamespaceRegistry: registry, diff --git a/service/worker/replicator/replication_message_processor.go b/service/worker/replicator/replication_message_processor.go index dc621c1611d..cbca47bca5d 100644 --- a/service/worker/replicator/replication_message_processor.go +++ b/service/worker/replicator/replication_message_processor.go @@ -40,6 +40,7 @@ func newReplicationMessageProcessor( remotePeer adminservice.AdminServiceClient, metricsHandler metrics.Handler, namespaceTaskExecutor nsreplication.TaskExecutor, + customTaskHandler func(ctx context.Context, task *replicationspb.ReplicationTask) error, hostInfo membership.HostInfo, serviceResolver membership.ServiceResolver, namespaceReplicationQueue persistence.NamespaceReplicationQueue, @@ -59,6 +60,7 @@ func newReplicationMessageProcessor( logger: logger, remotePeer: remotePeer, namespaceTaskExecutor: namespaceTaskExecutor, + customTaskHandler: customTaskHandler, metricsHandler: metricsHandler.WithTags(metrics.OperationTag(metrics.NamespaceReplicationTaskScope)), retryPolicy: retryPolicy, lastProcessedMessageID: -1, @@ -80,6 +82,7 @@ type ( logger log.Logger remotePeer adminservice.AdminServiceClient namespaceTaskExecutor nsreplication.TaskExecutor + customTaskHandler func(ctx context.Context, task *replicationspb.ReplicationTask) error metricsHandler metrics.Handler retryPolicy backoff.RetryPolicy lastProcessedMessageID int64 @@ -198,6 +201,9 @@ func (p *replicationMessageProcessor) putNamespaceReplicationTaskToDLQ( metrics.NamespaceTag(ns.Name().String()), ) default: + if p.customTaskHandler != nil { + return p.namespaceReplicationQueue.PublishToDLQ(ctx, task) + } return serviceerror.NewUnavailable( fmt.Sprintf("Namespace replication task type not supported: %v", task.TaskType), ) @@ -236,6 +242,9 @@ func (p *replicationMessageProcessor) handleReplicationTask( } return err default: + if p.customTaskHandler != nil { + return p.customTaskHandler(ctx, task) + } return fmt.Errorf("cannot handle replication task of type %v", task.TaskType) } } diff --git a/service/worker/replicator/replicator.go b/service/worker/replicator/replicator.go index 17a90130aee..407de2fad6d 100644 --- a/service/worker/replicator/replicator.go +++ b/service/worker/replicator/replicator.go @@ -9,6 +9,7 @@ import ( "time" "go.temporal.io/server/api/matchingservice/v1" + replicationspb "go.temporal.io/server/api/replication/v1" "go.temporal.io/server/client" "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" @@ -31,6 +32,7 @@ type ( status int32 clusterMetadata cluster.Metadata namespaceReplicationTaskExecutor nsreplication.TaskExecutor + customTaskHandler func(ctx context.Context, task *replicationspb.ReplicationTask) error clientBean client.Bean logger log.Logger metricsHandler metrics.Handler @@ -79,6 +81,12 @@ func NewReplicator( } } +// WithCustomTaskHandler returns a copy of the Replicator with the custom task handler set. +func (r *Replicator) WithCustomTaskHandler(handler func(ctx context.Context, task *replicationspb.ReplicationTask) error) *Replicator { + r.customTaskHandler = handler + return r +} + // Start is called to start replicator func (r *Replicator) Start() { if !atomic.CompareAndSwapInt32( @@ -146,6 +154,7 @@ func (r *Replicator) listenToClusterMetadataChange() { remoteAdminClient, r.metricsHandler, r.namespaceReplicationTaskExecutor, + r.customTaskHandler, r.hostInfo, r.serviceResolver, r.namespaceReplicationQueue, diff --git a/service/worker/scanner/build_ids/scavenger_test.go b/service/worker/scanner/build_ids/scavenger_test.go index ac0350851e9..9b2f7c99957 100644 --- a/service/worker/scanner/build_ids/scavenger_test.go +++ b/service/worker/scanner/build_ids/scavenger_test.go @@ -476,7 +476,7 @@ func (i *heartbeatRecordingInterceptor) Init(outbound interceptor.ActivityOutbou return i.ActivityInboundInterceptorBase.Init(i) } -func (i *heartbeatRecordingInterceptor) RecordHeartbeat(ctx context.Context, details ...interface{}) { +func (i *heartbeatRecordingInterceptor) RecordHeartbeat(ctx context.Context, details ...any) { d, ok := details[0].(heartbeatDetails) require.True(i.T, ok, "invalid heartbeat details") i.recordedHeartbeats = append(i.recordedHeartbeats, d) diff --git a/service/worker/scanner/executor/executor_test.go b/service/worker/scanner/executor/executor_test.go index a9558e5058a..27bee1bf52d 100644 --- a/service/worker/scanner/executor/executor_test.go +++ b/service/worker/scanner/executor/executor_test.go @@ -38,11 +38,11 @@ func (s *ExecutorTestSuite) TestTaskExecution() { e.Start() var runCounter int64 var startWG sync.WaitGroup - for i := 0; i < 5; i++ { + for range 5 { startWG.Add(1) go func() { defer startWG.Done() - for i := 0; i < 20; i++ { + for i := range 20 { if i%2 == 0 { e.Submit(&testTask{TaskStatusDefer, &runCounter}) continue diff --git a/service/worker/scanner/executor/runq.go b/service/worker/scanner/executor/runq.go index d862d65cf42..9fbff5b5aa3 100644 --- a/service/worker/scanner/executor/runq.go +++ b/service/worker/scanner/executor/runq.go @@ -82,7 +82,7 @@ func newThreadSafeList() *threadSafeList { } } -func (tl *threadSafeList) add(elem interface{}) { +func (tl *threadSafeList) add(elem any) { tl.Lock() defer tl.Unlock() tl.list.PushBack(elem) diff --git a/service/worker/scanner/history/scavenger.go b/service/worker/scanner/history/scavenger.go index 7b6ac2e9861..1908cc4cdf5 100644 --- a/service/worker/scanner/history/scavenger.go +++ b/service/worker/scanner/history/scavenger.go @@ -120,7 +120,7 @@ func (s *Scavenger) Run(ctx context.Context) (ScavengerHeartbeatDetails, error) reqCh := make(chan taskDetail, pageSize) go s.loadTasks(ctx, reqCh) - for i := 0; i < numWorker; i++ { + for range numWorker { s.WaitGroup.Add(1) go s.taskWorker(ctx, reqCh) } diff --git a/service/worker/scanner/scanner.go b/service/worker/scanner/scanner.go index 05f944afcb6..8e4681641f7 100644 --- a/service/worker/scanner/scanner.go +++ b/service/worker/scanner/scanner.go @@ -234,7 +234,7 @@ func (s *Scanner) Stop() { s.wg.Wait() } -func (s *Scanner) startWorkflowWithRetry(ctx context.Context, options sdkclient.StartWorkflowOptions, workflowType string, workflowArgs ...interface{}) { +func (s *Scanner) startWorkflowWithRetry(ctx context.Context, options sdkclient.StartWorkflowOptions, workflowType string, workflowArgs ...any) { defer s.wg.Done() policy := backoff.NewExponentialRetryPolicy(time.Second). @@ -262,7 +262,7 @@ func (s *Scanner) startWorkflow( client sdkclient.Client, options sdkclient.StartWorkflowOptions, workflowType string, - workflowArgs ...interface{}, + workflowArgs ...any, ) error { ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) _, err := client.ExecuteWorkflow(ctx, options, workflowType, workflowArgs...) diff --git a/service/worker/scanner/scanner_test.go b/service/worker/scanner/scanner_test.go index 19f60bb5071..cfd1f6821c7 100644 --- a/service/worker/scanner/scanner_test.go +++ b/service/worker/scanner/scanner_test.go @@ -230,7 +230,7 @@ func (s *scannerTestSuite) TestScannerEnabled() { _ context.Context, _ client.StartWorkflowOptions, _ string, - _ ...interface{}, + _ ...any, ) { wg.Done() }) @@ -304,7 +304,7 @@ func (s *scannerTestSuite) TestScannerShutdown() { ctx context.Context, _ client.StartWorkflowOptions, _ string, - _ ...interface{}, + _ ...any, ) (client.WorkflowRun, error) { wg.Done() <-ctx.Done() diff --git a/service/worker/scanner/taskqueue/mocks_test.go b/service/worker/scanner/taskqueue/mocks_test.go index 882f33f29dd..edc125e76bf 100644 --- a/service/worker/scanner/taskqueue/mocks_test.go +++ b/service/worker/scanner/taskqueue/mocks_test.go @@ -92,7 +92,7 @@ func (tbl *mockTaskQueueTable) get(name string) *p.PersistedTaskQueueInfo { } func (tbl *mockTaskTable) generate(count int, expired bool) { - for i := 0; i < count; i++ { + for range count { exp := time.Now().UTC().Add(time.Hour) ti := &persistencespb.AllocatedTaskInfo{ Data: &persistencespb.TaskInfo{ diff --git a/service/worker/scanner/taskqueue/scavenger_test.go b/service/worker/scanner/taskqueue/scavenger_test.go index 95afecb2f4b..c88c40592b4 100644 --- a/service/worker/scanner/taskqueue/scavenger_test.go +++ b/service/worker/scanner/taskqueue/scavenger_test.go @@ -51,7 +51,7 @@ func (s *ScavengerTestSuite) TearDownTest() { func (s *ScavengerTestSuite) TestAllExpiredTasks() { nTasks := 32 nTaskQueues := 3 - for i := 0; i < nTaskQueues; i++ { + for i := range nTaskQueues { name := fmt.Sprintf("test-expired-tq-%v", i) s.taskQueueTable.generate(name, true) tt := newMockTaskTable() @@ -70,7 +70,7 @@ func (s *ScavengerTestSuite) TestAllExpiredTasks() { func (s *ScavengerTestSuite) TestAllAliveTasks() { nTasks := 32 nTaskQueues := 3 - for i := 0; i < nTaskQueues; i++ { + for i := range nTaskQueues { name := fmt.Sprintf("test-Alive-tq-%v", i) s.taskQueueTable.generate(name, true) tt := newMockTaskTable() @@ -89,7 +89,7 @@ func (s *ScavengerTestSuite) TestAllAliveTasks() { func (s *ScavengerTestSuite) TestExpiredTasksFollowedByAlive() { nTasks := 32 nTaskQueues := 3 - for i := 0; i < nTaskQueues; i++ { + for i := range nTaskQueues { name := fmt.Sprintf("test-Alive-tq-%v", i) s.taskQueueTable.generate(name, true) tt := newMockTaskTable() @@ -110,7 +110,7 @@ func (s *ScavengerTestSuite) TestExpiredTasksFollowedByAlive() { func (s *ScavengerTestSuite) TestAliveTasksFollowedByExpired() { nTasks := 32 nTaskQueues := 3 - for i := 0; i < nTaskQueues; i++ { + for i := range nTaskQueues { name := fmt.Sprintf("test-Alive-tl-%v", i) s.taskQueueTable.generate(name, true) tt := newMockTaskTable() @@ -130,7 +130,7 @@ func (s *ScavengerTestSuite) TestAliveTasksFollowedByExpired() { func (s *ScavengerTestSuite) TestAllExpiredTasksWithErrors() { nTasks := 32 nTaskQueues := 3 - for i := 0; i < nTaskQueues; i++ { + for i := range nTaskQueues { name := fmt.Sprintf("test-expired-tl-%v", i) s.taskQueueTable.generate(name, true) tt := newMockTaskTable() diff --git a/service/worker/scheduler/activities.go b/service/worker/scheduler/activities.go index 627d0c45522..471faafbfb1 100644 --- a/service/worker/scheduler/activities.go +++ b/service/worker/scheduler/activities.go @@ -16,6 +16,7 @@ import ( "go.temporal.io/sdk/temporal" "go.temporal.io/server/api/historyservice/v1" schedulespb "go.temporal.io/server/api/schedule/v1" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" @@ -196,7 +197,7 @@ func (a *activities) WatchWorkflow(ctx context.Context, req *schedulespb.WatchWo // StartToCloseTimeout if ScheduleToCloseTimeout is set, so add a timeout here. // TODO: remove after https://github.com/temporalio/sdk-go/issues/1066 var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, defaultLocalActivityOptions.StartToCloseTimeout) + ctx, cancel = context.WithTimeout(ctx, defaultLocalActivityOptions().StartToCloseTimeout) defer cancel() } @@ -220,7 +221,7 @@ func (a *activities) WatchWorkflow(ctx context.Context, req *schedulespb.WatchWo func (a *activities) CancelWorkflow(ctx context.Context, req *schedulespb.CancelWorkflowRequest) error { // TODO: remove after https://github.com/temporalio/sdk-go/issues/1066 var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, defaultLocalActivityOptions.StartToCloseTimeout) + ctx, cancel = context.WithTimeout(ctx, defaultLocalActivityOptions().StartToCloseTimeout) defer cancel() rreq := &historyservice.RequestCancelWorkflowExecutionRequest{ @@ -243,7 +244,7 @@ func (a *activities) CancelWorkflow(ctx context.Context, req *schedulespb.Cancel func (a *activities) TerminateWorkflow(ctx context.Context, req *schedulespb.TerminateWorkflowRequest) error { // TODO: remove after https://github.com/temporalio/sdk-go/issues/1066 var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, defaultLocalActivityOptions.StartToCloseTimeout) + ctx, cancel = context.WithTimeout(ctx, defaultLocalActivityOptions().StartToCloseTimeout) defer cancel() rreq := &historyservice.TerminateWorkflowExecutionRequest{ @@ -372,3 +373,28 @@ func (r responseBuilder) makeResponse(result *commonpb.Payloads, failure *failur } return res } + +func (a *activities) MigrateScheduleToChasm(ctx context.Context, req *schedulerpb.CreateFromMigrationStateRequest) error { + if req.GetNamespaceId() != a.namespaceID.String() { + return temporal.NewNonRetryableApplicationError( + fmt.Sprintf("MigrateScheduleToChasm: request namespace ID %q does not match activity namespace ID %q", req.GetNamespaceId(), a.namespaceID), + "namespace_mismatch", + nil, + ) + } + _, err := a.SchedulerClient.CreateFromMigrationState(ctx, req) + if err != nil { + // Treat "already exists" as success (idempotency). + var alreadyExists *serviceerror.AlreadyExists + if errors.As(err, &alreadyExists) { + return nil + } + // Sentinel blocking migration is transient; will retry on next workflow wake-up. + var unavailableErr *serviceerror.Unavailable + if errors.As(err, &unavailableErr) { + return translateError(err, "MigrateScheduleToChasm: blocked by sentinel, will retry") + } + return translateError(err, "MigrateScheduleToChasm") + } + return nil +} diff --git a/service/worker/scheduler/activities_test.go b/service/worker/scheduler/activities_test.go new file mode 100644 index 00000000000..c667033b366 --- /dev/null +++ b/service/worker/scheduler/activities_test.go @@ -0,0 +1,102 @@ +package scheduler + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + "go.temporal.io/api/serviceerror" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + "google.golang.org/grpc" +) + +type mockSchedulerClient struct { + schedulerpb.SchedulerServiceClient + migrateErr error +} + +func (m *mockSchedulerClient) CreateFromMigrationState( + _ context.Context, + _ *schedulerpb.CreateFromMigrationStateRequest, + _ ...grpc.CallOption, +) (*schedulerpb.CreateFromMigrationStateResponse, error) { + return &schedulerpb.CreateFromMigrationStateResponse{}, m.migrateErr +} + +func newTestActivities(client schedulerpb.SchedulerServiceClient, nsID namespace.ID) *activities { + return &activities{ + activityDeps: activityDeps{ + Logger: log.NewNoopLogger(), + SchedulerClient: client, + MetricsHandler: metrics.NoopMetricsHandler, + }, + namespaceID: nsID, + } +} + +const testNamespaceID = "test-namespace-id" + +func TestMigrateScheduleToChasm_Success(t *testing.T) { + client := &mockSchedulerClient{} + a := newTestActivities(client, testNamespaceID) + + err := a.MigrateScheduleToChasm(context.Background(), &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: testNamespaceID, + }) + require.NoError(t, err) +} + +func TestMigrateScheduleToChasm_AlreadyExists(t *testing.T) { + client := &mockSchedulerClient{ + migrateErr: serviceerror.NewAlreadyExistsf("schedule %q is already registered", "test-schedule"), + } + a := newTestActivities(client, testNamespaceID) + + err := a.MigrateScheduleToChasm(context.Background(), &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: testNamespaceID, + }) + require.NoError(t, err, "already-exists should be treated as success") +} + +func TestMigrateScheduleToChasm_SentinelBlocked(t *testing.T) { + client := &mockSchedulerClient{ + migrateErr: serviceerror.NewUnavailable("schedule is a sentinel; please retry after sentinel expires"), + } + a := newTestActivities(client, testNamespaceID) + + err := a.MigrateScheduleToChasm(context.Background(), &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: testNamespaceID, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "blocked by sentinel") +} + +func TestMigrateScheduleToChasm_OtherError(t *testing.T) { + client := &mockSchedulerClient{ + migrateErr: errors.New("some transient error"), + } + a := newTestActivities(client, testNamespaceID) + + err := a.MigrateScheduleToChasm(context.Background(), &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: testNamespaceID, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "MigrateScheduleToChasm") +} + +func TestMigrateScheduleToChasm_NamespaceMismatch(t *testing.T) { + client := &mockSchedulerClient{} + a := newTestActivities(client, testNamespaceID) + + err := a.MigrateScheduleToChasm(context.Background(), &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: "different-namespace-id", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "namespace_mismatch") + require.Contains(t, err.Error(), "different-namespace-id") + require.Contains(t, err.Error(), testNamespaceID) +} diff --git a/service/worker/scheduler/calendar_test.go b/service/worker/scheduler/calendar_test.go index ac3aaf172dd..1ece799bf1e 100644 --- a/service/worker/scheduler/calendar_test.go +++ b/service/worker/scheduler/calendar_test.go @@ -414,7 +414,7 @@ func FuzzCalendar(f *testing.F) { t.Errorf("next %v not before now %v (for %+v)", next, now, cal) } gap := int(next.Sub(now) / time.Second) - for i := 0; i < 1000; i++ { + for range 1000 { ts1 := now.Add(time.Duration(rand.Intn(gap)) * time.Second) if !cc.next(ts1).Equal(next) { t.Errorf("next(%v) = %v should equal next(%v) = %v (for %+v)", ts1, cc.next(ts1), now, next, cal) diff --git a/service/worker/scheduler/fx.go b/service/worker/scheduler/fx.go index e76f5bfdc1a..3389361a852 100644 --- a/service/worker/scheduler/fx.go +++ b/service/worker/scheduler/fx.go @@ -10,6 +10,7 @@ import ( "go.temporal.io/sdk/workflow" schedulespb "go.temporal.io/server/api/schedule/v1" "go.temporal.io/server/chasm" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/metrics" @@ -51,6 +52,7 @@ type ( specBuilder *SpecBuilder // workflow dep activityDeps activityDeps enabledForNs dynamicconfig.BoolPropertyFnWithNamespaceFilter + enableCHASMMigration dynamicconfig.BoolPropertyFnWithNamespaceFilter globalNSStartWorkflowRPS dynamicconfig.TypedSubscribableWithNamespaceFilter[float64] maxBlobSize dynamicconfig.IntPropertyFnWithNamespaceFilter localActivitySleepLimit dynamicconfig.DurationPropertyFnWithNamespaceFilter @@ -58,10 +60,11 @@ type ( activityDeps struct { fx.In - MetricsHandler metrics.Handler - Logger log.Logger - HistoryClient resource.HistoryClient - FrontendClient workflowservice.WorkflowServiceClient + MetricsHandler metrics.Handler + Logger log.Logger + HistoryClient resource.HistoryClient + FrontendClient workflowservice.WorkflowServiceClient + SchedulerClient schedulerpb.SchedulerServiceClient } fxResult struct { @@ -85,6 +88,7 @@ func NewResult( specBuilder: specBuilder, activityDeps: params, enabledForNs: dynamicconfig.WorkerEnableScheduler.Get(dc), + enableCHASMMigration: dynamicconfig.EnableCHASMSchedulerMigration.Get(dc), globalNSStartWorkflowRPS: dynamicconfig.SchedulerNamespaceStartWorkflowRPS.Subscribe(dc), maxBlobSize: dynamicconfig.BlobSizeLimitError.Get(dc), localActivitySleepLimit: dynamicconfig.SchedulerLocalActivitySleepLimit.Get(dc), @@ -99,8 +103,9 @@ func (s *workerComponent) DedicatedWorkerOptions(ns *namespace.Namespace) *worke } func (s *workerComponent) Register(registry sdkworker.Registry, ns *namespace.Namespace, details workercommon.RegistrationDetails) func() { + enableMigration := s.enableCHASMMigration(ns.Name().String()) wfFunc := func(ctx workflow.Context, args *schedulespb.StartScheduleArgs) error { - return schedulerWorkflowWithSpecBuilder(ctx, args, s.specBuilder) + return schedulerWorkflowWithSpecBuilder(ctx, args, s.specBuilder, enableMigration) } registry.RegisterWorkflowWithOptions(wfFunc, workflow.RegisterOptions{Name: WorkflowType}) diff --git a/service/worker/scheduler/workflow.go b/service/worker/scheduler/workflow.go index d26244702e1..3c4368da05b 100644 --- a/service/worker/scheduler/workflow.go +++ b/service/worker/scheduler/workflow.go @@ -19,6 +19,7 @@ import ( "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" schedulespb "go.temporal.io/server/api/schedule/v1" + "go.temporal.io/server/chasm/lib/scheduler/migration" "go.temporal.io/server/common" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/payload" @@ -74,10 +75,11 @@ const ( // id, used for validation in the frontend. AppendedTimestampForValidation = "-2009-11-10T23:00:00Z" - SignalNameUpdate = "update" - SignalNamePatch = "patch" - SignalNameRefresh = "refresh" - SignalNameForceCAN = "force-continue-as-new" + SignalNameUpdate = "update" + SignalNamePatch = "patch" + SignalNameRefresh = "refresh" + SignalNameForceCAN = "force-continue-as-new" + SignalNameMigrateToChasm = "migrate-to-chasm" QueryNameDescribe = "describe" QueryNameListMatchingTimes = "listMatchingTimes" @@ -110,8 +112,9 @@ type ( // SpecBuilder is technically a non-deterministic dependency, but it's safe as // long as we only call methods on cspec inside of SideEffect (or in a query // without modifying state). - specBuilder *SpecBuilder - cspec *CompiledSpec + specBuilder *SpecBuilder + cspec *CompiledSpec + enableCHASMMigration bool tweakables TweakablePolicies @@ -156,6 +159,8 @@ type ( Version SchedulerWorkflowVersion // Used to keep track of schedules version to release new features and for backward compatibility // version 0 corresponds to the schedule version that comes before introducing the Version parameter + EnableCHASMMigration bool // Whether to automatically migrate this schedule to CHASM (V2) + // When introducing a new field with new workflow logic, consider generating a new // history for TestReplays using generate_history.sh. } @@ -169,8 +174,8 @@ type ( } ) -var ( - defaultLocalActivityOptions = workflow.LocalActivityOptions{ +func defaultLocalActivityOptions() workflow.LocalActivityOptions { + return workflow.LocalActivityOptions{ // This applies to watch, cancel, and terminate. Start workflow overrides this. ScheduleToCloseTimeout: 1 * time.Hour, // Each local activity is one or a few local RPCs. @@ -181,7 +186,9 @@ var ( MaximumInterval: 60 * time.Second, }, } +} +var ( // CurrentTweakablePolicies is a handful of options in a static value and use it as a MutableSideEffect within // the workflow so that we can change them without breaking existing executions or having // to use versioning. @@ -217,10 +224,10 @@ var ( ) func SchedulerWorkflow(ctx workflow.Context, args *schedulespb.StartScheduleArgs) error { - return schedulerWorkflowWithSpecBuilder(ctx, args, NewSpecBuilder()) + return schedulerWorkflowWithSpecBuilder(ctx, args, NewSpecBuilder(), false) } -func schedulerWorkflowWithSpecBuilder(ctx workflow.Context, args *schedulespb.StartScheduleArgs, specBuilder *SpecBuilder) error { +func schedulerWorkflowWithSpecBuilder(ctx workflow.Context, args *schedulespb.StartScheduleArgs, specBuilder *SpecBuilder, enableCHASMMigration bool) error { scheduler := &scheduler{ StartScheduleArgs: args, ctx: ctx, @@ -230,7 +237,8 @@ func schedulerWorkflowWithSpecBuilder(ctx workflow.Context, args *schedulespb.St "namespace": args.State.Namespace, metrics.ScheduleBackendTag: metrics.ScheduleBackendLegacy, }), - specBuilder: specBuilder, + specBuilder: specBuilder, + enableCHASMMigration: enableCHASMMigration, } return scheduler.run() } @@ -270,7 +278,10 @@ func (s *scheduler) run() error { info := workflow.GetInfo(s.ctx) suggestContinueAsNew := info.GetCurrentHistoryLength() >= impossibleHistorySize if s.tweakables.IterationsBeforeContinueAsNew > 0 { - suggestContinueAsNew = suggestContinueAsNew || iters <= 0 + // forceCAN must be checked here too, not just in the else branch, + // so that the force-continue-as-new signal is honored regardless + // of whether IterationsBeforeContinueAsNew is set. + suggestContinueAsNew = suggestContinueAsNew || iters <= 0 || s.forceCAN iters-- } else { suggestContinueAsNew = suggestContinueAsNew || info.GetContinueAsNewSuggested() || s.forceCAN @@ -310,6 +321,32 @@ func (s *scheduler) run() error { nil, ) } + + if s.tweakables.EnableCHASMMigration { + s.State.PendingMigration = true + } + if s.State.PendingMigration { + err := s.executeMigration() + if err == nil { + s.logger.Info("Migration to CHASM succeeded, closing V1 workflow", + "namespace", s.State.Namespace, + "schedule-id", s.State.ScheduleId, + ) + s.metrics.WithTags(map[string]string{ + metrics.ScheduleMigrationDirectionTag: metrics.ScheduleMigrationDirectionToChasm, + }).Counter(metrics.ScheduleMigrationCompleted.Name()).Inc(1) + return nil + } + s.logger.Error("Migration to CHASM failed, continuing V1 workflow", + "namespace", s.State.Namespace, + "schedule-id", s.State.ScheduleId, + "error", err, + ) + s.metrics.WithTags(map[string]string{ + metrics.ScheduleMigrationDirectionTag: metrics.ScheduleMigrationDirectionToChasm, + }).Counter(metrics.ScheduleMigrationFailed.Name()).Inc(1) + } + // process backfills if we have any too s.processBackfills() // try starting workflows in the buffer @@ -477,7 +514,7 @@ func (s *scheduler) getNextTimeV1(after time.Time) GetNextTimeResult { s.nextTimeCacheV1 = nil // Run this logic in a SideEffect so that we can fix bugs there without breaking // existing schedule workflows. - panicIfErr(workflow.SideEffect(s.ctx, func(ctx workflow.Context) interface{} { + panicIfErr(workflow.SideEffect(s.ctx, func(ctx workflow.Context) any { results := make(map[time.Time]GetNextTimeResult) for t := after; !t.IsZero() && len(results) < nextTimeCacheV1Size; { next := s.cspec.GetNextTime(s.jitterSeed(), t) @@ -549,7 +586,7 @@ func (s *scheduler) fillNextTimeCacheV2(start time.Time) { s.nextTimeCacheV2 = nil // Run this logic in a SideEffect so that we can fix bugs there without breaking // existing schedule workflows. - val := workflow.SideEffect(s.ctx, func(ctx workflow.Context) interface{} { + val := workflow.SideEffect(s.ctx, func(ctx workflow.Context) any { cache := &schedulespb.NextTimeCache{ Version: int64(s.tweakables.Version), StartTime: timestamppb.New(start), @@ -607,7 +644,7 @@ func (s *scheduler) getNextTime(after time.Time) GetNextTimeResult { // Run this logic in a SideEffect so that we can fix bugs there without breaking // existing schedule workflows. var next GetNextTimeResult - panicIfErr(workflow.SideEffect(s.ctx, func(ctx workflow.Context) interface{} { + panicIfErr(workflow.SideEffect(s.ctx, func(ctx workflow.Context) any { return s.cspec.GetNextTime(s.jitterSeed(), after) }).Get(&next)) return next @@ -714,6 +751,9 @@ func (s *scheduler) sleep(nextWakeup time.Time) { forceCAN := workflow.GetSignalChannel(s.ctx, SignalNameForceCAN) sel.AddReceive(forceCAN, s.handleForceCANSignal) + migrateCh := workflow.GetSignalChannel(s.ctx, SignalNameMigrateToChasm) + sel.AddReceive(migrateCh, s.handleMigrateSignal) + if s.hasMoreAllowAllBackfills() { // if we have more allow-all backfills to do, do a short sleep and continue nextWakeup = s.now().Add(1 * time.Second) @@ -957,6 +997,49 @@ func (s *scheduler) handleForceCANSignal(ch workflow.ReceiveChannel, _ bool) { s.forceCAN = true } +func (s *scheduler) handleMigrateSignal(ch workflow.ReceiveChannel, _ bool) { + ch.Receive(s.ctx, nil) + s.logger.Debug("Received migrate signal", + "namespace", s.State.Namespace, + "schedule-id", s.State.ScheduleId, + ) + s.State.PendingMigration = true +} + +func (s *scheduler) executeMigration() error { + s.logger.Info("Starting migration to CHASM", + "namespace", s.State.Namespace, + "schedule-id", s.State.ScheduleId, + ) + s.metrics.WithTags(map[string]string{ + metrics.ScheduleMigrationDirectionTag: metrics.ScheduleMigrationDirectionToChasm, + }).Counter(metrics.ScheduleMigrationStarted.Name()).Inc(1) + + workflowInfo := workflow.GetInfo(s.ctx) + + //nolint:staticcheck // SA1019 Migration needs raw proto format, not typed search attributes. + req := migration.LegacyToCreateFromMigrationStateRequest( + s.Schedule, + s.Info, + s.State, + workflowInfo.SearchAttributes, + workflowInfo.Memo, + s.now(), + ) + migrateOptions := workflow.LocalActivityOptions{ + ScheduleToCloseTimeout: 10 * time.Minute, + StartToCloseTimeout: 5 * time.Second, + RetryPolicy: &temporal.RetryPolicy{ + MaximumAttempts: 1, + }, + } + return workflow.ExecuteLocalActivity( + workflow.WithLocalActivityOptions(s.ctx, migrateOptions), + s.a.MigrateScheduleToChasm, + req). + Get(s.ctx, nil) +} + func (s *scheduler) processSignals() bool { scheduleChanged := false if s.pendingPatch != nil { @@ -1043,7 +1126,7 @@ func (s *scheduler) handleListMatchingTimesQuery(req *workflowservice.ListSchedu var out []*timestamppb.Timestamp t1 := timestamp.TimeValue(req.StartTime) - for i := 0; i < maxListMatchingTimesCount; i++ { + for range maxListMatchingTimesCount { // don't need to call GetNextTime in SideEffect because this is just a query t1 = s.cspec.GetNextTime(s.jitterSeed(), t1).Next if t1.IsZero() || t1.After(timestamp.TimeValue(req.EndTime)) { @@ -1149,7 +1232,7 @@ func (s *scheduler) updateMemoAndSearchAttributes() { // marshal manually to get proto encoding (default dataconverter will use json) newInfoBytes, err := newInfo.Marshal() if err == nil { - err = workflow.UpsertMemo(s.ctx, map[string]interface{}{ + err = workflow.UpsertMemo(s.ctx, map[string]any{ MemoFieldInfo: newInfoBytes, }) } @@ -1164,7 +1247,7 @@ func (s *scheduler) updateMemoAndSearchAttributes() { if currentPausedPayload == nil || payload.Decode(currentPausedPayload, ¤tPaused) != nil || currentPaused != s.Schedule.State.Paused { - err := workflow.UpsertSearchAttributes(s.ctx, map[string]interface{}{ + err := workflow.UpsertSearchAttributes(s.ctx, map[string]any{ //nolint:staticcheck // SA1019: untyped search attributes required here sadefs.TemporalSchedulePaused: s.Schedule.State.Paused, }) if err != nil { @@ -1182,8 +1265,13 @@ func (s *scheduler) checkConflict(token int64) error { func (s *scheduler) updateTweakables() { // Use MutableSideEffect so that we can change the defaults without breaking determinism. - get := func(ctx workflow.Context) interface{} { return CurrentTweakablePolicies } - eq := func(a, b interface{}) bool { return a.(TweakablePolicies) == b.(TweakablePolicies) } + enableCHASMMigration := s.enableCHASMMigration + get := func(ctx workflow.Context) any { + p := CurrentTweakablePolicies + p.EnableCHASMMigration = enableCHASMMigration + return p + } + eq := func(a, b any) bool { return a.(TweakablePolicies) == b.(TweakablePolicies) } if err := workflow.MutableSideEffect(s.ctx, "tweakables", get, eq).Get(&s.tweakables); err != nil { panic("can't decode TweakablePolicies:" + err.Error()) } @@ -1336,7 +1424,7 @@ func (s *scheduler) startWorkflow( // Set scheduleToCloseTimeout based on catchup window, which is the latest time that it's // acceptable to start this workflow. For manual starts (trigger immediately or backfill), // catch up window doesn't apply, so just use 60s. - options := defaultLocalActivityOptions + options := defaultLocalActivityOptions() if start.Manual { options.ScheduleToCloseTimeout = 60 * time.Second } else { @@ -1452,7 +1540,7 @@ func (s *scheduler) addSearchAttributes( } func (s *scheduler) refreshWorkflows(executions []*commonpb.WorkflowExecution) { - ctx := workflow.WithLocalActivityOptions(s.ctx, defaultLocalActivityOptions) + ctx := workflow.WithLocalActivityOptions(s.ctx, defaultLocalActivityOptions()) futures := make([]workflow.Future, len(executions)) for i, ex := range executions { req := &schedulespb.WatchWorkflowRequest{ @@ -1493,7 +1581,7 @@ func (s *scheduler) startLongPollWatcher(ex *commonpb.WorkflowExecution) { } func (s *scheduler) cancelWorkflow(ex *commonpb.WorkflowExecution) { - ctx := workflow.WithLocalActivityOptions(s.ctx, defaultLocalActivityOptions) + ctx := workflow.WithLocalActivityOptions(s.ctx, defaultLocalActivityOptions()) areq := &schedulespb.CancelWorkflowRequest{ RequestId: s.newUUIDString(), Identity: s.identity(), @@ -1511,7 +1599,7 @@ func (s *scheduler) cancelWorkflow(ex *commonpb.WorkflowExecution) { } func (s *scheduler) terminateWorkflow(ex *commonpb.WorkflowExecution) { - ctx := workflow.WithLocalActivityOptions(s.ctx, defaultLocalActivityOptions) + ctx := workflow.WithLocalActivityOptions(s.ctx, defaultLocalActivityOptions()) areq := &schedulespb.TerminateWorkflowRequest{ RequestId: s.newUUIDString(), Identity: s.identity(), @@ -1558,7 +1646,7 @@ func (s *scheduler) getLastEvent() time.Time { func (s *scheduler) newUUIDString() string { if len(s.uuidBatch) == 0 { - panicIfErr(workflow.SideEffect(s.ctx, func(ctx workflow.Context) interface{} { + panicIfErr(workflow.SideEffect(s.ctx, func(ctx workflow.Context) any { out := make([]string, 10) for i := range out { out[i] = uuid.NewString() diff --git a/service/worker/scheduler/workflow_test.go b/service/worker/scheduler/workflow_test.go index eab2768da6d..2d6d43bbc2e 100644 --- a/service/worker/scheduler/workflow_test.go +++ b/service/worker/scheduler/workflow_test.go @@ -20,6 +20,7 @@ import ( "go.temporal.io/sdk/testsuite" "go.temporal.io/sdk/workflow" schedulespb "go.temporal.io/server/api/schedule/v1" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common/payload" "go.temporal.io/server/common/payloads" "go.temporal.io/server/common/searchattribute/sadefs" @@ -1958,7 +1959,7 @@ func (s *workflowSuite) TestLotsOfIterations() { callBackRangeStartTime := time.Date(2022, 5, i, 0, 0, 0, 0, time.UTC) // add/process maxRuns schedules - for j := 0; j < maxRuns; j++ { + for j := range maxRuns { runStartTime := time.Date(2022, 5, i, j, 27+j%2, 0, 0, time.UTC) runs = append(runs, workflowRun{ id: "myid-" + runStartTime.Format(time.RFC3339), @@ -2269,3 +2270,311 @@ func (s *workflowSuite) TestCANBySignal() { s.True(s.env.IsWorkflowCompleted()) s.True(workflow.IsContinueAsNewError(s.env.GetWorkflowError())) } + +func (s *workflowSuite) TestMigrateSuccess() { + // Mock MigrateSchedule activity to succeed. + s.env.OnActivity(new(activities).MigrateScheduleToChasm, mock.Anything, mock.Anything).Once().Return(nil) + + // Send migrate signal after the first iteration. + s.env.RegisterDelayedCallback(func() { + s.env.SignalWorkflow(SignalNameMigrateToChasm, nil) + }, 1*time.Second) + + CurrentTweakablePolicies.IterationsBeforeContinueAsNew = 100 + s.env.SetStartTime(baseStartTime) + s.env.ExecuteWorkflow(SchedulerWorkflow, &schedulespb.StartScheduleArgs{ + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{ + Interval: durationpb.New(1 * time.Hour), + }}, + }, + Action: s.defaultAction("myid"), + }, + State: &schedulespb.InternalState{ + Namespace: "myns", + NamespaceId: "mynsid", + ScheduleId: "myschedule", + ConflictToken: InitialConflictToken, + }, + }) + + // Workflow should complete successfully (not CAN) after migration. + s.True(s.env.IsWorkflowCompleted()) + s.NoError(s.env.GetWorkflowError()) +} + +func (s *workflowSuite) TestMigrateFailure() { + // Mock MigrateSchedule activity to always fail. Migration is retried + // each iteration since PendingMigration is persisted in State. + migrateCalls := 0 + s.env.OnActivity(new(activities).MigrateScheduleToChasm, mock.Anything, mock.Anything).Return( + func(context.Context, *schedulerpb.CreateFromMigrationStateRequest) error { + migrateCalls++ + return errors.New("migration failed") + }) + + // Send migrate signal after the first iteration. + s.env.RegisterDelayedCallback(func() { + s.env.SignalWorkflow(SignalNameMigrateToChasm, nil) + }, 1*time.Second) + + // After ~5 iterations (5 hours of simulated time), the workflow should + // still be running -- migration failed but the scheduler continues. + stillRunning := false + s.env.RegisterDelayedCallback(func() { + stillRunning = !s.env.IsWorkflowCompleted() + }, 5*time.Hour) + + CurrentTweakablePolicies.IterationsBeforeContinueAsNew = 100 + s.env.SetStartTime(baseStartTime) + s.env.ExecuteWorkflow(SchedulerWorkflow, &schedulespb.StartScheduleArgs{ + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{ + Interval: durationpb.New(1 * time.Hour), + }}, + }, + Action: s.defaultAction("myid"), + }, + State: &schedulespb.InternalState{ + Namespace: "myns", + NamespaceId: "mynsid", + ScheduleId: "myschedule", + ConflictToken: InitialConflictToken, + }, + }) + + s.True(stillRunning, "workflow should still be running after migration failure") + // Migration is attempted every iteration after the signal. The first + // iteration runs before the signal, so total calls = iterations - 1. + s.Equal(99, migrateCalls, "migration should be retried each iteration") + + // Verify PendingMigration is persisted in CAN state. + var canErr *workflow.ContinueAsNewError + s.Require().ErrorAs(s.env.GetWorkflowError(), &canErr) + var canArgs schedulespb.StartScheduleArgs + s.Require().NoError(payloads.Decode(canErr.Input, &canArgs)) + s.True(canArgs.State.PendingMigration, "PendingMigration should be set in CAN state") +} + +func (s *workflowSuite) TestMigrateFailureThenRetrySuccess() { + // First attempt fails, second attempt succeeds (on next run loop iteration). + migrateCalls := 0 + s.env.OnActivity(new(activities).MigrateScheduleToChasm, mock.Anything, mock.Anything).Return( + func(context.Context, *schedulerpb.CreateFromMigrationStateRequest) error { + migrateCalls++ + if migrateCalls == 1 { + return errors.New("migration failed") + } + return nil + }) + + // Send migrate signal after the first iteration. + s.env.RegisterDelayedCallback(func() { + s.env.SignalWorkflow(SignalNameMigrateToChasm, nil) + }, 1*time.Second) + + CurrentTweakablePolicies.IterationsBeforeContinueAsNew = 100 + s.env.SetStartTime(baseStartTime) + s.env.ExecuteWorkflow(SchedulerWorkflow, &schedulespb.StartScheduleArgs{ + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{ + Interval: durationpb.New(1 * time.Hour), + }}, + }, + Action: s.defaultAction("myid"), + }, + State: &schedulespb.InternalState{ + Namespace: "myns", + NamespaceId: "mynsid", + ScheduleId: "myschedule", + ConflictToken: InitialConflictToken, + }, + }) + + // Migration should succeed on second attempt without a new signal, + // proving PendingMigration persists across run loop iterations. + s.True(s.env.IsWorkflowCompleted()) + s.Require().NoError(s.env.GetWorkflowError()) + s.Equal(2, migrateCalls, "migration should fail once then succeed on retry") +} + +func (s *workflowSuite) TestMigrateFailureThenSignal() { + // Mock MigrateSchedule activity to always fail. + migrateCalls := 0 + s.env.OnActivity(new(activities).MigrateScheduleToChasm, mock.Anything, mock.Anything).Return( + func(context.Context, *schedulerpb.CreateFromMigrationStateRequest) error { + migrateCalls++ + return errors.New("migration failed") + }) + + // Send migrate signal after the first iteration. + s.env.RegisterDelayedCallback(func() { + s.env.SignalWorkflow(SignalNameMigrateToChasm, nil) + }, 1*time.Second) + // After migration failure, send a pause patch and verify it's processed, + // proving the workflow kept running and still handles signals. + s.env.RegisterDelayedCallback(func() { + s.env.SignalWorkflow(SignalNamePatch, &schedulepb.SchedulePatch{ + Pause: "paused after failed migration", + }) + }, 5*time.Second) + + stillRunning := false + s.env.RegisterDelayedCallback(func() { + desc := s.describe() + s.True(desc.Schedule.State.Paused) + s.Equal("paused after failed migration", desc.Schedule.State.Notes) + stillRunning = !s.env.IsWorkflowCompleted() + // Send force-CAN to unblock the workflow (paused with no timer). + s.env.SignalWorkflow(SignalNameForceCAN, nil) + }, 10*time.Second) + + CurrentTweakablePolicies.IterationsBeforeContinueAsNew = 100 + s.env.SetStartTime(baseStartTime) + s.env.ExecuteWorkflow(SchedulerWorkflow, &schedulespb.StartScheduleArgs{ + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{ + Interval: durationpb.New(1 * time.Hour), + }}, + }, + Action: s.defaultAction("myid"), + }, + State: &schedulespb.InternalState{ + Namespace: "myns", + NamespaceId: "mynsid", + ScheduleId: "myschedule", + ConflictToken: InitialConflictToken, + }, + }) + + s.True(stillRunning, "workflow should still be running after migration failure") + // 2 calls: the migrate signal (1s) and pause signal (5s) each trigger an + // iteration with PendingMigration=true. The force-CAN signal (10s) causes + // the loop to break before migration runs on that iteration. + s.Equal(2, migrateCalls, "migration should be retried on subsequent iterations") + + // Verify PendingMigration is persisted in CAN state. + var canErr *workflow.ContinueAsNewError + s.Require().ErrorAs(s.env.GetWorkflowError(), &canErr) + var canArgs schedulespb.StartScheduleArgs + s.Require().NoError(payloads.Decode(canErr.Input, &canArgs)) + s.True(canArgs.State.PendingMigration, "PendingMigration should be set in CAN state") +} + +func (s *workflowSuite) TestMigrateDynamicConfig() { + // Enable migration by threading enableCHASMMigration=true through the closure (race-safe). + // Mock MigrateSchedule activity to succeed. + s.env.OnActivity(new(activities).MigrateScheduleToChasm, mock.Anything, mock.Anything).Once().Return(nil) + + prevTweakables := CurrentTweakablePolicies + CurrentTweakablePolicies.IterationsBeforeContinueAsNew = 100 + defer func() { CurrentTweakablePolicies = prevTweakables }() + + s.env.SetStartTime(baseStartTime) + s.env.ExecuteWorkflow(func(ctx workflow.Context, args *schedulespb.StartScheduleArgs) error { + return schedulerWorkflowWithSpecBuilder(ctx, args, NewSpecBuilder(), true) + }, &schedulespb.StartScheduleArgs{ + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{ + Interval: durationpb.New(1 * time.Hour), + }}, + }, + Action: s.defaultAction("myid"), + }, + State: &schedulespb.InternalState{ + Namespace: "myns", + NamespaceId: "mynsid", + ScheduleId: "myschedule", + ConflictToken: InitialConflictToken, + }, + }) + + // Workflow should complete successfully (not CAN) after migration triggered by tweakable. + s.True(s.env.IsWorkflowCompleted()) + s.NoError(s.env.GetWorkflowError()) +} + +func (s *workflowSuite) TestMigrateDynamicConfigFailure() { + // Enable migration by threading enableCHASMMigration=true through the closure (race-safe), + // but activity fails. + migrateCalls := 0 + s.env.OnActivity(new(activities).MigrateScheduleToChasm, mock.Anything, mock.Anything).Return( + func(context.Context, *schedulerpb.CreateFromMigrationStateRequest) error { + migrateCalls++ + return errors.New("migration failed") + }) + + prevTweakables := CurrentTweakablePolicies + CurrentTweakablePolicies.IterationsBeforeContinueAsNew = 5 + defer func() { CurrentTweakablePolicies = prevTweakables }() + + s.env.SetStartTime(baseStartTime) + s.env.ExecuteWorkflow(func(ctx workflow.Context, args *schedulespb.StartScheduleArgs) error { + return schedulerWorkflowWithSpecBuilder(ctx, args, NewSpecBuilder(), true) + }, &schedulespb.StartScheduleArgs{ + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{ + Interval: durationpb.New(1 * time.Hour), + }}, + }, + Action: s.defaultAction("myid"), + }, + State: &schedulespb.InternalState{ + Namespace: "myns", + NamespaceId: "mynsid", + ScheduleId: "myschedule", + ConflictToken: InitialConflictToken, + }, + }) + + // Workflow should CAN after all iterations, not terminate. + s.True(s.env.IsWorkflowCompleted()) + s.True(workflow.IsContinueAsNewError(s.env.GetWorkflowError())) + // Migration attempted every iteration. + s.Equal(5, migrateCalls) + + // PendingMigration should be preserved in CAN state. + var canErr *workflow.ContinueAsNewError + s.Require().ErrorAs(s.env.GetWorkflowError(), &canErr) + var canArgs schedulespb.StartScheduleArgs + s.Require().NoError(payloads.Decode(canErr.Input, &canArgs)) + s.True(canArgs.State.PendingMigration, "PendingMigration should be set in CAN state") +} + +func (s *workflowSuite) TestMigrateDynamicConfigDisabledNoMigration() { + // Ensure migration does NOT happen when EnableCHASMMigration is false (default). + prevTweakables := CurrentTweakablePolicies + CurrentTweakablePolicies.EnableCHASMMigration = false + defer func() { CurrentTweakablePolicies = prevTweakables }() + + // No activity mock registered -- if migration is attempted, the test will fail. + + CurrentTweakablePolicies.IterationsBeforeContinueAsNew = 3 + s.env.SetStartTime(baseStartTime) + s.env.ExecuteWorkflow(SchedulerWorkflow, &schedulespb.StartScheduleArgs{ + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{ + Interval: durationpb.New(1 * time.Hour), + }}, + }, + Action: s.defaultAction("myid"), + }, + State: &schedulespb.InternalState{ + Namespace: "myns", + NamespaceId: "mynsid", + ScheduleId: "myschedule", + ConflictToken: InitialConflictToken, + }, + }) + + // Workflow should CAN normally without attempting migration. + s.True(s.env.IsWorkflowCompleted()) + s.True(workflow.IsContinueAsNewError(s.env.GetWorkflowError())) +} diff --git a/service/worker/service.go b/service/worker/service.go index 869a6e1a808..3db475993d2 100644 --- a/service/worker/service.go +++ b/service/worker/service.go @@ -7,6 +7,7 @@ import ( "go.temporal.io/api/serviceerror" sdkworker "go.temporal.io/sdk/worker" "go.temporal.io/server/api/matchingservice/v1" + replicationspb "go.temporal.io/server/api/replication/v1" "go.temporal.io/server/client" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/config" @@ -35,6 +36,8 @@ import ( // ServiceName is the name used for the worker service health check. const ServiceName = "temporal.api.workflowservice.v1.WorkerService" +type CustomTaskHandler func(ctx context.Context, task *replicationspb.ReplicationTask) error + type ( // Service represents the temporal-worker service. This service hosts all background processing needed for temporal cluster: // Replicator: Handles applying replication tasks generated by remote clusters. @@ -62,10 +65,11 @@ type ( config *Config workerManager *workerManager - perNamespaceWorkerManager *perNamespaceWorkerManager + perNamespaceWorkerManager *PerNamespaceWorkerManager scanner *scanner.Scanner matchingClient matchingservice.MatchingServiceClient namespaceReplicationTaskExecutor nsreplication.TaskExecutor + customTaskHandler func(ctx context.Context, task *replicationspb.ReplicationTask) error server *grpc.Server grpcListener net.Listener @@ -121,7 +125,7 @@ func NewService( taskManager persistence.TaskManager, historyClient resource.HistoryClient, workerManager *workerManager, - perNamespaceWorkerManager *perNamespaceWorkerManager, + perNamespaceWorkerManager *PerNamespaceWorkerManager, visibilityManager manager.VisibilityManager, matchingClient resource.MatchingClient, namespaceReplicationTaskExecutor nsreplication.TaskExecutor, @@ -169,6 +173,11 @@ func NewService( return s, nil } +// SetCustomTaskHandler sets an optional handler for custom replication task types. +func (s *Service) SetCustomTaskHandler(handler CustomTaskHandler) { + s.customTaskHandler = handler +} + // NewConfig builds the new Config for worker service func NewConfig( dc *dynamicconfig.Collection, @@ -364,6 +373,9 @@ func (s *Service) startReplicator() { s.matchingClient, s.namespaceRegistry, ) + if s.customTaskHandler != nil { + msgReplicator.WithCustomTaskHandler(s.customTaskHandler) + } msgReplicator.Start() } diff --git a/service/worker/workerdeployment/activities.go b/service/worker/workerdeployment/activities.go index a76e2f72589..6ae8a6cf4d9 100644 --- a/service/worker/workerdeployment/activities.go +++ b/service/worker/workerdeployment/activities.go @@ -3,11 +3,15 @@ package workerdeployment import ( "cmp" "context" + "errors" "sync" + computepb "go.temporal.io/api/compute/v1" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" updatepb "go.temporal.io/api/update/v1" "go.temporal.io/sdk/activity" + "go.temporal.io/sdk/temporal" deploymentspb "go.temporal.io/server/api/deployment/v1" "go.temporal.io/server/api/matchingservice/v1" "go.temporal.io/server/common/namespace" @@ -271,6 +275,26 @@ func (a *Activities) StartWorkerDeploymentVersionWorkflow( ) error { logger := activity.GetLogger(ctx) logger.Info("starting worker deployment version workflow", "deploymentName", input.DeploymentName, "buildID", input.BuildId) - identity := "deployment workflow " + activity.GetInfo(ctx).WorkflowExecution.ID - return a.WorkerDeploymentClient.StartWorkerDeploymentVersion(ctx, a.namespace, input.DeploymentName, input.BuildId, identity, input.RequestId) + startIdentity := "deployment workflow " + activity.GetInfo(ctx).WorkflowExecution.ID + return a.WorkerDeploymentClient.StartWorkerDeploymentVersion(ctx, a.namespace, input.DeploymentName, input.BuildId, startIdentity, input.RequestId, input.GetIdentity(), input.GetComputeConfig()) +} + +func (a *Activities) UpdateWorkerControllerInstanceFromDeployment(ctx context.Context, input *deploymentspb.UpdateWorkerControllerInstanceInput) (*computepb.ComputeConfigSummary, error) { + upserts := scalingGroupUpdatesToWCI(input.GetUpsertScalingGroups()) + resp, err := a.WorkerControllerInstanceClient.UpdateWorkerControllerInstance(ctx, a.namespace, input.GetVersion(), nil, input.GetIdentity(), upserts, input.GetRemoveScalingGroups()) + if err != nil { + var invalidArgs *serviceerror.InvalidArgument + if errors.As(err, &invalidArgs) { + return nil, temporal.NewApplicationError(err.Error(), errInvalidComputeConfig) + } + return nil, err + } + if resp == nil { + return nil, nil + } + return wciSpecToComputeConfigSummary(resp.Spec), nil +} + +func (a *Activities) DeleteWorkerControllerInstanceFromDeployment(ctx context.Context, input *deploymentspb.DeleteWorkerControllerInstanceInput) error { + return a.WorkerControllerInstanceClient.DeleteWorkerControllerInstance(ctx, a.namespace, input.GetVersion(), input.GetIdentity()) } diff --git a/service/worker/workerdeployment/client.go b/service/worker/workerdeployment/client.go index 0e19bcc150f..747d293503d 100644 --- a/service/worker/workerdeployment/client.go +++ b/service/worker/workerdeployment/client.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "sort" + "strings" "time" "github.com/dgryski/go-farm" "github.com/google/uuid" commonpb "go.temporal.io/api/common/v1" + computepb "go.temporal.io/api/compute/v1" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" querypb "go.temporal.io/api/query/v1" @@ -17,6 +19,7 @@ import ( taskqueuepb "go.temporal.io/api/taskqueue/v1" updatepb "go.temporal.io/api/update/v1" "go.temporal.io/api/workflowservice/v1" + wciclient "go.temporal.io/auto-scaled-workers/wci/client" "go.temporal.io/sdk/temporal" deploymentspb "go.temporal.io/server/api/deployment/v1" "go.temporal.io/server/api/historyservice/v1" @@ -34,7 +37,6 @@ import ( "go.temporal.io/server/common/searchattribute/sadefs" "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/worker_versioning" - "google.golang.org/protobuf/types/known/timestamppb" ) type Client interface { @@ -60,6 +62,24 @@ type Client interface { deploymentName string, ) (*deploymentpb.WorkerDeploymentInfo, []byte, error) + CreateWorkerDeployment( + ctx context.Context, + namespaceEntry *namespace.Namespace, + deploymentName string, + identity string, + requestID string, + ) ([]byte, error) + + CreateWorkerDeploymentVersion( + ctx context.Context, + namespaceEntry *namespace.Namespace, + deploymentName string, + buildID string, + identity string, + requestID string, + computeConfig *computepb.ComputeConfig, + ) error + SetCurrentVersion( ctx context.Context, namespaceEntry *namespace.Namespace, @@ -114,6 +134,16 @@ type Client interface { identity string, ) (*deploymentpb.VersionMetadata, error) + UpdateVersionComputeConfig( + ctx context.Context, + namespaceEntry *namespace.Namespace, + version *deploymentpb.WorkerDeploymentVersion, + upsertScalingGroups map[string]*computepb.ComputeConfigScalingGroupUpdate, + removeScalingGroups []string, + identity string, + requestID string, + ) error + SetManager( ctx context.Context, namespaceEntry *namespace.Namespace, @@ -134,9 +164,8 @@ type Client interface { StartWorkerDeploymentVersion( ctx context.Context, namespaceEntry *namespace.Namespace, - deploymentName, buildID string, - identity string, - requestID string, + deploymentName, buildID, identity, requestID, modifierIdentity string, + computeConfig *computepb.ComputeConfigSummary, ) error // Used internally by the Worker Deployment workflow in its SyncWorkerDeploymentVersion Activity @@ -181,6 +210,15 @@ type Client interface { namespaceEntry *namespace.Namespace, deploymentName, buildID string, ) error + + ValidateComputeConfig( + ctx context.Context, + namespaceEntry *namespace.Namespace, + version *deploymentpb.WorkerDeploymentVersion, + upsertScalingGroups map[string]*computepb.ComputeConfigScalingGroupUpdate, + removeScalingGroups []string, + identity string, + ) error } type ErrRegister struct{ error } @@ -193,6 +231,7 @@ type ClientImpl struct { historyClient historyservice.HistoryServiceClient visibilityManager manager.VisibilityManager matchingClient resource.MatchingClient + workerControllerInstanceClient wciclient.Client maxIDLengthLimit dynamicconfig.IntPropertyFn visibilityMaxPageSize dynamicconfig.IntPropertyFnWithNamespaceFilter maxTaskQueuesInDeploymentVersion dynamicconfig.IntPropertyFnWithNamespaceFilter @@ -291,7 +330,7 @@ func (d *ClientImpl) RegisterTaskQueueWorker( // Creating request ID out of build ID + TQ name + TQ type. Many updates may come from multiple // matching partitions, we do not want them to create new update requests. - requestID := fmt.Sprintf("reg-ver-%v-%v-%d", farm.Fingerprint64([]byte(buildId)), farm.Fingerprint64([]byte(taskQueueName)), taskQueueType) + requestID := fmt.Sprintf("%s%v-%v-%d", AutoCreateRequestIDPrefix, farm.Fingerprint64([]byte(buildId)), farm.Fingerprint64([]byte(taskQueueName)), taskQueueType) updatePayload, err := sdk.PreferProtoDataConverter.ToPayloads(&deploymentspb.RegisterWorkerInWorkerDeploymentArgs{ TaskQueueName: taskQueueName, @@ -306,8 +345,13 @@ func (d *ClientImpl) RegisterTaskQueueWorker( return err } + err = validateVersionWfParams(worker_versioning.WorkerDeploymentBuildIDFieldName, buildId, d.maxIDLengthLimit()) + if err != nil { + return err + } + // starting and updating the deployment version workflow, which in turn starts a deployment workflow. - outcome, err := d.updateWithStartWorkerDeployment(ctx, namespaceEntry, deploymentName, buildId, &updatepb.Request{ + outcome, err := d.updateWithStartWorkerDeployment(ctx, namespaceEntry, deploymentName, &updatepb.Request{ Input: &updatepb.Input{Name: RegisterWorkerInWorkerDeployment, Args: updatePayload}, Meta: &updatepb.Meta{UpdateId: requestID, Identity: identity}, }, identity, requestID, d.getSyncBatchSize()) @@ -378,54 +422,34 @@ func (d *ClientImpl) DescribeVersion( return nil, nil, err } - workflowID := GenerateVersionWorkflowID(deploymentName, buildID) - - req := &historyservice.QueryWorkflowRequest{ - NamespaceId: namespaceEntry.ID().String(), - Request: &workflowservice.QueryWorkflowRequest{ - Namespace: namespaceEntry.Name().String(), - Execution: &commonpb.WorkflowExecution{ - WorkflowId: workflowID, - }, - Query: &querypb.WorkflowQuery{QueryType: QueryDescribeVersion}, - }, - } - - res, err := d.queryWorkflowWithRetry(ctx, req) - + versionState, err := d.queryVersionState(ctx, namespaceEntry, deploymentName, buildID) if err != nil { var notFound *serviceerror.NotFound if errors.As(err, ¬Found) { return nil, nil, serviceerror.NewNotFound("Worker Deployment Version not found") } - var queryFailed *serviceerror.QueryFailed - if errors.As(err, &queryFailed) && queryFailed.Error() == errVersionDeleted { - return nil, nil, serviceerror.NewNotFoundf(ErrWorkerDeploymentVersionNotFound, buildID, deploymentName) - } return nil, nil, err } - if rej := res.GetResponse().GetQueryRejected(); rej != nil { - // This should not happen - return nil, nil, serviceerror.NewInternalf("describe deployment query rejected with status %s", rej.GetStatus()) - } - - if res.GetResponse().GetQueryResult() == nil { - return nil, nil, serviceerror.NewInternal("Did not receive deployment info") - } - - var queryResponse deploymentspb.QueryDescribeVersionResponse - err = sdk.PreferProtoDataConverter.FromPayloads(res.GetResponse().GetQueryResult(), &queryResponse) + tqInfos, err := d.getTaskQueueDetails(ctx, namespaceEntry.ID(), versionState, reportTaskQueueStats) if err != nil { return nil, nil, err } - tqInfos, err := d.getTaskQueueDetails(ctx, namespaceEntry.ID(), queryResponse.VersionState, reportTaskQueueStats) + versionInfo := versionStateToVersionInfo(versionState, tqInfos) + + apiVersion := worker_versioning.ExternalWorkerDeploymentVersionFromVersion(versionState.Version) + wciDesc, _, err := d.workerControllerInstanceClient.DescribeWorkerControllerInstance(ctx, namespaceEntry, apiVersion) if err != nil { - return nil, nil, err + // WCI may not exist if no compute config was ever set. + var notFound *serviceerror.NotFound + if !errors.As(err, ¬Found) { + return nil, nil, err + } + } else { + versionInfo.ComputeConfig = wciSpecToComputeConfig(wciDesc.Spec) } - versionInfo := versionStateToVersionInfo(queryResponse.VersionState, tqInfos) return versionInfo, tqInfos, nil } @@ -481,6 +505,51 @@ func (d *ClientImpl) UpdateVersionMetadata( return res.Metadata, nil } +func (d *ClientImpl) UpdateVersionComputeConfig( + ctx context.Context, + namespaceEntry *namespace.Namespace, + version *deploymentpb.WorkerDeploymentVersion, + upsertScalingGroups map[string]*computepb.ComputeConfigScalingGroupUpdate, + removeScalingGroups []string, + identity string, + requestID string, +) (retErr error) { + //revive:disable-next-line:defer + defer d.convertAndRecordError("UpdateVersionComputeConfig", version.GetDeploymentName(), &retErr, namespaceEntry.Name(), version.GetBuildId(), identity)() + + updatePayload, err := sdk.PreferProtoDataConverter.ToPayloads(&deploymentspb.UpdateComputeConfigArgs{ + Identity: identity, + RequestId: requestID, + UpsertScalingGroups: upsertScalingGroups, + RemoveScalingGroups: removeScalingGroups, + }) + if err != nil { + return err + } + + workflowID := GenerateVersionWorkflowID(version.GetDeploymentName(), version.GetBuildId()) + outcome, err := updateWorkflow(ctx, d.historyClient, namespaceEntry, workflowID, &updatepb.Request{ + Input: &updatepb.Input{Name: UpdateVersionComputeConfig, Args: updatePayload}, + Meta: &updatepb.Meta{UpdateId: "_update_compute_config_" + requestID, Identity: identity}, + }) + if err != nil { + var notFound *serviceerror.NotFound + if errors.As(err, ¬Found) { + return serviceerror.NewNotFound(fmt.Sprintf(ErrWorkerDeploymentVersionNotFound, version.GetBuildId(), version.GetDeploymentName())) + } + return err + } + + if failure := outcome.GetFailure(); failure != nil { + if failure.GetApplicationFailureInfo().GetType() == errInvalidComputeConfig { + return serviceerror.NewInvalidArgument(failure.GetMessage()) + } + return serviceerror.NewInternalf("update version compute config failed: %s", failure.Message) + } + + return nil +} + func (d *ClientImpl) DescribeWorkerDeployment( ctx context.Context, namespaceEntry *namespace.Namespace, @@ -543,6 +612,54 @@ func (d *ClientImpl) DescribeWorkerDeployment( return dInfo, queryResponse.GetState().GetConflictToken(), nil } +func (d *ClientImpl) queryCreateRequestID( + ctx context.Context, + namespaceEntry *namespace.Namespace, + deploymentName string, +) (result *deploymentspb.CreateRequestIDQueryResponse, retErr error) { + deploymentWorkflowID := GenerateDeploymentWorkflowID(deploymentName) + + req := &historyservice.QueryWorkflowRequest{ + NamespaceId: namespaceEntry.ID().String(), + Request: &workflowservice.QueryWorkflowRequest{ + Namespace: namespaceEntry.Name().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: deploymentWorkflowID, + }, + Query: &querypb.WorkflowQuery{QueryType: QueryCreateRequestID}, + }, + } + + res, err := d.queryWorkflowWithRetry(ctx, req) + if err != nil { + var notFound *serviceerror.NotFound + if errors.As(err, ¬Found) { + return nil, err + } + var queryFailed *serviceerror.QueryFailed + if errors.As(err, &queryFailed) && queryFailed.Error() == errDeploymentDeleted { + return nil, serviceerror.NewNotFound(errDeploymentDeleted) + } + return nil, err + } + + if rej := res.GetResponse().GetQueryRejected(); rej != nil { + return nil, serviceerror.NewInternalf("create request id query rejected with status %s", rej.GetStatus()) + } + + if res.GetResponse().GetQueryResult() == nil { + return nil, serviceerror.NewInternal("Did not receive create request id query result") + } + + var queryResponse deploymentspb.CreateRequestIDQueryResponse + err = sdk.PreferProtoDataConverter.FromPayloads(res.GetResponse().GetQueryResult(), &queryResponse) + if err != nil { + return nil, err + } + + return &queryResponse, nil +} + func (d *ClientImpl) workerDeploymentExists( ctx context.Context, namespaceEntry *namespace.Namespace, @@ -669,19 +786,25 @@ func (d *ClientImpl) SetCurrentVersion( return nil, err } - // Generating a new updateID and requestID for each request. No-ops are handled by the worker-deployment workflow. updateID := uuid.NewString() requestID := uuid.NewString() var outcome *updatepb.Outcome if allowNoPollers { - // we want to start the Worker Deployment workflow if it hasn't been started by a poller + if b := versionObj.GetBuildId(); b != "" { + // Empty build id is accepted for unset. + err = validateVersionWfParams(worker_versioning.WorkerDeploymentBuildIDFieldName, versionObj.GetBuildId(), d.maxIDLengthLimit()) + if err != nil { + return nil, err + } + } outcome, err = d.updateWithStartWorkerDeployment( ctx, namespaceEntry, deploymentName, - versionObj.GetBuildId(), &updatepb.Request{ + // we want to start the Worker Deployment workflow if it hasn't been started by a poller + // Generating a new updateID and requestID for each request. No-ops are handled by the worker-deployment workflow. Input: &updatepb.Input{Name: SetCurrentVersion, Args: updatePayload}, Meta: &updatepb.Meta{UpdateId: updateID, Identity: identity}, }, @@ -788,11 +911,17 @@ func (d *ClientImpl) SetRampingVersion( var outcome *updatepb.Outcome if allowNoPollers { // we want to start the Worker Deployment workflow if it hasn't been started by a poller + if b := versionObj.GetBuildId(); b != "" { + // Empty build id is accepted for unset. + err = validateVersionWfParams(worker_versioning.WorkerDeploymentBuildIDFieldName, versionObj.GetBuildId(), d.maxIDLengthLimit()) + if err != nil { + return nil, err + } + } outcome, err = d.updateWithStartWorkerDeployment( ctx, namespaceEntry, deploymentName, - versionObj.GetBuildId(), &updatepb.Request{ Input: &updatepb.Input{Name: SetRampingVersion, Args: updatePayload}, Meta: &updatepb.Meta{UpdateId: updateID, Identity: identity}, @@ -970,6 +1099,215 @@ func (d *ClientImpl) DeleteWorkerDeployment( return nil } +func (d *ClientImpl) CreateWorkerDeployment( + ctx context.Context, + namespaceEntry *namespace.Namespace, + deploymentName string, + identity string, + requestID string, +) (_ []byte, retErr error) { + //revive:disable-next-line:defer + defer d.convertAndRecordError("CreateWorkerDeployment", deploymentName, &retErr, namespaceEntry.Name(), identity)() + + // Validate deployment name + err := validateVersionWfParams(worker_versioning.WorkerDeploymentNameFieldName, deploymentName, d.maxIDLengthLimit()) + if err != nil { + return nil, err + } + + conflictToken, err := d.ensureWorkerDeploymentDoesNotExist(ctx, namespaceEntry, deploymentName, requestID) + if err != nil { + // WD already exists, or other errors + return nil, err + } + if conflictToken != nil { + // WD exists with matching request ID, returning success with conflict token + return conflictToken, nil + } + + // Check resource limits + count, err := d.countWorkerDeployments(ctx, namespaceEntry) + if err != nil { + return nil, err + } + limit := d.maxDeployments(namespaceEntry.Name().String()) + if count >= int64(limit) { + return nil, newResourceExhaustedError(fmt.Sprintf("reached maximum deployments in namespace (%d)", limit)) + } + + // Start the deployment workflow + workflowID := GenerateDeploymentWorkflowID(deploymentName) + + input, err := sdk.PreferProtoDataConverter.ToPayloads(&deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: namespaceEntry.Name().String(), + NamespaceId: namespaceEntry.ID().String(), + DeploymentName: deploymentName, + State: &deploymentspb.WorkerDeploymentLocalState{ + SyncBatchSize: d.getSyncBatchSize(), + CreateRequestId: requestID, + LastModifierIdentity: identity, + }, + }) + if err != nil { + return nil, err + } + + updateArgs, err := sdk.PreferProtoDataConverter.ToPayloads(&deploymentspb.CreateWorkerDeploymentArgs{ + Identity: identity, + RequestId: requestID, + }) + if err != nil { + return nil, err + } + + updateRequest := &updatepb.Request{ + Input: &updatepb.Input{Name: CreateWorkerDeployment, Args: updateArgs}, + // the WorkflowUpdate's request ID is not used as the `create_request_id` inside the + // workflow state. The reason I add the prefix is to differentiate the request id for + // create vs other APIs such as setcurrent etc. in case user sends the same request + // ID, Temporal still treat them as defferent WorkflowUpdates. + Meta: &updatepb.Meta{UpdateId: "_create_" + requestID, Identity: identity}, + } + + outcome, err := updateWorkflowWithStart( + ctx, + d.historyClient, + namespaceEntry, + WorkerDeploymentWorkflowType, + workflowID, + nil, + input, + updateRequest, + identity, + requestID, + ) + if err != nil { + return nil, err + } + if failure := outcome.GetFailure(); failure.GetApplicationFailureInfo().GetType() == errDeploymentAlreadyExists { + return nil, serviceerror.NewAlreadyExists(fmt.Sprintf(ErrWorkerDeploymentAlreadyExists, deploymentName)) + } else if failure != nil { + return nil, serviceerror.NewInternalf("create deployment failed %s", failure.Message) + } + + var res deploymentspb.CreateWorkerDeploymentResponse + success := outcome.GetSuccess() + if err := sdk.PreferProtoDataConverter.FromPayloads(success, &res); err != nil { + return nil, err + } + + return res.GetConflictToken(), nil +} + +func (d *ClientImpl) ensureWorkerDeploymentDoesNotExist( + ctx context.Context, + namespaceEntry *namespace.Namespace, + deploymentName string, + requestID string, +) ([]byte, error) { + // Check if deployment already exists and whether it was created by this request. + // The reason for this query is that we don't want to send updates to the + // workflow needlessly because it generates history events (ideally, we should + // right nothing in case of duplicate request ID.) + res, err := d.queryCreateRequestID(ctx, namespaceEntry, deploymentName) + if err != nil { + var notFound *serviceerror.NotFound + if errors.As(err, ¬Found) { + return nil, nil + } + return nil, err + } + + if res.GetRequestId() == requestID { + _, conflictToken, err := d.DescribeWorkerDeployment(ctx, namespaceEntry, deploymentName) + if err != nil { + return nil, err + } + return conflictToken, nil + } + + if strings.HasPrefix(res.GetRequestId(), AutoCreateRequestIDPrefix) { + return nil, serviceerror.NewAlreadyExists( + fmt.Sprintf(ErrWorkerDeploymentAlreadyExists+" (auto-created from worker polls)", deploymentName), + ) + } + + return nil, serviceerror.NewAlreadyExists(fmt.Sprintf(ErrWorkerDeploymentAlreadyExists, deploymentName)) +} + +func (d *ClientImpl) CreateWorkerDeploymentVersion( + ctx context.Context, + namespaceEntry *namespace.Namespace, + deploymentName string, + buildID string, + identity string, + requestID string, + computeConfig *computepb.ComputeConfig, +) (retErr error) { + //revive:disable-next-line:defer + defer d.convertAndRecordError("CreateWorkerDeploymentVersion", deploymentName, &retErr, namespaceEntry.Name(), identity)() + + err := validateVersionWfParams(worker_versioning.WorkerDeploymentNameFieldName, deploymentName, d.maxIDLengthLimit()) + if err != nil { + return err + } + err = validateVersionWfParams(worker_versioning.WorkerDeploymentBuildIDFieldName, buildID, d.maxIDLengthLimit()) + if err != nil { + return err + } + + version := worker_versioning.WorkerDeploymentVersionToStringV31(&deploymentspb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, + }) + + workflowID := GenerateDeploymentWorkflowID(deploymentName) + + updateArgs, err := sdk.PreferProtoDataConverter.ToPayloads(&deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: requestID, + Version: version, + ComputeConfig: computeConfig, + }) + if err != nil { + return err + } + + updateRequest := &updatepb.Request{ + Input: &updatepb.Input{Name: CreateWorkerDeploymentVersion, Args: updateArgs}, + Meta: &updatepb.Meta{UpdateId: "_create_version_" + requestID, Identity: identity}, + } + + outcome, err := updateWorkflow( + ctx, + d.historyClient, + namespaceEntry, + workflowID, + updateRequest, + ) + if err != nil { + var notFound *serviceerror.NotFound + if errors.As(err, ¬Found) { + return serviceerror.NewNotFound(fmt.Sprintf(ErrWorkerDeploymentNotFound, deploymentName)) + } + return err + } + if failure := outcome.GetFailure(); failure != nil { + if failure.GetApplicationFailureInfo().GetType() == errVersionAlreadyExists { + return serviceerror.NewAlreadyExists(fmt.Sprintf(ErrWorkerDeploymentVersionAlreadyExists, version)) + } + if failure.GetApplicationFailureInfo().GetType() == errTooManyVersions { + return newResourceExhaustedError(failure.GetMessage()) + } + if failure.GetApplicationFailureInfo().GetType() == errInvalidComputeConfig { + return serviceerror.NewInvalidArgument(failure.GetMessage()) + } + return serviceerror.NewInternalf("create deployment version failed: %s", failure.Message) + } + + return nil +} + func (d *ClientImpl) StartWorkerDeployment( ctx context.Context, namespaceEntry *namespace.Namespace, @@ -986,6 +1324,9 @@ func (d *ClientImpl) StartWorkerDeployment( NamespaceName: namespaceEntry.Name().String(), NamespaceId: namespaceEntry.ID().String(), DeploymentName: deploymentName, + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: requestID, + }, }) if err != nil { return err @@ -1005,9 +1346,8 @@ func (d *ClientImpl) StartWorkerDeployment( func (d *ClientImpl) StartWorkerDeploymentVersion( ctx context.Context, namespaceEntry *namespace.Namespace, - deploymentName, buildID string, - identity string, - requestID string, + deploymentName, buildID, identity, requestID, modifierIdentity string, + computeConfig *computepb.ComputeConfigSummary, ) (retErr error) { //revive:disable-next-line:defer defer d.convertAndRecordError("StartWorkerDeploymentVersion", deploymentName, &retErr, namespaceEntry.Name(), identity)() @@ -1022,7 +1362,8 @@ func (d *ClientImpl) StartWorkerDeploymentVersion( } workflowID := GenerateVersionWorkflowID(deploymentName, buildID) - input, err := sdk.PreferProtoDataConverter.ToPayloads(d.makeVersionWorkflowArgs(deploymentName, buildID, namespaceEntry)) + args := d.makeVersionWorkflowArgs(deploymentName, buildID, namespaceEntry, modifierIdentity, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED, computeConfig) + input, err := sdk.PreferProtoDataConverter.ToPayloads(args) if err != nil { return err } @@ -1098,7 +1439,7 @@ func (d *ClientImpl) SyncVersionWorkflowFromWorkerDeployment( func (d *ClientImpl) updateWithStartWorkerDeployment( ctx context.Context, namespaceEntry *namespace.Namespace, - deploymentName, buildID string, + deploymentName string, updateRequest *updatepb.Request, identity string, requestID string, @@ -1108,10 +1449,6 @@ func (d *ClientImpl) updateWithStartWorkerDeployment( if err != nil { return nil, err } - err = validateVersionWfParams(worker_versioning.WorkerDeploymentBuildIDFieldName, buildID, d.maxIDLengthLimit()) - if err != nil { - return nil, err - } workflowID := GenerateDeploymentWorkflowID(deploymentName) @@ -1136,7 +1473,8 @@ func (d *ClientImpl) updateWithStartWorkerDeployment( NamespaceId: namespaceEntry.ID().String(), DeploymentName: deploymentName, State: &deploymentspb.WorkerDeploymentLocalState{ - SyncBatchSize: syncBatchSize, + SyncBatchSize: syncBatchSize, + CreateRequestId: requestID, }, }) if err != nil { @@ -1157,6 +1495,8 @@ func (d *ClientImpl) updateWithStartWorkerDeployment( ) } +// TODO: this is an expensive query that is called every time a new deployment name is seen. +// If user passes a ton of unique deployment names, we're in trouble. Fix it. func (d *ClientImpl) countWorkerDeployments( ctx context.Context, namespaceEntry *namespace.Namespace, @@ -1196,7 +1536,12 @@ func (d *ClientImpl) updateWithStartWorkerDeploymentVersion( } workflowID := GenerateVersionWorkflowID(deploymentName, buildID) - input, err := sdk.PreferProtoDataConverter.ToPayloads(d.makeVersionWorkflowArgs(deploymentName, buildID, namespaceEntry)) + input, err := sdk.PreferProtoDataConverter.ToPayloads(d.makeVersionWorkflowArgs(deploymentName, + buildID, + namespaceEntry, + "", + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, + nil)) if err != nil { return nil, err } @@ -1307,17 +1652,18 @@ func versionStateToVersionInfo( } return &deploymentpb.WorkerDeploymentVersionInfo{ - Version: worker_versioning.WorkerDeploymentVersionToStringV31(state.Version), - DeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromVersion(state.Version), - Status: state.Status, - CreateTime: state.CreateTime, - RoutingChangedTime: state.RoutingUpdateTime, - CurrentSinceTime: state.CurrentSinceTime, - RampingSinceTime: state.RampingSinceTime, - RampPercentage: state.RampPercentage, - TaskQueueInfos: infos, - DrainageInfo: drainageInfo, - Metadata: state.Metadata, + Version: worker_versioning.WorkerDeploymentVersionToStringV31(state.Version), + DeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromVersion(state.Version), + Status: state.Status, + CreateTime: state.CreateTime, + RoutingChangedTime: state.RoutingUpdateTime, + CurrentSinceTime: state.CurrentSinceTime, + RampingSinceTime: state.RampingSinceTime, + RampPercentage: state.RampPercentage, + TaskQueueInfos: infos, + DrainageInfo: drainageInfo, + Metadata: state.Metadata, + LastModifierIdentity: state.LastModifierIdentity, } } @@ -1430,6 +1776,7 @@ func (d *ClientImpl) deploymentStateToDeploymentInfo(deploymentName string, stat LastCurrentTime: v.GetLastCurrentTime(), LastDeactivationTime: v.GetLastDeactivationTime(), Status: v.GetStatus(), + ComputeConfig: v.GetComputeConfig(), }) } @@ -1666,7 +2013,7 @@ func (d *ClientImpl) SignalVersionReactivation( func (d *ClientImpl) getSyncBatchSize() int32 { syncBatchSize := int32(25) - if n, ok := testhooks.Get[int](d.testHooks, testhooks.TaskQueuesInDeploymentSyncBatchSize); ok && n > 0 { + if n, ok := testhooks.Get(d.testHooks, testhooks.TaskQueuesInDeploymentSyncBatchSize, testhooks.GlobalScope); ok && n > 0 { // In production, the testhook would be set to 0 and never reach here! syncBatchSize = int32(n) } @@ -1676,23 +2023,69 @@ func (d *ClientImpl) getSyncBatchSize() int32 { func (d *ClientImpl) makeVersionWorkflowArgs( deploymentName, buildID string, namespaceEntry *namespace.Namespace, + identity string, + initialStatus enumspb.WorkerDeploymentVersionStatus, + computeConfig *computepb.ComputeConfigSummary, ) *deploymentspb.WorkerDeploymentVersionWorkflowArgs { return &deploymentspb.WorkerDeploymentVersionWorkflowArgs{ NamespaceName: namespaceEntry.Name().String(), NamespaceId: namespaceEntry.ID().String(), - VersionState: &deploymentspb.VersionLocalState{ - Version: &deploymentspb.WorkerDeploymentVersion{ - DeploymentName: deploymentName, - BuildId: buildID, + VersionState: makeNewVersionState(deploymentName, buildID, time.Now(), identity, initialStatus, computeConfig, d.getSyncBatchSize()), + } +} + +func (d *ClientImpl) ValidateComputeConfig( + ctx context.Context, + namespaceEntry *namespace.Namespace, + version *deploymentpb.WorkerDeploymentVersion, + upsertScalingGroups map[string]*computepb.ComputeConfigScalingGroupUpdate, + removeScalingGroups []string, + identity string, +) error { + upserts := scalingGroupUpdatesToWCI(upsertScalingGroups) + if err := d.workerControllerInstanceClient.ValidateWorkerControllerInstanceSpec(ctx, namespaceEntry, version, identity, upserts, removeScalingGroups); err != nil { + return serviceerror.NewInvalidArgument(err.Error()) + } + return nil +} + +// queryVersionState queries the version workflow for its current state. +func (d *ClientImpl) queryVersionState( + ctx context.Context, + namespaceEntry *namespace.Namespace, + deploymentName, buildID string, +) (*deploymentspb.VersionLocalState, error) { + workflowID := GenerateVersionWorkflowID(deploymentName, buildID) + req := &historyservice.QueryWorkflowRequest{ + NamespaceId: namespaceEntry.ID().String(), + Request: &workflowservice.QueryWorkflowRequest{ + Namespace: namespaceEntry.Name().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, }, - CreateTime: timestamppb.Now(), - RoutingUpdateTime: nil, - CurrentSinceTime: nil, // not current - RampingSinceTime: nil, // not ramping - RampPercentage: 0, // not ramping - DrainageInfo: &deploymentpb.VersionDrainageInfo{}, // not draining or drained - Metadata: nil, - SyncBatchSize: d.getSyncBatchSize(), + Query: &querypb.WorkflowQuery{QueryType: QueryDescribeVersion}, }, } + res, err := d.queryWorkflowWithRetry(ctx, req) + if err != nil { + var queryFailed *serviceerror.QueryFailed + if errors.As(err, &queryFailed) && queryFailed.Error() == errVersionDeleted { + return nil, serviceerror.NewNotFoundf(ErrWorkerDeploymentVersionNotFound, buildID, deploymentName) + } + return nil, err + } + + if rej := res.GetResponse().GetQueryRejected(); rej != nil { + return nil, serviceerror.NewInternalf("describe deployment query rejected with status %s", rej.GetStatus()) + } + + if res.GetResponse().GetQueryResult() == nil { + return nil, serviceerror.NewInternal("Did not receive deployment info") + } + + var queryResponse deploymentspb.QueryDescribeVersionResponse + if err := sdk.PreferProtoDataConverter.FromPayloads(res.GetResponse().GetQueryResult(), &queryResponse); err != nil { + return nil, err + } + return queryResponse.VersionState, nil } diff --git a/service/worker/workerdeployment/compute_util.go b/service/worker/workerdeployment/compute_util.go new file mode 100644 index 00000000000..33637f93aaf --- /dev/null +++ b/service/worker/workerdeployment/compute_util.go @@ -0,0 +1,105 @@ +package workerdeployment + +import ( + computepb "go.temporal.io/api/compute/v1" + wciiface "go.temporal.io/auto-scaled-workers/wci/workflow/iface" + "go.temporal.io/sdk/workflow" +) + +func computeConfigScalingGroupsToWCISpec(scalingGroups map[string]*computepb.ComputeConfigScalingGroup) *wciiface.WorkerControllerInstanceSpec { + specs := make(map[string]wciiface.ScalingGroupSpec, len(scalingGroups)) + for name, sg := range scalingGroups { + groupSpec := wciiface.ScalingGroupSpec{ + TaskTypes: sg.GetTaskQueueTypes(), + Compute: wciiface.ComputeProviderSpec{ + ProviderType: wciiface.ComputeProviderType(sg.GetProvider().GetType()), + }, + } + if scaler := sg.GetScaler(); scaler != nil { + groupSpec.Scaling = &wciiface.ScalingAlgorithmSpec{ + ScalingAlgorithm: wciiface.ScalingAlgorithmType(scaler.GetType()), + } + } + specs[name] = groupSpec + } + return &wciiface.WorkerControllerInstanceSpec{ + ScalingGroupSpecs: specs, + } +} + +func scalingGroupUpdatesToWCI(updates map[string]*computepb.ComputeConfigScalingGroupUpdate) map[string]wciiface.ScalingGroupSpecUpdate { + result := make(map[string]wciiface.ScalingGroupSpecUpdate, len(updates)) + for name, update := range updates { + sg := update.GetScalingGroup() + spec := wciiface.ScalingGroupSpec{ + TaskTypes: sg.GetTaskQueueTypes(), + Compute: wciiface.ComputeProviderSpec{ + ProviderType: wciiface.ComputeProviderType(sg.GetProvider().GetType()), + Config: sg.GetProvider().GetDetails(), + }, + } + if scaler := sg.GetScaler(); scaler != nil { + spec.Scaling = &wciiface.ScalingAlgorithmSpec{ + ScalingAlgorithm: wciiface.ScalingAlgorithmType(scaler.GetType()), + Config: scaler.GetDetails(), + } + } + result[name] = wciiface.ScalingGroupSpecUpdate{ + Spec: spec, + UpdateMask: update.GetUpdateMask().GetPaths(), + } + } + return result +} + +func wciSpecToComputeConfig(spec *wciiface.WorkerControllerInstanceSpec) *computepb.ComputeConfig { + if spec == nil || len(spec.ScalingGroupSpecs) == 0 { + return nil + } + groups := make(map[string]*computepb.ComputeConfigScalingGroup, len(spec.ScalingGroupSpecs)) + for name, sg := range spec.ScalingGroupSpecs { + group := &computepb.ComputeConfigScalingGroup{ + TaskQueueTypes: sg.TaskTypes, + Provider: &computepb.ComputeProvider{ + Type: string(sg.Compute.ProviderType), + Details: sg.Compute.Config, + }, + } + if sg.Scaling != nil { + group.Scaler = &computepb.ComputeScaler{ + Type: string(sg.Scaling.ScalingAlgorithm), + Details: sg.Scaling.Config, + } + } + groups[name] = group + } + return &computepb.ComputeConfig{ScalingGroups: groups} +} + +func wciSpecToComputeConfigSummary(spec *wciiface.WorkerControllerInstanceSpec) *computepb.ComputeConfigSummary { + if spec == nil || len(spec.ScalingGroupSpecs) == 0 { + return nil + } + groups := make(map[string]*computepb.ComputeConfigScalingGroupSummary, len(spec.ScalingGroupSpecs)) + names := workflow.DeterministicKeys(spec.ScalingGroupSpecs) + for _, name := range names { + sg := spec.ScalingGroupSpecs[name] + groups[name] = &computepb.ComputeConfigScalingGroupSummary{ + TaskQueueTypes: sg.TaskTypes, + ProviderType: string(sg.Compute.ProviderType), + } + } + return &computepb.ComputeConfigSummary{ScalingGroups: groups} +} + +func scalingGroupsToUpsertUpdates(scalingGroups map[string]*computepb.ComputeConfigScalingGroup) map[string]*computepb.ComputeConfigScalingGroupUpdate { + updates := make(map[string]*computepb.ComputeConfigScalingGroupUpdate, len(scalingGroups)) + names := workflow.DeterministicKeys(scalingGroups) + for _, name := range names { + sg := scalingGroups[name] + updates[name] = &computepb.ComputeConfigScalingGroupUpdate{ + ScalingGroup: sg, + } + } + return updates +} diff --git a/service/worker/workerdeployment/compute_util_test.go b/service/worker/workerdeployment/compute_util_test.go new file mode 100644 index 00000000000..ef0479a95fb --- /dev/null +++ b/service/worker/workerdeployment/compute_util_test.go @@ -0,0 +1,51 @@ +package workerdeployment + +import ( + "testing" + + "github.com/stretchr/testify/assert" + computepb "go.temporal.io/api/compute/v1" + enumspb "go.temporal.io/api/enums/v1" + wciiface "go.temporal.io/auto-scaled-workers/wci/workflow/iface" +) + +func TestComputeConfigScalingGroupsToWCISpec_EmptyGroups(t *testing.T) { + t.Parallel() + result := computeConfigScalingGroupsToWCISpec(nil) + assert.NotNil(t, result) + assert.Empty(t, result.ScalingGroupSpecs) + + result = computeConfigScalingGroupsToWCISpec(map[string]*computepb.ComputeConfigScalingGroup{}) + assert.NotNil(t, result) + assert.Empty(t, result.ScalingGroupSpecs) +} + +func TestComputeConfigScalingGroupsToWCISpec_WithComputeAndScaling(t *testing.T) { + t.Parallel() + groups := map[string]*computepb.ComputeConfigScalingGroup{ + "group1": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW}, + Provider: &computepb.ComputeProvider{Type: "aws-lambda"}, + Scaler: &computepb.ComputeScaler{Type: "rate-based"}, + }, + "group2": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + Provider: &computepb.ComputeProvider{Type: "aws-ecs"}, + }, + } + + result := computeConfigScalingGroupsToWCISpec(groups) + + assert.Len(t, result.ScalingGroupSpecs, 2) + + g1 := result.ScalingGroupSpecs["group1"] + assert.Equal(t, []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW}, g1.TaskTypes) + assert.Equal(t, wciiface.ComputeProviderType("aws-lambda"), g1.Compute.ProviderType) + assert.NotNil(t, g1.Scaling) + assert.Equal(t, wciiface.ScalingAlgorithmType("rate-based"), g1.Scaling.ScalingAlgorithm) + + g2 := result.ScalingGroupSpecs["group2"] + assert.Equal(t, []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, g2.TaskTypes) + assert.Equal(t, wciiface.ComputeProviderType("aws-ecs"), g2.Compute.ProviderType) + assert.Nil(t, g2.Scaling, "no scaler means nil scaling spec") +} diff --git a/service/worker/workerdeployment/fx.go b/service/worker/workerdeployment/fx.go index 42ad5ea809a..703024e87ec 100644 --- a/service/worker/workerdeployment/fx.go +++ b/service/worker/workerdeployment/fx.go @@ -3,6 +3,7 @@ package workerdeployment import ( "time" + wciclient "go.temporal.io/auto-scaled-workers/wci/client" sdkworker "go.temporal.io/sdk/worker" "go.temporal.io/sdk/workflow" deploymentspb "go.temporal.io/server/api/deployment/v1" @@ -40,12 +41,13 @@ type ( activityDeps struct { fx.In - MetricsHandler metrics.Handler - Logger log.Logger - ClientFactory sdk.ClientFactory - MatchingClient resource.MatchingClient - HistoryClient resource.HistoryClient - WorkerDeploymentClient Client + MetricsHandler metrics.Handler + Logger log.Logger + ClientFactory sdk.ClientFactory + MatchingClient resource.MatchingClient + HistoryClient resource.HistoryClient + WorkerDeploymentClient Client + WorkerControllerInstanceClient wciclient.Client } fxResult struct { @@ -54,9 +56,14 @@ type ( } ) +var ClientModule = fx.Options( + wciclient.Module, + fx.Provide(ClientProvider), +) + var Module = fx.Options( + ClientModule, fx.Provide(NewResult), - fx.Provide(ClientProvider), ) func ClientProvider( @@ -64,6 +71,7 @@ func ClientProvider( historyClient resource.HistoryClient, matchingClient resource.MatchingClient, visibilityManager manager.VisibilityManager, + workerControllerInstanceClient wciclient.Client, dc *dynamicconfig.Collection, testHooks testhooks.TestHooks, metricsHandler metrics.Handler, @@ -73,6 +81,7 @@ func ClientProvider( historyClient: historyClient, visibilityManager: visibilityManager, matchingClient: matchingClient, + workerControllerInstanceClient: workerControllerInstanceClient, maxIDLengthLimit: dynamicconfig.MaxIDLengthLimit.Get(dc), visibilityMaxPageSize: dynamicconfig.FrontendVisibilityMaxPageSize.Get(dc), maxTaskQueuesInDeploymentVersion: dynamicconfig.MatchingMaxTaskQueuesInDeploymentVersion.Get(dc), diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/expected_counts.txt b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/expected_counts.txt new file mode 100644 index 00000000000..5dc7cf1e5b9 --- /dev/null +++ b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/expected_counts.txt @@ -0,0 +1,6 @@ +# Expected workflow counts for replay testing +# Generated by generate_history.sh on Wed Apr 8 14:55:10 PDT 2026 +EXPECTED_DEPLOYMENT_WORKFLOWS=19 +EXPECTED_VERSION_WORKFLOWS=11 +ACTUAL_DEPLOYMENT_WORKFLOWS=19 +ACTUAL_VERSION_WORKFLOWS=11 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_019d6f17-068c-7a06-8ed1-fcacd6f8afea.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_019d6f17-068c-7a06-8ed1-fcacd6f8afea.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..fc49ba03eff1f69af43b824721eca54bcb219023 GIT binary patch literal 3842 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 E0B&!AKmY&$ literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_224a7ffd-ae5c-4733-9685-2ffb63fdd662.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_224a7ffd-ae5c-4733-9685-2ffb63fdd662.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..fd913f5f01accf497cb1105cb691813d8ece9318 GIT binary patch literal 3667 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_46754e89-0f02-45a7-b2d0-51f5d7f28d86.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_46754e89-0f02-45a7-b2d0-51f5d7f28d86.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..239a4ae31d20495864852300aa007b4995c96bb8 GIT binary patch literal 4839 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_51ef5374-249a-47bd-85b1-4a51a910a079.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_51ef5374-249a-47bd-85b1-4a51a910a079.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..8d8ce7b15e01fb5c171508ea72057f5482e051a6 GIT binary patch literal 3868 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_5abc2150-36c6-41bb-8ed9-320d10890207.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_5abc2150-36c6-41bb-8ed9-320d10890207.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..e29cbfe1b6f9d89a040b8a36bbed276770d8feec GIT binary patch literal 2487 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 F004~!EDrzx literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_9a28076e-569d-4d27-baac-7ecabc6a5bdf.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_9a28076e-569d-4d27-baac-7ecabc6a5bdf.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..941278892d021055f6eac4f6f674c3e2ef5aad73 GIT binary patch literal 3875 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 l9tm`LbJOAHczMH~F!<3+v6VmK1@rxn{{v_GS7* zX8$j*n}mIl0hzznuGZWhjjux~X;~;Nl8&$Pqg5{ArK1 z%#G+UV7+n6rZO*W{P=6tQGDa&h(@RNGGJZw11|iY`72qr>-J}3uW8kr$MU^#Z#>?Z zrR&GiZKbFcc6aRd2T{1$=>ShXACr}kmw*0Q`@~B2Xov|8@kR?vQTA=0xczo~Q_Y?i zH6Ol-1(cmys4{By>%Fzxe`zrYaGNd_faL3;omZ#L#yGNJeM)-uX`z=J zb~1~hW%M@bwi>MtX-s&-Ek75p%b@tI!O|3X#p~jvP%HDdH6y&O*{G$1PN`S-icjWq zv2o8A^CpIE<3?}$*R7s9GTN;ClUCKoZmUm5x7p71cq~nVd^<3Q17tn7o8@Uyp46^= zTi1-ZT(l@xo_;UgCCl-}?@u+}{I0w08x4jYt(YCX)4b91^{5HeE^df@qdoX;Lp5R( zA2b_YWVCIU^l|}d+`IRk;6{fX&tkJxc!+0j(ly%So2=H>jkXs5%o^=%^=sKL;e0)? z@=8e9EL!<;SnQZZyAyR*ce$u4Q9s5Qf2PVAKFI7SQ)5dkQ3$M;vi*MO_9KP|OMGB$&Y?t* zWKp1s%JB{lIYGoSXF!Awp24qY$a;d$CA-_G)rd9 z)M~et7d%T=^@ncT?PG8ICRD6K#|Ad1V8P}Dj8qQGj=%}3;P4JnWXYn*mK=8&gxk&V zKqZm`WR8~aa|5f*6ln*tgbPTi!nznMam$ztY*haH zbQow%zeUy*bnPDNh2dFi3K+1Eh=EgBteSz@p8-(M%B!f(fXas6$m-_0t$e*(sA+CN z|2Azka!(}h!RA9{U_l{dW%?Fdj`8;o+e_fD-BVX$W+510n_jyk_ZLl{_IHY@Grfvu zJ!IMRyh~qvDmCKN`eVnTELb?i>iH@ZL9&!yC`UX5B<>N(3C=;VCbmsIi_IWm z*WGZsDIE6uf2tka!B)T(v7Pn(E_XNdIe6vyHunBQsDmRfOg1&7-y<3Z-DjGe&WFK9ugaS&i4J$$;#BDPX%i2Q zs)3L~4FsN-lGt>j8tAvhyMl305!Bs^@PfrZrwTzrpEi~MyqfX`f=rHsbc?%LMaE-f z?ml3w&tr^#%i^MSYXa6k3Q60s23V}a(k8yO{mf>W)p#@DN&r`2_w6%AM2C#?B*$>F zo{H)8REFRLJuPaL8>8}AG4$KY^#T19_|;cE9E-R@ZGT9PDCWHAat4>Pl$a61X2SKh zc#}P&=A28m+56t$z~@~o^B@O3#zMC${Mf-uUl`^A@@?*D?Z?{Z|9;xjOJ=mkKl!i| zQPffm_Ml_E4S`F{K6TYdDOU|rPgphgcU7ZOTY-Gl&?@@ZN3B+?nA!W9QQg0e60yl> z5YXWZ^R8|95_5UQY%5>!S~@>AJ^nPG?^Ior zQ|g+io)e+r84$V{&HSq>BLs%yP&+&Ut9Oe0_kwBHJg!zhdDkb#pASc%n-AGRp~r^b z0PLnkSgeZW=7SelFxR!mX{SQ(1UI?1Z?+oZlLadSt7ljB-r(7S!pP1~I=CTJwVL^A z%hj7J6BjUT;^<7BIK~=1&i12zIQPVV6O&ecz5Py({Djy4}3W<1L#2l=h@uEx+^%x9KH-UApOiz~Py2h>2 zPo<#MsE^mB^x{~o#=_=lcuMehR&aB!SHbe!BFD0bl?zw0Br6Kso0$GNX#+l`xMg%s zD=c0@Q^%;>H?P?!@%W+DA^F(fdC42Qm$1|?0Z0H+E*V99+qjf;iA%j>T4=I0M2 z^1dJ_O1wjPP84{H6I4s!6yCBpYOA&^Q-UO_oR-P2)=d4Txn8#K4}0@HLkIQ=yl?Iw z2&p8FUk13_|4BOxcgnz>ZfU_mnfs{@S1nHFQx~A96x>;+UY5~-Q z9m00#j+jVJw*bo;TO&%gcHksIo`wstT@SI_$8E}xD2##+c4T1pHoiaUAVHdg1Ern? zm8$k^MB5I~@hy`}WZ5>s>lS!bZf3mpm)mX6rS(!w!o$#_I6P@n{VqQvf$B5+6%MC& zzUSK0Z671vEbp0d5OjpSSx;AhU+eAj46M4o9k0jXOil%7s>Htn&W_Lky6If*$AAD*yh*xchJ!_oi;DWB1SRcF3iz8TqTFfCQZW{+3J!8OS&kuXky+n$}1Wg}%j8=Hsw&G{MJQ;+xW z@nYawAg#YVb2%d*+wy+-ei5hR1rhO*%uyZzP7qYUsj^HsEKv#}0+bn;T`twj7&cWV z%|W^NfZQIvnN8f%1O?N+{ql%{WlsJN_3H0YJ+vCRDJ`eshiwvj_))PcjE#0+_FD5J z?Mm@Bu(EC4?2O~1@ddqbOQ0g;jk_m)TyJi1%A3t&@&>E(4bJP_xDWmN+)d(90Os>3 zSG?Eicy&z|rnh1)EjNu1z7KtGW9hGlc}~U-loC5c;oLzy1{2BM5I-BlgD7nfugX@` z^~*Y3p#Ob9hu5*fTq1ezz2-}z)hijv>#NPQ^=G#Bjpjq8bKk2^NTdEtv~+9VztLc8 zWKGQL+4}DoJ07QDX4(3$>22V*8k4}9f;hPxv~+$t`|ioC=vuqF!UMe-$=@D|+Wj9B zEnK&=zcO0*Hqk(sD%q*Xh(E=vK18PGeCrTMCLR5KEUG%=3P%9bU87oCB=TQFoYoEH z$)Xq2ie7kwfC{_>RNgP3k}8v_0ucyj52zsi%#Sq?-y@&`uuUkhh@1j!hZAf?;VeKP z=P-xZu_zQ&cywoj2$1km0xGF;K&pUV@z9au0Ttow0hM)S*bz{XE&-LK8l=ky$tsl2 zn+jw~Ao1r-1;TgP=_ENw5($<$n*huSJg_-Sp(@8qQ`y#6|Y^?5tOk&dYic{~lHj^33KVi7-{MRZfs3hr>v+IiM;8h*VWA z**VI3RpFzg5fMzK2r@$84Op+tpPTirE5nZGd4UV-O{xJ(sRjysgH(VF&X)>6sZ#-Z zhuYo7Onj@1z%muUq&~oHJd0OdvI6f+1G^=^)hG>jK8QA#pn+e%jqH4`KR+ZUR$C7_ z(0VBGDJWsbxVdhuQ^G>cEbC+cPH%_n8G-EUDdF!H@n3t4?!PeNkAzh4r-%TY-RYO5 z^E&AiexBSM4&b~=4ndm`oYFu3A9QoMUWJzyn&~wNN4vnkDLrZ^2O6ks)3qP z4HWTpZ<|`a%H?|TE|>i(ap<*E`tn6HT2P|ngUZpTs?=^A5*6Chy^esvU$$766X zm|(NzsQ>*Pvm^%0;*_LmPb#$(d)8-P<{AF^a&eI%6EX;c%(sjGqu(wr{#pFHes0+G zMFwR4+IU)b`y_e}Stv3r&#-bGf^P!!y00 zFW$87^*dc_)Qy|+eXXum>w4~`Z-|24Y6&c_08V&$HT6e5$MdK9jNozT`h!}CMj^5F zXgW0vN3JyvNwlx^j$)lw%XxW2-wn`mQz7XO{Soq*U>-102Qp%8GW7g;pN`B-$S9z* zG3%i+&tv@f>t&*7;gvBNpEk>oPB93W@G08$xGa;8KtH)ve!N;1aNA1X} zUE7nwYP=o1B2uGaJZf&|T7{uUszz7qwy(89bKHhX54D7W-Wfh(sEjG#gLcas>mBUc z{d{1z8g8Q-Tx+oFSrl5uy9j!-p5B>WXVs3Tchu-Jt9P=s%Zgt{g=SzCY^QH}4L(Ekzp_IDzst{JF09z^oQ zUyyBv4KfLB>Xe;gjlgCr><>bBFs8h}mIZXorPz=-0xieJ47Pd1@Q%PS2=Rcyl1*es zQjiRs%m(|op^HL)w4joka@{CXu2XCGWxaK8l#RNf)^DoMvMb)zAGsZOfV|yB5X^Is zhb$w?SY~)uRv1KB$N(A1j)mzOfgK;oaYsS8I}8gHTXcZLuyh zS`AZUcUdnD=tWHy-*JMR+D4tC^5 zroIw%S{DsEq`8!y3F0M%=s|<>#B!CXRmcym&iC(0Vkcb5n;L8i5rv!jY>2ta_tRxW zYkoC1TlojO;KBA?b!b68#Lyd9l-G)N)h%jQoW)&@%>Ha*1JlEUGHoMd%|g{PHFj#a zQ`hRtp?P`LrF=WI`Z(5Kcke3Sw|VBTWYCqV&IpDGlbkzoe-Zf9?P9bx{{t0aHRa>-#uS; zV1}0f&sapnj15>plCUBp3&%Tm-GG`e^P2lavh(FO=~9lvbLgQH}? zryv84h2Rxrpxu;ii~2!AP?8DZSr-2sDFiWk+9v+I2rN2bd0W19{LFTnm;GkIR~~!?$;+n}5gAdNC%%W1=~PHb zr!sp2otD(e@{;cYrhn{#SdPpn?PXBL`0BVn{3 z8{hx_vS*geWRHLHVF6K6Q*HL3C%!8J*E0K5t`XC54P~1XxTbMi)2sCtAz#zgs&;v= z*6URx+oc60+BJ@o)laVa#NhPd@O1kwJ1q9;_FI51T7adR zP-)+Jfd%tDJWUG&qi|47>kpqS$WL%#)0RP}{=sZ!J>2&uruOd?zi#EI_|+a7(^5`d|NRuj|4vbSjDhkp z|FOifl2R;7ryS3y8zm~0kA+l)SUhh`9jwH7Q6?DosTiJb3)a0jJsF#u6!(&TDhFn( zIo-t4vt#kH7Ye7*DZ$@G!);i<1k3ZQ9Lp+J&V7|7D1!Z&?4QFnVAG+S1IjtIu<#k0 zI%djUdF@u2MR#V`E=2wgctS0EOm&+38J7Ab0uq3fEW;^p8M zwCna}+krpoFOSR|NRz*B>KkzBFs^WC&wWFdJ8$kAyi4+%2nsc%EX3HhC^Nk5unYo3 zWGq3HMGg|gIpA3Go0_}Q2T6VdJ{2LzJe1F${e~=m?(8=>lKuvNME?71xn&@vBm)t= z?mT_tHmBD0dsWl)+k+Hw=v;kcNH<7SjLGmAZ45_#=wshA29yQ{m@KEL%?G%(KveGe zY}X29%m!hg)g0BO2q-OMLnjRK-<8lfFjy`67+{LBy>z?PBgtFcwTs3J=%m}|4j1ek zeY;p=;o|TSx#(!zet$Tr79ggh1G?Pjat4a9kZi9JjXFV+jd#H7xJdRaj(8rgjUWDO zeL-2U`sL?(f(BQm9$T}C3yaUhnu0x!&qi%QVE9+d^YOuLp-`C_MeSO%q0ATbVs6&$ zl^$<9{$*4tY)m3Qi!EH zWw|57PS9)OQ7Lwsic8@16nvhjN>E&U?r4FidSD!4zrSNo2V>eX2;S52KTGibm8@4Y z@5)o^eLb0#v=IlrH@(hAtT5*j%WLiAZ@^=vA)&D^V^8YeTA zl#ZzcRy={JRIP3_je7fJ-!QfcghO{i)zs;};kw7pj^d*osK1N2Ylp8I0cExb(;uta zF8PrApH+jmU{pQ+{3Su%e4-4jx9_UmM!z|;Tg@jsRiTL1@vj&9Yi8t1HGZmh^@RF0 zBdd0ey4C@{*_s8`97F+|z|`3J;+^aNN9)*q3(DhY3}c(o=YCPU5|@l#a?xXhpKt5e zn`j+RR2OHBMNMBrEzR{xC#T28`cKg{st>A`w$5$sFzrSbHUkSU?G`@y{&vY>3URX) zrOZ)i5fq+OTbC3)1pKC39**X>zUSikZsahknWIKOb%`d(TNaW5c4M}Sg%1|hnQ1<8 z92ZmgM{9zciBmLl`{RCmq$v852-a~x)Mu&A(Q{@8`sX zkeGZ4#~u?oi@ID7}HxA1@qC@AMx>e}VzT z{}NAAObKm{djn5X;66Rgck?u*R2UazMLxTyDM)AbG{yH2tv1Glr4o+e1jlB0Q5G0Q zhJ>*k%*lWSGA7C~Pm_A?ALW!Alv5FcB)`QSFI0D*-P0702wLgW(~OgWl9CL>Q}k`k zD3sOWy?$5MtI>ZF4veg>X*@(5F(n+hoyDb%bf@&QwS8{HSY8Nu>b>;N%P1Qso9Dh38K>dvb_Tm%Mz4Pva)VJ`Sxd=!QGlAH-PaQso0=2myQpV_g!@ZLDt+L&A-W@Y7h2lL4QS z3>5gf)7TDw-M3<^^9HT^T zH*_3J% zU3}^_HQzDYQEiI3T<_x*u`73~M${Wkt!(XM`u!LFM#M1>o#Qe`Q>~vR^k*xf|NNzc e{r6tDgDzF%;h_ZQmxy70eEEN57CFPfX#fCTYRG*6 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_fb09a05b-ff03-486f-a132-4f8260d7156d.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_version_wf_run_fb09a05b-ff03-486f-a132-4f8260d7156d.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..05b957adade75a3a7fd9648ef84f5c576bda1d32 GIT binary patch literal 3942 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 AE&u=k literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_019d6f17-0239-7795-8331-4eaa45970f83.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_019d6f17-0239-7795-8331-4eaa45970f83.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..acb7532ac3619cf853994a88298806365a566387 GIT binary patch literal 4036 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_20309e89-6521-4882-b4c8-1f33f04010ff.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_20309e89-6521-4882-b4c8-1f33f04010ff.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..e3367b0de1bbf2f5f8ba56cd6bd6d410a64ecb3c GIT binary patch literal 4060 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 PKY#iUo=;I(VL1Q*V{Ycx literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_2962db09-ce1a-4488-877c-12c5c2c1094a.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_2962db09-ce1a-4488-877c-12c5c2c1094a.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..729cdb00f5a63f3e76dd310a3529ffbc458c45f8 GIT binary patch literal 2831 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_411d483f-d549-4756-8bde-7bb3f7a5ba2d.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_411d483f-d549-4756-8bde-7bb3f7a5ba2d.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..5a38ac8f875f03bc3c077797702d857f6ba51a1e GIT binary patch literal 4120 zcmV+z5a;h7iwFR{&emxH1MM7XlcLD-`~HdvKQFTshrmQk#PdKGTd+kA<;6w^N2w@> zBhVfj^WR_L)u?#T(=)TPuREe=S|qb7EAOnV`un#JA4>2U260sS^x?ng(}xd#Z~oms z7aa6O36y?6daAfSOrH}tQO0lz!&!v@pJ?h6#eD<}iPOM1g;>ot5WL1q%9Ap923-*#g!T2<*P+FpANln!7rhm#BLb$Vsjot zhfjyA#PQs@$Qqh_Lue>G$lrQ#aVqtWD9A#wapJaL;(Bsl|4aTMhI@ViVfR@wHXkZ!F9nJvI8($RfSi&}qCK43=%rdA9x7 zv!}9WHx9==pYVY}Ay9fGBr z{DWIdn%!P!6h8I=2du#G>_D1X1nn6OZYYc*Vb&t8^=Q!bTTG|BLa-0LEo{q$>#4Y^ zE!=6~Ja?+&+8~JNx>`|9XA%Bs_}p`Q!Y5+26`{HN7QrTBk6Q2ur*CX=vSKH)s-v1P zUI>-3fN<~TCAvH@rA&l-w#8i+#?q|wa5^6x!L*1yE&V`kJe!s0$;u zT0uYcq!BL7u3%H_;EzApFZDj+7TJwWT~4_*bN-wzno1}k&aa&sm>Eqiahe_u)e!WF z25_3(a6q=a0fOtV#f7-G*WyIQrHQzb(zTydR$_e+qrZVT8Lgx>Pl$?5A->R}qAuDB ze3JK|&f|={-rj{v=3(B2<)) ze6W{U&~WaK+`#nCCqV(=ph1V2LsJ|^F?1PYEeB$ZWsyV1q%D(hvY3YMJc`fJ;DB?= zLqrBnkQ~M^9F4Ig2`~zpCPi}$K4i%(8Z%A}T)JQ&^VElacxs!5F7%w}%-)prjm51W z46b+%ThL;ef%I&-+`{%b+#*`Q7=8Zy!z~v2tg{6y7F@NCMp67mZ1jXOQh#KL1Y`!J zm7zCqLz8EguDTspK-wY@ZKZ;vUrMVB%MQ|}&O}_dC&ChG(pm(ei&P$&Z4EbdI_MHy zV)$MlOlAzi2kfAg&RRG6t+lChi7*M>T+8Mz+ySYrY#(WKOXi?kJH_*W%rB6c~vkC9n@~WQgODSz*G{0||r|0;1l{p^I_)EQP3N$7nho6P zi|H7;thJP$%60ZMbXBL(2a?*=WK9JeuTC2+WvcP%JgsJ9?eJ1KKs+^cSv8S%ZRkKA zLUQqiRKpjYMt3DDA&}}J-sw?GX`(J9MKZJShAzzu!v}cE_h+Xsr}C-UO?iwg!%ZAg z4dJNsWJhUi=798Ug=C{TAdzSE8+a-Qx7XE1@+T5V{?^-dU5*isy3=p1Haxy5zm-&e z7t(<*8+absjS2F^#2}@Kpo`HG0PeEa{hI4_BWTX;@r9Ut;{YAFgihmvM^JVlb8Tz} zBdEc7ggn&SxCA5_GwXsoh0$so@w87Yr@_82;e zccYB))2a+nT4Pbdniod!a&DAK0`6rBS%@iAnFGa{a-+D!USi1|b*1QtZKraUx;O_t zcjMJ5bI{-a>5a{?X+@Z5+SM?eVvq9>BR+beZF>K4;LuFGIE!iyza%i)4Pr5rMS(Jj z!Lo&NO&pXl)3$AlWrtu$F@!^tcoul92tcx@2{4@J``JPk@wZbO za8nGZLSn$uY;oPJb;QRue^n5ed_g$a-)|FzC?ij&!vD6C@*NnNrNfzu`H@7)LxGwa zp@_~iN9SIaJoX^q_#>6|7e@vQ4Je)3w^3L+edmSV68K1gk05{gC<=me6z61VxLZ!? zVscu>xjW=ktjG_%{3P?Wj$FScpD)^4^39aRGPJ|Fb3-tNYuKoE`Q50FIstVt`tVb_Z*itf! z;QSD6_f8Vl`}UMBuFrcaoTkF$p%@AdCBv%Gls%({F9Z+2FVDMI)|i5_M$>O-p*j2w zEVP(xa;hE*2>9U;+VLH>I+x|YmYH^i+hX;Nv%Yf+^ZJ&+pkJM}eMG(v(4=WX?9tsp z--|4wF|`*(m!dX~9vY)iFm!ri5oxK>?$!L++9IfDySW^jI@PNv!mB-(zZE7dLehl6 zjW}T_YV|0lD*y~jr?%zHU6yw{>rtj$-y@n$I*IHYZZGxTDB(@Q%^gLAIjUV-1bCZrfRRY zaFbmJF16~=jfV;Kkb!Z}zCZ2sT??n3$K~26tZzGt(>ZY6sw+4ui53y$b}}BQZYzVr zUAOz->O2QIPC;JY+l23z-^)*E%ExaZiJZq4G~>HEee+UZfFFnO@qG0ddak`XuiB;^ zyeLFK0o1eUZASVC@T1W_Y$CR4I2_hOI2^jRi^f53632BKbLsktpTz`D(d^!-Y!kLr zjp*EEPa?Q4XjczmW2Qgjwgo1OSYR5b%S3Std?UeQF4^B#CV1e7Y|G=oB%w*!m_?NV zMsX&H^XGV&@ zXR{j4ISx+S7{f9{jDjTciX3NQBw?};C2$VO4R6&wDTXZnQ|g{fAwghYv*B^uN3!6p z>z>ELkYi}e@0HK27_bG!pv)8zgGagZ$RoeY_nyQ~U1$i`WFf~5pfyL9GIn7kqIhNK z-g`p&QciRfw{F{Jpd-E?@cV@h`0;OlQ0(+DAhrKK+2i=81B1%uARZwpJor}}7<7Nu zfk8=Ay;eihxZ0VTp0dXu93VVd3c4K7Ckr!wukKo zKvcA!cX05Cj*YBN4zLFuIUHhhJ3-_+8Tr}Tux)V?!DbJB_7%`-%UWwF^IBKoIYUvk zj@IB_4urlO0q8d+cT>vw~Sg!(@sbHBTL;G?AATiqs^htV)k_< zt(zCbk3bJLB+jO1j%fWAG{p$1{ zJwB6n-r0|}tfdLE#~`D(2i+^ExBWNnBn4c-_>L=o)6VuC zAwPL@Yx}Fq2XN}n|5BbS-gCx860cx9j9-DfKCVmbmS z=5;!~|V*MF5wUg=N7Dz@A%JlQ2RXm>#gLoybvAan9IO?YMJjI@v zFIP#^x$i7eoYS?Z=m6+(JI_zxS!C~sN^v-g=XO9OSwiFhcushD7Q|Tyn29}#(i80W z-<}u9H{4m^#ckMUfI3Qu^jqn-!*{2Cw53_EbnhNIhG>Wncd}Z4P}`bPX15UKr`sq!l2odVyy?Iij6G{jihgIK)H_Jo zR46q=fl^o2Exkjwbun%0E121`Dx&y*2sp#R4kxd)6Q??=%Hx$> z(PY3~-r-j3cQMJ^fP1mSUGHSy)cd>B{fGe5r1rG*B15`6fRQ&#Niizg0iGI@wOL6X z@zo0at1qN@Z8OcN;mN=M&VQ@-Kw5MswJj}|*3A1lT{ZMr1<7x`8k!9n=E7?PBsOBy zqa@@tgkhg<27Q3*ZIqR=aW~4uAk~SIsX6Z_l}xGkQ}`PxlToH_L`uH4W}r}2ENsclu4Rj~L<_dyk{2U(SMxuUnV zCx3Ye8qfWaAKAhACdw{9EVCX1t_xGJ2^u!Rvkbv;SP)#A;#h|~#5DBhNqUZkM1ofy zB90&_S^!{yB>)v77;M`ND{$nHrw`HCY3|^1hM`mB)&XCuwmXf7!|e*61fHZA>WI4` z4&zg2HuQ1eC1(z*WNlq^{mDZs=dcA`oE@o4#}`&^D3A`-M1#?nFTdQy!L`K~aB(Qab3$W$+i^x@7PhYBsPWa$ssR$1?o3FN1l>R7 zNKen|c0*bWD06Byg56#x@=BTXQ*h7l8ym_*EUS7a|1A`~zg8w4Wcff#)Eqm#>dXnf zr?orOcnsID-fQ%ANN}wrxDr+|cl7lww94r-L1HcyES;blxS^WUHirG(zR!BT&(2!i zOxKV547@Pd?oW@*v=8`m9;sPVfOV7p)@45P&!ElJv7`*c8X!;Vkgrc6p_b!SNq|Le z273v-dt;hPMj(iqSygltsG7kVJv4xOYEviKp@FojDVchY)VdubXm<_vYc3OT$t$NJ zu#R;o7shW#i4Spo<}|eCpqA77xNdl)t!?02u-}^;kwtY3Tsk}twlbKyp#<`jqJa^B zj`7<)0B^?LS96oE0o?BzWN@zooyJsZ>Qm{fW(v9~N``5wLrrQ{fiAf3`a9mLlq|P1^l>`HR-1)fS!D1bpnmXf6Y5s#5)wK+& z7KgI{L%qAh%AA9^b8M&hYN<}spD2oC5H~mn#p{_rKLbVR0F>nr$6o-Q=X_3WC&)q5 z`2rWr(8t*JaGnYOaVc-#5&Lq5{&^V3do9_m_lW6IXM?)Avr+zpI2M)JBAtzG0pG2C z73)Upg|-Gqp0_n}8F$?=_7=e_J&pfa%OS}dE_z%G;sjqWlnyF_TZKXw5{dwZR9q;p zbecH&ib!r~G9^c1JJqv1ba`;!PqS0uz~BGsj_tASB!ptS)i|CK_w$&bJ_WIB2mkW$ z&|U=TA*wn4w!q1L5k@Ev!Tk^<;-K&(d6IA_jw2kzmTex}$i>bf@YqkF>ddas)Cat(222q(;Ax8H3#&n`t=zZ77ZriKtO!s3_s^+9a-dJA z%Kx^S@)HDkNQXC7E@u*@01_}GflMv1N9R$NK8_&h_>wt{7e@{Y7F19D+bAxbzVl3P z34LPFCvJ!oRBt<_)B&*xTBDYSDDYuJtF z-Ybf^dv(vjJ(mtR;sGc8;4D{ix9K$Dd*>PV)n@a@OPS@1dFN}H=NZ8F3%Tf1RvB$u zEg@u)^izR9m8MYqPUQ>Oz8_ux=fjm%O3N!X#s4iMlu99DUu>YByTHBAzSU~V1+Avc z++a1veOJ`Ka!}*BFL-Ll4c9Z{VSo#kQvELP8)8huBST@YNSZ{?~HTzWlmc{mEV5_&xLG zc7MNDowY(B-$JlymQuQGr{4<_hmxl5swgXJkh5(a)%a?e}@+Bo3%d?RZt69`mL(#T>8v=|MRta;c)<^iy*N>%T~{u zd5ux(J`Wqc_dwsWNJ8l5DhbgAZAV~#UKcdCCia(fK@S4QKf4Wz+=e#x8U`357-NQL zyQjsio{Ag`jooOP?xy{&f52$z6~A{@ef`OHDPoFgC87&u^!Hl(9`sWdyAMjcE=%%D z?!9~-+#!lW=9~IBQ9xot|JE}fH!0&z>#?W2PawZfAiqx_|EvjQ&FE~Weh>BKpzEKn z?R!>`48&l%omTBKtFwt^ksa^&+|2t)+o{|Afs)twq->OhGLdQcWm`Ul8pt46^er#w zT5JgVn(txbLE_}~lza_KGn;zX5ZZehI)*3pkZN>H%{0({D%lhdrf%at$aG-6K6Tsf zGV697bYgQjb*sh$OPv>n&nK~?=F-b2hC|QX#TfIp$=`{XsrB6+z|JzzjcT9PW?m?f zxgNaER~Ph8p+NsOJHD>RfTQm9n%P#yH`!T-Qgsm>z>DBFicCQt(-I-!ZQ~fA}x##Fw*cvM5Yds-6oPY{3=*mse9=ELTNY zjDXuz{`YrqYg}5Trze@r>#o>ofxDdTch5QZT>kv<@na6pu^)xG-#`8rzJ2`o=jQkR zx@g0V9LoK6bX9dcoLr|!Do>CML2@dEerFl@_X)8aVha7!7OUx|g`f+v`lR+Ag{Mwa zd8js;ohP9ylr>ds%1>>zsHj5axou>Glid_ZhA(jW=Ua>$a=)dYojCr6X0_OUcxq{aCOkcea^*>GDpIj=d0@w% z#?k4ZQ)A@@hW#wu{m-9A-{46PM}}=g#z_x*5&BkOJN_`4RL^tcUboIR=+erhm3hzC zy`$+LdJIEji1&iGYkFkW_*Jj7h%HKA*`B^?c-4i`W+(k_DKu#}>UUVO;rEse*PdHm zb?jYCnm99zGXMJxux5?_NPfK5NC!>Bpokng)Qm#C-C;d);WOtUN;I!6K zrG=z*k&;))l`7J*p|;4TDnw1yqWfhul0Ds(#^PG)XrxpVjUv^bo2v(^I1C!)x@S`y zQxhJXa$IY*ABVx$zQ@lSV3 z*TY`Rs~4KBb?{Ig!W_oM_GTPZtkrz!p|ly3*<3e@sWD38^Bf*4m04}!)JA9H8Zd6W z8<$x%+3Ev~JDYCDjiqs{sHbi8GPJRGZ334?C9POL>qi`Nr6T-P;%cxo7%|Sbn9rZN zZ4u7Sh^UOJj|c$I?npWHBzTq)IP>zmYT^uH>~qNvg$woLEWK77bN6(TMM2PR?g97C8SO$`dPU7!$3_cU;3VRYUNiFi9dKm~%NXr3q( zc$VO38W9XO42IZ*sm;1sI{4tDME41aQ+4FXpV04$xi&4^%z zFV9VfUsxVbnp7S2hhM(@a0G+z9WDcc;Z^-;6xFLn{kE7Fy#yu1Q1s+gzf&dqx-vC8 zn$vVduojVOs6`S!j9w9!O|YisSXzUzFTs-55(*rE5`b}?G&-!`qWHM)xxP4_7N9?5 z`SoPhLV8>ssJ18<7%1jpPLADi!@l57#BtzAZDt`l3kNKHVb*xo?104!>}Wfmx@S_H zGiWcI9u0kXaIe~TEnl9R6tlk{@B27^hOHR2YTT#viC%NJw2#GwH10)!=I|M5(pbpL zYAgA~lG>tVRX`mFc;nYcX16poJ-gHI*4IXdk6YAe1mE3$^%lQcB%m${NW)6{mS4RN zS|t2B@WL&*mM15?8o~VTpJ!z=&!xPcEO+yCZO`4zPxbYv2mL2}m7O{dq(o`iUj@2B(z1=3g+V4kJ==vYA!D*dP!1|D=QkSbx|L#$#s=v z2O3gJdR14uq^xUhy{fvcgzm}P>%kW7Z}CZ5OR!*xG<;SYK6whr0Njv{?Y29t2l}7P z0r(@6_sdn=$Wow>r=))`YbZ&Q*L{D|Ari=h_Gy{-h!&6xO zWQSbHWpdH1wpNlFAh`mcGP9H`JtV2RvEjMru0TghJsMVEuSzERaGSpt%tN9;57%4% zE=SNu#qQQu8(C+fW239%L)(Lm$jI!Vk>yXErXMKWFO4%WHn^-)v!>Mh z<%Fi4jd({$-R$l7p4gG-lUeZtS zjvfPNn}s$)Hpg=~?W{sWUrkicqR4TR;WwLep8o z=g2VKq-ivZamHYAgbf=fLD`>|LdY$#*ON!T4}xH?CEM|KJzYw=QQ4Aiz_2Vyw@q%T zUH&Rmw8piJTj4z8mdLnmjj$cNH_~lCEgKdlxj;22yFrLwj@eVe>Agasvj`=RD2B~0 zlp9GJmha8(hw%ZhNo-@m2`VM?D) zmH%xuz2H!)Qk90Fl5z*ckzR$mnPuLaYNcw4Rh;;ir7YQMf! z)$5j~4G+lgAeb~uNo}^(>$;&yRVUUWD=W&Q@S!>k#D0ezn^cmjw%TQHx;80hZq=3} zqr_E;&Z!c(za79Q1WC`?_cYSN~_TwJs)JiQgsO=m$ASb;105W4vm-VTAW zK_i1-HY za*VE+_{Ym;e8bFNySDt*LI~Nlg~L|U^Y(kU?q6PgE+WiPDHpgvU72ij+(|Nu(N$Ps z$Pc=-9v?OpJ$=$eT(8)=h+rpI{&-7&j?V}#G`qs$f?gkksO3ucVZd z`i*v~I?-sig6B#?Y24(r#kb}#c>_x7}A>QXON{9SzYD?}W%px)q&Vmp33Y@*Z5yMgJ z3n=@_jTrt%Y{al!-sG48OR$W=5)4z|36sH?u-So)kYSMafWKY9`4hKZP^QBOd5)s; znI(U7?}elCmt}w7-pdS!iH!bF))>9+TC^V8nF=lhZ*f!s4{JhyyCzf?i~KUI6$R&O zaaa;k>j^1+8>`FIS|nE<0+RdoT)|!u*Ym2>f;N7C+vZyx+b!*M{aUt4)!?+E{>y9E zmKTlue2XoGYi|GociR+|(WYqh_O%8a_4n2qTw&PnG{xr00VV+zNt0lBlOZ_LGzo0+ zR-u4xI*+;g{LQv1mLGb@&-%g4+txf?@n(N)9Rtc%$iVX?_tu3B98JA#Ap`m$l?*6P zndE@6i9$Y~Cm5Ed2$LSAfKtQ=}~Du3W_QU?I)>O5v$@gUAV9e0M7v_BL{~PxHbdl z-eaCG1W)KFdfM)(Vq1DYmGldpO6sIfyIjmPyvZq|-clO3{vD@?RzK?$k+@| zEkl$ht$TD+mD8tF_O|-U(=CeI%XBj2n3lUwcHF&2<}HceSVmXeiz&i`t!Wi0x;g8(qW}SB_+V2X!u|Puzy8_uEsZ1_^-YZA4MY)$ZyWii$@gDQO$Gq<`?|aPq{}uDT lAN6>Do59<@&A{8@4A3lC8!6z6dap}v)|DjJGKYri*yMHda z=!+64{dVwF^8%PYCvc*S;S`3m8Ueo0)EA2S1jsT)Q06hjYPx|C3_;c&wf=+f*ljEK z^;Wz4DD;G?u8D2=QPp@w6KXH8i4{+FDBu)F(HTS*(QGpCqlK`9&OGs=P))2^0$m|p zj!%-AXU`Lu(zT~31GGcu#LV0Qvv~SkA!Ci;vBSG-03vU>@PwmR6x?uZe z?S2@G(Ui2;!+s~aC%c`s*s9*&@8+*2U?P>P!>A^y)Iu1_flY9oN@dzZ^q{}l`R4g# zabMZsM=e`pUPWt|yEUTWY&6-d(cG>PKQtQKHLBY+TD9e9B3BWNyNB^}_dC8si)LgfoM{;V35x6EB=2KeP8cXwI#Rf-D#~ zI7?t8Ycm)HC>yhIhrtMTz*rRJ0Lb8_1C#X<&q|`%Mnl`OCd$atdG%2ix{snPY9g<- zm1jSh1&wCj&@-%AI<}Ny;CS+GhOZUNnM8xZW`$22%g_XK zz})}_(XkU7c+hv_6US8QZd`Ni*)vP0umxS470OSx$1R+I!)>Ao%;DFszudK<&t0~F zYlExi!75r%PfS%Ti$UrV#8?dEmD#Q1rlCyjuI{xx5t)}rv@{+^zm!oEmu+Nz?Xk3O zjm0H0y|n})5BWLd-woXA(qV_-=4RlB;&{rS`G6BPQ>=yTx(1@!A$-I@v5au?+^!#+ zfg`~8h1Gy&I!xwTDU$pC;m83;Lu0^<1rIvDyC!}%H%EyteS&jPKB#epC@VD|o z>q_H(g0>>`JJO}GP?oh$S+0q5N$V`znuvEjB)_mZvU`=O9k^Yy*IZj&Zmu_sRi8-R zYCSad0(g2Uo&qh6l^vc2Vf8lnwcRf8%$}I)2QG#<(wpUIp>*aYOClN?jM04_zJ9HD4JZPN+KWKWILu8A4q*40+H2=-I?*8`V^A z4=m)F_N@*!fz@43)NUumH{JG9SzoY&CYQ<>(VNnAjZbT7MWMfuJlFDuPcM3j=ta77 zU50!r!v)&HRy!F!xjiM7q2V-?TW~nhDP(^u#MernL|bf7$5UQh!vk%t2oH&n*OT=I z4LV;fbC2GO)W~wVSHty(NA@Zps(t;jASCM`(#_&>bKpfqdA`adNDdez?~1re&mK@F|=mR=uDEfiHuyh zgl_%J&adrqsOrcHhtPnt7=7tLagI)y%KlRu^&=v}m zl&mRN#ji!xXgY&W{5**i7{lZt^0vnqhZ*qx>6QG~o_oH#4Sj9IRhnikm*ObkFtiOh z!lfJnP_#=zk~rZ_Y4*yf`COD-je@BU6SzZ&It6iN&`RLhV;GYBL=ZRy81obqE~nn? z1QdV_D9r$d%>kXId`>LePeGH}92U&NM$mF$n(chg%gwgLUOs*KGKiwRmTcGC&6Ia) zMHD%;q5wg2MNX|YhcjyRPN?av3rALE;`GQWm9grKpgZ@koLBv2$4PN|n5IQlKZ@b` zWH^)c-6|Bam{1rF6c@^sQ^YcPMkF^JAjSt`JJz#w>vGX^FIgQ62mSrKI{K)OZ4TqzA{@uz0~Z1eCmod60~}(kO%a$qU|51; zNXmB0S>TZuC(&%BdTUr&ZOJGR6xynfEDz4|_s>Ee@x!qXxT*$JAvK`Nptx;T+tNcz zxTpwJz9KyP-=9;3IHOO;%Kx^S@+Sx~ONTp=@*|0oj{-0~Mv zO6`(ycZRKyB&qM3U|F}Fka9Ncp$!x0y8!t+mJC1(>(W1f2&ai1+De9Vh1FUFnM zBF__mA33q;b5t2_j?)4ZMUpQEH}wfh5+`ELY(EY@|MB6>Dy640ImO?W5fWdB*o%kc zr!H{svv0MUazU$Mm>aA{e^3QQdvWrrz$?|eM_$tuv7&2&dftvQ_{^sQN$k zTk-HLf8JBv*QB`lig~N9F$HyvW8Tt3GjHIbrTHeOQc*x43i_Za zjL(t}3mv#y0=hON-7*H8frC+Hg2gO`rZBclQ2@9$H6RYMYHPQYvQ6Kmt!KG?uFt!? z|H7PV`d{cEl{=Sv9swM0AE`#4TE%?Bs#j5+lhMYh>FpkzHPpPerOfeG43YPm&t_@k z)ze8Lv!!%4jPnAc3nurc{Ete!#(LoTm?-sd~H9_;AJA8#4YaRr?VgWup>_^KuvqSn9p)HZ{K0yf zy*pz=$+pv|*F4j^gM*^2b^Bw`(_+D4t>9(c=A8ZfxctgP`EV4{yjfyHE4iti?`pO_ zgUM|55c!_7I!)BmEaU_M=fQ8t=b#Q0ETCPjG*E=Ks!qN*U)Xh%=j^zqV{(9|& zBlBf{TkT~A<5Wifkv+y}`VMKtZlQ(?Avo6-URx~6%ecV{-W@-z2$}t4*tTlQGP9qE zwP1)l-;Vc_4uT2IEzGwjbUXU-9wc`v_2{V0T>ARjz2&TtpO)AF{+3SQW}7P1Yd{l! zLZbmk{i%%x7r6C>BV5xkY#7ag zZNt-rQ1;h0F@Qo{3^d8I#9MbU&^Z3KT@2tCwK0$s>*529hK_?#Hf~`YVR0CnmK(}6 zgG1ZB*2aLcGw?TQV*rKv7-*WI%WvPuK;v)T#{jN0GJt`mF8nA`b;6Z6W z2#O{O>Z8=IiFNUkF3_jCfOw8PW#YkDLUGN&yY-Ufnal|sq)yv{Ds-jyt2_Uot2>$N z6p@#E%;4o+$0>4+rL(!lBCjXQbJtip#At**qxG(~z2lp+CBf8;QkMT2j`Y7n$ z;HQN~(ns^BJd)MT^@q5>x$UTAf^{RYyY&@RH`ktsEMK2*g{IdKLK#P}z*!+&eD^`0 zBz4{A1jVRG8t~=xdP5JG=$YKqaC)EvrDD_#t%oaWNB7&RPT&2_u0KqippN93UP1Da zjUWCqa@|L%H!FxP^xW~byp{2yI?=982&o=r;r%|56oyZ) zpS|Kb#<5=h_17_;;T6B%m2sy5o8&b?c7uR_ZN1&zp$hD3pgD;6#za3p{2%+Y)E%@@004HECKUhx literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_a1927d1c-0c7f-4811-bc65-6cad0abef0cc.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_a1927d1c-0c7f-4811-bc65-6cad0abef0cc.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..36f1d043b972fb013241ecf743aa9c30361efaae GIT binary patch literal 3724 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_ab1dce9d-e117-4615-9483-27a962f44fd9.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_ab1dce9d-e117-4615-9483-27a962f44fd9.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..608c3c6deae9460be87dd6d6db0a6346b45445db GIT binary patch literal 3338 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 UU-==go4$Ye9|6a3vwTMY0OaMWOaK4? literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_be54f7d5-2026-4803-b4ee-9fd823b9b028.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_be54f7d5-2026-4803-b4ee-9fd823b9b028.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..213efe7bc44b12e1f6a188e4baaef0ec00609cde GIT binary patch literal 2891 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_ce0bd42f-c6fd-4e13-bbf6-bb4cb33731a3.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_ce0bd42f-c6fd-4e13-bbf6-bb4cb33731a3.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..7666b16c350c985aea76603f3264aaa2e786b731 GIT binary patch literal 3841 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 DC=QaX literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_d5ec8a92-81b8-4f1d-a34a-5c4ab8def082.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_d5ec8a92-81b8-4f1d-a34a-5c4ab8def082.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..0c11cf78476bde9dfd788969ecd99f4b7bf0e709 GIT binary patch literal 2665 zcmV-v3YPUBiwFR|&emxH1MOOAbD~-k{yx7V<>z6-VG)?Bsk$!cU_>JzC~r-r4seVp zi(&48)z#I{_3!O{ z$-xgwp!9j~uIBnEyHAl+nIIX0Wtp!u{e@yV!13nch}CR8Le#~McBc(*#XF;` z+%(!<<4zoiwZ0~GJ9ny9Q8cmsGB$zn^l1wu!*gZf<u zVzyi*Ng~%u6O_rdm1t+s({^5-;Aa8c2$djz7Dmt`;#Ev!15iXbXy%10AJ0r|rzky- zef<2gK(^stiqgwwpW&&4m`MICecpX{>IQ3>wTgOk*XxUY@$N?I)bBc7MXt2-7j}bL znj9`VgDWqD&dcF$e}3*=;WvGhKnEu9V214x26pJU!F{%=q3@-`L5pqE)s;mn^I@R- zd)t4WF^=Iq+8(^q)DycYtcJ!SwJCk&`1-2tHx^K3r{0lnE zS5YNAe+B<=V<;PDU1F*?x7XKl)w}8F!q^!o^8wzmX*5Vv=dlbjEk8)0Qy)Iali><~ zU&xaR)z*fqjwUhk#9nqLBHgV^D=Zt3YvgiYV~;ymKXOsE1-p_4nazi5h?U5=l=F^; zp}+GI*ni6ogTxI|Y@b_u#dB^m1qC5J2Z^kdpCgI@g^J=**p{|f(I5)FeBg) zkY*sk+6=UK2aQOc+MQ|SMsf1%vcM#B2${mb&Rrk*;h`;$T;w_Nk$skowTG%dJ1F^h zY(Wc$L1%8c!ov0i(xO_xy#My?w_7;AHuwS-j#jO`RW!emn5tBk{LBKSSn{`)qs%O$ z?{-~jCD$cvEfo@f7`-knyOTcIoyZUEiL{h6s{)~m%@n&iorDG(^e7=UeJ_wEGY-!O z?4XtH+As&L2WSYXGzr~2Pap*(UVk~p8hUUV&(Ac+07>Rb*YiD(VesBszD4n=tTGG9 zUAQ)_jGgcLLpHxo3aU!#>VQ-leQ|kf^g%=Ol#xEw)jsLnYM!9!VxzC?osrsSt39B! z^l3knV^2RiJ{_C#@IgyutUK2+yx0`IX|~38U9+h#tfs(PhMqbiJKpGRy*GEHF5DsA z@`eM&)0^H#*OSZx4HN8L@n_tZCt|sy^~&;toZ|0`4%V&VwuDjd+qcu6=rPw5#Rx|B z_(WV^I|Th9eAo5ZhtDUz$`>swf>EXzZO5g1@| ztc3*1VQdO8tV1K3I?_!kdSk%YjXuUs;H`!J%tM&|Zf0eUhu9H<2|CN%DUt!4c{~&? zXKr*f6oAvD|3%JB&k3}=Y-kdts2~9uBj}(k(ErS%B^zPS6ZWs8Fx;YKv)}a#HV=6z6ZtK;kU_(!!b-#^^K;+#x)d z9tvIPPy_(@;)n7|;>igoj^u)zGu}J4L!4!y%fa{EWOaBr`1>E~7>=P86Ed``VK^mj zqmZD!c%cowKQJkUsh8|d6`?;<6hAG(IwRW|m3gAfa3f4d1Hz6o z-UGt3|NS*6#5;I81poUYE{?^E?{x zn8QV%S!J+JJ$=j~>E|S9+L+>GXDa2)_PBTb4XX``9E^zC! zFSVL-L95|}3#_JptBQ*D;^bAaqSU_MRWwbJs(npVPutOs6{oWpMbB(oJxD|^t2dw2 z&qs>;mJ~N1n3rOWD~L5-cux;qeg_XNr|X7S)*Ai=wKijpLifeJGg*JFzHbX8E>S^Jfnh z#mw#IatsYdt!UDlBbUDc6A@!H5oir30<%Vsu){P+Hre;TB&7A;9=~^`|JU;wkEaWA znZG!^^B22Z?h60{Pjl~ISm7D|FDfvL&4_>;W8-hA%peK_2XLB5%p(SKvc19Gt7SIz)MgRV&ogkQd-BW-x2OY9) zc++eZI8azI=UxtRJ-GAZJ=1tV6?7|Q;g{UX1#yOY)vsI@a&pYAxUuE>c)07=j@)zB zB9$g~=pT{XIK>0w_;=<-?~;nY*;@3LXVcZW6?D8}37L4$SFbIp6;a{lTx5nsyS&lWjpCZL=S3vmb4oE@zehS&%QwRQ#Jqq*8?{} literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_dc01d2a8-3692-4dc6-a64b-df567415ffee.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_dc01d2a8-3692-4dc6-a64b-df567415ffee.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..e6557f6fb4026063aa90e929f67a73dacb71ae77 GIT binary patch literal 4110 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 literal 0 HcmV?d00001 diff --git a/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_e81fc082-38b7-414b-a0c7-28f7a44c1e70.json.gz b/service/worker/workerdeployment/replaytester/testdata/v2/run_1775685309/replay_worker_deployment_wf_run_e81fc082-38b7-414b-a0c7-28f7a44c1e70.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ccdd50a33d78e3cb7170c8b1957a3d72258be0ba GIT binary patch literal 2976 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 WYC8@1B` it's ok to drop all signals and updates. // There is no pending signal or update, but the state is dirty or forceCaN is requested: (!d.signalHandler.signalSelector.HasPending() && d.signalHandler.processingSignals == 0 && workflow.AllHandlersFinished(ctx) && - // And there is a force CaN or a propagated state change - (d.forceCAN || (d.stateChanged && d.asyncPropagationsInProgress == 0))) + // And there is a force CaN or a propagated state change or history got too large + (d.forceCAN || (d.stateChanged && d.asyncPropagationsInProgress == 0) || workflow.GetInfo(ctx).GetContinueAsNewSuggested())) }) if err != nil { return err @@ -334,6 +356,8 @@ func (d *VersionWorkflowRunner) handleUpdateVersionMetadata(ctx workflow.Context delete(d.VersionState.Metadata.GetEntries(), key) // if m is nil, delete is a no-op } + d.VersionState.LastModifierIdentity = args.GetIdentity() + // although the handler might have not changed the metadata at all, still // it's better to CaN because some history events are built now. d.setStateChanged() @@ -343,6 +367,50 @@ func (d *VersionWorkflowRunner) handleUpdateVersionMetadata(ctx workflow.Context }, nil } +func (d *VersionWorkflowRunner) validateUpdateVersionComputeConfig(args *deploymentspb.UpdateComputeConfigArgs) error { + return d.ensureNotDeleted() +} + +func (d *VersionWorkflowRunner) handleUpdateVersionComputeConfig(ctx workflow.Context, args *deploymentspb.UpdateComputeConfigArgs) (*deploymentspb.UpdateComputeConfigResponse, error) { + if err := d.preUpdateChecks(ctx); err != nil { + return nil, err + } + + if err := d.computeConfigLock.Lock(ctx); err != nil { + d.logger.Error("Could not acquire compute config lock in version workflow", "error", err) + return nil, err + } + defer d.computeConfigLock.Unlock() + + // Update or delete the Worker Controller Instance based on the new config. + apiVersion := &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: d.VersionState.Version.DeploymentName, + BuildId: d.VersionState.Version.BuildId, + } + activityCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions) + var computeConfigSummary *computepb.ComputeConfigSummary + err := workflow.ExecuteActivity(activityCtx, d.a.UpdateWorkerControllerInstance, &deploymentspb.UpdateWorkerControllerInstanceInput{ + Version: apiVersion, + Identity: args.GetIdentity(), + UpsertScalingGroups: args.GetUpsertScalingGroups(), + RemoveScalingGroups: args.GetRemoveScalingGroups(), + }).Get(ctx, &computeConfigSummary) + if err != nil { + var appErr *temporal.ApplicationError + if errors.As(err, &appErr) && appErr.Type() == errInvalidComputeConfig { + return nil, appErr + } + return nil, serviceerror.NewInternalf("update worker controller instance: %v", err) + } + + d.VersionState.ComputeConfig = computeConfigSummary + d.VersionState.LastModifierIdentity = args.GetIdentity() + d.setStateChanged() + d.syncSummary(ctx) + + return &deploymentspb.UpdateComputeConfigResponse{}, nil +} + func (d *VersionWorkflowRunner) startDrainage(ctx workflow.Context) { if d.VersionState.GetDrainageInfo().GetStatus() == enumspb.VERSION_DRAINAGE_STATUS_UNSPECIFIED { now := timestamppb.New(workflow.Now(ctx)) @@ -380,13 +448,20 @@ func (d *VersionWorkflowRunner) handleDeleteVersion(ctx workflow.Context, args * // use lock to enforce only one update at a time err := d.lock.Lock(ctx) if err != nil { - d.logger.Error("Could not acquire workflow lock") - return serviceerror.NewDeadlineExceeded("Could not acquire workflow lock") + d.logger.Error("Could not acquire workflow lock in version workflow", "error", err) + return err + } + // also lock compute config lock to ensure that the compute config is not updated while the version is being deleted. + err = d.computeConfigLock.Lock(ctx) + if err != nil { + d.logger.Error("Could not acquire compute config lock in version workflow", "error", err) + return err } defer func() { // although the handler might have not changed the state and had returned an error, still // it's better to CaN because some history events are built now. d.setStateChanged() + d.computeConfigLock.Unlock() d.lock.Unlock() }() @@ -431,10 +506,29 @@ func (d *VersionWorkflowRunner) handleDeleteVersion(ctx workflow.Context, args * return err } + if workflow.GetVersion(ctx, "delete-wci", workflow.DefaultVersion, 1) != workflow.DefaultVersion { + apiVersion := &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: d.VersionState.Version.DeploymentName, + BuildId: d.VersionState.Version.BuildId, + } + err = workflow.ExecuteActivity(activityCtx, d.a.DeleteWorkerControllerInstance, &deploymentspb.DeleteWorkerControllerInstanceInput{ + Version: apiVersion, + Identity: args.GetIdentity(), + }).Get(ctx, nil) + if err != nil { + return serviceerror.NewInternalf("delete worker controller instance: %v", err) + } + } + if args.AsyncPropagation { d.deleteVersion = true - if d.hasMinVersion(VersionDataRevisionNumber) { - d.syncTaskQueuesAsync(ctx, nil, true) + if workflow.GetVersion(ctx, "serialDelete", workflow.DefaultVersion, 1) == workflow.DefaultVersion { + if d.hasMinVersion(VersionDataRevisionNumber) { + d.syncTaskQueuesAsync(ctx, nil, true) + } else { + d.asyncPropagationsInProgress++ + workflow.Go(ctx, d.deleteVersionFromTaskQueuesAsync) + } } else { d.asyncPropagationsInProgress++ workflow.Go(ctx, d.deleteVersionFromTaskQueuesAsync) @@ -575,16 +669,29 @@ func (d *VersionWorkflowRunner) handleRegisterWorker(ctx workflow.Context, args err = workflow.Await(ctx, func() bool { return d.asyncPropagationsInProgress == 0 }) + if err != nil { + return err + } } - if err != nil { - return err - } + if d.deleteVersion { - // In case it was marked as deleted we make it undeleted - d.deleteVersion = false - if withRevisionNumbers { - // If we're changing the version data, we need to increment the revision number - d.GetVersionState().RevisionNumber++ + if workflow.GetVersion(ctx, "awaitSerialDelete", workflow.DefaultVersion, 1) == workflow.DefaultVersion { + // In case it was marked as deleted we make it undeleted + d.deleteVersion = false + if withRevisionNumbers { + // If we're changing the version data, we need to increment the revision number + d.GetVersionState().RevisionNumber++ + } + } else { + // In case this version just got deleted, we wait until it finished propagating delete to all task queues before reviving it. + // This is because the deleted flag propagation is not protected by revision number and if done parallel to other propagations, it can cause a race condition. + err = workflow.Await(ctx, func() bool { + return d.asyncPropagationsInProgress == 0 + }) + if err != nil { + return err + } + d.reviveDeleted(ctx) } } @@ -606,6 +713,12 @@ func (d *VersionWorkflowRunner) handleRegisterWorker(ctx workflow.Context, args d.VersionState.TaskQueueFamilies[args.TaskQueueName].TaskQueues[int32(args.TaskQueueType)] = &deploymentspb.TaskQueueVersionData{} + // Transition from CREATED to INACTIVE once a poller registers a task queue. + if d.VersionState.Status == enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED { + d.VersionState.Status = enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE + // deployment workflow updates the status in version summary to INACTIVE + } + if withRevisionNumbers && args.GetRoutingConfig() != nil { // Still need to check RoutingConfig not being nil because of edge cases during enabling dynamic config. // i.e. the deployment workflow might run old version and not send the routing config. @@ -616,6 +729,19 @@ func (d *VersionWorkflowRunner) handleRegisterWorker(ctx workflow.Context, args return err } +func (d *VersionWorkflowRunner) reviveDeleted(ctx workflow.Context) { + // Resetting state to get rid of the info from the past life. + state := makeNewVersionState(d.VersionState.Version.DeploymentName, + d.VersionState.Version.BuildId, + workflow.Now(ctx), + "", + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, + nil, + d.VersionState.SyncBatchSize) + d.VersionState = state + d.deleteVersion = false +} + func (d *VersionWorkflowRunner) syncRegisteredTaskQueueOld(ctx workflow.Context, args *deploymentspb.RegisterWorkerInVersionArgs) error { // initial data var data *deploymentspb.DeploymentVersionData @@ -885,6 +1011,7 @@ func versionStateToSummary(s *deploymentspb.VersionLocalState) *deploymentspb.Wo LastCurrentTime: s.LastCurrentTime, LastDeactivationTime: s.LastDeactivationTime, Status: s.Status, + ComputeConfig: s.ComputeConfig, } } diff --git a/service/worker/workerdeployment/version_workflow_test.go b/service/worker/workerdeployment/version_workflow_test.go index b59d5783e85..225ead0a779 100644 --- a/service/worker/workerdeployment/version_workflow_test.go +++ b/service/worker/workerdeployment/version_workflow_test.go @@ -3,17 +3,20 @@ package workerdeployment import ( "context" "fmt" + "sync/atomic" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" + computepb "go.temporal.io/api/compute/v1" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/testsuite" "go.temporal.io/sdk/workflow" deploymentspb "go.temporal.io/server/api/deployment/v1" + "go.temporal.io/server/common/testing/protorequire" "go.temporal.io/server/common/testing/testvars" "go.temporal.io/server/common/worker_versioning" "go.uber.org/mock/gomock" @@ -23,6 +26,7 @@ import ( type VersionWorkflowSuite struct { suite.Suite testsuite.WorkflowTestSuite + protorequire.ProtoAssertions controller *gomock.Controller env *testsuite.TestWorkflowEnvironment workerDeploymentClient *ClientImpl @@ -30,18 +34,12 @@ type VersionWorkflowSuite struct { } func TestVersionWorkflowSuite(t *testing.T) { - t.Run("v0", func(t *testing.T) { - suite.Run(t, &VersionWorkflowSuite{workflowVersion: InitialVersion}) - }) - t.Run("v1", func(t *testing.T) { - suite.Run(t, &VersionWorkflowSuite{workflowVersion: AsyncSetCurrentAndRamping}) - }) - t.Run("v2", func(t *testing.T) { - suite.Run(t, &VersionWorkflowSuite{workflowVersion: VersionDataRevisionNumber}) - }) + t.Parallel() + suite.Run(t, &VersionWorkflowSuite{workflowVersion: VersionDataRevisionNumber}) } func (s *VersionWorkflowSuite) SetupTest() { + s.ProtoAssertions = protorequire.New(s.T()) s.controller = gomock.NewController(s.T()) s.env = s.WorkflowTestSuite.NewTestWorkflowEnvironment() @@ -96,7 +94,7 @@ func (s *VersionWorkflowSuite) syncStateInBatches(totalWorkers int) { s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil) - for workerNum := 0; workerNum < totalWorkers; workerNum++ { + for workerNum := range totalWorkers { s.env.RegisterDelayedCallback(func() { registerWorkerArgs := &deploymentspb.RegisterWorkerInVersionArgs{ @@ -111,7 +109,7 @@ func (s *VersionWorkflowSuite) syncStateInBatches(totalWorkers int) { }, OnAccept: func() { }, - OnComplete: func(i interface{}, err error) { + OnComplete: func(i any, err error) { }, }, registerWorkerArgs) }, 1*time.Millisecond) @@ -163,13 +161,13 @@ func (s *VersionWorkflowSuite) syncStateInBatches(totalWorkers int) { }, OnAccept: func() { }, - OnComplete: func(i interface{}, err error) { + OnComplete: func(i any, err error) { }, }, syncStateArgs) }, 30*time.Millisecond) - for i := 0; i < totalWorkers; i++ { + for i := range totalWorkers { syncReq.Sync = append(syncReq.Sync, &deploymentspb.SyncDeploymentVersionUserDataRequest_SyncUserData{ Name: tv.TaskQueue().Name + fmt.Sprintf("%03d", i), Types: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW}, @@ -277,7 +275,7 @@ func (s *VersionWorkflowSuite) Test_SyncRoutingConfigAsync() { s.Fail("sync state update should not have failed", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncStateArgs) @@ -357,7 +355,7 @@ func (s *VersionWorkflowSuite) Test_AsyncPropagationsPreventsCanUntilComplete() s.Fail("sync state update should not have failed") }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncStateArgs) @@ -391,8 +389,8 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_Success() { tv := testvars.New(s.T()) var a *VersionActivities - s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) - s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + s.env.RegisterActivity(a.DeleteWorkerControllerInstance) + s.env.OnActivity(a.DeleteWorkerControllerInstance, mock.Anything, mock.Anything).Return(nil).Maybe() taskQueueName := tv.TaskQueue().Name @@ -424,7 +422,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_Success() { s.Fail("delete version should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete version should complete without error") }, }, deleteArgs) @@ -460,8 +458,8 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_QueryAfterDeletion() { tv := testvars.New(s.T()) var a *VersionActivities - s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) - s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + s.env.RegisterActivity(a.DeleteWorkerControllerInstance) + s.env.OnActivity(a.DeleteWorkerControllerInstance, mock.Anything, mock.Anything).Return(nil).Maybe() taskQueueName := tv.TaskQueue().Name @@ -491,7 +489,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_QueryAfterDeletion() { s.Fail("delete version should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete version should complete without error") }, }, deleteArgs) @@ -551,7 +549,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_FailsWhenDraining() { s.Fail("delete version should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().Error(err, "delete version should fail when version is draining") var applicationError *temporal.ApplicationError s.Require().ErrorAs(err, &applicationError) @@ -595,8 +593,8 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_SucceedsWhenDrainingWithSkipFl now := timestamppb.New(time.Now()) var a *VersionActivities - s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) - s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + s.env.RegisterActivity(a.DeleteWorkerControllerInstance) + s.env.OnActivity(a.DeleteWorkerControllerInstance, mock.Anything, mock.Anything).Return(nil).Maybe() taskQueueName := tv.TaskQueue().Name @@ -625,7 +623,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_SucceedsWhenDrainingWithSkipFl s.Fail("delete version should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete version should succeed when SkipDrainage is true") }, }, deleteArgs) @@ -684,7 +682,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_FailsWithActivePollers() { s.Fail("delete version should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().Error(err, "delete version should fail when version has active pollers") var applicationError *temporal.ApplicationError s.Require().ErrorAs(err, &applicationError) @@ -771,8 +769,8 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_AsyncPropagation() { tv := testvars.New(s.T()) var a *VersionActivities - s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) - s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + s.env.RegisterActivity(a.DeleteWorkerControllerInstance) + s.env.OnActivity(a.DeleteWorkerControllerInstance, mock.Anything, mock.Anything).Return(nil).Maybe() taskQueueName := tv.TaskQueue().Name @@ -807,8 +805,10 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_AsyncPropagation() { OnReject: func(err error) { s.Fail("delete version should not have been rejected", err) }, - OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnAccept: func() { + fmt.Println("delete version accepted") + }, + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete version should complete without error even with async propagation") }, }, deleteArgs) @@ -914,7 +914,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_AsyncPropagation_BlocksWorkerR s.Fail("delete version should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete version should complete without error") deleteCompleted = true }, @@ -936,7 +936,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_AsyncPropagation_BlocksWorkerR s.Fail("register worker should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { // Worker registration should complete only after propagation finishes s.True(deleteCompleted, "worker registration should complete after delete propagation") s.Require().NoError(err, "register worker should complete without error") @@ -965,7 +965,7 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_AsyncPropagation_BlocksWorkerR s.Fail("second delete should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "second delete should complete without error") }, }, deleteArgs2) @@ -997,16 +997,16 @@ func (s *VersionWorkflowSuite) Test_DeleteVersion_AsyncPropagation_BlocksWorkerR s.True(workerRegistrationCompleted, "worker registration should have completed") } -// Test_RegisterWorker_IncrementsRevisionNumber_WhenRevivingDeletedVersion tests that the revision number -// is incremented when a worker registers on a version that was previously deleted -func (s *VersionWorkflowSuite) Test_RegisterWorker_IncrementsRevisionNumber_WhenRevivingDeletedVersion() { +// Test_RegisterWorker_ResetRevisionNumber_WhenRevivingDeletedVersion tests that the revision number +// is reset to 0 when a worker registers on a version that was previously deleted +func (s *VersionWorkflowSuite) Test_RegisterWorker_ResetRevisionNumber_WhenRevivingDeletedVersion() { s.skipBeforeVersion(VersionDataRevisionNumber) tv := testvars.New(s.T()) var a *VersionActivities - s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) - s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + s.env.RegisterActivity(a.DeleteWorkerControllerInstance) + s.env.OnActivity(a.DeleteWorkerControllerInstance, mock.Anything, mock.Anything).Return(nil).Maybe() taskQueueName := tv.TaskQueue().Name newTaskQueueName := tv.TaskQueue().Name + "_new" @@ -1017,7 +1017,7 @@ func (s *VersionWorkflowSuite) Test_RegisterWorker_IncrementsRevisionNumber_When // Mock delete and register propagation s.env.OnActivity(a.SyncDeploymentVersionUserData, mock.Anything, mock.Anything).Return( func(ctx context.Context, req *deploymentspb.SyncDeploymentVersionUserDataRequest) (*deploymentspb.SyncDeploymentVersionUserDataResponse, error) { - if req.UpsertVersionData != nil && req.UpsertVersionData.Deleted { + if req.GetForgetVersion() { // This is the delete call s.Equal(int64(6), req.UpsertVersionData.RevisionNumber, "Revision number should be incremented from 5 to 6 on delete") return &deploymentspb.SyncDeploymentVersionUserDataResponse{ @@ -1027,7 +1027,7 @@ func (s *VersionWorkflowSuite) Test_RegisterWorker_IncrementsRevisionNumber_When // This is a register worker propagation call s.NotNil(req.UpsertVersionData, "UpsertVersionData should be present for registration") s.False(req.UpsertVersionData.Deleted, "Deleted should be false after revival") - s.Equal(int64(7), req.UpsertVersionData.RevisionNumber, "Revision number should be incremented from 6 to 7 on revival") + s.Equal(int64(0), req.UpsertVersionData.RevisionNumber, "Revision number should be reset to 0 on revival") return &deploymentspb.SyncDeploymentVersionUserDataResponse{ TaskQueueMaxVersions: map[string]int64{newTaskQueueName: 1}, }, nil @@ -1049,7 +1049,7 @@ func (s *VersionWorkflowSuite) Test_RegisterWorker_IncrementsRevisionNumber_When s.Fail("delete should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, deleteArgs) @@ -1076,8 +1076,36 @@ func (s *VersionWorkflowSuite) Test_RegisterWorker_IncrementsRevisionNumber_When s.Fail("register should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { + s.Require().NoError(err) + + // Capture state after revive + queryResp := &deploymentspb.QueryDescribeVersionResponse{} + val, err := s.env.QueryWorkflow(QueryDescribeVersion) + s.Require().NoError(err) + err = val.Get(queryResp) s.Require().NoError(err) + stateAfterRevive := queryResp.VersionState + + // Verify that status is reset to INACTIVE + s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, stateAfterRevive.Status) + + // Verify that timing fields are reset + s.Nil(stateAfterRevive.CurrentSinceTime, "CurrentSinceTime should be reset") + s.Nil(stateAfterRevive.RampingSinceTime, "RampingSinceTime should be reset") + s.Nil(stateAfterRevive.RoutingUpdateTime, "RoutingUpdateTime should be reset") + s.Nil(stateAfterRevive.FirstActivationTime, "FirstActivationTime should be reset") + s.Nil(stateAfterRevive.LastCurrentTime, "LastCurrentTime should be reset") + s.Nil(stateAfterRevive.LastDeactivationTime, "LastDeactivationTime should be reset") + + // Verify that ramp percentage is reset + s.InDelta(float32(0), stateAfterRevive.RampPercentage, 0) + + // Verify that drainage info is reset (drainage info is set to an empty struct and not nil) + s.Equal((&deploymentpb.VersionDrainageInfo{}).String(), stateAfterRevive.DrainageInfo.String(), "DrainageInfo should be reset") + + // Verify that metadata is reset + s.Nil(stateAfterRevive.Metadata, "Metadata should be reset") }, }, registerArgs) }, 50*time.Millisecond) @@ -1097,7 +1125,9 @@ func (s *VersionWorkflowSuite) Test_RegisterWorker_IncrementsRevisionNumber_When }, }, }, - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, + DrainageInfo: &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, + Metadata: &deploymentpb.VersionMetadata{}, RevisionNumber: 5, SyncBatchSize: int32(s.workerDeploymentClient.getSyncBatchSize()), StartedDeploymentWorkflow: true, @@ -1158,7 +1188,7 @@ func (s *VersionWorkflowSuite) Test_SyncState_IncrementsRevisionNumber_InAsyncMo s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1200,21 +1230,21 @@ func (s *VersionWorkflowSuite) Test_MultipleSyncStates_BlocksCaNUntilAllComplete taskQueueName := tv.TaskQueue().Name - syncCallCount := 0 + var syncCallCount atomic.Int32 s.env.OnActivity(a.SyncDeploymentVersionUserData, mock.Anything, mock.Anything).Return( func(ctx context.Context, req *deploymentspb.SyncDeploymentVersionUserDataRequest) (*deploymentspb.SyncDeploymentVersionUserDataResponse, error) { - syncCallCount++ + count := syncCallCount.Add(1) return &deploymentspb.SyncDeploymentVersionUserDataResponse{ - TaskQueueMaxVersions: map[string]int64{taskQueueName: int64(syncCallCount * 10)}, + TaskQueueMaxVersions: map[string]int64{taskQueueName: int64(count * 10)}, }, nil }, ).Maybe() // Mock propagation check with delay to simulate slow propagation - propagationCheckCount := 0 + var propagationCheckCount atomic.Int32 s.env.OnActivity(a.CheckWorkerDeploymentUserDataPropagation, mock.Anything, mock.Anything). Return(func(ctx context.Context, req *deploymentspb.CheckWorkerDeploymentUserDataPropagationRequest) error { - propagationCheckCount++ + propagationCheckCount.Add(1) return nil }). After(100 * time.Millisecond). @@ -1241,7 +1271,7 @@ func (s *VersionWorkflowSuite) Test_MultipleSyncStates_BlocksCaNUntilAllComplete s.Fail("first sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1267,7 +1297,7 @@ func (s *VersionWorkflowSuite) Test_MultipleSyncStates_BlocksCaNUntilAllComplete s.Fail("second sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1296,7 +1326,7 @@ func (s *VersionWorkflowSuite) Test_MultipleSyncStates_BlocksCaNUntilAllComplete s.True(s.env.IsWorkflowCompleted()) // Both propagations should have completed before CaN - s.Equal(2, propagationCheckCount, "Both propagations should complete before CaN") + s.Equal(2, int(propagationCheckCount.Load()), "Both propagations should complete before CaN") } // Test_SyncState_And_RegisterWorker_ConcurrentPropagations tests concurrent async propagations @@ -1312,10 +1342,10 @@ func (s *VersionWorkflowSuite) Test_SyncState_And_RegisterWorker_ConcurrentPropa taskQueueName := tv.TaskQueue().Name newTaskQueueName := tv.TaskQueue().Name + "_new" - syncActivityCalls := 0 + var syncActivityCalls atomic.Int32 s.env.OnActivity(a.SyncDeploymentVersionUserData, mock.Anything, mock.Anything).Return( func(ctx context.Context, req *deploymentspb.SyncDeploymentVersionUserDataRequest) (*deploymentspb.SyncDeploymentVersionUserDataResponse, error) { - syncActivityCalls++ + syncActivityCalls.Add(1) if req.UpdateRoutingConfig != nil && len(req.Sync) == 1 && req.Sync[0].Name == taskQueueName { // This is the SyncState propagation return &deploymentspb.SyncDeploymentVersionUserDataResponse{ @@ -1329,10 +1359,10 @@ func (s *VersionWorkflowSuite) Test_SyncState_And_RegisterWorker_ConcurrentPropa }, ).Maybe() - propagationChecks := 0 + var propagationChecks atomic.Int32 s.env.OnActivity(a.CheckWorkerDeploymentUserDataPropagation, mock.Anything, mock.Anything). Return(func(ctx context.Context, req *deploymentspb.CheckWorkerDeploymentUserDataPropagationRequest) error { - propagationChecks++ + propagationChecks.Add(1) return nil }). After(50 * time.Millisecond). @@ -1359,7 +1389,7 @@ func (s *VersionWorkflowSuite) Test_SyncState_And_RegisterWorker_ConcurrentPropa s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1386,7 +1416,7 @@ func (s *VersionWorkflowSuite) Test_SyncState_And_RegisterWorker_ConcurrentPropa s.Fail("register should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, registerArgs) @@ -1415,9 +1445,9 @@ func (s *VersionWorkflowSuite) Test_SyncState_And_RegisterWorker_ConcurrentPropa s.True(s.env.IsWorkflowCompleted()) // Both syncs should complete - s.GreaterOrEqual(syncActivityCalls, 2, "Both propagations should complete before CaN") + s.GreaterOrEqual(int(syncActivityCalls.Load()), 2, "Both propagations should complete before CaN") // Only syncVersionState should wait for propagation - s.GreaterOrEqual(propagationChecks, 1, "Both propagations should complete before CaN") + s.GreaterOrEqual(int(propagationChecks.Load()), 1, "Both propagations should complete before CaN") } // Test_SyncState_SignalsPropagationComplete_WithCorrectRevisionNumber tests that the deployment @@ -1451,7 +1481,7 @@ func (s *VersionWorkflowSuite) Test_SyncState_SignalsPropagationComplete_WithCor "", PropagationCompleteSignal, mock.Anything, - ).Return(func(namespace string, workflowID string, runID string, signalName string, arg interface{}) error { + ).Return(func(namespace string, workflowID string, runID string, signalName string, arg any) error { capturedSignalArg = arg.(*deploymentspb.PropagationCompletionInfo) return nil }).Maybe() @@ -1474,7 +1504,7 @@ func (s *VersionWorkflowSuite) Test_SyncState_SignalsPropagationComplete_WithCor s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1553,7 +1583,7 @@ func (s *VersionWorkflowSuite) Test_RegisterWorker_DoesNotSignalPropagationCompl s.Fail("register should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, registerArgs) @@ -1588,7 +1618,7 @@ func (s *VersionWorkflowSuite) Test_BatchTaskQueuesForSync_SingleBatch() { // Create 5 task queues, batch size is 25, so should be single batch taskQueues := make(map[string]*deploymentspb.VersionLocalState_TaskQueueFamilyData) - for i := 0; i < 5; i++ { + for i := range 5 { tqName := fmt.Sprintf("%s_%d", tv.TaskQueue().Name, i) taskQueues[tqName] = &deploymentspb.VersionLocalState_TaskQueueFamilyData{ TaskQueues: map[int32]*deploymentspb.TaskQueueVersionData{ @@ -1627,7 +1657,7 @@ func (s *VersionWorkflowSuite) Test_BatchTaskQueuesForSync_SingleBatch() { s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1663,7 +1693,7 @@ func (s *VersionWorkflowSuite) Test_BatchTaskQueuesForSync_MultipleBatches() { // Create 50 task queues, batch size is 25, so should be 2 batches taskQueues := make(map[string]*deploymentspb.VersionLocalState_TaskQueueFamilyData) - for i := 0; i < 50; i++ { + for i := range 50 { tqName := fmt.Sprintf("%s_%03d", tv.TaskQueue().Name, i) taskQueues[tqName] = &deploymentspb.VersionLocalState_TaskQueueFamilyData{ TaskQueues: map[int32]*deploymentspb.TaskQueueVersionData{ @@ -1714,7 +1744,7 @@ func (s *VersionWorkflowSuite) Test_BatchTaskQueuesForSync_MultipleBatches() { s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1750,7 +1780,7 @@ func (s *VersionWorkflowSuite) Test_BatchTaskQueuesForSync_PartialLastBatch() { // Create 27 task queues, batch size is 10, so should be 3 batches: 10, 10, 7 taskQueues := make(map[string]*deploymentspb.VersionLocalState_TaskQueueFamilyData) - for i := 0; i < 27; i++ { + for i := range 27 { tqName := fmt.Sprintf("%s_%02d", tv.TaskQueue().Name, i) taskQueues[tqName] = &deploymentspb.VersionLocalState_TaskQueueFamilyData{ TaskQueues: map[int32]*deploymentspb.TaskQueueVersionData{ @@ -1796,7 +1826,7 @@ func (s *VersionWorkflowSuite) Test_BatchTaskQueuesForSync_PartialLastBatch() { s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) }, }, syncArgs) @@ -1875,7 +1905,7 @@ func (s *VersionWorkflowSuite) Test_FindNewVersionStatusFromRoutingConfig_Curren s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) resp := result.(*deploymentspb.SyncVersionStateResponse) s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING, resp.Summary.Status, @@ -1961,7 +1991,7 @@ func (s *VersionWorkflowSuite) Test_FindNewVersionStatusFromRoutingConfig_Rampin s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) resp := result.(*deploymentspb.SyncVersionStateResponse) s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, resp.Summary.Status, @@ -2045,7 +2075,7 @@ func (s *VersionWorkflowSuite) Test_FindNewVersionStatusFromRoutingConfig_Inacti s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) resp := result.(*deploymentspb.SyncVersionStateResponse) s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, resp.Summary.Status, @@ -2126,7 +2156,7 @@ func (s *VersionWorkflowSuite) Test_UpdateStateFromRoutingConfig_UpdatesTimestam s.Fail("sync should not be rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) resp := result.(*deploymentspb.SyncVersionStateResponse) s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING, resp.Summary.Status) @@ -2233,6 +2263,164 @@ func (s *VersionWorkflowSuite) Test_DrainageStatusChange_TriggersAsyncPropagatio s.True(asyncPropagationTriggered, "Async propagation should be triggered for drainage status change") } +func (s *VersionWorkflowSuite) Test_UpdateComputeConfig_Success() { + tv := testvars.New(s.T()) + + var a *VersionActivities + s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + + s.env.RegisterActivity(a.UpdateWorkerControllerInstance) + s.env.OnActivity(a.UpdateWorkerControllerInstance, mock.Anything, mock.Anything).Return((*computepb.ComputeConfigSummary)(nil), nil).Once() + + // Mock external signal to deployment workflow + s.env.OnSignalExternalWorkflow(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() + + s.env.RegisterDelayedCallback(func() { + args := &deploymentspb.UpdateComputeConfigArgs{ + Identity: tv.ClientIdentity(), + RequestId: "req-1", + UpsertScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "group1": {ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: &computepb.ComputeProvider{Type: "aws-lambda"}, + }}, + }, + } + s.env.UpdateWorkflow(UpdateVersionComputeConfig, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("update should not be rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err) + }, + }, args) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentVersionWorkflowType, &deploymentspb.WorkerDeploymentVersionWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + VersionState: &deploymentspb.VersionLocalState{ + Version: &deploymentspb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), + }, + TaskQueueFamilies: map[string]*deploymentspb.VersionLocalState_TaskQueueFamilyData{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +func (s *VersionWorkflowSuite) Test_UpdateComputeConfig_RejectedWhenDeleted() { + tv := testvars.New(s.T()) + + var a *VersionActivities + s.env.RegisterActivity(a.DeleteWorkerControllerInstance) + s.env.OnActivity(a.DeleteWorkerControllerInstance, mock.Anything, mock.Anything).Return(nil).Maybe() + + s.env.OnActivity(a.SyncDeploymentVersionUserData, mock.Anything, mock.Anything).Return( + &deploymentspb.SyncDeploymentVersionUserDataResponse{}, nil, + ).Maybe() + s.env.OnActivity(a.CheckWorkerDeploymentUserDataPropagation, mock.Anything, mock.Anything).Return(nil).Maybe() + s.env.OnActivity(a.CheckIfTaskQueuesHavePollers, mock.Anything, mock.Anything).Return(false, nil).Maybe() + + // First delete the version. + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(DeleteVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { s.Fail("delete should not be rejected", err) }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err) + }, + }, &deploymentspb.DeleteVersionArgs{ + SkipDrainage: true, + }) + args := &deploymentspb.UpdateComputeConfigArgs{ + Identity: tv.ClientIdentity(), + RequestId: "req-1", + UpsertScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "group1": {ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: &computepb.ComputeProvider{Type: "aws-lambda"}, + }}, + }, + } + // Send update at the same time as delete so both operations arrive before workflow completes. + s.env.UpdateWorkflow(UpdateVersionComputeConfig, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Require().ErrorContains(err, errVersionDeleted) + }, + OnComplete: func(result any, err error) { + s.Fail("update should not complete on deleted version") + }, + }, args) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentVersionWorkflowType, &deploymentspb.WorkerDeploymentVersionWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + VersionState: &deploymentspb.VersionLocalState{ + Version: &deploymentspb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), + }, + TaskQueueFamilies: map[string]*deploymentspb.VersionLocalState_TaskQueueFamilyData{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +func (s *VersionWorkflowSuite) Test_UpdateComputeConfig_UpdateInstanceFailure_DoesNotModifyState() { + tv := testvars.New(s.T()) + + var a *VersionActivities + s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + + s.env.RegisterActivity(a.UpdateWorkerControllerInstance) + s.env.OnActivity(a.UpdateWorkerControllerInstance, mock.Anything, mock.Anything).Return( + (*computepb.ComputeConfigSummary)(nil), + temporal.NewNonRetryableApplicationError("invalid config", errInvalidComputeConfig, nil), + ).Maybe() + + s.env.RegisterDelayedCallback(func() { + args := &deploymentspb.UpdateComputeConfigArgs{ + Identity: tv.ClientIdentity(), + RequestId: "req-1", + UpsertScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "group1": {ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: &computepb.ComputeProvider{Type: "bad-provider"}, + }}, + }, + } + s.env.UpdateWorkflow(UpdateVersionComputeConfig, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("update should not be rejected at validation stage", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().Error(err) + s.Require().ErrorContains(err, errInvalidComputeConfig) + }, + }, args) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentVersionWorkflowType, &deploymentspb.WorkerDeploymentVersionWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + VersionState: &deploymentspb.VersionLocalState{ + Version: &deploymentspb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), + }, + TaskQueueFamilies: map[string]*deploymentspb.VersionLocalState_TaskQueueFamilyData{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + func (s *VersionWorkflowSuite) skipBeforeVersion(version DeploymentWorkflowVersion) { if s.workflowVersion < version { s.T().Skipf("test supports version %v and newer", version) @@ -2244,3 +2432,176 @@ func (s *VersionWorkflowSuite) skipFromVersion(version DeploymentWorkflowVersion s.T().Skipf("test supports version older than %v", version) } } + +// Test_ReactivateVersion_FromDrained tests that a drained version can be reactivated +// via the ReactivateVersionSignal and properly resets its state +func (s *VersionWorkflowSuite) Test_ReactivateVersion_FromDrained() { + tv := testvars.New(s.T()) + now := timestamppb.New(time.Now()) + + var a *VersionActivities + s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + + // Mock SyncDeploymentVersionUserData for reactivation + s.env.OnActivity(a.SyncDeploymentVersionUserData, mock.Anything, mock.Anything).Return( + &deploymentspb.SyncDeploymentVersionUserDataResponse{}, nil, + ).Maybe() + + // Mock external signal to deployment workflow + s.env.OnSignalExternalWorkflow(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() + + // Schedule reactivation signal + s.env.RegisterDelayedCallback(func() { + // Send reactivation signal + s.env.SignalWorkflow(ReactivateVersionSignalName, nil) + + // Wait a bit, then query to verify state + s.env.RegisterDelayedCallback(func() { + queryResp := &deploymentspb.QueryDescribeVersionResponse{} + val, err := s.env.QueryWorkflow(QueryDescribeVersion) + s.Require().NoError(err) + err = val.Get(queryResp) + s.Require().NoError(err) + + // Verify that status is DRAINING after reactivation + s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, queryResp.VersionState.Status) + + // Verify drainage info is set up for monitoring + s.NotNil(queryResp.VersionState.DrainageInfo) + s.Equal(enumspb.VERSION_DRAINAGE_STATUS_DRAINING, queryResp.VersionState.DrainageInfo.Status) + s.NotNil(queryResp.VersionState.DrainageInfo.LastChangedTime) + s.NotNil(queryResp.VersionState.DrainageInfo.LastCheckedTime) + }, 10*time.Millisecond) + }, 10*time.Millisecond) + + // Start workflow with DRAINED status + s.env.ExecuteWorkflow(WorkerDeploymentVersionWorkflowType, &deploymentspb.WorkerDeploymentVersionWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + VersionState: &deploymentspb.VersionLocalState{ + Version: &deploymentspb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), + }, + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, + DrainageInfo: &deploymentpb.VersionDrainageInfo{ + Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, + LastChangedTime: now, + LastCheckedTime: now, + }, + SyncBatchSize: int32(s.workerDeploymentClient.getSyncBatchSize()), + StartedDeploymentWorkflow: true, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_ReactivateVersion_FromInactive tests that an inactive version can be reactivated +func (s *VersionWorkflowSuite) Test_ReactivateVersion_FromInactive() { + tv := testvars.New(s.T()) + + var a *VersionActivities + s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + + // Mock SyncDeploymentVersionUserData for reactivation + s.env.OnActivity(a.SyncDeploymentVersionUserData, mock.Anything, mock.Anything).Return( + &deploymentspb.SyncDeploymentVersionUserDataResponse{}, nil, + ).Maybe() + + // Mock external signal to deployment workflow + s.env.OnSignalExternalWorkflow(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() + + // Schedule reactivation signal + s.env.RegisterDelayedCallback(func() { + // Send reactivation signal + s.env.SignalWorkflow(ReactivateVersionSignalName, nil) + + // Wait a bit, then query to verify state + s.env.RegisterDelayedCallback(func() { + queryResp := &deploymentspb.QueryDescribeVersionResponse{} + val, err := s.env.QueryWorkflow(QueryDescribeVersion) + s.Require().NoError(err) + err = val.Get(queryResp) + s.Require().NoError(err) + + // Verify that status is DRAINING after reactivation + s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, queryResp.VersionState.Status) + + // Verify drainage info is set up + s.NotNil(queryResp.VersionState.DrainageInfo) + s.Equal(enumspb.VERSION_DRAINAGE_STATUS_DRAINING, queryResp.VersionState.DrainageInfo.Status) + }, 10*time.Millisecond) + }, 10*time.Millisecond) + + // Start workflow with INACTIVE status + s.env.ExecuteWorkflow(WorkerDeploymentVersionWorkflowType, &deploymentspb.WorkerDeploymentVersionWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + VersionState: &deploymentspb.VersionLocalState{ + Version: &deploymentspb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), + }, + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, + SyncBatchSize: int32(s.workerDeploymentClient.getSyncBatchSize()), + StartedDeploymentWorkflow: true, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_ReactivateVersion_IgnoredWhenCurrent tests that reactivation signal is ignored +// when the version is already CURRENT +func (s *VersionWorkflowSuite) Test_ReactivateVersion_IgnoredWhenNotDrainedOrInactive() { + tv := testvars.New(s.T()) + now := timestamppb.New(time.Now()) + + var a *VersionActivities + s.env.RegisterActivity(a.StartWorkerDeploymentWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentWorkflow, mock.Anything, mock.Anything).Return(nil).Maybe() + + // No mocks for SyncDeploymentVersionUserData since reactivation should be ignored + + // Schedule reactivation signal + s.env.RegisterDelayedCallback(func() { + // Send reactivation signal + s.env.SignalWorkflow(ReactivateVersionSignalName, nil) + + // Wait a bit, then query to verify state hasn't changed + s.env.RegisterDelayedCallback(func() { + queryResp := &deploymentspb.QueryDescribeVersionResponse{} + val, err := s.env.QueryWorkflow(QueryDescribeVersion) + s.Require().NoError(err) + err = val.Get(queryResp) + s.Require().NoError(err) + + // Verify that status remains CURRENT + s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, queryResp.VersionState.Status) + + // Verify no drainage info is set + s.Nil(queryResp.VersionState.DrainageInfo) + }, 10*time.Millisecond) + }, 10*time.Millisecond) + + // Start workflow with CURRENT status + s.env.ExecuteWorkflow(WorkerDeploymentVersionWorkflowType, &deploymentspb.WorkerDeploymentVersionWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + VersionState: &deploymentspb.VersionLocalState{ + Version: &deploymentspb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), + }, + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + CurrentSinceTime: now, + SyncBatchSize: int32(s.workerDeploymentClient.getSyncBatchSize()), + StartedDeploymentWorkflow: true, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} diff --git a/service/worker/workerdeployment/workflow.go b/service/worker/workerdeployment/workflow.go index aa190f8fbce..3dd17da8798 100644 --- a/service/worker/workerdeployment/workflow.go +++ b/service/worker/workerdeployment/workflow.go @@ -7,6 +7,7 @@ import ( "slices" "github.com/google/uuid" + computepb "go.temporal.io/api/compute/v1" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" @@ -39,7 +40,6 @@ type ( logger sdklog.Logger metrics sdkclient.MetricsHandler lock workflow.Mutex - conflictToken []byte deleteDeployment bool unsafeMaxVersion func() int // stateChanged is used to track if the state of the workflow has undergone a local state change since the last signal/update. @@ -82,8 +82,8 @@ func getWorkflowVersion(ctx workflow.Context, unsafeWorkflowVersionGetter func() if workflow.GetVersion(ctx, "workflowVersionAdded", workflow.DefaultVersion, 0) >= 0 { var ver DeploymentWorkflowVersion err := workflow.MutableSideEffect(ctx, "workflowVersion", - func(_ workflow.Context) interface{} { return unsafeWorkflowVersionGetter() }, - func(a, b interface{}) bool { return a == b }). + func(_ workflow.Context) any { return unsafeWorkflowVersionGetter() }, + func(a, b any) bool { return a == b }). Get(&ver) if err == nil { return ver @@ -119,7 +119,7 @@ func (d *WorkflowRunner) listenToSignals(ctx workflow.Context) { defer func() { d.signalHandler.processingSignals-- }() var summary *deploymentspb.WorkerDeploymentVersionSummary c.Receive(ctx, &summary) - d.syncVersionSummaryFromVersionWorkflow(summary) + d.syncVersionSummaryFromVersionWorkflow(ctx, summary) d.setStateChanged() }) d.signalHandler.signalSelector.AddReceive(propagationCompleteChannel, func(c workflow.ReceiveChannel, more bool) { @@ -139,13 +139,21 @@ func (d *WorkflowRunner) listenToSignals(ctx workflow.Context) { // syncVersionSummary ensures the version summary in the deployment workflow stays consistent // with the version workflow. This helps prevent discrepancies if they ever fall out of sync. -func (d *WorkflowRunner) syncVersionSummaryFromVersionWorkflow(summary *deploymentspb.WorkerDeploymentVersionSummary) { - if _, ok := d.State.Versions[summary.GetVersion()]; !ok { +func (d *WorkflowRunner) syncVersionSummaryFromVersionWorkflow(ctx workflow.Context, summary *deploymentspb.WorkerDeploymentVersionSummary) { + existing, ok := d.State.Versions[summary.GetVersion()] + if !ok { d.logger.Error("received summary for a non-existing version, ignoring it", "version", summary.GetVersion()) return } + // Preserve create_request_id since the version workflow doesn't know about it. + summary.CreateRequestId = existing.GetCreateRequestId() d.State.Versions[summary.GetVersion()] = summary + if workflow.GetVersion(ctx, "update-memo-with-summary", workflow.DefaultVersion, 0) != workflow.DefaultVersion { + if err := d.updateMemo(ctx); err != nil { + d.logger.Error("failed to update memo", "error", err) + } + } } // handlePropagationComplete handles the propagation complete signal from version workflows @@ -273,6 +281,42 @@ func (d *WorkflowRunner) run(ctx workflow.Context) error { return err } + err = workflow.SetQueryHandler(ctx, QueryCreateRequestID, func() (*deploymentspb.CreateRequestIDQueryResponse, error) { + if d.deleteDeployment { + return nil, errors.New(errDeploymentDeleted) + } + return &deploymentspb.CreateRequestIDQueryResponse{ + RequestId: d.GetState().GetCreateRequestId(), + ConflictToken: d.State.GetConflictToken(), + }, nil + }) + if err != nil { + d.logger.Info("SetQueryHandler failed for WorkerDeployment create request-id query with error: " + err.Error()) + return err + } + + if err := workflow.SetUpdateHandlerWithOptions( + ctx, + CreateWorkerDeployment, + d.handleCreateWorkerDeployment, + workflow.UpdateHandlerOptions{ + Validator: d.validateCreateWorkerDeployment, + }, + ); err != nil { + return err + } + + if err := workflow.SetUpdateHandlerWithOptions( + ctx, + CreateWorkerDeploymentVersion, + d.handleCreateWorkerDeploymentVersion, + workflow.UpdateHandlerOptions{ + Validator: d.validateCreateWorkerDeploymentVersion, + }, + ); err != nil { + return err + } + if err := workflow.SetUpdateHandler( ctx, RegisterWorkerInWorkerDeployment, @@ -345,7 +389,7 @@ func (d *WorkflowRunner) run(ctx workflow.Context) error { canContinue := d.deleteDeployment || // deployment is deleted -> it's ok to drop all signals and updates. // There is no pending signal or update, but the state is dirty or forceCaN is requested: (!d.signalHandler.signalSelector.HasPending() && d.signalHandler.processingSignals == 0 && workflow.AllHandlersFinished(ctx) && - (d.forceCAN || d.stateChanged)) + (d.forceCAN || d.stateChanged || workflow.GetInfo(ctx).GetContinueAsNewSuggested())) // TODO(carlydf): remove verbose logging if canContinue { @@ -417,6 +461,162 @@ func (d *WorkflowRunner) ensureNotDeleted() error { return nil } +func (d *WorkflowRunner) validateCreateWorkerDeployment(args *deploymentspb.CreateWorkerDeploymentArgs) error { + // Only valid if deployment is deleted or the request ID matches the current one. + if d.State.GetCreateRequestId() == args.GetRequestId() { + return nil + } + + return temporal.NewNonRetryableApplicationError(errDeploymentAlreadyExists, errDeploymentAlreadyExists, nil) +} + +func (d *WorkflowRunner) handleCreateWorkerDeployment(ctx workflow.Context, args *deploymentspb.CreateWorkerDeploymentArgs) (*deploymentspb.CreateWorkerDeploymentResponse, error) { + // use lock to enforce only one update at a time + err := d.lock.Lock(ctx) + if err != nil { + d.logger.Error("Could not acquire workflow lock") + return nil, serviceerror.NewDeadlineExceeded("Could not acquire workflow lock") + } + defer func() { + // Even if the update doesn't change the state we mark it as dirty because of created history events. + d.setStateChanged() + d.lock.Unlock() + }() + + // Re-validate after acquiring lock + err = d.validateCreateWorkerDeployment(args) + if err != nil { + return nil, err + } + + if d.State.GetCreateRequestId() == args.GetRequestId() { + // Duplicate request, return success without writing anything. + return &deploymentspb.CreateWorkerDeploymentResponse{ + ConflictToken: d.State.ConflictToken, + }, nil + } + + // At this point this a brand-new workflow. + d.State.LastModifierIdentity = args.GetIdentity() + d.State.CreateRequestId = args.GetRequestId() + d.State.ConflictToken, _ = workflow.Now(ctx).MarshalBinary() + + return &deploymentspb.CreateWorkerDeploymentResponse{ + ConflictToken: d.State.ConflictToken, + }, nil +} + +func (d *WorkflowRunner) validateCreateWorkerDeploymentVersion(args *deploymentspb.CreateWorkerDeploymentVersionArgs) error { + if d.deleteDeployment { + return temporal.NewNonRetryableApplicationError(errDeploymentDeleted, errDeploymentDeleted, nil) + } + if existing, ok := d.State.Versions[args.GetVersion()]; ok { + if existing.GetCreateRequestId() == args.GetRequestId() { + return nil + } + return temporal.NewNonRetryableApplicationError(errVersionAlreadyExists, errVersionAlreadyExists, nil) + } + return nil +} + +func (d *WorkflowRunner) handleCreateWorkerDeploymentVersion(ctx workflow.Context, args *deploymentspb.CreateWorkerDeploymentVersionArgs) (*deploymentspb.CreateWorkerDeploymentVersionResponse, error) { + err := d.lock.Lock(ctx) + if err != nil { + d.logger.Error("Could not acquire workflow lock") + return nil, serviceerror.NewDeadlineExceeded("Could not acquire workflow lock") + } + defer func() { + d.setStateChanged() + d.lock.Unlock() + }() + + // Re-validate after acquiring lock. + err = d.validateCreateWorkerDeploymentVersion(args) + if err != nil { + return nil, err + } + + // Idempotent: version exists with the same request ID. + if existing, ok := d.State.Versions[args.GetVersion()]; ok && existing.GetCreateRequestId() == args.GetRequestId() { + return &deploymentspb.CreateWorkerDeploymentVersionResponse{}, nil + } + + // Check max versions limit. + maxVersions := d.getMaxVersions(ctx) + if len(d.State.Versions) >= maxVersions { + err := d.tryDeleteVersion(ctx) + if err != nil { + return nil, temporal.NewApplicationError( + fmt.Sprintf("cannot add version %s since maximum number of versions (%d) have been registered in the deployment", args.GetVersion(), maxVersions), + errTooManyVersions, + ) + } + } + + // Parse version string to get deployment name and build ID. + versionObj, err := worker_versioning.WorkerDeploymentVersionFromStringV31(args.GetVersion()) + if err != nil { + return nil, serviceerror.NewInvalidArgument("invalid version string: " + err.Error()) + } + + // Create or update the Worker Controller Instance for this version. + computeConfig := args.GetComputeConfig() + var computeConfigSummary *computepb.ComputeConfigSummary + if computeConfig != nil { + updateCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions) + err = workflow.ExecuteActivity(updateCtx, d.a.UpdateWorkerControllerInstanceFromDeployment, &deploymentspb.UpdateWorkerControllerInstanceInput{ + Version: worker_versioning.ExternalWorkerDeploymentVersionFromVersion(versionObj), + Identity: args.GetIdentity(), + UpsertScalingGroups: scalingGroupsToUpsertUpdates(computeConfig.GetScalingGroups()), + }).Get(ctx, &computeConfigSummary) + if err != nil { + var appErr *temporal.ApplicationError + if errors.As(err, &appErr) && appErr.Type() == errInvalidComputeConfig { + return nil, appErr + } + return nil, serviceerror.NewInternalf("update worker controller instance: %v", err) + } + } + + // Start the version workflow via activity. + activityCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions) + err = workflow.ExecuteActivity(activityCtx, d.a.StartWorkerDeploymentVersionWorkflow, &deploymentspb.StartWorkerDeploymentVersionRequest{ + DeploymentName: versionObj.DeploymentName, + BuildId: versionObj.BuildId, + RequestId: args.GetRequestId(), + Identity: args.GetIdentity(), + ComputeConfig: computeConfigSummary, + }).Get(ctx, nil) + if err != nil { + if computeConfig != nil { + deleteCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions) + deleteErr := workflow.ExecuteActivity(deleteCtx, d.a.DeleteWorkerControllerInstanceFromDeployment, &deploymentspb.DeleteWorkerControllerInstanceInput{ + Version: worker_versioning.ExternalWorkerDeploymentVersionFromVersion(versionObj), + Identity: args.GetIdentity(), + }).Get(ctx, nil) + if deleteErr != nil { + d.logger.Warn("Failed to delete worker controller instance", "error", err) + } + } + return nil, err + } + + // Add version to local state. + d.State.Versions[args.GetVersion()] = &deploymentspb.WorkerDeploymentVersionSummary{ + Version: args.GetVersion(), + CreateTime: timestamppb.New(workflow.Now(ctx)), + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED, + CreateRequestId: args.GetRequestId(), + ComputeConfig: computeConfigSummary, + } + d.metrics.Counter(metrics.WorkerDeploymentVersionCreated.Name()).Inc(1) + + if err := d.updateMemo(ctx); err != nil { + return nil, err + } + return &deploymentspb.CreateWorkerDeploymentVersionResponse{}, nil +} + func (d *WorkflowRunner) addVersionToWorkerDeployment(ctx workflow.Context, args *deploymentspb.AddVersionUpdateArgs) error { if d.State.Versions == nil { return nil @@ -469,9 +669,11 @@ func (d *WorkflowRunner) handleRegisterWorker(ctx workflow.Context, args *deploy d.lock.Unlock() }() + version := worker_versioning.WorkerDeploymentVersionToStringV31(args.Version) + // Add version to local state of the workflow, if not already present. err = d.addVersionToWorkerDeployment(ctx, &deploymentspb.AddVersionUpdateArgs{ - Version: worker_versioning.WorkerDeploymentVersionToStringV31(args.Version), + Version: version, CreateTime: timestamppb.New(workflow.Now(ctx)), }) if err != nil { @@ -488,7 +690,7 @@ func (d *WorkflowRunner) handleRegisterWorker(ctx workflow.Context, args *deploy TaskQueueName: args.TaskQueueName, TaskQueueType: args.TaskQueueType, MaxTaskQueues: args.MaxTaskQueues, - Version: worker_versioning.WorkerDeploymentVersionToStringV31(args.Version), + Version: version, RoutingConfig: routingConfigToSync, }).Get(ctx, nil) if err != nil { @@ -504,6 +706,11 @@ func (d *WorkflowRunner) handleRegisterWorker(ctx workflow.Context, args *deploy return err } + if d.State.Versions[version].Status == enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED { + // now that a poller is seen, we should update the status to INACTIVE + d.State.Versions[version].Status = enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE + } + // update memo return d.updateMemo(ctx) } @@ -1261,10 +1468,10 @@ func (d *WorkflowRunner) validateAddVersionToWorkerDeployment(args *deploymentsp } func (d *WorkflowRunner) getMaxVersions(ctx workflow.Context) int { - getMaxVersionsInDeployment := func(ctx workflow.Context) interface{} { + getMaxVersionsInDeployment := func(ctx workflow.Context) any { return d.unsafeMaxVersion() } - intEq := func(a, b interface{}) bool { + intEq := func(a, b any) bool { return a == b } var maxVersions int @@ -1545,5 +1752,6 @@ func (d *WorkflowRunner) getWorkerDeploymentInfoVersionSummary(versionSummary *d FirstActivationTime: versionSummary.GetFirstActivationTime(), LastCurrentTime: versionSummary.GetLastCurrentTime(), LastDeactivationTime: versionSummary.GetLastDeactivationTime(), + ComputeConfig: versionSummary.GetComputeConfig(), } } diff --git a/service/worker/workerdeployment/workflow_test.go b/service/worker/workerdeployment/workflow_test.go index 78fd1537f88..2d7ee6a0dd8 100644 --- a/service/worker/workerdeployment/workflow_test.go +++ b/service/worker/workerdeployment/workflow_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" + computepb "go.temporal.io/api/compute/v1" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/temporal" @@ -32,20 +33,12 @@ type WorkerDeploymentSuite struct { func TestWorkerDeploymentSuite(t *testing.T) { t.Parallel() - t.Run("v0", func(t *testing.T) { - suite.Run(t, &WorkerDeploymentSuite{workflowVersion: InitialVersion}) - }) - t.Run("v1", func(t *testing.T) { - suite.Run(t, &WorkerDeploymentSuite{workflowVersion: AsyncSetCurrentAndRamping}) - }) - t.Run("v2", func(t *testing.T) { - suite.Run(t, &WorkerDeploymentSuite{workflowVersion: VersionDataRevisionNumber}) - }) + suite.Run(t, &WorkerDeploymentSuite{workflowVersion: VersionDataRevisionNumber}) } func (s *WorkerDeploymentSuite) SetupTest() { s.controller = gomock.NewController(s.T()) - s.env = s.WorkflowTestSuite.NewTestWorkflowEnvironment() + s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflowWithOptions(s.getDeploymentWorkflowFunc(), workflow.RegisterOptions{Name: WorkerDeploymentWorkflowType}) // Initialize an empty ClientImpl to use its helper methods @@ -69,6 +62,198 @@ func (s *WorkerDeploymentSuite) skipFromVersion(version DeploymentWorkflowVersio } } +// Test_CreateWorkerDeployment_Success tests successful handling of CreateWorkerDeployment update with matching request ID +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeployment_Success() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + requestID := tv.Any().String() + identity := tv.ClientIdentity() + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeployment, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("CreateWorkerDeployment should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err, "CreateWorkerDeployment should complete without error") + + var resp deploymentspb.CreateWorkerDeploymentResponse + err = s.env.GetWorkflowResult(&resp) + s.Require().NoError(err) + s.NotNil(resp.ConflictToken) + }, + }, &deploymentspb.CreateWorkerDeploymentArgs{ + Identity: identity, + RequestId: requestID, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: requestID, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeployment_Idempotent tests that CreateWorkerDeployment with the same request ID is idempotent +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeployment_Idempotent() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + requestID := tv.Any().String() + identity := tv.ClientIdentity() + existingConflictToken := []byte("existing-token") + + // Send two identical CreateWorkerDeployment updates + for i := range 2 { + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeployment, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("CreateWorkerDeployment should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err, "CreateWorkerDeployment should be idempotent") + + resp, ok := result.(*deploymentspb.CreateWorkerDeploymentResponse) + s.Require().True(ok, "response should be CreateWorkerDeploymentResponse") + if i == 0 { + existingConflictToken = resp.ConflictToken + } else { + s.Equal(existingConflictToken, resp.ConflictToken, "conflict token should not change for idempotent request") + } + }, + }, &deploymentspb.CreateWorkerDeploymentArgs{ + Identity: identity, + RequestId: requestID, + }) + }, time.Duration(i+1)*time.Millisecond) + } + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: requestID, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeployment_RejectDifferentRequestID tests that CreateWorkerDeployment with different request ID is rejected +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeployment_RejectDifferentRequestID() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + originalRequestID := tv.Any().String() + differentRequestID := tv.Any().String() + identity := tv.ClientIdentity() + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeployment, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Require().ErrorContains(err, errDeploymentAlreadyExists, "should reject with deployment already exists error") + }, + OnAccept: func() { + s.Fail("CreateWorkerDeployment should have been rejected") + }, + OnComplete: func(result any, err error) { + s.Fail("CreateWorkerDeployment should not have completed") + }, + }, &deploymentspb.CreateWorkerDeploymentArgs{ + Identity: identity, + RequestId: differentRequestID, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: originalRequestID, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeployment_ReviveDeletedDeployment tests that CreateWorkerDeployment can revive a deleted deployment +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeployment_ReviveDeletedDeployment() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + newRequestID := tv.Any().String() + identity := tv.ClientIdentity() + syncBatchSize := int32(10) + + // First delete the deployment + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(DeleteDeployment, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("delete deployment should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err, "delete deployment should complete without error") + }, + }, nil) + }, 1*time.Millisecond) + + // Then try to create it again with a new request ID (revival) + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeployment, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("CreateWorkerDeployment should not have been rejected for deleted deployment", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err, "CreateWorkerDeployment should succeed for deleted deployment") + + resp, ok := result.(*deploymentspb.CreateWorkerDeploymentResponse) + s.Require().True(ok, "response should be CreateWorkerDeploymentResponse") + s.NotNil(resp.ConflictToken) + + // Verify the deployment was revived by checking a query + val, err := s.env.QueryWorkflow(QueryDescribeDeployment) + s.Require().NoError(err, "query should succeed after revival") + + var queryResp deploymentspb.QueryDescribeWorkerDeploymentResponse + err = val.Get(&queryResp) + s.Require().NoError(err) + s.NotNil(queryResp.State) + s.Equal(newRequestID, queryResp.State.CreateRequestId, "should have new request ID") + s.Equal(identity, queryResp.State.LastModifierIdentity, "should have new identity") + s.Equal(syncBatchSize, queryResp.State.SyncBatchSize, "should keep the original sync batch size") + }, + }, &deploymentspb.CreateWorkerDeploymentArgs{ + Identity: identity, + RequestId: newRequestID, + }) + }, 5*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "old-request-id", + SyncBatchSize: syncBatchSize, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + // Test_SetCurrentVersion_RejectStaleConcurrentUpdate tests that a stale concurrent update is rejected. // // The scenario that this test is testing is as follows: @@ -109,14 +294,14 @@ func (s *WorkerDeploymentSuite) Test_SetCurrentVersion_RejectStaleConcurrentUpda }, OnAccept: func() { }, - OnComplete: func(a interface{}, err error) { + OnComplete: func(a any, err error) { // Update #2 clears the validator and waits for the first update to complete. Once it starts // being processed, it should be rejected since completion of the first update changed the state. s.Require().ErrorContains(err, errNoChangeType) }, }, updateArgs) }, - OnComplete: func(a interface{}, err error) { + OnComplete: func(a any, err error) { }, }, updateArgs) @@ -180,7 +365,7 @@ func (s *WorkerDeploymentSuite) Test_SetRampingVersion_RejectStaleConcurrentUpda }, OnAccept: func() { }, - OnComplete: func(a interface{}, err error) { + OnComplete: func(a any, err error) { // Update #2 clears the validator and waits for the first update to complete. Once it starts // being processed, it should be rejected since completion of the first update changed the state. s.Require().ErrorContains(err, errNoChangeType) @@ -188,7 +373,7 @@ func (s *WorkerDeploymentSuite) Test_SetRampingVersion_RejectStaleConcurrentUpda }, updateArgs) }, - OnComplete: func(a interface{}, err error) { + OnComplete: func(a any, err error) { }, }, updateArgs) @@ -227,7 +412,7 @@ func (s *WorkerDeploymentSuite) syncUnversionedRampInBatches(totalWorkers int) { var a *Activities taskQueueInfos := make([]*deploymentpb.WorkerDeploymentVersionInfo_VersionTaskQueueInfo, totalWorkers) - for i := 0; i < totalWorkers; i++ { + for i := range totalWorkers { taskQueueInfos[i] = &deploymentpb.WorkerDeploymentVersionInfo_VersionTaskQueueInfo{ Name: tv.TaskQueue().Name + fmt.Sprintf("%03d", i), Type: enumspb.TASK_QUEUE_TYPE_WORKFLOW, @@ -258,7 +443,7 @@ func (s *WorkerDeploymentSuite) syncUnversionedRampInBatches(totalWorkers int) { }, OnAccept: func() { }, - OnComplete: func(a interface{}, err error) { + OnComplete: func(a any, err error) { }, }, &deploymentspb.SetRampingVersionArgs{ Version: worker_versioning.UnversionedVersionId, @@ -312,7 +497,7 @@ func (s *WorkerDeploymentSuite) Test_RevisionIncrementsWithAsyncSetCurrentAndRam s.Fail("SetCurrentVersion update should not have failed", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) // Query after SetRampingVersion - revision should be 1 @@ -349,7 +534,7 @@ func (s *WorkerDeploymentSuite) Test_RevisionIncrementsWithAsyncSetCurrentAndRam s.Fail("SetRampingVersion update should not have failed", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) // After SetRamping completes, verify revision number is 2 s.verifyRevisionNumber(2) @@ -389,7 +574,7 @@ func (s *WorkerDeploymentSuite) Test_NoRevisionIncrementsWithoutAsyncSetCurrentA s.Fail("SetCurrentVersion update should not have failed", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) // Query after SetRampingVersion - revision should be 0 @@ -426,7 +611,7 @@ func (s *WorkerDeploymentSuite) Test_NoRevisionIncrementsWithoutAsyncSetCurrentA s.Fail("SetRampingVersion update should not have failed", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) // After SetRamping completes, verify revision number is 0 s.verifyRevisionNumber(0) @@ -487,7 +672,7 @@ func (s *WorkerDeploymentSuite) Test_RevisionNumberPassedToContinueAsNew() { s.Fail("SetCurrentVersion update should not have failed", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) // Verify revision number is 46 after update s.verifyRevisionNumber(46) @@ -551,7 +736,7 @@ func (s *WorkerDeploymentSuite) Test_RevisionNumberDoesNotIncrementOnFailedSetCu s.Fail("SetCurrentVersion update should have been accepted by validator") }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { // The update should fail due to activity failure s.Require().Error(err) s.Require().ErrorContains(err, "sync failed") @@ -612,7 +797,7 @@ func (s *WorkerDeploymentSuite) Test_RevisionNumberDoesNotIncrementOnFailedSetRa s.Fail("SetRampingVersion update should have been accepted by validator") }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { // The update should fail due to activity failure s.Require().Error(err) s.Require().ErrorContains(err, "sync failed") @@ -761,7 +946,7 @@ func (s *WorkerDeploymentSuite) Test_DeleteDeployment_Success() { s.Fail("delete deployment should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete deployment should complete without error") }, }, nil) // DeleteDeployment takes no arguments @@ -796,7 +981,7 @@ func (s *WorkerDeploymentSuite) Test_DeleteDeployment_FailsWithVersions() { OnAccept: func() { s.Fail("delete deployment should have been rejected by validator") }, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Fail("delete deployment should not have reached completion") }, }, nil) @@ -832,7 +1017,7 @@ func (s *WorkerDeploymentSuite) Test_DeleteDeployment_QueryAfterDeletion() { s.Fail("delete deployment should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete deployment should complete without error") }, }, nil) @@ -905,7 +1090,7 @@ func (s *WorkerDeploymentSuite) Test_DeleteVersion_Success() { s.Fail("delete version should not have been rejected", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "delete version should complete without error") }, }, &deploymentspb.DeleteVersionArgs{ @@ -961,7 +1146,7 @@ func (s *WorkerDeploymentSuite) Test_DeleteVersion_FailsWhenCurrentOrRamping() { OnAccept: func() { s.Fail("delete version should have been rejected by validator") }, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Fail("delete version should not have reached completion") }, }, &deploymentspb.DeleteVersionArgs{ @@ -1008,7 +1193,7 @@ func (s *WorkerDeploymentSuite) Test_DeleteVersion_FailsWhenVersionNotFound() { OnAccept: func() { s.Fail("delete version should have been rejected by validator") }, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Fail("delete version should not have reached completion") }, }, &deploymentspb.DeleteVersionArgs{ @@ -1063,14 +1248,14 @@ func (s *WorkerDeploymentSuite) Test_DeleteVersion_ConcurrentDeletes() { s.Fail("second delete should have been accepted by validator") }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { // Second delete should fail because version is already deleted s.Require().Error(err, "second delete should fail") s.Require().ErrorContains(err, errVersionNotFound) }, }, deleteArgs) }, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err, "first delete should complete without error") }, }, deleteArgs) @@ -1128,7 +1313,7 @@ func (s *WorkerDeploymentSuite) Test_SetCurrent_AddsPropagatingRevision() { s.Fail("SetCurrentVersion update should not have failed", err) }, OnAccept: func() {}, - OnComplete: func(result interface{}, err error) { + OnComplete: func(result any, err error) { s.Require().NoError(err) // Query the state to verify propagating revision was added @@ -1177,3 +1362,498 @@ func (s *WorkerDeploymentSuite) Test_SetCurrent_AddsPropagatingRevision() { s.True(s.env.IsWorkflowCompleted()) } + +// Test_CreateWorkerDeploymentVersion_Success tests successful creation of a new version +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_Success() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + requestID := tv.Any().String() + identity := tv.ClientIdentity() + version := tv.DeploymentVersionString() + + var a *Activities + s.env.RegisterActivity(a.StartWorkerDeploymentVersionWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentVersionWorkflow, mock.Anything, mock.MatchedBy(func(args *deploymentspb.StartWorkerDeploymentVersionRequest) bool { + return args.DeploymentName == tv.DeploymentSeries() && args.BuildId == tv.BuildID() && args.RequestId == requestID + })).Return(nil).Once() + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("CreateWorkerDeploymentVersion should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err, "CreateWorkerDeploymentVersion should complete without error") + + // Verify version was added to state + queryResult, err := s.env.QueryWorkflow(QueryDescribeDeployment) + s.Require().NoError(err) + var state deploymentspb.QueryDescribeWorkerDeploymentResponse + s.Require().NoError(queryResult.Get(&state)) + s.Contains(state.State.Versions, version) + s.Equal(version, state.State.Versions[version].Version) + s.Equal(requestID, state.State.Versions[version].CreateRequestId) + s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED, state.State.Versions[version].Status) + s.NotNil(state.State.Versions[version].CreateTime) + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: requestID, + Version: version, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_WithComputeConfig tests that creating a version with a +// compute config calls both ValidateWorkerControllerInstanceSpec and UpdateWorkerControllerInstance. +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_WithComputeConfig() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + requestID := tv.Any().String() + identity := tv.ClientIdentity() + version := tv.DeploymentVersionString() + + var a *Activities + s.env.RegisterActivity(a.StartWorkerDeploymentVersionWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentVersionWorkflow, mock.Anything, mock.Anything).Return(nil).Once() + + s.env.RegisterActivity(a.UpdateWorkerControllerInstanceFromDeployment) + updateCalled := false + s.env.OnActivity(a.UpdateWorkerControllerInstanceFromDeployment, mock.Anything, mock.Anything).Return((*computepb.ComputeConfigSummary)(nil), nil).Run(func(args mock.Arguments) { + updateCalled = true + }).Once() + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("CreateWorkerDeploymentVersion should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err) + s.True(updateCalled, "UpdateWorkerControllerInstance should have been called") + + // Verify version was added to state. + queryResult, qErr := s.env.QueryWorkflow(QueryDescribeDeployment) + s.Require().NoError(qErr) + var state deploymentspb.QueryDescribeWorkerDeploymentResponse + s.Require().NoError(queryResult.Get(&state)) + s.Contains(state.State.Versions, version) + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: requestID, + Version: version, + ComputeConfig: &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "group1": {Provider: &computepb.ComputeProvider{Type: "aws-lambda"}}, + }, + }, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_Idempotent tests that the same request ID is idempotent +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_Idempotent() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + requestID := tv.Any().String() + identity := tv.ClientIdentity() + version := tv.DeploymentVersionString() + + // Only register the activity once - the second update should not call it + var a *Activities + s.env.RegisterActivity(a.StartWorkerDeploymentVersionWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentVersionWorkflow, mock.Anything, mock.Anything).Return(nil).Once() + + // Send two identical CreateWorkerDeploymentVersion updates + for i := range 2 { + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("CreateWorkerDeploymentVersion should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err, "CreateWorkerDeploymentVersion should be idempotent") + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: requestID, + Version: version, + }) + }, time.Duration(i+1)*time.Millisecond) + } + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_UpdateWorkerControllerInstanceFailure tests that a failure in +// UpdateWorkerControllerInstance prevents the version from being created. +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_UpdateWorkerControllerInstanceFailure() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + requestID := tv.Any().String() + identity := tv.ClientIdentity() + version := tv.DeploymentVersionString() + + var a *Activities + s.env.RegisterActivity(a.UpdateWorkerControllerInstanceFromDeployment) + s.env.OnActivity(a.UpdateWorkerControllerInstanceFromDeployment, mock.Anything, mock.Anything).Return( + (*computepb.ComputeConfigSummary)(nil), + temporal.NewNonRetryableApplicationError("controller update failed", errInvalidComputeConfig, nil), + ).Once() + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("should not be rejected at validator", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().Error(err) + s.Require().ErrorContains(err, errInvalidComputeConfig) + + // Verify version was NOT added to state. + queryResult, qErr := s.env.QueryWorkflow(QueryDescribeDeployment) + s.Require().NoError(qErr) + var state deploymentspb.QueryDescribeWorkerDeploymentResponse + s.Require().NoError(queryResult.Get(&state)) + s.NotContains(state.State.Versions, version, "version should not be created when UpdateWorkerControllerInstance fails") + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: requestID, + Version: version, + ComputeConfig: &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "group1": {Provider: &computepb.ComputeProvider{Type: "bad-provider"}}, + }, + }, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_RejectDifferentRequestID tests that a different request ID is rejected +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_RejectDifferentRequestID() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + version := tv.DeploymentVersionString() + identity := tv.ClientIdentity() + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Require().ErrorContains(err, errVersionAlreadyExists, "should reject with version already exists error") + }, + OnAccept: func() { + s.Fail("CreateWorkerDeploymentVersion should have been rejected") + }, + OnComplete: func(result any, err error) { + s.Fail("CreateWorkerDeploymentVersion should not have completed") + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: "different-request-id", + Version: version, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{ + version: { + Version: version, + CreateRequestId: "original-request-id", + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, + }, + }, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_RejectAutoCreatedVersion tests that auto-created versions +// are rejected when an explicit create is attempted with a different request ID +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_RejectAutoCreatedVersion() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + version := tv.DeploymentVersionString() + identity := tv.ClientIdentity() + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Require().ErrorContains(err, errVersionAlreadyExists, "should reject auto-created version with different request ID") + }, + OnAccept: func() { + s.Fail("CreateWorkerDeploymentVersion should have been rejected") + }, + OnComplete: func(result any, err error) { + s.Fail("CreateWorkerDeploymentVersion should not have completed") + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: "explicit-request-id", + Version: version, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{ + version: { + Version: version, + CreateRequestId: AutoCreateRequestIDPrefix + "some-auto-id", + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, + }, + }, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_RejectDeletedDeployment tests that creating a version +// on a deleted deployment is rejected +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_RejectDeletedDeployment() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + version := tv.DeploymentVersionString() + identity := tv.ClientIdentity() + + // First delete the deployment + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(DeleteDeployment, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("delete deployment should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err) + }, + }, nil) + }, 1*time.Millisecond) + + // Then try to create a version on the deleted deployment + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Require().ErrorContains(err, errDeploymentDeleted) + }, + OnAccept: func() { + s.Fail("CreateWorkerDeploymentVersion should have been rejected on deleted deployment") + }, + OnComplete: func(result any, err error) { + s.Fail("CreateWorkerDeploymentVersion should not have completed on deleted deployment") + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: "some-request-id", + Version: version, + }) + }, 5*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_MaxVersionsLimit tests that creating a version fails +// when the max versions limit is reached and no version can be auto-deleted +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_MaxVersionsLimit() { + tv := testvars.New(s.T()) + + newVersion := tv.DeploymentVersionString() + identity := tv.ClientIdentity() + + // Pre-populate with a version that is current (can't be auto-deleted) + existingVersion := tv.WithBuildIDNumber(2).DeploymentVersionString() + + // Use a custom workflow function with maxVersions=1 + s.env = s.NewTestWorkflowEnvironment() + s.env.RegisterWorkflowWithOptions(func(ctx workflow.Context, args *deploymentspb.WorkerDeploymentWorkflowArgs) error { + workflowVersionGetter := func() DeploymentWorkflowVersion { + return s.workflowVersion + } + maxVersionsGetter := func() int { + return 1 // Only allow 1 version + } + return Workflow(ctx, workflowVersionGetter, maxVersionsGetter, args) + }, workflow.RegisterOptions{Name: WorkerDeploymentWorkflowType}) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("should have been accepted by validator", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().Error(err) + s.Require().ErrorContains(err, errTooManyVersions) + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: "new-version-request-id", + Version: newVersion, + }) + }, 1*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{ + existingVersion: { + Version: existingVersion, + CreateTime: timestamppb.Now(), + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }, + }, + RoutingConfig: &deploymentpb.RoutingConfig{ + CurrentVersion: existingVersion, + }, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} + +// Test_CreateWorkerDeploymentVersion_SyncSummaryPreservesCreateRequestID tests that +// syncing a version summary from the version workflow preserves the create_request_id +func (s *WorkerDeploymentSuite) Test_CreateWorkerDeploymentVersion_SyncSummaryPreservesCreateRequestID() { + tv := testvars.New(s.T()) + s.env.OnUpsertMemo(mock.Anything).Return(nil) + + requestID := tv.Any().String() + identity := tv.ClientIdentity() + version := tv.DeploymentVersionString() + + var a *Activities + s.env.RegisterActivity(a.StartWorkerDeploymentVersionWorkflow) + s.env.OnActivity(a.StartWorkerDeploymentVersionWorkflow, mock.Anything, mock.Anything).Return(nil).Once() + + // First create the version + s.env.RegisterDelayedCallback(func() { + s.env.UpdateWorkflow(CreateWorkerDeploymentVersion, "", &testsuite.TestUpdateCallback{ + OnReject: func(err error) { + s.Fail("CreateWorkerDeploymentVersion should not have been rejected", err) + }, + OnAccept: func() {}, + OnComplete: func(result any, err error) { + s.Require().NoError(err) + }, + }, &deploymentspb.CreateWorkerDeploymentVersionArgs{ + Identity: identity, + RequestId: requestID, + Version: version, + }) + }, 1*time.Millisecond) + + // Then send a SyncVersionSummary signal (simulating version workflow syncing back) + s.env.RegisterDelayedCallback(func() { + s.env.SignalWorkflow(SyncVersionSummarySignal, &deploymentspb.WorkerDeploymentVersionSummary{ + Version: version, + CreateTime: timestamppb.Now(), + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, + // Note: no CreateRequestId in the signal - version workflow doesn't set it + }) + }, 5*time.Millisecond) + + // Verify create_request_id is preserved after sync + s.env.RegisterDelayedCallback(func() { + queryResult, err := s.env.QueryWorkflow(QueryDescribeDeployment) + s.Require().NoError(err) + var state deploymentspb.QueryDescribeWorkerDeploymentResponse + s.Require().NoError(queryResult.Get(&state)) + s.Require().Contains(state.State.Versions, version) + s.Equal(requestID, state.State.Versions[version].CreateRequestId, + "create_request_id should be preserved after summary sync") + }, 10*time.Millisecond) + + s.env.ExecuteWorkflow(WorkerDeploymentWorkflowType, &deploymentspb.WorkerDeploymentWorkflowArgs{ + NamespaceName: tv.NamespaceName().String(), + NamespaceId: tv.NamespaceID().String(), + DeploymentName: tv.DeploymentSeries(), + State: &deploymentspb.WorkerDeploymentLocalState{ + CreateRequestId: "deployment-request-id", + Versions: map[string]*deploymentspb.WorkerDeploymentVersionSummary{}, + }, + }) + + s.True(s.env.IsWorkflowCompleted()) +} diff --git a/temporal/fx.go b/temporal/fx.go index a812566d9c0..a016a5b7bce 100644 --- a/temporal/fx.go +++ b/temporal/fx.go @@ -26,6 +26,7 @@ import ( chasmworkflow "go.temporal.io/server/chasm/lib/workflow" "go.temporal.io/server/client" "go.temporal.io/server/common/archiver" + "go.temporal.io/server/common/archiver/provider" "go.temporal.io/server/common/authorization" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/config" @@ -95,7 +96,7 @@ type ( serverOptionsProvider struct { fx.Out ServerOptions *serverOptions - StopChan chan interface{} + StopChan chan any StartupSynchronizationMode synchronizationModeParams Config *config.Config @@ -105,9 +106,11 @@ type ( ServiceNames resource.ServiceNames NamespaceLogger resource.NamespaceLogger - ServiceResolver resolver.ServiceResolver - CustomDataStoreFactory persistenceClient.AbstractDataStoreFactory - CustomVisibilityStore visibility.VisibilityStoreFactory + ServiceResolver resolver.ServiceResolver + CustomDataStoreFactory persistenceClient.AbstractDataStoreFactory + CustomVisibilityStore visibility.VisibilityStoreFactory + CustomHistoryArchiverFactory provider.CustomHistoryArchiverFactory + CustomVisibilityArchiverFactory provider.CustomVisibilityArchiverFactory SearchAttributesMapper searchattribute.Mapper CustomFrontendInterceptors []grpc.UnaryServerInterceptor @@ -192,7 +195,7 @@ func ServerOptionsProvider(opts []ServerOption) (serverOptionsProvider, error) { return serverOptionsProvider{}, err } - stopChan := make(chan interface{}) + stopChan := make(chan any) // ClientFactoryProvider clientFactoryProvider := so.clientFactoryProvider @@ -214,7 +217,7 @@ func ServerOptionsProvider(opts []ServerOption) (serverOptionsProvider, error) { if dcClient == nil { dcConfig := so.config.DynamicConfigClient if dcConfig != nil { - dcClient, err = dynamicconfig.NewFileBasedClient(dcConfig, logger, stopChan) + dcClient, err = dynamicconfig.NewFileBasedClientWithMetrics(dcConfig, logger, stopChan, metricHandler) if err != nil { return serverOptionsProvider{}, fmt.Errorf("unable to create dynamic config client: %w", err) } @@ -289,9 +292,11 @@ func ServerOptionsProvider(opts []ServerOption) (serverOptionsProvider, error) { ServiceHosts: so.hostsByService, NamespaceLogger: so.namespaceLogger, - ServiceResolver: so.persistenceServiceResolver, - CustomDataStoreFactory: so.customDataStoreFactory, - CustomVisibilityStore: so.customVisibilityStoreFactory, + ServiceResolver: so.persistenceServiceResolver, + CustomDataStoreFactory: so.customDataStoreFactory, + CustomVisibilityStore: so.customVisibilityStoreFactory, + CustomHistoryArchiverFactory: so.customHistoryArchiverFactory, + CustomVisibilityArchiverFactory: so.customVisibilityArchiverFactory, SearchAttributesMapper: so.searchAttributesMapper, CustomFrontendInterceptors: so.customFrontendInterceptors, @@ -344,30 +349,32 @@ type ( ServiceProviderParamsCommon struct { fx.In - Cfg *config.Config - ServiceNames resource.ServiceNames - Logger log.Logger - NamespaceLogger resource.NamespaceLogger - DynamicConfigClient dynamicconfig.Client - MetricsHandler metrics.Handler - EsClient esclient.Client - TlsConfigProvider encryption.TLSConfigProvider - PersistenceConfig config.Persistence - ClusterMetadata *cluster.Config - ClientFactoryProvider client.FactoryProvider - AudienceGetter authorization.JWTAudienceMapper - PersistenceServiceResolver resolver.ServiceResolver - PersistenceFactoryProvider persistenceClient.FactoryProviderFn - SearchAttributesMapper searchattribute.Mapper - CustomFrontendInterceptors []grpc.UnaryServerInterceptor - Authorizer authorization.Authorizer - ClaimMapper authorization.ClaimMapper - DataStoreFactory persistenceClient.AbstractDataStoreFactory - VisibilityStoreFactory visibility.VisibilityStoreFactory - SpanExporters []otelsdktrace.SpanExporter - InstanceID resource.InstanceID `optional:"true"` - StaticServiceHosts map[primitives.ServiceName]static.Hosts `optional:"true"` - TaskCategoryRegistry tasks.TaskCategoryRegistry + Cfg *config.Config + ServiceNames resource.ServiceNames + Logger log.Logger + NamespaceLogger resource.NamespaceLogger + DynamicConfigClient dynamicconfig.Client + MetricsHandler metrics.Handler + EsClient esclient.Client + TlsConfigProvider encryption.TLSConfigProvider //nolint:staticcheck // should be TLSConfigProvider + PersistenceConfig config.Persistence + ClusterMetadata *cluster.Config + ClientFactoryProvider client.FactoryProvider + AudienceGetter authorization.JWTAudienceMapper + PersistenceServiceResolver resolver.ServiceResolver + PersistenceFactoryProvider persistenceClient.FactoryProviderFn + SearchAttributesMapper searchattribute.Mapper + CustomFrontendInterceptors []grpc.UnaryServerInterceptor + Authorizer authorization.Authorizer + ClaimMapper authorization.ClaimMapper + DataStoreFactory persistenceClient.AbstractDataStoreFactory + VisibilityStoreFactory visibility.VisibilityStoreFactory + CustomHistoryArchiverFactory provider.CustomHistoryArchiverFactory + CustomVisibilityArchiverFactory provider.CustomVisibilityArchiverFactory + SpanExporters []otelsdktrace.SpanExporter + InstanceID resource.InstanceID `optional:"true"` + StaticServiceHosts map[primitives.ServiceName]static.Hosts `optional:"true"` + TaskCategoryRegistry tasks.TaskCategoryRegistry } ) @@ -401,6 +408,12 @@ func (params ServiceProviderParamsCommon) GetCommonServiceOptions(serviceName pr func() visibility.VisibilityStoreFactory { return params.VisibilityStoreFactory }, + func() provider.CustomHistoryArchiverFactory { + return params.CustomHistoryArchiverFactory + }, + func() provider.CustomVisibilityArchiverFactory { + return params.CustomVisibilityArchiverFactory + }, func() client.FactoryProvider { return params.ClientFactoryProvider }, @@ -542,7 +555,7 @@ func genericFrontendServiceProvider( case primitives.FrontendService: return params.ClaimMapper case primitives.InternalFrontendService: - return authorization.NewNoopClaimMapper() + return authorization.NewInternalClaimMapper() default: panic("Unexpected frontend service name") } @@ -651,9 +664,7 @@ func ApplyClusterMetadataConfigProvider( } indexSearchAttributes := make(map[string]*persistencespb.IndexSearchAttributes) for _, ds := range visDataStores { - if ds.SQL != nil || ds.CustomDataStoreConfig != nil { - indexSearchAttributes[ds.GetIndexName()] = sadefs.GetDBIndexSearchAttributes(visCSAOverride) - } + indexSearchAttributes[ds.GetIndexName()] = sadefs.GetDBIndexSearchAttributes(visCSAOverride) } clusterMetadata := svc.ClusterMetadata diff --git a/temporal/interrupt.go b/temporal/interrupt.go index f2ad8aad3fb..13a1063c2cd 100644 --- a/temporal/interrupt.go +++ b/temporal/interrupt.go @@ -6,11 +6,11 @@ import ( "syscall" ) -func InterruptCh() <-chan interface{} { +func InterruptCh() <-chan any { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) - ret := make(chan interface{}, 1) + ret := make(chan any, 1) go func() { s := <-c ret <- s diff --git a/temporal/server_impl.go b/temporal/server_impl.go index ba62286829b..b51b1e4187b 100644 --- a/temporal/server_impl.go +++ b/temporal/server_impl.go @@ -28,7 +28,7 @@ type ( ServerImpl struct { so *serverOptions servicesMetadata []*ServicesMetadata - stoppedCh chan interface{} + stoppedCh chan any logger log.Logger namespaceLogger resource.NamespaceLogger @@ -57,7 +57,7 @@ func NewServerFxImpl( opts *serverOptions, logger log.Logger, namespaceLogger resource.NamespaceLogger, - stoppedCh chan interface{}, + stoppedCh chan any, servicesGroup ServicesGroupIn, persistenceConfig config.Persistence, clusterMetadata *cluster.Config, diff --git a/temporal/server_option.go b/temporal/server_option.go index 4245971abbb..1785bcd2ac1 100644 --- a/temporal/server_option.go +++ b/temporal/server_option.go @@ -4,6 +4,7 @@ import ( "net/http" "go.temporal.io/server/client" + "go.temporal.io/server/common/archiver/provider" "go.temporal.io/server/common/authorization" "go.temporal.io/server/common/config" "go.temporal.io/server/common/dynamicconfig" @@ -70,7 +71,7 @@ func WithStaticHosts(hostsByService map[primitives.ServiceName]static.Hosts) Ser } // InterruptOn interrupts server on the signal from server. If channel is nil Start() will block forever. -func InterruptOn(interruptCh <-chan interface{}) ServerOption { +func InterruptOn(interruptCh <-chan any) ServerOption { return applyFunc(func(s *serverOptions) { s.startupSynchronizationMode.blockingStart = true s.startupSynchronizationMode.interruptCh = interruptCh @@ -154,6 +155,22 @@ func WithCustomVisibilityStoreFactory(customFactory visibility.VisibilityStoreFa }) } +// WithCustomHistoryArchiverFactory sets a custom history archiver factory. +// NOTE: this option is experimental and may be changed in future release. +func WithCustomHistoryArchiverFactory(factory provider.CustomHistoryArchiverFactory) ServerOption { + return applyFunc(func(s *serverOptions) { + s.customHistoryArchiverFactory = factory + }) +} + +// WithCustomVisibilityArchiverFactory sets a custom visibility archiver factory. +// NOTE: this option is experimental and may be changed in future release. +func WithCustomVisibilityArchiverFactory(factory provider.CustomVisibilityArchiverFactory) ServerOption { + return applyFunc(func(s *serverOptions) { + s.customVisibilityArchiverFactory = factory + }) +} + // WithClientFactoryProvider sets a custom ClientFactoryProvider // NOTE: this option is experimental and may be changed or removed in future release. func WithClientFactoryProvider(clientFactoryProvider client.FactoryProvider) ServerOption { diff --git a/temporal/server_options.go b/temporal/server_options.go index 3f431fe6b23..ef54b93b1c0 100644 --- a/temporal/server_options.go +++ b/temporal/server_options.go @@ -7,6 +7,7 @@ import ( "slices" "go.temporal.io/server/client" + "go.temporal.io/server/common/archiver/provider" "go.temporal.io/server/common/authorization" "go.temporal.io/server/common/config" "go.temporal.io/server/common/dynamicconfig" @@ -25,7 +26,7 @@ import ( type ( synchronizationModeParams struct { blockingStart bool - interruptCh <-chan interface{} + interruptCh <-chan any } serverOptions struct { @@ -40,21 +41,23 @@ type ( startupSynchronizationMode synchronizationModeParams - logger log.Logger - namespaceLogger log.Logger - authorizer authorization.Authorizer - tlsConfigProvider encryption.TLSConfigProvider - claimMapper authorization.ClaimMapper - audienceGetter authorization.JWTAudienceMapper - persistenceServiceResolver resolver.ServiceResolver - elasticsearchHttpClient *http.Client - dynamicConfigClient dynamicconfig.Client - customDataStoreFactory persistenceClient.AbstractDataStoreFactory - customVisibilityStoreFactory visibility.VisibilityStoreFactory - clientFactoryProvider client.FactoryProvider - searchAttributesMapper searchattribute.Mapper - customFrontendInterceptors []grpc.UnaryServerInterceptor - metricHandler metrics.Handler + logger log.Logger + namespaceLogger log.Logger + authorizer authorization.Authorizer + tlsConfigProvider encryption.TLSConfigProvider + claimMapper authorization.ClaimMapper + audienceGetter authorization.JWTAudienceMapper + persistenceServiceResolver resolver.ServiceResolver + elasticsearchHttpClient *http.Client //nolint:staticcheck // should be elasticsearchHTTPClient + dynamicConfigClient dynamicconfig.Client + customDataStoreFactory persistenceClient.AbstractDataStoreFactory + customVisibilityStoreFactory visibility.VisibilityStoreFactory + customHistoryArchiverFactory provider.CustomHistoryArchiverFactory + customVisibilityArchiverFactory provider.CustomVisibilityArchiverFactory + clientFactoryProvider client.FactoryProvider + searchAttributesMapper searchattribute.Mapper + customFrontendInterceptors []grpc.UnaryServerInterceptor + metricHandler metrics.Handler } ) diff --git a/temporal/server_test.go b/temporal/server_test.go index a3e5ad243cc..f5d3d7f702f 100644 --- a/temporal/server_test.go +++ b/temporal/server_test.go @@ -3,6 +3,7 @@ package temporal_test import ( "context" "fmt" + "math" "path" "strings" "sync/atomic" @@ -11,21 +12,32 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/workflow" + "go.temporal.io/server/api/adminservice/v1" + "go.temporal.io/server/common" "go.temporal.io/server/common/config" + "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/persistence/serialization" _ "go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite" // needed to register the sqlite plugin "go.temporal.io/server/common/testing/testtelemetry" "go.temporal.io/server/service/frontend" "go.temporal.io/server/temporal" "go.temporal.io/server/tests/testutils" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/durationpb" ) // TestNewServer verifies that NewServer doesn't cause any fx errors, and that there are no unexpected error logs after // running for a few seconds. func TestNewServer(t *testing.T) { - startAndStopServer(t) + runAndTestServer(t) } // TestNewServerWithOTEL verifies that NewServer doesn't cause any fx errors when OTEL is enabled. @@ -38,45 +50,114 @@ func TestNewServerWithOTEL(t *testing.T) { collector, err := testtelemetry.StartMemoryCollector(t) require.NoError(t, err) t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", collector.Addr()) - startAndStopServer(t) + runAndTestServer(t) require.NotEmpty(t, collector.Spans(), "expected at least one OTEL span") }) t.Run("without OTEL Collector running", func(t *testing.T) { - startAndStopServer(t) + runAndTestServer(t) }) } -func startAndStopServer(t *testing.T) { +// TestNewServerWithJSONEncoding verifies that NewServer works when JSON encoding is enabled. +func TestNewServerWithJSONEncoding(t *testing.T) { + t.Setenv(serialization.SerializerDataEncodingEnvVar, enumspb.ENCODING_TYPE_JSON.String()) + runAndTestServer(t) +} + +func runAndTestServer(t *testing.T) { + t.Helper() cfg := loadConfig(t) - // The prometheus reporter does not shut down in-between test runs. - // This will assign a random port to the prometheus reporter, - // so that it doesn't conflict with other tests. - cfg.Global.Metrics.Prometheus.ListenAddress = ":0" + logDetector := newErrorLogDetector(t, log.NewTestLogger()) logDetector.Start() + t.Cleanup(func() { + logDetector.Stop() + }) + + // Tweak matching to ensure tasks are written to persistence immediately. + dcClient := dynamicconfig.StaticClient{ + dynamicconfig.MatchingSyncMatchWaitDuration.Key(): time.Duration(0), + dynamicconfig.MatchingNumTaskqueueWritePartitions.Key(): 1, + dynamicconfig.MatchingNumTaskqueueReadPartitions.Key(): 1, + } server, err := temporal.NewServer( temporal.ForServices(temporal.DefaultServices), temporal.WithConfig(cfg), temporal.WithLogger(logDetector), temporal.WithChainedFrontendGrpcInterceptors(getFrontendInterceptors()), + temporal.WithDynamicConfigClient(dcClient), ) require.NoError(t, err) t.Cleanup(func() { - logDetector.Stop() assert.NoError(t, server.Stop()) }) - require.NoError(t, server.Start()) - time.Sleep(10 * time.Second) //nolint:forbidigo + + ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second) + defer cancel() + + // Create SDK client + frontendHostPort := fmt.Sprintf("127.0.0.1:%d", cfg.Services["frontend"].RPC.GRPCPort) + namespace := "test-" + common.GenerateRandomString(8) + c, err := client.Dial(client.Options{ + HostPort: frontendHostPort, + Namespace: namespace, + }) + require.NoError(t, err) + defer c.Close() + + // Register the namespace. + _, err = c.WorkflowService().RegisterNamespace(ctx, &workflowservice.RegisterNamespaceRequest{ + Namespace: namespace, + WorkflowExecutionRetentionPeriod: durationpb.New(24 * time.Hour), + }) + require.NoError(t, err) + + // Start workflow. + taskQueue := "test-task-queue" + run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{TaskQueue: taskQueue}, SimpleWorkflow) + require.NoError(t, err) + + // Check that the workflow task was backlogged (to test task persistence). + adminConn, err := grpc.NewClient(frontendHostPort, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + defer func() { _ = adminConn.Close() }() + adminClient := adminservice.NewAdminServiceClient(adminConn) + assert.Eventually(t, func() bool { + response, err := adminClient.GetTaskQueueTasks(ctx, &adminservice.GetTaskQueueTasksRequest{ + Namespace: namespace, + TaskQueue: taskQueue, + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + MinTaskId: 0, + MaxTaskId: math.MaxInt64, + BatchSize: 10, + }) + if err != nil { + return false + } + return len(response.Tasks) > 0 + }, 20*time.Second, 100*time.Millisecond) + + // Start worker. + w := worker.New(c, taskQueue, worker.Options{}) + w.RegisterWorkflow(SimpleWorkflow) + err = w.Start() + require.NoError(t, err) + defer w.Stop() + + // Wait for the workflow to complete. + var result string + err = run.Get(ctx, &result) + require.NoError(t, err) + assert.Equal(t, "Hello World", result) } func loadConfig(t *testing.T) *config.Config { cfg := loadSQLiteConfig(t) setTestPorts(cfg) - return cfg } @@ -95,20 +176,29 @@ func loadSQLiteConfig(t *testing.T) *config.Config { return cfg } -// setTestPorts sets the ports of all services to something different from the default ports, so that we can run the -// tests in parallel. +// setTestPorts sets the ports of all services to something different from the default ports. func setTestPorts(cfg *config.Config) { port := 10000 + // The prometheus reporter does not shut down in-between test runs. + // This will assign a random port to the prometheus reporter, + // so that it doesn't conflict with other tests. + cfg.Global.Metrics.Prometheus.ListenAddress = ":0" + for k, v := range cfg.Services { - rpc := v.RPC - rpc.GRPCPort = port + v.RPC.GRPCPort = port + port++ + + v.RPC.MembershipPort = port port++ - rpc.MembershipPort = port + + v.RPC.HTTPPort = port port++ - v.RPC = rpc + cfg.Services[k] = v } + + cfg.Global.PProf.Port = port } func getFrontendInterceptors() func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { @@ -138,9 +228,13 @@ func (d *errorLogDetector) logUnexpected(operation string, msg string, tags []ta d.logger.Error(msg, tags...) } -func (d *errorLogDetector) Debug(string, ...tag.Tag) {} +func (d *errorLogDetector) Debug(msg string, tags ...tag.Tag) { + d.logger.Debug(msg, tags...) +} -func (d *errorLogDetector) Info(string, ...tag.Tag) {} +func (d *errorLogDetector) Info(msg string, tags ...tag.Tag) { + d.logger.Info(msg, tags...) +} func (d *errorLogDetector) DPanic(msg string, tags ...tag.Tag) { d.logUnexpected("DPanic", msg, tags) @@ -163,10 +257,13 @@ func (d *errorLogDetector) Stop() { } func (d *errorLogDetector) Warn(msg string, tags ...tag.Tag) { + d.logger.Warn(msg, tags...) for _, s := range []string{ "error creating sdk client", "Failed to poll for task", - "OTEL error", // logged when OTEL collector is not running + "Fail to process task", // transient startup error + "OTEL error", // logged when OTEL collector is not running + "network dial error", // transient shutdown error } { if strings.Contains(msg, s) { return @@ -177,10 +274,17 @@ func (d *errorLogDetector) Warn(msg string, tags ...tag.Tag) { } func (d *errorLogDetector) Error(msg string, tags ...tag.Tag) { + d.logger.Error(msg, tags...) for _, s := range []string{ "Unable to process new range", "Unable to call", "service failures", + "Queue reader unable to retrieve tasks", // transient startup error + "error from matching when initializing", // transient startup error + "error fetching user data from parent", // transient startup error + "Failed to check Nexus endpoints ownership", // transient startup error + "Failed to force load non-root partition", // transient shutdown error + "error refreshing endpoints by background job", // transient shutdown error } { if strings.Contains(msg, s) { return @@ -251,3 +355,7 @@ func TestErrorLogDetector(t *testing.T) { d.Fatal("fatal") assert.Empty(t, f.errorLogs, "should not fail the test if the detector is stopped") } + +func SimpleWorkflow(ctx workflow.Context) (string, error) { + return "Hello World", nil +} diff --git a/temporaltest/logger.go b/temporaltest/logger.go index ad8c2c57d48..e7cce99e741 100644 --- a/temporaltest/logger.go +++ b/temporaltest/logger.go @@ -15,27 +15,27 @@ type testLogger struct { t *testing.T } -func (tl *testLogger) logLevel(lvl, msg string, keyvals ...interface{}) { +func (tl *testLogger) logLevel(lvl, msg string, keyvals ...any) { if tl.t == nil { return } - args := []interface{}{lvl, msg} + args := []any{lvl, msg} args = append(args, keyvals...) tl.t.Log(args...) } -func (tl *testLogger) Debug(msg string, keyvals ...interface{}) { +func (tl *testLogger) Debug(msg string, keyvals ...any) { tl.logLevel("DEBUG", msg, keyvals) } -func (tl *testLogger) Info(msg string, keyvals ...interface{}) { +func (tl *testLogger) Info(msg string, keyvals ...any) { tl.logLevel("INFO ", msg, keyvals) } -func (tl *testLogger) Warn(msg string, keyvals ...interface{}) { +func (tl *testLogger) Warn(msg string, keyvals ...any) { tl.logLevel("WARN ", msg, keyvals) } -func (tl *testLogger) Error(msg string, keyvals ...interface{}) { +func (tl *testLogger) Error(msg string, keyvals ...any) { tl.logLevel("ERROR", msg, keyvals) } diff --git a/temporaltest/server_test.go b/temporaltest/server_test.go index ebabcb015c3..0d6a5c5643a 100644 --- a/temporaltest/server_test.go +++ b/temporaltest/server_test.go @@ -61,6 +61,7 @@ func ExampleNewServer() { } func TestNewServer(t *testing.T) { + t.Parallel() ts := temporaltest.NewServer(temporaltest.WithT(t)) ts.NewWorker("hello_world", func(registry worker.Registry) { @@ -91,6 +92,7 @@ func TestNewServer(t *testing.T) { } func TestNewWorkerWithOptions(t *testing.T) { + t.Parallel() ts := temporaltest.NewServer(temporaltest.WithT(t)) c := ts.GetDefaultClient() @@ -139,6 +141,7 @@ func TestNewWorkerWithOptions(t *testing.T) { } func TestDefaultWorkerOptions(t *testing.T) { + t.Parallel() ts := temporaltest.NewServer( temporaltest.WithT(t), temporaltest.WithBaseWorkerOptions( @@ -188,6 +191,7 @@ func (denyAllClaimMapper) GetClaims(*authorization.AuthInfo) (*authorization.Cla } func TestBaseServerOptions(t *testing.T) { + t.Parallel() // This test verifies that we can set custom claim mappers and authorizers // with BaseServerOptions. ts := temporaltest.NewServer( @@ -217,6 +221,7 @@ func TestBaseServerOptions(t *testing.T) { } func TestClientWithCustomInterceptor(t *testing.T) { + t.Parallel() var opts client.Options opts.Interceptors = append(opts.Interceptors, NewTestInterceptor()) ts := temporaltest.NewServer( @@ -255,6 +260,7 @@ func TestClientWithCustomInterceptor(t *testing.T) { } func TestSearchAttributeRegistration(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() ts := temporaltest.NewServer(temporaltest.WithT(t)) @@ -365,7 +371,7 @@ func BenchmarkRunWorkflow(b *testing.B) { } func SearchAttrWorkflow(ctx workflow.Context, searchAttr string) error { - return workflow.UpsertSearchAttributes(ctx, map[string]interface{}{ + return workflow.UpsertSearchAttributes(ctx, map[string]any{ //nolint:staticcheck // SA1019: untyped search attributes used in test searchAttr: "foo", }) } @@ -432,7 +438,7 @@ func (i *WorkflowInterceptor) Init(outbound interceptor.WorkflowOutboundIntercep return i.Next.Init(outbound) } -func (i *WorkflowInterceptor) ExecuteWorkflow(ctx workflow.Context, in *interceptor.ExecuteWorkflowInput) (interface{}, error) { +func (i *WorkflowInterceptor) ExecuteWorkflow(ctx workflow.Context, in *interceptor.ExecuteWorkflowInput) (any, error) { version := workflow.GetVersion(ctx, "version", workflow.DefaultVersion, 1) var err error diff --git a/tests/activity_api_batch_reset_test.go b/tests/activity_api_batch_reset_test.go index 0eddab54be2..43e17d157e9 100644 --- a/tests/activity_api_batch_reset_test.go +++ b/tests/activity_api_batch_reset_test.go @@ -1,7 +1,6 @@ package tests import ( - "context" "fmt" "testing" "time" @@ -9,84 +8,92 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" "github.com/temporalio/sqlparser" batchpb "go.temporal.io/api/batch/v1" commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" sdkclient "go.temporal.io/sdk/client" + "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/searchattribute/sadefs" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/grpc/codes" ) -type ActivityApiBatchResetClientTestSuite struct { - testcore.FunctionalTestBase +type ActivityAPIBatchResetClientTestSuite struct { + parallelsuite.Suite[*ActivityAPIBatchResetClientTestSuite] } -func TestActivityApiBatchResetClientTestSuite(t *testing.T) { - s := new(ActivityApiBatchResetClientTestSuite) - suite.Run(t, s) +func TestActivityAPIBatchResetClientTestSuite(t *testing.T) { + parallelsuite.Run(t, &ActivityAPIBatchResetClientTestSuite{}) } -func (s *ActivityApiBatchResetClientTestSuite) createWorkflow(ctx context.Context, workflowFn WorkflowFunction) sdkclient.WorkflowRun { +func newBatchResetEnv(t *testing.T) *testcore.TestEnv { + return testcore.NewEnv( + t, + // These tests intentionally start multiple batch operations in the same namespace. + // The default per-namespace limit is 1, so raise it to the functional test limit. + testcore.WithDynamicConfig(dynamicconfig.FrontendMaxConcurrentBatchOperationPerNamespace, testcore.ClientSuiteLimit), + ) +} + +func (s *ActivityAPIBatchResetClientTestSuite) createBatchResetWorkflow(env *testcore.TestEnv, workflowFn WorkflowFunction) sdkclient.WorkflowRun { workflowOptions := sdkclient.StartWorkflowOptions{ - ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + ID: testcore.RandomizeStr("wf_id"), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(env.Context(), workflowOptions, workflowFn) s.NoError(err) s.NotNil(workflowRun) return workflowRun } -func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() +func (s *ActivityAPIBatchResetClientTestSuite) TestActivityBatchReset_Success() { + env := newBatchResetEnv(s.T()) internalWorkflow := newInternalWorkflow() - s.Worker().RegisterWorkflow(internalWorkflow.WorkflowFunc) - s.Worker().RegisterActivity(internalWorkflow.ActivityFunc) + env.SdkWorker().RegisterWorkflow(internalWorkflow.WorkflowFunc) + env.SdkWorker().RegisterActivity(internalWorkflow.ActivityFunc) - workflowRun1 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) - workflowRun2 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) + workflowRun1 := s.createBatchResetWorkflow(env, internalWorkflow.WorkflowFunc) + workflowRun2 := s.createBatchResetWorkflow(env, internalWorkflow.WorkflowFunc) // wait for activity to start in both workflows s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 100*time.Millisecond) // pause activities in both workflows pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{}, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } pauseRequest.Execution.WorkflowId = workflowRun1.GetID() - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(env.Context(), pauseRequest) s.NoError(err) s.NotNil(resp) pauseRequest.Execution.WorkflowId = workflowRun2.GetID() - resp, err = s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err = env.FrontendClient().PauseActivity(env.Context(), pauseRequest) s.NoError(err) s.NotNil(resp) // wait for activities to be paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.True(t, description.PendingActivities[0].Paused) @@ -103,20 +110,20 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success() query := fmt.Sprintf("(WorkflowType='%s' AND %s)", workflowTypeName, resetCause) s.EventuallyWithT(func(t *assert.CollectT) { - listResp, err = s.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListWorkflowExecutions(env.Context(), &workflowservice.ListWorkflowExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 10, Query: query, }) require.NoError(t, err) require.NotNil(t, listResp) require.Len(t, listResp.GetExecutions(), 2) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 500*time.Millisecond) // reset the activities in both workflows with batch reset - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{ Activity: &batchpb.BatchOperationResetActivities_Type{Type: activityTypeName}, @@ -131,25 +138,25 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success() // make sure activities are restarted and still paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) - require.Equal(t, description.PendingActivities[0].Attempt, int32(1)) + require.Equal(t, int32(1), description.PendingActivities[0].Attempt) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) - require.Equal(t, description.PendingActivities[0].Attempt, int32(1)) + require.Equal(t, int32(1), description.PendingActivities[0].Attempt) }, 5*time.Second, 100*time.Millisecond) // let activities succeed internalWorkflow.letActivitySucceed.Store(true) // reset the activities in both workflows with batch reset - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{ Activity: &batchpb.BatchOperationResetActivities_Type{Type: activityTypeName}, @@ -163,58 +170,57 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success() s.NoError(err) var out string - err = workflowRun1.Get(ctx, &out) + err = workflowRun1.Get(env.Context(), &out) s.NoError(err) - err = workflowRun2.Get(ctx, &out) + err = workflowRun2.Get(env.Context(), &out) s.NoError(err) } -func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success_Protobuf() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() +func (s *ActivityAPIBatchResetClientTestSuite) TestActivityBatchReset_Success_Protobuf() { + env := newBatchResetEnv(s.T()) internalWorkflow := newInternalWorkflow() - s.Worker().RegisterWorkflow(internalWorkflow.WorkflowFunc) - s.Worker().RegisterActivity(internalWorkflow.ActivityFunc) + env.SdkWorker().RegisterWorkflow(internalWorkflow.WorkflowFunc) + env.SdkWorker().RegisterActivity(internalWorkflow.ActivityFunc) - workflowRun1 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) - workflowRun2 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) + workflowRun1 := s.createBatchResetWorkflow(env, internalWorkflow.WorkflowFunc) + workflowRun2 := s.createBatchResetWorkflow(env, internalWorkflow.WorkflowFunc) // wait for activity to start in both workflows s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 100*time.Millisecond) // pause activities in both workflows pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{}, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } pauseRequest.Execution.WorkflowId = workflowRun1.GetID() - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(env.Context(), pauseRequest) s.NoError(err) s.NotNil(resp) pauseRequest.Execution.WorkflowId = workflowRun2.GetID() - resp, err = s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err = env.FrontendClient().PauseActivity(env.Context(), pauseRequest) s.NoError(err) s.NotNil(resp) // wait for activities to be paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.True(t, description.PendingActivities[0].Paused) @@ -231,20 +237,20 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success_Pr query := fmt.Sprintf("(WorkflowType='%s' AND %s)", workflowTypeName, resetCause) s.EventuallyWithT(func(t *assert.CollectT) { - listResp, err = s.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListWorkflowExecutions(env.Context(), &workflowservice.ListWorkflowExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 10, Query: query, }) require.NoError(t, err) require.NotNil(t, listResp) require.Len(t, listResp.GetExecutions(), 2) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 500*time.Millisecond) // reset the activities in both workflows with batch reset - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{ Activity: &batchpb.BatchOperationResetActivities_Type{Type: activityTypeName}, @@ -259,25 +265,25 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success_Pr // make sure activities are restarted and still paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) - require.Equal(t, description.PendingActivities[0].Attempt, int32(1)) + require.Equal(t, int32(1), description.PendingActivities[0].Attempt) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) - require.Equal(t, description.PendingActivities[0].Attempt, int32(1)) + require.Equal(t, int32(1), description.PendingActivities[0].Attempt) }, 5*time.Second, 100*time.Millisecond) // let activities succeed internalWorkflow.letActivitySucceed.Store(true) // reset the activities in both workflows with batch reset - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{ Activity: &batchpb.BatchOperationResetActivities_Type{Type: activityTypeName}, @@ -291,58 +297,57 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Success_Pr s.NoError(err) var out string - err = workflowRun1.Get(ctx, &out) + err = workflowRun1.Get(env.Context(), &out) s.NoError(err) - err = workflowRun2.Get(ctx, &out) + err = workflowRun2.Get(env.Context(), &out) s.NoError(err) } -func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_DontResetAttempts() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() +func (s *ActivityAPIBatchResetClientTestSuite) TestActivityBatchReset_DontResetAttempts() { + env := newBatchResetEnv(s.T()) internalWorkflow := newInternalWorkflow() - s.Worker().RegisterWorkflow(internalWorkflow.WorkflowFunc) - s.Worker().RegisterActivity(internalWorkflow.ActivityFunc) + env.SdkWorker().RegisterWorkflow(internalWorkflow.WorkflowFunc) + env.SdkWorker().RegisterActivity(internalWorkflow.ActivityFunc) - workflowRun1 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) - workflowRun2 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) + workflowRun1 := s.createBatchResetWorkflow(env, internalWorkflow.WorkflowFunc) + workflowRun2 := s.createBatchResetWorkflow(env, internalWorkflow.WorkflowFunc) // wait for activity to start in both workflows s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 100*time.Millisecond) // pause activities in both workflows pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{}, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } pauseRequest.Execution.WorkflowId = workflowRun1.GetID() - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(env.Context(), pauseRequest) s.NoError(err) s.NotNil(resp) pauseRequest.Execution.WorkflowId = workflowRun2.GetID() - resp, err = s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err = env.FrontendClient().PauseActivity(env.Context(), pauseRequest) s.NoError(err) s.NotNil(resp) // wait for activities to be paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.True(t, description.PendingActivities[0].Paused) @@ -359,20 +364,20 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_DontResetA query := fmt.Sprintf("(WorkflowType='%s' AND %s)", workflowTypeName, resetCause) s.EventuallyWithT(func(t *assert.CollectT) { - listResp, err = s.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListWorkflowExecutions(env.Context(), &workflowservice.ListWorkflowExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 10, Query: query, }) require.NoError(t, err) require.NotNil(t, listResp) require.Len(t, listResp.GetExecutions(), 2) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 500*time.Millisecond) // reset the activities in both workflows with batch reset - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{ Activity: &batchpb.BatchOperationResetActivities_Type{Type: activityTypeName}, @@ -388,23 +393,23 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_DontResetA // make sure activities are restarted and still paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) - require.NotEqual(t, description.PendingActivities[0].Attempt, int32(1)) + require.NotEqual(t, int32(1), description.PendingActivities[0].Attempt) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(env.Context(), workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) - require.NotEqual(t, description.PendingActivities[0].Attempt, int32(1)) + require.NotEqual(t, int32(1), description.PendingActivities[0].Attempt) }, 5*time.Second, 100*time.Millisecond) // let activities succeed internalWorkflow.letActivitySucceed.Store(true) // reset the activities in both workflows with batch reset - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{ Activity: &batchpb.BatchOperationResetActivities_Type{Type: activityTypeName}, @@ -418,17 +423,19 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_DontResetA s.NoError(err) var out string - err = workflowRun1.Get(ctx, &out) + err = workflowRun1.Get(env.Context(), &out) s.NoError(err) - err = workflowRun2.Get(ctx, &out) + err = workflowRun2.Get(env.Context(), &out) s.NoError(err) } -func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Failed() { +func (s *ActivityAPIBatchResetClientTestSuite) TestActivityBatchReset_Failed() { + env := newBatchResetEnv(s.T()) + // neither activity type not "match all" is provided - _, err := s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err := env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{}, }, @@ -441,8 +448,8 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Failed() { s.ErrorAs(err, new(*serviceerror.InvalidArgument)) // neither activity type not "match all" is provided - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ ResetActivitiesOperation: &batchpb.BatchOperationResetActivities{ Activity: &batchpb.BatchOperationResetActivities_Type{Type: ""}, @@ -455,4 +462,18 @@ func (s *ActivityApiBatchResetClientTestSuite) TestActivityBatchReset_Failed() { s.Error(err) s.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + + // malformed visibility query should be rejected before the batch workflow starts + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), + Operation: &workflowservice.StartBatchOperationRequest_DeletionOperation{ + DeletionOperation: &batchpb.BatchOperationDeletion{}, + }, + VisibilityQuery: "()", + JobId: uuid.NewString(), + Reason: "test", + }) + s.Error(err) + s.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) + s.ErrorAs(err, new(*serviceerror.InvalidArgument)) } diff --git a/tests/activity_api_batch_security_test.go b/tests/activity_api_batch_security_test.go new file mode 100644 index 00000000000..0f0c3fce3cf --- /dev/null +++ b/tests/activity_api_batch_security_test.go @@ -0,0 +1,95 @@ +package tests + +import ( + "testing" + "time" + + "github.com/google/uuid" + commandpb "go.temporal.io/api/command/v1" + 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" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/primitives" + "go.temporal.io/server/common/testing/taskpoller" + "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/tests/testcore" + "google.golang.org/protobuf/types/known/durationpb" +) + +// TestScheduleActivityOnPerNSTQ_Blocked verifies that a normal workflow +// running in a task queue that is not the internal per-namespace task queue +// cannot schedule an activity on the internal per-namespace task queue. +func TestScheduleActivityOnPerNSTQ_Blocked(t *testing.T) { + t.Parallel() + env := testcore.NewEnv(t) + + id := testcore.RandomizeStr(t.Name()) + wt := "test-schedule-activity-per-ns-tq-type" + tl := "test-schedule-activity-per-ns-tq" + identity := "worker1" + + workflowType := &commonpb.WorkflowType{Name: wt} + taskQueue := &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + we, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(), &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: env.Namespace().String(), + WorkflowId: id, + WorkflowType: workflowType, + TaskQueue: taskQueue, + Input: nil, + WorkflowRunTimeout: durationpb.New(100 * time.Second), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + Identity: identity, + }) + env.NoError(err) + + tv := testvars.New(t).WithTaskQueue(tl) + + // Workflow task handler that tries to schedule an activity on the internal per-ns task queue. + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ + ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: "activity1", + ActivityType: &commonpb.ActivityType{Name: "test-activity"}, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: primitives.PerNSWorkerTaskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + StartToCloseTimeout: durationpb.New(10 * time.Second), + }, + }, + }}, + }, nil + } + + poller := taskpoller.New(t, env.FrontendClient(), env.Namespace().String()) + + _, err = poller.PollAndHandleWorkflowTask(tv, wtHandler) + env.Error(err, "Expected error when scheduling activity on internal per-namespace task queue") + var invalidArgument *serviceerror.InvalidArgument + env.ErrorAs(err, &invalidArgument) + env.Contains(err.Error(), "internal per-namespace task queue") + + // Verify a WorkflowTaskFailed event was recorded with the correct cause. + historyEvents := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{ + WorkflowId: id, + RunId: we.RunId, + }) + var foundTaskFailed bool + for _, event := range historyEvents { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_FAILED { + foundTaskFailed = true + attrs := event.GetWorkflowTaskFailedEventAttributes() + env.Equal(enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_ACTIVITY_ATTRIBUTES, attrs.GetCause()) + env.Contains(attrs.GetFailure().GetMessage(), "internal per-namespace task queue") + break + } + } + env.True(foundTaskFailed, "WorkflowTaskFailed event should be recorded") +} diff --git a/tests/activity_api_batch_unpause_test.go b/tests/activity_api_batch_unpause_test.go index 47f576fa9ab..1c3be332f63 100644 --- a/tests/activity_api_batch_unpause_test.go +++ b/tests/activity_api_batch_unpause_test.go @@ -11,27 +11,27 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" "github.com/temporalio/sqlparser" batchpb "go.temporal.io/api/batch/v1" commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" sdkclient "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" "go.temporal.io/server/common/searchattribute/sadefs" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/grpc/codes" ) type ActivityApiBatchUnpauseClientTestSuite struct { - testcore.FunctionalTestBase + parallelsuite.Suite[*ActivityApiBatchUnpauseClientTestSuite] } func TestActivityApiBatchUnpauseClientTestSuite(t *testing.T) { - s := new(ActivityApiBatchUnpauseClientTestSuite) - suite.Run(t, s) + parallelsuite.Run(t, &ActivityApiBatchUnpauseClientTestSuite{}) } type internalTestWorkflow struct { @@ -79,12 +79,12 @@ func (w *internalTestWorkflow) ActivityFunc() (string, error) { return "done!", nil } -func (s *ActivityApiBatchUnpauseClientTestSuite) createWorkflow(ctx context.Context, workflowFn WorkflowFunction) sdkclient.WorkflowRun { +func (s *ActivityApiBatchUnpauseClientTestSuite) createWorkflow(env *testcore.TestEnv, workflowFn WorkflowFunction) sdkclient.WorkflowRun { workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(env.Context(), workflowOptions, workflowFn) s.NoError(err) s.NotNil(workflowRun) @@ -92,49 +92,50 @@ func (s *ActivityApiBatchUnpauseClientTestSuite) createWorkflow(ctx context.Cont } func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Success() { + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() internalWorkflow := newInternalWorkflow() - s.Worker().RegisterWorkflow(internalWorkflow.WorkflowFunc) - s.Worker().RegisterActivity(internalWorkflow.ActivityFunc) + env.SdkWorker().RegisterWorkflow(internalWorkflow.WorkflowFunc) + env.SdkWorker().RegisterActivity(internalWorkflow.ActivityFunc) - workflowRun1 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) - workflowRun2 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) + workflowRun1 := s.createWorkflow(env, internalWorkflow.WorkflowFunc) + workflowRun2 := s.createWorkflow(env, internalWorkflow.WorkflowFunc) // wait for activity to start in both workflows s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 100*time.Millisecond) // pause activities in both workflows pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{}, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } pauseRequest.Execution.WorkflowId = workflowRun1.GetID() - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(resp) pauseRequest.Execution.WorkflowId = workflowRun2.GetID() - resp, err = s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err = env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(resp) // wait for activities to be paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.True(t, description.PendingActivities[0].Paused) @@ -150,8 +151,8 @@ func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Succes query := fmt.Sprintf("(WorkflowType='%s' AND %s)", workflowTypeName, unpauseCause) s.EventuallyWithT(func(t *assert.CollectT) { - listResp, err = s.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 10, Query: query, }) @@ -161,8 +162,8 @@ func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Succes }, 5*time.Second, 500*time.Millisecond) // unpause the activities in both workflows with batch unpause - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UnpauseActivitiesOperation{ UnpauseActivitiesOperation: &batchpb.BatchOperationUnpauseActivities{ Activity: &batchpb.BatchOperationUnpauseActivities_Type{Type: activityTypeName}, @@ -176,11 +177,11 @@ func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Succes // make sure activities are unpaused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.False(t, description.PendingActivities[0].Paused) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.False(t, description.PendingActivities[0].Paused) @@ -198,9 +199,11 @@ func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Succes } func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Failed() { + env := testcore.NewEnv(s.T()) + // neither activity type not "match all" is provided - _, err := s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err := env.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UnpauseActivitiesOperation{ UnpauseActivitiesOperation: &batchpb.BatchOperationUnpauseActivities{}, }, @@ -213,8 +216,8 @@ func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Failed s.ErrorAs(err, new(*serviceerror.InvalidArgument)) // neither activity type not "match all" is provided - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UnpauseActivitiesOperation{ UnpauseActivitiesOperation: &batchpb.BatchOperationUnpauseActivities{ Activity: &batchpb.BatchOperationUnpauseActivities_Type{Type: ""}, @@ -228,3 +231,85 @@ func (s *ActivityApiBatchUnpauseClientTestSuite) TestActivityBatchUnpause_Failed s.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) s.ErrorAs(err, new(*serviceerror.InvalidArgument)) } + +// TestBatchTerminate_NamespaceIsolation verifies that a batch terminate operation +// scoped to the primary namespace does not affect workflows in a separate namespace. +// This is an end-to-end complement to the unit-level checkNamespace tests: it +// exercises the full path from StartBatchOperation through the batcher worker. +func (s *ActivityApiBatchUnpauseClientTestSuite) TestBatchTerminate_NamespaceIsolation() { + env := testcore.NewEnv(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + // Register a uniquely-named workflow type to avoid interference from parallel tests. + wfTypeName := testcore.RandomizeStr("isolation-wf") + sleepWorkflow := func(ctx workflow.Context) error { + return workflow.Sleep(ctx, 24*time.Hour) + } + env.SdkWorker().RegisterWorkflowWithOptions(sleepWorkflow, workflow.RegisterOptions{Name: wfTypeName}) + + // Start two workflows in the primary namespace (worker is registered and will execute them). + startWf := func(client sdkclient.Client, taskQueue string) sdkclient.WorkflowRun { + run, err := client.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ + ID: testcore.RandomizeStr("wf"), + TaskQueue: taskQueue, + }, wfTypeName) + s.NoError(err) + return run + } + primaryRun1 := startWf(env.SdkClient(), env.WorkerTaskQueue()) + primaryRun2 := startWf(env.SdkClient(), env.WorkerTaskQueue()) + + // Create a client for the external namespace and start two workflows there. + // No worker polls this task queue in the external namespace, so these workflows + // will remain in RUNNING state without executing. + extClient, err := sdkclient.Dial(sdkclient.Options{ + HostPort: env.FrontendGRPCAddress(), + Namespace: env.ExternalNamespace().String(), + }) + s.NoError(err) + defer extClient.Close() + extRun1 := startWf(extClient, env.WorkerTaskQueue()) + extRun2 := startWf(extClient, env.WorkerTaskQueue()) + + // Wait for both primary-namespace workflows to be indexed in visibility before + // submitting the batch, which uses a visibility query to find its targets. + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := env.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ + Namespace: env.Namespace().String(), + Query: fmt.Sprintf("WorkflowType='%s'", wfTypeName), + PageSize: 10, + }) + require.NoError(t, err) + require.Len(t, resp.GetExecutions(), 2) + }, 10*time.Second, 500*time.Millisecond) + + // Batch-terminate all workflows of this type in the primary namespace only. + _, err = env.SdkClient().WorkflowService().StartBatchOperation(ctx, &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), + VisibilityQuery: fmt.Sprintf("WorkflowType='%s'", wfTypeName), + JobId: uuid.NewString(), + Reason: "namespace-isolation-test", + Operation: &workflowservice.StartBatchOperationRequest_TerminationOperation{ + TerminationOperation: &batchpb.BatchOperationTermination{}, + }, + }) + s.NoError(err) + + // Primary-namespace workflows must reach TERMINATED status. + s.EventuallyWithT(func(t *assert.CollectT) { + for _, run := range []sdkclient.WorkflowRun{primaryRun1, primaryRun2} { + desc, err := env.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) + require.NoError(t, err) + require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, desc.WorkflowExecutionInfo.Status) + } + }, 10*time.Second, 500*time.Millisecond) + + // External-namespace workflows must remain RUNNING — the batch must not cross namespace boundaries. + for _, run := range []sdkclient.WorkflowRun{extRun1, extRun2} { + desc, err := extClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) + s.NoError(err) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, desc.WorkflowExecutionInfo.Status, + "batch terminate in primary namespace must not affect external namespace workflows") + } +} diff --git a/tests/activity_api_batch_update_options_test.go b/tests/activity_api_batch_update_options_test.go index ccf8d095007..e095d62fce9 100644 --- a/tests/activity_api_batch_update_options_test.go +++ b/tests/activity_api_batch_update_options_test.go @@ -1,7 +1,6 @@ package tests import ( - "context" "fmt" "testing" "time" @@ -9,7 +8,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" "github.com/temporalio/sqlparser" activitypb "go.temporal.io/api/activity/v1" batchpb "go.temporal.io/api/batch/v1" @@ -17,78 +15,80 @@ import ( "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" sdkclient "go.temporal.io/sdk/client" + "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/searchattribute/sadefs" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/fieldmaskpb" ) -type ActivityApiBatchUpdateOptionsClientTestSuite struct { - testcore.FunctionalTestBase +type ActivityAPIBatchUpdateOptionsSuite struct { + parallelsuite.Suite[*ActivityAPIBatchUpdateOptionsSuite] } func TestActivityApiBatchUpdateOptionsClientTestSuite(t *testing.T) { - s := new(ActivityApiBatchUpdateOptionsClientTestSuite) - suite.Run(t, s) + parallelsuite.Run(t, &ActivityAPIBatchUpdateOptionsSuite{}) } -func (s *ActivityApiBatchUpdateOptionsClientTestSuite) createWorkflow(ctx context.Context, workflowFn WorkflowFunction) sdkclient.WorkflowRun { +func (s *ActivityAPIBatchUpdateOptionsSuite) createBatchUpdateOptionsWorkflow(env *testcore.TestEnv, workflowFn WorkflowFunction) sdkclient.WorkflowRun { workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(env.Context(), workflowOptions, workflowFn) s.NoError(err) s.NotNil(workflowRun) return workflowRun } -func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOptions_Success() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() +func (s *ActivityAPIBatchUpdateOptionsSuite) TestActivityBatchUpdateOptionsSuccess() { + env := testcore.NewEnv(s.T(), testcore.WithDynamicConfig(dynamicconfig.FrontendMaxConcurrentBatchOperationPerNamespace, testcore.ClientSuiteLimit)) + + ctx := env.Context() internalWorkflow := newInternalWorkflow() - s.Worker().RegisterWorkflow(internalWorkflow.WorkflowFunc) - s.Worker().RegisterActivity(internalWorkflow.ActivityFunc) + env.SdkWorker().RegisterWorkflow(internalWorkflow.WorkflowFunc) + env.SdkWorker().RegisterActivity(internalWorkflow.ActivityFunc) - workflowRun1 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) - workflowRun2 := s.createWorkflow(ctx, internalWorkflow.WorkflowFunc) + workflowRun1 := s.createBatchUpdateOptionsWorkflow(env, internalWorkflow.WorkflowFunc) + workflowRun2 := s.createBatchUpdateOptionsWorkflow(env, internalWorkflow.WorkflowFunc) // wait for activity to start in both workflows - s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + env.EventuallyWithT(func(t *assert.CollectT) { + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) - require.Greater(t, internalWorkflow.startedActivityCount.Load(), int32(0)) + require.Positive(t, internalWorkflow.startedActivityCount.Load()) }, 5*time.Second, 100*time.Millisecond) // pause activities in both workflows pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{}, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } pauseRequest.Execution.WorkflowId = workflowRun1.GetID() - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) - s.NoError(err) - s.NotNil(resp) + resp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) + env.NoError(err) + env.NotNil(resp) pauseRequest.Execution.WorkflowId = workflowRun2.GetID() - resp, err = s.FrontendClient().PauseActivity(ctx, pauseRequest) - s.NoError(err) - s.NotNil(resp) + resp, err = env.FrontendClient().PauseActivity(ctx, pauseRequest) + env.NoError(err) + env.NotNil(resp) // wait for activities to be paused - s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + env.EventuallyWithT(func(t *assert.CollectT) { + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.True(t, description.PendingActivities[0].Paused) @@ -103,9 +103,9 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp unpauseCause := fmt.Sprintf("%s = %s", sadefs.TemporalPauseInfo, escapedSearchValue) query := fmt.Sprintf("(WorkflowType='%s' AND %s)", workflowTypeName, unpauseCause) - s.EventuallyWithT(func(t *assert.CollectT) { - listResp, err = s.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + env.EventuallyWithT(func(t *assert.CollectT) { + listResp, err = env.FrontendClient().ListWorkflowExecutions(ctx, &workflowservice.ListWorkflowExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 10, Query: query, }) @@ -115,8 +115,8 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp }, 5*time.Second, 500*time.Millisecond) // unpause the activities in both workflows with batch unpause - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UpdateActivityOptionsOperation{ UpdateActivityOptionsOperation: &batchpb.BatchOperationUpdateActivityOptions{ Activity: &batchpb.BatchOperationUpdateActivityOptions_Type{Type: activityTypeName}, @@ -132,26 +132,26 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp JobId: uuid.NewString(), Reason: "test", }) - s.NoError(err) + env.NoError(err) // make sure activities are unpaused - s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + env.EventuallyWithT(func(t *assert.CollectT) { + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) - require.Equal(t, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration(), 10*time.Second) + require.Equal(t, 10*time.Second, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration()) require.True(t, description.PendingActivities[0].Paused) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) - require.Equal(t, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration(), 10*time.Second) + require.Equal(t, 10*time.Second, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration()) require.True(t, description.PendingActivities[0].Paused) }, 5*time.Second, 100*time.Millisecond) // unpause the activities in both workflows with batch unpause - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UnpauseActivitiesOperation{ UnpauseActivitiesOperation: &batchpb.BatchOperationUnpauseActivities{ Activity: &batchpb.BatchOperationUnpauseActivities_Type{Type: activityTypeName}, @@ -161,21 +161,21 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp JobId: uuid.NewString(), Reason: "test", }) - s.NoError(err) + env.NoError(err) // make sure activities are unpaused - s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) + env.EventuallyWithT(func(t *assert.CollectT) { + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun1.GetID(), workflowRun1.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) - require.Equal(t, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration(), 10*time.Second) - require.Equal(t, description.PendingActivities[0].Paused, false) + require.Equal(t, 10*time.Second, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration()) + require.False(t, description.PendingActivities[0].Paused) - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun2.GetID(), workflowRun2.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) - require.Equal(t, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration(), 10*time.Second) - require.Equal(t, description.PendingActivities[0].Paused, false) + require.Equal(t, 10*time.Second, description.PendingActivities[0].ActivityOptions.ScheduleToCloseTimeout.AsDuration()) + require.False(t, description.PendingActivities[0].Paused) }, 5*time.Second, 100*time.Millisecond) // let both of the activities succeed @@ -183,16 +183,18 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp var out string err = workflowRun1.Get(ctx, &out) - s.NoError(err) + env.NoError(err) err = workflowRun2.Get(ctx, &out) - s.NoError(err) + env.NoError(err) } -func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOptions_Failed() { +func (s *ActivityAPIBatchUpdateOptionsSuite) TestActivityBatchUpdateOptionsFailed() { + env := testcore.NewEnv(s.T(), testcore.WithDynamicConfig(dynamicconfig.FrontendMaxConcurrentBatchOperationPerNamespace, testcore.ClientSuiteLimit)) + // neither activity type nor "match all" is provided - _, err := s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err := env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UpdateActivityOptionsOperation{ UpdateActivityOptionsOperation: &batchpb.BatchOperationUpdateActivityOptions{}, }, @@ -200,13 +202,13 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp JobId: uuid.NewString(), Reason: "test", }) - s.Error(err) - s.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) - s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + env.Error(err) + env.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) + env.ErrorAs(err, new(*serviceerror.InvalidArgument)) // neither activity type nor "match all" is provided - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UpdateActivityOptionsOperation{ UpdateActivityOptionsOperation: &batchpb.BatchOperationUpdateActivityOptions{ Activity: &batchpb.BatchOperationUpdateActivityOptions_Type{Type: ""}, @@ -216,13 +218,13 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp JobId: uuid.NewString(), Reason: "test", }) - s.Error(err) - s.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) - s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + env.Error(err) + env.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) + env.ErrorAs(err, new(*serviceerror.InvalidArgument)) // cannot set activity options and restore original - _, err = s.SdkClient().WorkflowService().StartBatchOperation(context.Background(), &workflowservice.StartBatchOperationRequest{ - Namespace: s.Namespace().String(), + _, err = env.SdkClient().WorkflowService().StartBatchOperation(env.Context(), &workflowservice.StartBatchOperationRequest{ + Namespace: env.Namespace().String(), Operation: &workflowservice.StartBatchOperationRequest_UpdateActivityOptionsOperation{ UpdateActivityOptionsOperation: &batchpb.BatchOperationUpdateActivityOptions{ Activity: &batchpb.BatchOperationUpdateActivityOptions_Type{Type: "activity-type"}, @@ -236,7 +238,7 @@ func (s *ActivityApiBatchUpdateOptionsClientTestSuite) TestActivityBatchUpdateOp JobId: uuid.NewString(), Reason: "test", }) - s.Error(err) - s.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) - s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + env.Error(err) + env.Equal(codes.InvalidArgument, serviceerror.ToStatus(err).Code()) + env.ErrorAs(err, new(*serviceerror.InvalidArgument)) } diff --git a/tests/activity_api_pause_test.go b/tests/activity_api_pause_test.go index 4a55867a4c8..2cffca6e895 100644 --- a/tests/activity_api_pause_test.go +++ b/tests/activity_api_pause_test.go @@ -9,64 +9,49 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" sdkclient "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" - "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/util" "go.temporal.io/server/tests/testcore" ) -type ActivityApiPauseClientTestSuite struct { - testcore.FunctionalTestBase - tv *testvars.TestVars - initialRetryInterval time.Duration - scheduleToCloseTimeout time.Duration - startToCloseTimeout time.Duration - - activityRetryPolicy *temporal.RetryPolicy +type ActivityAPIPauseClientTestSuite struct { + parallelsuite.Suite[*ActivityAPIPauseClientTestSuite] } -func TestActivityApiPauseClientTestSuite(t *testing.T) { - s := new(ActivityApiPauseClientTestSuite) - suite.Run(t, s) +func TestActivityAPIPauseClientTestSuite(t *testing.T) { + parallelsuite.Run(t, &ActivityAPIPauseClientTestSuite{}) } -func (s *ActivityApiPauseClientTestSuite) SetupTest() { - s.FunctionalTestBase.SetupTest() - - s.tv = testvars.New(s.T()).WithTaskQueue(s.TaskQueue()).WithNamespaceName(s.Namespace()) - - s.initialRetryInterval = 1 * time.Second - s.scheduleToCloseTimeout = 30 * time.Minute - s.startToCloseTimeout = 15 * time.Minute +func (s *ActivityAPIPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { + env := testcore.NewEnv(s.T(), testcore.WithSdkWorker()) - s.activityRetryPolicy = &temporal.RetryPolicy{ - InitialInterval: s.initialRetryInterval, + initialRetryInterval := 1 * time.Second + scheduleToCloseTimeout := 30 * time.Minute + startToCloseTimeout := 15 * time.Minute + activityRetryPolicy := &temporal.RetryPolicy{ + InitialInterval: initialRetryInterval, BackoffCoefficient: 1, } -} - -func (s *ActivityApiPauseClientTestSuite) makeWorkflowFunc(activityFunction ActivityFunctions) WorkflowFunction { - return func(ctx workflow.Context) error { - - var ret string - err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ - ActivityID: "activity-id", - DisableEagerExecution: true, - StartToCloseTimeout: s.startToCloseTimeout, - ScheduleToCloseTimeout: s.scheduleToCloseTimeout, - RetryPolicy: s.activityRetryPolicy, - }), activityFunction).Get(ctx, &ret) - return err + makeWorkflowFunc := func(activityFunction ActivityFunctions) WorkflowFunction { + return func(ctx workflow.Context) error { + var ret string + err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + ActivityID: "activity-id", + DisableEagerExecution: true, + StartToCloseTimeout: startToCloseTimeout, + ScheduleToCloseTimeout: scheduleToCloseTimeout, + RetryPolicy: activityRetryPolicy, + }), activityFunction).Get(ctx, &ret) + return err + } } -} -func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -77,28 +62,28 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { activityFunction := func() (string, error) { startedActivityCount.Add(1) if startedActivityCount.Load() == 1 { - s.WaitForChannel(ctx, activityPausedCn) + env.WaitForChannel(ctx, activityPausedCn) return "", activityErr } return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := makeWorkflowFunc(activityFunction) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.Equal(t, int32(1), startedActivityCount.Load()) @@ -108,7 +93,7 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { testIdentity := "test-identity" testReason := "test-reason" pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -116,13 +101,13 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { Identity: testIdentity, Reason: testReason, } - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(resp) // make sure activity is paused on server while running on worker s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSE_REQUESTED, description.PendingActivities[0].State) @@ -130,19 +115,19 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { }, 5*time.Second, 500*time.Millisecond) // unblock the activity - activityPausedCn <- struct{}{} + env.SendToChannel(ctx, activityPausedCn) // make sure activity is paused on server and completed on the worker s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED, description.PendingActivities[0].State) require.Equal(t, int32(1), startedActivityCount.Load()) }, 5*time.Second, 500*time.Millisecond) - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) s.NoError(err) - s.Equal(1, len(description.PendingActivities)) + s.Len(description.PendingActivities, 1) s.True(description.PendingActivities[0].Paused) // wait long enough for activity to retry if pause is not working @@ -151,9 +136,9 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { s.NoError(err) // make sure activity is not completed, and was not retried - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) s.NoError(err) - s.Equal(1, len(description.PendingActivities)) + s.Len(description.PendingActivities, 1) s.True(description.PendingActivities[0].Paused) s.Equal(int32(2), description.PendingActivities[0].Attempt) s.NotNil(description.PendingActivities[0].LastFailure) @@ -165,13 +150,13 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { // unpause the activity unpauseRequest := &workflowservice.UnpauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"}, } - unpauseResp, err := s.FrontendClient().UnpauseActivity(ctx, unpauseRequest) + unpauseResp, err := env.FrontendClient().UnpauseActivity(ctx, unpauseRequest) s.NoError(err) s.NotNil(unpauseResp) @@ -181,7 +166,7 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRunning() { s.NoError(err) } -func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsOnFailure() { +func (s *ActivityAPIPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsOnFailure() { /* * 1. Run an activity that runs forever * 2. Pause the activity @@ -189,6 +174,29 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO * 4. Validate activity failed * 5. Validate number of activity attempts increased */ + env := testcore.NewEnv(s.T(), testcore.WithSdkWorker()) + + initialRetryInterval := 1 * time.Second + scheduleToCloseTimeout := 30 * time.Minute + startToCloseTimeout := 15 * time.Minute + activityRetryPolicy := &temporal.RetryPolicy{ + InitialInterval: initialRetryInterval, + BackoffCoefficient: 1, + } + makeWorkflowFunc := func(activityFunction ActivityFunctions) WorkflowFunction { + return func(ctx workflow.Context) error { + var ret string + err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + ActivityID: "activity-id", + DisableEagerExecution: true, + StartToCloseTimeout: startToCloseTimeout, + ScheduleToCloseTimeout: scheduleToCloseTimeout, + RetryPolicy: activityRetryPolicy, + }), activityFunction).Get(ctx, &ret) + return err + } + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -200,7 +208,7 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO activityFunction := func() (string, error) { startedActivityCount.Add(1) if startedActivityCount.Load() == 1 { - s.WaitForChannel(ctx, activityPausedCn) + env.WaitForChannel(ctx, activityPausedCn) return "", activityErr } if shouldSucceed.Load() { @@ -209,22 +217,22 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO return "", activityErr } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := makeWorkflowFunc(activityFunction) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.Equal(t, int32(1), startedActivityCount.Load()) @@ -234,7 +242,7 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO testIdentity := "test-identity" testReason := "test-reason" pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -242,13 +250,13 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO Identity: testIdentity, Reason: testReason, } - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(resp) // make sure activity is paused on server while running on worker s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSE_REQUESTED, description.PendingActivities[0].State) @@ -256,13 +264,13 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO }, 5*time.Second, 500*time.Millisecond) // End the activity - activityPausedCn <- struct{}{} + env.SendToChannel(ctx, activityPausedCn) s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.NotNil(t, description) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) require.Equal(t, int32(2), description.PendingActivities[0].Attempt) require.NotNil(t, description.PendingActivities[0].LastFailure) @@ -279,13 +287,13 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO // unpause the activity unpauseRequest := &workflowservice.UnpauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"}, } - unpauseResp, err := s.FrontendClient().UnpauseActivity(ctx, unpauseRequest) + unpauseResp, err := env.FrontendClient().UnpauseActivity(ctx, unpauseRequest) s.NoError(err) s.NotNil(unpauseResp) @@ -300,18 +308,35 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_IncreaseAttemptsO s.NoError(err) } -func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileWaiting() { +func (s *ActivityAPIPauseClientTestSuite) TestActivityPauseApi_WhileWaiting() { // In this case, pause happens when activity is in retry state. // Make sure that activity is paused and then unpaused. // Also check that activity will not be retried while unpaused. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + env := testcore.NewEnv(s.T(), testcore.WithSdkWorker()) - s.initialRetryInterval = 1 * time.Second - s.activityRetryPolicy = &temporal.RetryPolicy{ - InitialInterval: s.initialRetryInterval, + initialRetryInterval := 1 * time.Second + scheduleToCloseTimeout := 30 * time.Minute + startToCloseTimeout := 15 * time.Minute + activityRetryPolicy := &temporal.RetryPolicy{ + InitialInterval: initialRetryInterval, BackoffCoefficient: 1, } + makeWorkflowFunc := func(activityFunction ActivityFunctions) WorkflowFunction { + return func(ctx workflow.Context) error { + var ret string + err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + ActivityID: "activity-id", + DisableEagerExecution: true, + StartToCloseTimeout: startToCloseTimeout, + ScheduleToCloseTimeout: scheduleToCloseTimeout, + RetryPolicy: activityRetryPolicy, + }), activityFunction).Get(ctx, &ret) + return err + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() var startedActivityCount atomic.Int32 @@ -324,24 +349,24 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileWaiting() { return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := makeWorkflowFunc(activityFunction) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.Equal(t, int32(1), startedActivityCount.Load()) }, 5*time.Second, 100*time.Millisecond) @@ -349,7 +374,7 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileWaiting() { testIdentity := "test-identity" testReason := "test-reason" pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -357,17 +382,17 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileWaiting() { Identity: testIdentity, Reason: testReason, } - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(resp) // wait long enough for activity to retry if pause is not working - util.InterruptibleSleep(ctx, 2*time.Second) + s.NoError(util.InterruptibleSleep(ctx, 2*time.Second)) // make sure activity is not completed, and was not retried - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) s.NoError(err) - s.Equal(1, len(description.PendingActivities)) + s.Len(description.PendingActivities, 1) s.True(description.PendingActivities[0].Paused) s.Equal(int32(2), description.PendingActivities[0].Attempt) s.NotNil(description.PendingActivities[0].PauseInfo) @@ -377,13 +402,13 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileWaiting() { // unpause the activity unpauseRequest := &workflowservice.UnpauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"}, } - unpauseResp, err := s.FrontendClient().UnpauseActivity(ctx, unpauseRequest) + unpauseResp, err := env.FrontendClient().UnpauseActivity(ctx, unpauseRequest) s.NoError(err) s.NotNil(unpauseResp) @@ -396,21 +421,37 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileWaiting() { err = workflowRun.Get(ctx, &out) s.NoError(err) - } -func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRetryNoWait() { +func (s *ActivityAPIPauseClientTestSuite) TestActivityPauseApi_WhileRetryNoWait() { // In this case, pause can happen when activity is in retry state. // Make sure that activity is paused and then unpaused. // Also tests noWait flag. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + env := testcore.NewEnv(s.T(), testcore.WithSdkWorker()) - s.initialRetryInterval = 30 * time.Second - s.activityRetryPolicy = &temporal.RetryPolicy{ - InitialInterval: s.initialRetryInterval, + initialRetryInterval := 30 * time.Second + scheduleToCloseTimeout := 30 * time.Minute + startToCloseTimeout := 15 * time.Minute + activityRetryPolicy := &temporal.RetryPolicy{ + InitialInterval: initialRetryInterval, BackoffCoefficient: 1, } + makeWorkflowFunc := func(activityFunction ActivityFunctions) WorkflowFunction { + return func(ctx workflow.Context) error { + var ret string + err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + ActivityID: "activity-id", + DisableEagerExecution: true, + StartToCloseTimeout: startToCloseTimeout, + ScheduleToCloseTimeout: scheduleToCloseTimeout, + RetryPolicy: activityRetryPolicy, + }), activityFunction).Get(ctx, &ret) + return err + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() var startedActivityCount atomic.Int32 @@ -423,22 +464,22 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRetryNoWait( return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := makeWorkflowFunc(activityFunction) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, int32(1), startedActivityCount.Load()) @@ -446,25 +487,25 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRetryNoWait( // pause activity pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(resp) // unpause the activity unpauseRequest := &workflowservice.UnpauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"}, } - unpauseResp, err := s.FrontendClient().UnpauseActivity(ctx, unpauseRequest) + unpauseResp, err := env.FrontendClient().UnpauseActivity(ctx, unpauseRequest) s.NoError(err) s.NotNil(unpauseResp) @@ -479,16 +520,33 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WhileRetryNoWait( s.NoError(err) } -func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WithReset() { +func (s *ActivityAPIPauseClientTestSuite) TestActivityPauseApi_WithReset() { // pause/unpause the activity with reset option and noWait flag - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + env := testcore.NewEnv(s.T(), testcore.WithSdkWorker()) - s.initialRetryInterval = 1 * time.Second - s.activityRetryPolicy = &temporal.RetryPolicy{ - InitialInterval: s.initialRetryInterval, + initialRetryInterval := 1 * time.Second + scheduleToCloseTimeout := 30 * time.Minute + startToCloseTimeout := 15 * time.Minute + activityRetryPolicy := &temporal.RetryPolicy{ + InitialInterval: initialRetryInterval, BackoffCoefficient: 1, } + makeWorkflowFunc := func(activityFunction ActivityFunctions) WorkflowFunction { + return func(ctx workflow.Context) error { + var ret string + err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + ActivityID: "activity-id", + DisableEagerExecution: true, + StartToCloseTimeout: startToCloseTimeout, + ScheduleToCloseTimeout: scheduleToCloseTimeout, + RetryPolicy: activityRetryPolicy, + }), activityFunction).Get(ctx, &ret) + return err + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() var startedActivityCount atomic.Int32 activityWasReset := false @@ -501,26 +559,26 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WithReset() { activityErr := errors.New("bad-luck-please-retry") return "", activityErr } - s.WaitForChannel(ctx, activityCompleteCn) + env.WaitForChannel(ctx, activityCompleteCn) return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := makeWorkflowFunc(activityFunction) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start/fail few times s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Greater(t, startedActivityCount.Load(), int32(1)) @@ -528,44 +586,44 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WithReset() { // pause activity pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } - resp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + resp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(resp) // wait for activity to be in paused state and waiting for retry s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_PAUSED, description.PendingActivities[0].State) // also verify that the number of attempts was not reset - require.True(t, description.PendingActivities[0].Attempt > 1) + require.Greater(t, description.PendingActivities[0].Attempt, int32(1)) }, 5*time.Second, 100*time.Millisecond) activityWasReset = true // unpause the activity with reset, and set noWait flag unpauseRequest := &workflowservice.UnpauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"}, ResetAttempts: true, } - unpauseResp, err := s.FrontendClient().UnpauseActivity(ctx, unpauseRequest) + unpauseResp, err := env.FrontendClient().UnpauseActivity(ctx, unpauseRequest) s.NoError(err) s.NotNil(unpauseResp) // wait for activity to be running s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State) @@ -574,7 +632,7 @@ func (s *ActivityApiPauseClientTestSuite) TestActivityPauseApi_WithReset() { }, 5*time.Second, 100*time.Millisecond) // let activity finish - activityCompleteCn <- struct{}{} + env.SendToChannel(ctx, activityCompleteCn) // wait for workflow to finish var out string diff --git a/tests/activity_api_reset_test.go b/tests/activity_api_reset_test.go index 091e896120f..0e96bfa0010 100644 --- a/tests/activity_api_reset_test.go +++ b/tests/activity_api_reset_test.go @@ -33,7 +33,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" @@ -42,51 +41,28 @@ import ( "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" "go.temporal.io/server/common/payloads" - "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/util" "go.temporal.io/server/tests/testcore" ) type ActivityApiResetClientTestSuite struct { - testcore.FunctionalTestBase - tv *testvars.TestVars - initialRetryInterval time.Duration - scheduleToCloseTimeout time.Duration - startToCloseTimeout time.Duration - - activityRetryPolicy *temporal.RetryPolicy + parallelsuite.Suite[*ActivityApiResetClientTestSuite] } func TestActivityApiResetClientTestSuite(t *testing.T) { - s := new(ActivityApiResetClientTestSuite) - suite.Run(t, s) -} - -func (s *ActivityApiResetClientTestSuite) SetupTest() { - s.FunctionalTestBase.SetupTest() - - s.tv = testvars.New(s.T()).WithTaskQueue(s.TaskQueue()).WithNamespaceName(s.Namespace()) - - s.initialRetryInterval = 1 * time.Second - s.scheduleToCloseTimeout = 30 * time.Minute - s.startToCloseTimeout = 15 * time.Minute - - s.activityRetryPolicy = &temporal.RetryPolicy{ - InitialInterval: s.initialRetryInterval, - BackoffCoefficient: 1, - } + parallelsuite.Run(t, &ActivityApiResetClientTestSuite{}) } -func (s *ActivityApiResetClientTestSuite) makeWorkflowFunc(activityFunction ActivityFunctions) WorkflowFunction { +func (s *ActivityApiResetClientTestSuite) makeWorkflowFunc(activityFunction ActivityFunctions, retryPolicy *temporal.RetryPolicy) WorkflowFunction { return func(ctx workflow.Context) error { - var ret string err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ActivityID: "activity-id", DisableEagerExecution: true, - StartToCloseTimeout: s.startToCloseTimeout, - ScheduleToCloseTimeout: s.scheduleToCloseTimeout, - RetryPolicy: s.activityRetryPolicy, + StartToCloseTimeout: 15 * time.Minute, + ScheduleToCloseTimeout: 30 * time.Minute, + RetryPolicy: retryPolicy, }), activityFunction).Get(ctx, &ret) return err } @@ -94,6 +70,8 @@ func (s *ActivityApiResetClientTestSuite) makeWorkflowFunc(activityFunction Acti func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_AfterRetry() { // activity reset is called after multiple attempts, + env := testcore.NewEnv(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -109,40 +87,43 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_AfterRetry() { return "", activityErr } - s.WaitForChannel(ctx, activityCompleteCh) + env.WaitForChannel(ctx, activityCompleteCh) return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := s.makeWorkflowFunc(activityFunction, &temporal.RetryPolicy{ + InitialInterval: 1 * time.Second, + BackoffCoefficient: 1, + }) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) wfId := testcore.RandomizeStr("wfid-" + s.T().Name()) workflowOptions := sdkclient.StartWorkflowOptions{ ID: wfId, - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start/fail few times s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Greater(t, startedActivityCount.Load(), int32(1)) }, 5*time.Second, 200*time.Millisecond) resetRequest := &workflowservice.ResetActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.ResetActivityRequest_Id{Id: "activity-id"}, } - resp, err := s.FrontendClient().ResetActivity(ctx, resetRequest) + resp, err := env.FrontendClient().ResetActivity(ctx, resetRequest) s.NoError(err) s.NotNil(resp) @@ -150,7 +131,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_AfterRetry() { // wait for activity to be running s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State) @@ -170,6 +151,8 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_AfterRetry() { func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_WhileRunning() { // activity reset is called while activity is running + env := testcore.NewEnv(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -177,39 +160,42 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_WhileRunning() { var startedActivityCount atomic.Int32 activityFunction := func() (string, error) { startedActivityCount.Add(1) - s.WaitForChannel(ctx, activityCompleteCh) + env.WaitForChannel(ctx, activityCompleteCh) return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := s.makeWorkflowFunc(activityFunction, &temporal.RetryPolicy{ + InitialInterval: 1 * time.Second, + BackoffCoefficient: 1, + }) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ - ID: s.tv.WorkflowID(), - TaskQueue: s.TaskQueue(), + ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State) }, 5*time.Second, 200*time.Millisecond) resetRequest := &workflowservice.ResetActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.ResetActivityRequest_Id{Id: "activity-id"}, } - resp, err := s.FrontendClient().ResetActivity(ctx, resetRequest) + resp, err := env.FrontendClient().ResetActivity(ctx, resetRequest) s.NoError(err) s.NotNil(resp) @@ -218,7 +204,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_WhileRunning() { // check if workflow and activity are still running s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State) @@ -240,11 +226,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_WhileRunning() { func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_InRetry() { // reset is called while activity is in retry - s.initialRetryInterval = 1 * time.Minute - s.activityRetryPolicy = &temporal.RetryPolicy{ - InitialInterval: s.initialRetryInterval, - BackoffCoefficient: 1, - } + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -260,47 +242,50 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_InRetry() { return "", activityErr } - s.WaitForChannel(ctx, activityCompleteCh) + env.WaitForChannel(ctx, activityCompleteCh) return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := s.makeWorkflowFunc(activityFunction, &temporal.RetryPolicy{ + InitialInterval: 1 * time.Minute, + BackoffCoefficient: 1, + }) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) wfId := testcore.RandomizeStr("wf_id-" + s.T().Name()) workflowOptions := sdkclient.StartWorkflowOptions{ ID: wfId, - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start, fail and wait for retry s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_SCHEDULED, description.PendingActivities[0].State) require.Equal(t, int32(1), startedActivityCount.Load()) }, 5*time.Second, 200*time.Millisecond) resetRequest := &workflowservice.ResetActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.ResetActivityRequest_Id{Id: "activity-id"}, } - resp, err := s.FrontendClient().ResetActivity(ctx, resetRequest) + resp, err := env.FrontendClient().ResetActivity(ctx, resetRequest) s.NoError(err) s.NotNil(resp) // wait for activity to start. Wait time is shorter than original retry interval s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_STARTED, description.PendingActivities[0].State) @@ -320,11 +305,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_InRetry() { func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_KeepPaused() { // reset is called while activity is in retry - s.initialRetryInterval = 1 * time.Minute - s.activityRetryPolicy = &temporal.RetryPolicy{ - InitialInterval: s.initialRetryInterval, - BackoffCoefficient: 1, - } + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -341,27 +322,30 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_KeepPaused() { return "", activityErr } - s.WaitForChannel(ctx, activityCompleteCh) + env.WaitForChannel(ctx, activityCompleteCh) return "done!", nil } - workflowFn := s.makeWorkflowFunc(activityFunction) + workflowFn := s.makeWorkflowFunc(activityFunction, &temporal.RetryPolicy{ + InitialInterval: 1 * time.Minute, + BackoffCoefficient: 1, + }) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) wfId := testcore.RandomizeStr("wf_id-" + s.T().Name()) workflowOptions := sdkclient.StartWorkflowOptions{ ID: wfId, - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start, fail few times and wait for retry s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) require.Equal(t, enumspb.PENDING_ACTIVITY_STATE_SCHEDULED, description.PendingActivities[0].State) @@ -370,19 +354,19 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_KeepPaused() { // pause the activity pauseRequest := &workflowservice.PauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.PauseActivityRequest_Id{Id: "activity-id"}, } - pauseResp, err := s.FrontendClient().PauseActivity(ctx, pauseRequest) + pauseResp, err := env.FrontendClient().PauseActivity(ctx, pauseRequest) s.NoError(err) s.NotNil(pauseResp) // verify that activity is paused s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.NotNil(t, description) require.Len(t, description.GetPendingActivities(), 1) @@ -394,20 +378,20 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_KeepPaused() { // reset the activity, while keeping it paused resetRequest := &workflowservice.ResetActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.ResetActivityRequest_Id{Id: "activity-id"}, KeepPaused: true, } - resp, err := s.FrontendClient().ResetActivity(ctx, resetRequest) + resp, err := env.FrontendClient().ResetActivity(ctx, resetRequest) s.NoError(err) s.NotNil(resp) // verify that activity is still paused, and reset s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.NotNil(t, description) require.Len(t, description.GetPendingActivities(), 1) @@ -421,13 +405,13 @@ func (s *ActivityApiResetClientTestSuite) TestActivityResetApi_KeepPaused() { // unpause the activity unpauseRequest := &workflowservice.UnpauseActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, Activity: &workflowservice.UnpauseActivityRequest_Id{Id: "activity-id"}, } - unpauseResp, err := s.FrontendClient().UnpauseActivity(ctx, unpauseRequest) + unpauseResp, err := env.FrontendClient().UnpauseActivity(ctx, unpauseRequest) s.NoError(err) s.NotNil(unpauseResp) @@ -457,6 +441,12 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { // 2. First invocation of activity sets heartbeat details and fails upon request. // 3. Second invocation triggers waits to be triggered, and then send new heartbeat until requested to finish. // 6. Once workflow completes -- we're done. + env := testcore.NewEnv(s.T()) + + activityRetryPolicy := &temporal.RetryPolicy{ + InitialInterval: 1 * time.Second, + BackoffCoefficient: 1, + } activityCompleteCh := make(chan struct{}) var activityIteration atomic.Int32 @@ -472,7 +462,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { return "", errors.New("bad-luck-please-retry") } // not the first iteration - s.WaitForChannel(ctx, activityCompleteCh) + env.WaitForChannel(ctx, activityCompleteCh) for activityShouldFinish.Load() == false { activity.RecordHeartbeat(ctx, "second") time.Sleep(time.Second) //nolint:forbidigo @@ -486,25 +476,25 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ActivityID: activityId, DisableEagerExecution: true, - StartToCloseTimeout: s.startToCloseTimeout, - ScheduleToCloseTimeout: s.scheduleToCloseTimeout, - RetryPolicy: s.activityRetryPolicy, + StartToCloseTimeout: 15 * time.Minute, + ScheduleToCloseTimeout: 30 * time.Minute, + RetryPolicy: activityRetryPolicy, }), activityFn).Get(ctx, &ret) return ret, err } - s.Worker().RegisterActivity(activityFn) - s.Worker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFn) + env.SdkWorker().RegisterWorkflow(workflowFn) wfId := "functional-test-heartbeat-details-after-reset" workflowOptions := sdkclient.StartWorkflowOptions{ ID: wfId, - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), WorkflowRunTimeout: 20 * time.Second, } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) s.NotNil(workflowRun) @@ -513,7 +503,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { // make sure activity is running and sending heartbeats s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) requirePayload(t, "first", description.PendingActivities[0].GetHeartbeatDetails()) @@ -522,7 +512,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { // reset the activity, with heartbeats resetRequest := &workflowservice.ResetActivityRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -530,7 +520,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { ResetHeartbeat: true, } - resp, err := s.FrontendClient().ResetActivity(ctx, resetRequest) + resp, err := env.FrontendClient().ResetActivity(ctx, resetRequest) s.NoError(err) s.NotNil(resp) @@ -539,7 +529,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { // wait for activity to fail and retried s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.PendingActivities, 1) ap := description.PendingActivities[0] @@ -555,7 +545,7 @@ func (s *ActivityApiResetClientTestSuite) TestActivityReset_HeartbeatDetails() { // make sure activity is running and sending heartbeats s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Equal(t, int32(1), activityIteration.Load()) require.Len(t, description.PendingActivities, 1) diff --git a/tests/activity_api_rules_test.go b/tests/activity_api_rules_test.go index 65b163723f8..89e5db6e572 100644 --- a/tests/activity_api_rules_test.go +++ b/tests/activity_api_rules_test.go @@ -164,7 +164,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_CRUD() { }) s.NoError(err) s.NotNil(nsResp) - s.Len(nsResp.Rules, 0) + s.Empty(nsResp.Rules) // create a rule ruleID1 := "pause-activity-rule-1" @@ -268,7 +268,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_CRUD() { }) require.NoError(t, err) require.NotNil(t, nsResp) - require.Len(t, nsResp.Rules, 0) + require.Empty(t, nsResp.Rules) }, 5*time.Second, 200*time.Millisecond) } @@ -277,8 +277,8 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_RetryActivity() { defer cancel() testWorkflow := newInternalRulesTestWorkflow(ctx, &s.FunctionalTestBase, s.Logger) - s.Worker().RegisterWorkflow(testWorkflow.WorkflowFuncForRetryActivity) - s.Worker().RegisterActivity(testWorkflow.ActivityFuncForRetryActivity) + s.SdkWorker().RegisterWorkflow(testWorkflow.WorkflowFuncForRetryActivity) + s.SdkWorker().RegisterActivity(testWorkflow.ActivityFuncForRetryActivity) workflowRun := s.createWorkflow(ctx, testWorkflow.WorkflowFuncForRetryActivity) @@ -326,7 +326,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_RetryActivity() { // make sure activity pause info is set description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) s.NoError(err) - s.Equal(1, len(description.PendingActivities)) + s.Len(description.PendingActivities, 1) s.True(description.PendingActivities[0].Paused) s.NotNil(description.PendingActivities[0].PauseInfo) rule := description.PendingActivities[0].PauseInfo.GetRule() @@ -351,7 +351,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_RetryActivity() { }) require.NoError(t, err) require.NotNil(t, nsResp) - require.Len(t, nsResp.Rules, 0) + require.Empty(t, nsResp.Rules) }, 5*time.Second, 200*time.Millisecond) // Let namespace config propagate. @@ -411,8 +411,8 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_RetryTask() { s.initialRetryInterval = 4 * time.Second s.activityRetryPolicy.InitialInterval = s.initialRetryInterval - s.Worker().RegisterWorkflow(testRetryTaskWorkflow.WorkflowFuncForRetryTask) - s.Worker().RegisterActivity(testRetryTaskWorkflow.ActivityFuncForRetryTask) + s.SdkWorker().RegisterWorkflow(testRetryTaskWorkflow.WorkflowFuncForRetryTask) + s.SdkWorker().RegisterActivity(testRetryTaskWorkflow.ActivityFuncForRetryTask) // 1. Start workflow workflowRun := s.createWorkflow(ctx, testRetryTaskWorkflow.WorkflowFuncForRetryTask) @@ -462,7 +462,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_RetryTask() { // make sure activity pause info is set description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) s.NoError(err) - s.Equal(1, len(description.PendingActivities)) + s.Len(description.PendingActivities, 1) s.True(description.PendingActivities[0].Paused) s.NotNil(description.PendingActivities[0].PauseInfo) rule := description.PendingActivities[0].PauseInfo.GetRule() @@ -487,7 +487,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_RetryTask() { }) require.NoError(t, err) require.NotNil(t, nsResp) - require.Len(t, nsResp.Rules, 0) + require.Empty(t, nsResp.Rules) }, 5*time.Second, 200*time.Millisecond) // Let namespace config propagate. @@ -543,8 +543,8 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_PrePause() { testRetryTaskWorkflow := newInternalRulesTestWorkflow(ctx, &s.FunctionalTestBase, s.Logger) - s.Worker().RegisterWorkflow(testRetryTaskWorkflow.WorkflowFuncForPrePause) - s.Worker().RegisterActivity(testRetryTaskWorkflow.ActivityFuncForPrePause) + s.SdkWorker().RegisterWorkflow(testRetryTaskWorkflow.WorkflowFuncForPrePause) + s.SdkWorker().RegisterActivity(testRetryTaskWorkflow.ActivityFuncForPrePause) // 1. Create rule to pause activity ruleID := "pause-activity" @@ -590,7 +590,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_PrePause() { // make sure activity pause info is set description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) s.NoError(err) - s.Equal(1, len(description.PendingActivities)) + s.Len(description.PendingActivities, 1) s.True(description.PendingActivities[0].Paused) s.NotNil(description.PendingActivities[0].PauseInfo) s.Equal(ruleID, description.PendingActivities[0].PauseInfo.GetRule().GetRuleId()) @@ -610,7 +610,7 @@ func (s *ActivityApiRulesClientTestSuite) TestActivityRulesApi_PrePause() { }) require.NoError(t, err) require.NotNil(t, nsResp) - require.Len(t, nsResp.Rules, 0) + require.Empty(t, nsResp.Rules) }, 5*time.Second, 200*time.Millisecond) // 8. Let namespace config changes propagate to the history service. diff --git a/tests/activity_api_update_test.go b/tests/activity_api_update_test.go index d2d432f4652..13fd9a5e1b7 100644 --- a/tests/activity_api_update_test.go +++ b/tests/activity_api_update_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" activitypb "go.temporal.io/api/activity/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -17,37 +16,23 @@ import ( sdkclient "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" - "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/fieldmaskpb" ) const ( - defaultMaximumAttempts = 100 + defaultMaximumAttempts = 100 + activityUpdateWorkflowID = "activity-update-workflow-id" ) -type ActivityApiUpdateClientTestSuite struct { - testcore.FunctionalTestBase - tv *testvars.TestVars -} - -func TestActivityApiUpdateClientTestSuite(t *testing.T) { - s := new(ActivityApiUpdateClientTestSuite) - suite.Run(t, s) -} - -func (s *ActivityApiUpdateClientTestSuite) SetupTest() { - s.FunctionalTestBase.SetupTest() - s.tv = testvars.New(s.T()).WithTaskQueue(s.TaskQueue()).WithNamespaceName(s.Namespace()) -} - type ( ActivityFunctions func() (string, error) - WorkflowFunction func(context2 workflow.Context) error + WorkflowFunction func(workflow.Context) error ) -func (s *ActivityApiUpdateClientTestSuite) makeWorkflowFunc( +func makeActivityUpdateWorkflowFunc( activityFunction ActivityFunctions, scheduleToCloseTimeout time.Duration, initialRetryInterval time.Duration, @@ -72,7 +57,17 @@ func (s *ActivityApiUpdateClientTestSuite) makeWorkflowFunc( } } -func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeRetryInterval() { +type ActivityAPIUpdateClientTestSuite struct { + parallelsuite.Suite[*ActivityAPIUpdateClientTestSuite] +} + +func TestActivityAPIUpdateClientTestSuite(t *testing.T) { + parallelsuite.Run(t, &ActivityAPIUpdateClientTestSuite{}) +} + +func (s *ActivityAPIUpdateClientTestSuite) TestActivityUpdateApi_ChangeRetryInterval() { + env := testcore.NewEnv(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -87,34 +82,34 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeRetryInte return "", activityErr } - s.WaitForChannel(ctx, activityUpdated) + env.WaitForChannel(ctx, activityUpdated) return "done!", nil } scheduleToCloseTimeout := 30 * time.Minute retryTimeout := 10 * time.Minute - workflowFn := s.makeWorkflowFunc(activityFunction, scheduleToCloseTimeout, retryTimeout) + workflowFn := makeActivityUpdateWorkflowFunc(activityFunction, scheduleToCloseTimeout, retryTimeout) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ - ID: s.tv.WorkflowID(), - TaskQueue: s.TaskQueue(), + ID: activityUpdateWorkflowID, + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, int32(1), startedActivityCount.Load()) }, 10*time.Second, 500*time.Millisecond) updateRequest := &workflowservice.UpdateActivityOptionsRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -126,18 +121,18 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeRetryInte }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"retry_policy.initial_interval"}}, } - resp, err := s.FrontendClient().UpdateActivityOptions(ctx, updateRequest) + resp, err := env.FrontendClient().UpdateActivityOptions(ctx, updateRequest) s.NoError(err) s.NotNil(resp) - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) s.NoError(err) - s.Equal(1, len(description.PendingActivities)) + s.Len(description.PendingActivities, 1) activityUpdated <- struct{}{} s.EventuallyWithT(func(t *assert.CollectT) { - description, err = s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err = env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Empty(t, description.GetPendingActivities()) require.Equal(t, int32(2), startedActivityCount.Load()) @@ -149,7 +144,9 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeRetryInte s.NoError(err) } -func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleToClose() { +func (s *ActivityAPIUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleToClose() { + env := testcore.NewEnv(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -166,22 +163,22 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT scheduleToCloseTimeout := 30 * time.Minute retryTimeout := 10 * time.Minute - workflowFn := s.makeWorkflowFunc(activityFunction, scheduleToCloseTimeout, retryTimeout) + workflowFn := makeActivityUpdateWorkflowFunc(activityFunction, scheduleToCloseTimeout, retryTimeout) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ - ID: s.tv.WorkflowID(), - TaskQueue: s.TaskQueue(), + ID: activityUpdateWorkflowID, + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start (and fail) s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, int32(1), startedActivityCount.Load()) @@ -190,7 +187,7 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT // update schedule_to_close_timeout updateRequest := &workflowservice.UpdateActivityOptionsRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -200,13 +197,13 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"schedule_to_close_timeout"}}, } - resp, err := s.FrontendClient().UpdateActivityOptions(ctx, updateRequest) + resp, err := env.FrontendClient().UpdateActivityOptions(ctx, updateRequest) s.NoError(err) s.NotNil(resp) // activity should fail immediately s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Empty(t, description.GetPendingActivities()) require.Equal(t, int32(1), startedActivityCount.Load()) @@ -215,20 +212,22 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT var out string err = workflowRun.Get(ctx, &out) var activityError *temporal.ActivityError - s.True(errors.As(err, &activityError)) + s.ErrorAs(err, &activityError) // SCHEDULE_TO_CLOSE timeout now returns RETRY_STATE_TIMEOUT instead of RETRY_STATE_NON_RETRYABLE_FAILURE s.Equal(enumspb.RETRY_STATE_TIMEOUT, activityError.RetryState()) var timeoutError *temporal.TimeoutError - s.True(errors.As(activityError.Unwrap(), &timeoutError)) + s.ErrorAs(activityError, &timeoutError) s.Equal(enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, timeoutError.TimeoutType()) s.Equal(int32(1), startedActivityCount.Load()) } -func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleToCloseAndRetry() { +func (s *ActivityAPIUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleToCloseAndRetry() { // change both schedule to close and retry policy // initial values are chosen in such a way that activity will fail due to schedule to close timeout // we change schedule to close to a longer value and retry policy to a shorter value // after that activity should succeed + env := testcore.NewEnv(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -247,18 +246,18 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT scheduleToCloseTimeout := 8 * time.Second retryInterval := 5 * time.Second - workflowFn := s.makeWorkflowFunc( + workflowFn := makeActivityUpdateWorkflowFunc( activityFunction, scheduleToCloseTimeout, retryInterval) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ - ID: s.tv.WorkflowID(), - TaskQueue: s.TaskQueue(), + ID: activityUpdateWorkflowID, + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start (and fail) @@ -270,7 +269,7 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT // also update retry policy interval, make it shorter newScheduleToCloseTimeout := 10 * time.Second updateRequest := &workflowservice.UpdateActivityOptionsRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -284,7 +283,7 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"schedule_to_close_timeout", "retry_policy.initial_interval"}}, } - resp, err := s.FrontendClient().UpdateActivityOptions(ctx, updateRequest) + resp, err := env.FrontendClient().UpdateActivityOptions(ctx, updateRequest) s.NoError(err) s.NotNil(resp) // check that the update was successful @@ -294,7 +293,7 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT // now activity should succeed s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Empty(t, description.GetPendingActivities()) require.Equal(t, int32(2), startedActivityCount.Load()) @@ -305,13 +304,14 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ChangeScheduleT s.NoError(err) } -func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOptions() { +func (s *ActivityAPIUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOptions() { // plan: // 1. start the workflow, wait for activity to start and fail, // 2. update activity options to change retry policy maximum attempts // 3. reset activity options to default, verify that retry policy is reset to default // 4. update activity options again, this time change schedule to close timeout and retry policy initial interval // 5. let activity finish, verify that it finished with updated options + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -327,28 +327,28 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOpt return "", activityErr } - s.WaitForChannel(ctx, activityUpdated) + env.WaitForChannel(ctx, activityUpdated) return "done!", nil } scheduleToCloseTimeout := 30 * time.Minute retryTimeout := 10 * time.Minute - workflowFn := s.makeWorkflowFunc(activityFunction, scheduleToCloseTimeout, retryTimeout) + workflowFn := makeActivityUpdateWorkflowFunc(activityFunction, scheduleToCloseTimeout, retryTimeout) - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + env.SdkWorker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ - ID: s.tv.WorkflowID(), - TaskQueue: s.TaskQueue(), + ID: activityUpdateWorkflowID, + TaskQueue: env.WorkerTaskQueue(), } - workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) // wait for activity to start (and fail) s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Len(t, description.GetPendingActivities(), 1) require.Equal(t, int32(1), startedActivityCount.Load()) @@ -356,7 +356,7 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOpt // update activity options, set retry policy to 1000 attempts updateRequest := &workflowservice.UpdateActivityOptionsRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowRun.GetID(), }, @@ -368,15 +368,15 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOpt }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"retry_policy.maximum_attempts"}}, } - resp, err := s.FrontendClient().UpdateActivityOptions(ctx, updateRequest) + resp, err := env.FrontendClient().UpdateActivityOptions(ctx, updateRequest) s.NoError(err) s.NotNil(resp) // check that the update was successful s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.Equal(t, int32(1000), description.PendingActivities[0].GetActivityOptions().GetRetryPolicy().GetMaximumAttempts()) }, 3*time.Second, 200*time.Millisecond) @@ -384,15 +384,15 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOpt updateRequest.ActivityOptions = nil updateRequest.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{}} updateRequest.RestoreOriginal = true - resp, err = s.FrontendClient().UpdateActivityOptions(ctx, updateRequest) + resp, err = env.FrontendClient().UpdateActivityOptions(ctx, updateRequest) s.NoError(err) s.NotNil(resp) // check that the update was successful s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.Equal(t, int32(defaultMaximumAttempts), description.PendingActivities[0].GetActivityOptions().GetRetryPolicy().GetMaximumAttempts()) }, 3*time.Second, 200*time.Millisecond) @@ -406,7 +406,7 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOpt } updateRequest.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"schedule_to_close_timeout", "retry_policy.initial_interval"}} updateRequest.RestoreOriginal = false - resp, err = s.FrontendClient().UpdateActivityOptions(ctx, updateRequest) + resp, err = env.FrontendClient().UpdateActivityOptions(ctx, updateRequest) s.NoError(err) s.NotNil(resp) @@ -415,7 +415,7 @@ func (s *ActivityApiUpdateClientTestSuite) TestActivityUpdateApi_ResetDefaultOpt // wait for activity to finish s.EventuallyWithT(func(t *assert.CollectT) { - description, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) + description, err := env.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) require.Empty(t, description.GetPendingActivities()) require.Equal(t, int32(2), startedActivityCount.Load()) diff --git a/tests/activity_test.go b/tests/activity_test.go index af964ce928e..036af5dd385 100644 --- a/tests/activity_test.go +++ b/tests/activity_test.go @@ -89,8 +89,8 @@ func (s *ActivityClientTestSuite) TestActivityScheduleToClose_FiredDuringBackoff return "done!", err } - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + s.SdkWorker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFunction) wfId := "functional-test-gethistoryreverse" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -107,9 +107,9 @@ func (s *ActivityClientTestSuite) TestActivityScheduleToClose_FiredDuringBackoff s.Error(err) var wfExecutionError *temporal.WorkflowExecutionError - s.True(errors.As(err, &wfExecutionError)) + s.ErrorAs(err, &wfExecutionError) var activityError *temporal.ActivityError - s.True(errors.As(wfExecutionError.Unwrap(), &activityError)) + s.ErrorAs(wfExecutionError, &activityError) s.Equal(enumspb.RETRY_STATE_TIMEOUT, activityError.RetryState()) s.Equal(int32(2), activityCompleted.Load()) @@ -159,8 +159,8 @@ func (s *ActivityClientTestSuite) TestActivityScheduleToClose_FiredDuringActivit return "done!", err } - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + s.SdkWorker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFunction) workflowOptions := sdkclient.StartWorkflowOptions{ ID: s.T().Name(), @@ -174,10 +174,10 @@ func (s *ActivityClientTestSuite) TestActivityScheduleToClose_FiredDuringActivit var out string err = workflowRun.Get(ctx, &out) var activityError *temporal.ActivityError - s.True(errors.As(err, &activityError)) + s.ErrorAs(err, &activityError) s.Equal(enumspb.RETRY_STATE_TIMEOUT, activityError.RetryState()) var timeoutError *temporal.TimeoutError - s.True(errors.As(activityError.Unwrap(), &timeoutError)) + s.ErrorAs(activityError, &timeoutError) s.Equal(enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, timeoutError.TimeoutType()) // schedule to close timeout should fire while last activity is still running. s.Equal(int32(2), activityCompleted.Load()) @@ -207,7 +207,7 @@ func (s *ActivityClientTestSuite) Test_ActivityTimeouts() { // so here, we reduce the duration between two heartbeats, so that they are // more likely be sent in the heartbeat batch at 1.6s // (basically increasing the room for delay in heartbeat goroutine from 0.1s to 1s) - for i := 0; i < 3; i++ { + for i := range 3 { activity.RecordHeartbeat(ctx, i) time.Sleep(200 * time.Millisecond) //nolint:forbidigo } @@ -298,8 +298,8 @@ func (s *ActivityClientTestSuite) Test_ActivityTimeouts() { return nil } - s.Worker().RegisterActivity(activityFn) - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFn) + s.SdkWorker().RegisterWorkflow(workflowFn) workflowOptions := sdkclient.StartWorkflowOptions{ ID: "functional-test-activity-timeouts", @@ -312,7 +312,7 @@ func (s *ActivityClientTestSuite) Test_ActivityTimeouts() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) err = workflowRun.Get(ctx, nil) s.NoError(err) @@ -449,8 +449,8 @@ func (s *ActivityTestSuite) TestActivityHeartBeatWorkflow_Success() { atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { s.Equal(id, task.WorkflowExecution.GetWorkflowId()) s.Equal(activityName, task.ActivityType.GetName()) - for i := 0; i < 10; i++ { - s.Logger.Info("Heartbeating for activity", tag.WorkflowActivityID(task.ActivityId), tag.Counter(i)) + for i := range 10 { + s.Logger.Info("Heartbeating for activity", tag.ActivityID(task.ActivityId), tag.Counter(i)) _, err := s.FrontendClient().RecordActivityTaskHeartbeat(testcore.NewContext(), &workflowservice.RecordActivityTaskHeartbeatRequest{ Namespace: s.Namespace().String(), TaskToken: task.TaskToken, @@ -678,10 +678,10 @@ func (s *ActivityTestSuite) TestActivityRetry() { descResp, err = describeWorkflowExecution() s.NoError(err) s.Len(descResp.GetPendingActivities(), 1) - s.Equal(descResp.GetPendingActivities()[0].GetActivityId(), "B") + s.Equal("B", descResp.GetPendingActivities()[0].GetActivityId()) s.Logger.Info("Waiting for workflow to complete", tag.WorkflowRunID(we.RunId)) - for i := 0; i < 3; i++ { + for i := range 3 { s.False(workflowComplete) s.Logger.Info("Processing workflow task:", tag.Counter(i)) @@ -844,7 +844,7 @@ func (s *ActivityTestSuite) TestActivityHeartBeatWorkflow_Timeout() { if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -908,6 +908,8 @@ func (s *ActivityTestSuite) TestActivityHeartBeatWorkflow_Timeout() { } func (s *ActivityTestSuite) TestTryActivityCancellationFromWorkflow() { + ctx := testcore.NewContext() + id := "functional-activity-cancellation-test" wt := "functional-activity-cancellation-test-type" tl := "functional-activity-cancellation-test-taskqueue" @@ -930,7 +932,7 @@ func (s *ActivityTestSuite) TestTryActivityCancellationFromWorkflow() { Identity: identity, } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + we, err0 := s.FrontendClient().StartWorkflowExecution(ctx, request) s.NoError(err0) s.Logger.Info("StartWorkflowExecution: response", tag.WorkflowRunID(we.GetRunId())) @@ -944,7 +946,7 @@ func (s *ActivityTestSuite) TestTryActivityCancellationFromWorkflow() { if scheduleActivity { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) activityScheduledID = task.StartedEventId + 2 return []*commandpb.Command{{ @@ -985,8 +987,8 @@ func (s *ActivityTestSuite) TestTryActivityCancellationFromWorkflow() { atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { s.Equal(id, task.WorkflowExecution.GetWorkflowId()) s.Equal(activityName, task.ActivityType.GetName()) - for i := 0; i < 10; i++ { - s.Logger.Info("Heartbeating for activity", tag.WorkflowActivityID(task.ActivityId), tag.Counter(i)) + for i := range 10 { + s.Logger.Info("Heartbeating for activity", tag.ActivityID(task.ActivityId), tag.Counter(i)) response, err := s.FrontendClient().RecordActivityTaskHeartbeat(testcore.NewContext(), &workflowservice.RecordActivityTaskHeartbeatRequest{ Namespace: s.Namespace().String(), @@ -1045,7 +1047,12 @@ func (s *ActivityTestSuite) TestTryActivityCancellationFromWorkflow() { s.True(err == nil || errors.Is(err, testcore.ErrNoTasks)) s.Logger.Info("Waiting for cancel to complete.", tag.WorkflowRunID(we.RunId)) - <-cancelCh + select { + case <-cancelCh: + case <-ctx.Done(): + s.Fail("Test timed out for activity cancellation", ctx.Err()) + return + } s.True(activityCanceled, "Activity was not cancelled.") s.Logger.Info("Activity cancelled.", tag.WorkflowRunID(we.RunId)) } @@ -1087,7 +1094,7 @@ func (s *ActivityTestSuite) TestActivityCancellationNotStarted() { if scheduleActivity { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) s.Logger.Info("Scheduling activity") activityScheduledID = task.StartedEventId + 2 return []*commandpb.Command{{ @@ -1231,8 +1238,8 @@ func (s *ActivityClientTestSuite) TestActivityHeartbeatDetailsDuringRetry() { return nil } - s.Worker().RegisterActivity(activityFn) - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFn) + s.SdkWorker().RegisterWorkflow(workflowFn) wfId := "functional-test-heartbeat-details-during-retry" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -1246,7 +1253,7 @@ func (s *ActivityClientTestSuite) TestActivityHeartbeatDetailsDuringRetry() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) runId := workflowRun.GetRunID() @@ -1435,7 +1442,8 @@ func (s *ActivityTestSuite) TestActivityTaskCompleteForceCompletion() { s.EventuallyWithT(func(t *assert.CollectT) { description, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) require.NoError(t, err) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) + require.NotNil(t, description.PendingActivities[0].LastFailure) require.Equal(t, "mock error of an activity", description.PendingActivities[0].LastFailure.Message) }, 10*time.Second, @@ -1466,7 +1474,8 @@ func (s *ActivityTestSuite) TestActivityTaskCompleteRejectCompletion() { s.EventuallyWithT(func(t *assert.CollectT) { description, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) require.NoError(t, err) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) + require.NotNil(t, description.PendingActivities[0].LastFailure) require.Equal(t, "mock error of an activity", description.PendingActivities[0].LastFailure.Message) }, 10*time.Second, @@ -1523,8 +1532,8 @@ func (s *ActivityClientTestSuite) TestActivity_AttemptsExceeded() { return err } - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFunction) + s.SdkWorker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFunction) wfID := testcore.RandomizeStr(s.T().Name()) workflowOptions := sdkclient.StartWorkflowOptions{ @@ -1540,10 +1549,10 @@ func (s *ActivityClientTestSuite) TestActivity_AttemptsExceeded() { var wfExecutionError *temporal.WorkflowExecutionError s.ErrorAs(err, &wfExecutionError) var activityError *temporal.ActivityError - s.ErrorAs(wfExecutionError.Unwrap(), &activityError) + s.ErrorAs(wfExecutionError, &activityError) s.Equal(enumspb.RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED, activityError.RetryState()) var applicationErr *temporal.ApplicationError - s.ErrorAs(activityError.Unwrap(), &applicationErr) + s.ErrorAs(activityError, &applicationErr) s.Equal("non-retryable-error", applicationErr.Message()) history := s.GetHistory(string(s.Namespace()), &commonpb.WorkflowExecution{WorkflowId: workflowRun.GetID()}) diff --git a/tests/admin_batch_refresh_workflow_tasks_test.go b/tests/admin_batch_refresh_workflow_tasks_test.go index ea841ed4dad..9ec5975851b 100644 --- a/tests/admin_batch_refresh_workflow_tasks_test.go +++ b/tests/admin_batch_refresh_workflow_tasks_test.go @@ -45,7 +45,7 @@ func (s *AdminBatchRefreshWorkflowTasksTestSuite) simpleWorkflow(ctx workflow.Co return "done", nil } -func (s *AdminBatchRefreshWorkflowTasksTestSuite) createWorkflow(ctx context.Context, workflowFn interface{}) sdkclient.WorkflowRun { +func (s *AdminBatchRefreshWorkflowTasksTestSuite) createWorkflow(ctx context.Context, workflowFn any) sdkclient.WorkflowRun { workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), TaskQueue: s.TaskQueue(), @@ -60,7 +60,7 @@ func (s *AdminBatchRefreshWorkflowTasksTestSuite) TestStartAdminBatchOperation_R ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - s.Worker().RegisterWorkflow(s.simpleWorkflow) + s.SdkWorker().RegisterWorkflow(s.simpleWorkflow) // Create two workflows workflowRun1 := s.createWorkflow(ctx, s.simpleWorkflow) @@ -95,7 +95,7 @@ func (s *AdminBatchRefreshWorkflowTasksTestSuite) TestStartAdminBatchOperation_R ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - s.Worker().RegisterWorkflow(s.simpleWorkflow) + s.SdkWorker().RegisterWorkflow(s.simpleWorkflow) // Create workflows workflowRun1 := s.createWorkflow(ctx, s.simpleWorkflow) @@ -212,7 +212,7 @@ func (s *AdminBatchRefreshWorkflowTasksTestSuite) TestStartAdminBatchOperation_0 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - s.Worker().RegisterWorkflow(s.simpleWorkflow) + s.SdkWorker().RegisterWorkflow(s.simpleWorkflow) s.OverrideDynamicConfig(dynamicconfig.FrontendMaxConcurrentBatchOperationPerNamespace, 1) s.OverrideDynamicConfig(dynamicconfig.FrontendMaxConcurrentAdminBatchOperationPerNamespace, 1) diff --git a/tests/admin_test.go b/tests/admin_test.go index 7980625ec6d..1f026494a54 100644 --- a/tests/admin_test.go +++ b/tests/admin_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" sdkclient "go.temporal.io/sdk/client" "go.temporal.io/sdk/workflow" @@ -15,43 +14,37 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/primitives/timestamp" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/testing/testvars" "go.temporal.io/server/tests/testcore" ) type AdminTestSuite struct { - testcore.FunctionalTestBase - testContext context.Context + parallelsuite.Suite[*AdminTestSuite] } -func TestAdminTestSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(AdminTestSuite)) +func TestAdminRebuildMutableState_ChasmDisabled(t *testing.T) { + parallelsuite.Run(t, &AdminTestSuite{}, false) } -func (s *AdminTestSuite) SetupSuite() { - // Call parent setup to initialize the test cluster - s.FunctionalTestBase.SetupSuite() - s.testContext = context.Background() +func TestAdminRebuildMutableState_ChasmEnabled(t *testing.T) { + parallelsuite.Run(t, &AdminTestSuite{}, true) } -func (s *AdminTestSuite) TestAdminRebuildMutableState_ChasmDisabled() { - rebuildMutableStateWorkflowHelper(s, false) -} - -func (s *AdminTestSuite) TestAdminRebuildMutableState_ChasmEnabled() { - cleanup := s.OverrideDynamicConfig(dynamicconfig.EnableChasm, true) - defer cleanup() +func (s *AdminTestSuite) TestAdminRebuildMutableState(testWithChasm bool) { + var opts []testcore.TestOption + if testWithChasm { + opts = append(opts, testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true)) + } + env := testcore.NewEnv(s.T(), opts...) - configValues := s.GetTestCluster().Host().DcClient().GetValue(dynamicconfig.EnableChasm.Key()) - s.NotEmpty(configValues, "EnableChasm config should be set") - configValue, _ := configValues[0].Value.(bool) - s.True(configValue, "EnableChasm config should be true") - rebuildMutableStateWorkflowHelper(s, true) -} + if testWithChasm { + configValues := env.GetTestCluster().Host().DcClient().GetValue(dynamicconfig.EnableChasm.Key()) + s.NotEmpty(configValues, "EnableChasm config should be set") + configValue, _ := configValues[0].Value.(bool) + s.True(configValue, "EnableChasm config should be true") + } -// common test helper -func rebuildMutableStateWorkflowHelper(s *AdminTestSuite, testWithChasm bool) { tv := testvars.New(s.T()) workflowFn := func(ctx workflow.Context) error { var randomUUID string @@ -65,18 +58,18 @@ func rebuildMutableStateWorkflowHelper(s *AdminTestSuite, testWithChasm bool) { return nil } - s.Worker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterWorkflow(workflowFn) workflowID := tv.Any().String() workflowOptions := sdkclient.StartWorkflowOptions{ ID: workflowID, - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), WorkflowRunTimeout: 20 * time.Second, } - ctx, cancel := context.WithTimeout(s.testContext, 30*time.Second) + ctx, cancel := context.WithTimeout(env.Context(), 30*time.Second) defer cancel() - workflowRun, err := s.SdkClient().ExecuteWorkflow(s.testContext, workflowOptions, workflowFn) + workflowRun, err := env.SdkClient().ExecuteWorkflow(env.Context(), workflowOptions, workflowFn) s.NoError(err) runID := workflowRun.GetRunID() @@ -92,8 +85,8 @@ func rebuildMutableStateWorkflowHelper(s *AdminTestSuite, testWithChasm bool) { var response1 *adminservice.DescribeMutableStateResponse for { - response1, err = s.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{ - Namespace: s.Namespace().String(), + response1, err = env.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowID, RunId: runID, @@ -112,8 +105,8 @@ func rebuildMutableStateWorkflowHelper(s *AdminTestSuite, testWithChasm bool) { time.Sleep(20 * time.Millisecond) //nolint:forbidigo } - _, err = s.AdminClient().RebuildMutableState(ctx, &adminservice.RebuildMutableStateRequest{ - Namespace: s.Namespace().String(), + _, err = env.AdminClient().RebuildMutableState(ctx, &adminservice.RebuildMutableStateRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowID, RunId: runID, @@ -121,8 +114,8 @@ func rebuildMutableStateWorkflowHelper(s *AdminTestSuite, testWithChasm bool) { }) s.NoError(err) - response2, err := s.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{ - Namespace: s.Namespace().String(), + response2, err := env.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: workflowID, RunId: runID, diff --git a/tests/advanced_visibility_test.go b/tests/advanced_visibility_test.go index 48c752c6d5d..bc0f83b21fa 100644 --- a/tests/advanced_visibility_test.go +++ b/tests/advanced_visibility_test.go @@ -136,7 +136,7 @@ func (s *AdvancedVisibilitySuite) TestListOpenWorkflow() { startFilter := &filterpb.StartTimeFilter{} startFilter.EarliestTime = timestamppb.New(startTime) var openExecution *workflowpb.WorkflowExecutionInfo - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { startFilter.LatestTime = timestamppb.New(time.Now().UTC()) resp, err := s.FrontendClient().ListOpenWorkflowExecutions(testcore.NewContext(), &workflowservice.ListOpenWorkflowExecutionsRequest{ Namespace: s.Namespace().String(), @@ -157,13 +157,13 @@ func (s *AdvancedVisibilitySuite) TestListOpenWorkflow() { s.NotNil(openExecution) s.Equal(we.GetRunId(), openExecution.GetExecution().GetRunId()) - s.Equal(1, len(openExecution.GetSearchAttributes().GetIndexedFields())) + s.Len(openExecution.GetSearchAttributes().GetIndexedFields(), 1) attrPayloadFromResponse, attrExist := openExecution.GetSearchAttributes().GetIndexedFields()[testSearchAttributeKey] s.True(attrExist) s.Equal(attrPayload.GetData(), attrPayloadFromResponse.GetData()) attrType, typeSet := attrPayloadFromResponse.GetMetadata()[sadefs.MetadataType] s.True(typeSet) - s.True(len(attrType) > 0) + s.NotEmpty(attrType) } func (s *AdvancedVisibilitySuite) TestListWorkflow() { @@ -298,14 +298,14 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_SearchAttribute() { descResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), descRequest) s.NoError(err) // Add one for BuildIds={unversioned} - s.Equal(len(searchAttributes.GetIndexedFields())+1, len(descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields())) + s.Len(descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields(), len(searchAttributes.GetIndexedFields())+1) for attrName, expectedPayload := range searchAttributes.GetIndexedFields() { respAttr, ok := descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields()[attrName] s.True(ok) s.Equal(expectedPayload.GetData(), respAttr.GetData()) attrType, typeSet := respAttr.GetMetadata()[sadefs.MetadataType] s.True(typeSet) - s.True(len(attrType) > 0) + s.NotEmpty(attrType) } } @@ -375,7 +375,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrQuery() { PageSize: testcore.DefaultPageSize, Query: query1, } - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -386,7 +386,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrQuery() { } s.NotNil(openExecution) s.Equal(we1.GetRunId(), openExecution.GetExecution().GetRunId()) - s.True(!openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) + s.False(openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) searchValBytes := openExecution.SearchAttributes.GetIndexedFields()[key] var searchVal int _ = payload.Decode(searchValBytes, &searchVal) @@ -396,7 +396,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrQuery() { query2 := fmt.Sprintf(`CustomIntField = %d or CustomIntField = %d`, 1, 2) listRequest.Query = query2 var openExecutions []*workflowpb.WorkflowExecutionInfo - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 2 { @@ -405,7 +405,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrQuery() { } time.Sleep(waitTimeInMs * time.Millisecond) //nolint:forbidigo } - s.Equal(2, len(openExecutions)) + s.Len(openExecutions, 2) e1 := openExecutions[0] e2 := openExecutions[1] if e1.GetExecution().GetRunId() != we1.GetRunId() { @@ -421,7 +421,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrQuery() { // query for open query3 := fmt.Sprintf(`(CustomIntField = %d or CustomIntField = %d) and ExecutionStatus = 'Running'`, 2, 3) listRequest.Query = query3 - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 2 { @@ -430,7 +430,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrQuery() { } time.Sleep(waitTimeInMs * time.Millisecond) //nolint:forbidigo } - s.Equal(2, len(openExecutions)) + s.Len(openExecutions, 2) e1 = openExecutions[0] e2 = openExecutions[1] s.Equal(we3.GetRunId(), e1.GetExecution().GetRunId()) @@ -476,7 +476,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_KeywordQuery() { ) s.NotNil(openExecution) s.Equal(we1.GetRunId(), openExecution.GetExecution().GetRunId()) - s.True(!openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) + s.False(openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) saPayload := openExecution.SearchAttributes.GetIndexedFields()["CustomKeywordField"] var saValue string err = payload.Decode(saPayload, &saValue) @@ -491,7 +491,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_KeywordQuery() { } resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) - s.Len(resp.GetExecutions(), 0) + s.Empty(resp.GetExecutions()) // Inordered match on Keyword (not supported) listRequest = &workflowservice.ListWorkflowExecutionsRequest{ @@ -501,7 +501,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_KeywordQuery() { } resp, err = s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) - s.Len(resp.GetExecutions(), 0) + s.Empty(resp.GetExecutions()) // Prefix search listRequest = &workflowservice.ListWorkflowExecutionsRequest{ @@ -526,7 +526,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_KeywordQuery() { } resp, err = s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) - s.Len(resp.GetExecutions(), 0) + s.Empty(resp.GetExecutions()) } func (s *AdvancedVisibilitySuite) TestListWorkflow_StringQuery() { @@ -553,7 +553,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_StringQuery() { PageSize: testcore.DefaultPageSize, Query: `CustomTextField = "nothing else matters"`, } - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -564,7 +564,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_StringQuery() { } s.NotNil(openExecution) s.Equal(we1.GetRunId(), openExecution.GetExecution().GetRunId()) - s.True(!openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) + s.False(openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) saPayload := openExecution.SearchAttributes.GetIndexedFields()["CustomTextField"] var saValue string err = payload.Decode(saPayload, &saValue) @@ -599,7 +599,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_MaxWindowSize() { tl := "es-functional-list-workflow-max-window-size-test-taskqueue" startRequest := s.createStartWorkflowExecutionRequest(id, wt, tl) - for i := 0; i < testcore.DefaultPageSize; i++ { + for i := range testcore.DefaultPageSize { startRequest.RequestId = uuid.NewString() startRequest.WorkflowId = id + strconv.Itoa(i) _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startRequest) @@ -618,7 +618,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_MaxWindowSize() { Query: fmt.Sprintf(`WorkflowType = '%s' and ExecutionStatus = "Running"`, wt), } // get first page - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == testcore.DefaultPageSize { @@ -628,13 +628,13 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_MaxWindowSize() { time.Sleep(waitTimeInMs * time.Millisecond) //nolint:forbidigo } s.NotNil(listResp) - s.True(len(listResp.GetNextPageToken()) != 0) + s.NotEmpty(listResp.GetNextPageToken()) // the last request listRequest.NextPageToken = listResp.GetNextPageToken() resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) - s.True(len(resp.GetExecutions()) == 0) + s.Empty(resp.GetExecutions()) s.Nil(resp.GetNextPageToken()) } @@ -649,7 +649,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrderBy() { tl := "es-functional-list-workflow-order-by-test-taskqueue" initialTime := time.Now().UTC() - for i := 0; i < testcore.DefaultPageSize+1; i++ { // start 6 + for i := range testcore.DefaultPageSize + 1 { // start 6 startRequest := s.createStartWorkflowExecutionRequest(id, wt, tl) startRequest.RequestId = uuid.NewString() startRequest.WorkflowId = id + strconv.Itoa(i) @@ -725,7 +725,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrderBy() { } // greatest effort to reduce duplicate code - testHelper := func(query, searchAttrKey string, prevVal, currVal interface{}) { + testHelper := func(query, searchAttrKey string, prevVal, currVal any) { listRequest.Query = query listRequest.NextPageToken = []byte{} resp, err := s.FrontendClient().ListWorkflowExecutions(ctx, listRequest) @@ -767,7 +767,7 @@ func (s *AdvancedVisibilitySuite) TestListWorkflow_OrderBy() { listRequest.NextPageToken = resp.GetNextPageToken() resp, err = s.FrontendClient().ListWorkflowExecutions(ctx, listRequest) // last page s.NoError(err) - s.Equal(1, len(resp.GetExecutions())) + s.Len(resp.GetExecutions(), 1) } // order by CustomIntField desc @@ -801,7 +801,7 @@ func (s *AdvancedVisibilitySuite) testListWorkflowHelper( wid, wType string, ) { // start enough number of workflows - for i := 0; i < numOfWorkflows; i++ { + for i := range numOfWorkflows { startRequest.RequestId = uuid.NewString() startRequest.WorkflowId = wid + strconv.Itoa(i) _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startRequest) @@ -821,7 +821,7 @@ func (s *AdvancedVisibilitySuite) testListWorkflowHelper( } // test first page - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { listResponse, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(listResponse.GetExecutions()) == pageSize { @@ -833,12 +833,12 @@ func (s *AdvancedVisibilitySuite) testListWorkflowHelper( } s.NotNil(openExecutions) s.NotNil(nextPageToken) - s.True(len(nextPageToken) > 0) + s.NotEmpty(nextPageToken) // test last page listRequest.NextPageToken = nextPageToken inIf := false - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { listResponse, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(listResponse.GetExecutions()) == numOfWorkflows-pageSize { @@ -862,7 +862,7 @@ func (s *AdvancedVisibilitySuite) testHelperForReadOnce(expectedRunID string, qu Query: query, } - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { listResponse, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(listResponse.GetExecutions()) == 1 { @@ -874,7 +874,7 @@ func (s *AdvancedVisibilitySuite) testHelperForReadOnce(expectedRunID string, qu } s.NotNil(openExecution) s.Equal(expectedRunID, openExecution.GetExecution().GetRunId()) - s.True(!openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) + s.False(openExecution.GetExecutionTime().AsTime().Before(openExecution.GetStartTime().AsTime())) return openExecution } @@ -901,7 +901,7 @@ func (s *AdvancedVisibilitySuite) TestCountWorkflow() { Query: query, } var resp *workflowservice.CountWorkflowExecutionsResponse - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err = s.FrontendClient().CountWorkflowExecutions(testcore.NewContext(), countRequest) s.NoError(err) if resp.GetCount() == int64(1) { @@ -925,7 +925,7 @@ func (s *AdvancedVisibilitySuite) TestCountGroupByWorkflow() { numWorkflows := 10 numClosedWorkflows := 4 - for i := 0; i < numWorkflows; i++ { + for i := range numWorkflows { wfid := id + strconv.Itoa(i) request := s.createStartWorkflowExecutionRequest(wfid, wt, tl) we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) @@ -952,7 +952,7 @@ func (s *AdvancedVisibilitySuite) TestCountGroupByWorkflow() { } var resp *workflowservice.CountWorkflowExecutionsResponse var err error - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err = s.FrontendClient().CountWorkflowExecutions(testcore.NewContext(), countRequest) s.NoError(err) if resp.GetCount() == int64(numWorkflows) { @@ -961,13 +961,13 @@ func (s *AdvancedVisibilitySuite) TestCountGroupByWorkflow() { time.Sleep(waitTimeInMs * time.Millisecond) //nolint:forbidigo } s.Equal(int64(numWorkflows), resp.GetCount()) - s.Equal(2, len(resp.Groups)) + s.Len(resp.Groups, 2) - runningStatusPayload, _ := searchattribute.EncodeValue( + runningStatusPayload, _ := sadefs.EncodeValue( enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING.String(), enumspb.INDEXED_VALUE_TYPE_KEYWORD, ) - terminatedStatusPayload, _ := searchattribute.EncodeValue( + terminatedStatusPayload, _ := sadefs.EncodeValue( enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED.String(), enumspb.INDEXED_VALUE_TYPE_KEYWORD, ) @@ -1122,7 +1122,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() s.NotNil(newTask.WorkflowTask) s.Equal(int64(3), newTask.WorkflowTask.GetPreviousStartedEventId()) s.Equal(int64(7), newTask.WorkflowTask.GetStartedEventId()) - s.Equal(4, len(newTask.WorkflowTask.History.Events)) + s.Len(newTask.WorkflowTask.History.Events, 4) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, newTask.WorkflowTask.History.Events[0].GetEventType()) s.Equal(enumspb.EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, newTask.WorkflowTask.History.Events[1].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, newTask.WorkflowTask.History.Events[2].GetEventType()) @@ -1137,7 +1137,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() Query: fmt.Sprintf(`WorkflowType = '%s' and ExecutionStatus = 'Running'`, wt), } verified := false - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -1168,7 +1168,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() newTask = res.NewTask s.NotNil(newTask) s.NotNil(newTask.WorkflowTask) - s.Equal(4, len(newTask.WorkflowTask.History.Events)) + s.Len(newTask.WorkflowTask.History.Events, 4) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, newTask.WorkflowTask.History.Events[0].GetEventType()) s.Equal(enumspb.EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, newTask.WorkflowTask.History.Events[1].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, newTask.WorkflowTask.History.Events[2].GetEventType()) @@ -1190,7 +1190,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() newTask = res.NewTask s.NotNil(newTask) s.NotNil(newTask.WorkflowTask) - s.Equal(4, len(newTask.WorkflowTask.History.Events)) + s.Len(newTask.WorkflowTask.History.Events, 4) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, newTask.WorkflowTask.History.Events[0].GetEventType()) s.Equal(enumspb.EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, newTask.WorkflowTask.History.Events[1].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, newTask.WorkflowTask.History.Events[2].GetEventType()) @@ -1205,7 +1205,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() Query: fmt.Sprintf(`WorkflowType = '%s' and ExecutionStatus = 'Running'`, wt), } verified = false - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -1229,7 +1229,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() Query: fmt.Sprintf(`WorkflowType = '%s' and ExecutionStatus = 'Running' and CustomTextField is null and CustomIntField is null`, wt), } verified = false - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -1250,16 +1250,15 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() descResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), descRequest) s.NoError(err) expectedSearchAttributes, _ := searchattribute.Encode( - map[string]interface{}{ + map[string]any{ "CustomDoubleField": 22.0878, sadefs.BinaryChecksums: []string{"binary-v1", "binary-v2"}, sadefs.BuildIds: []string{worker_versioning.UnversionedSearchAttribute}, }, nil, ) - s.Equal( - len(expectedSearchAttributes.GetIndexedFields()), - len(descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields()), + s.Len( + descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields(), len(expectedSearchAttributes.GetIndexedFields()), ) for attrName, expectedPayload := range expectedSearchAttributes.GetIndexedFields() { respAttr, ok := descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields()[attrName] @@ -1267,7 +1266,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() s.Equal(expectedPayload.GetData(), respAttr.GetData()) attrType, typeSet := respAttr.GetMetadata()[searchattribute.MetadataType] s.True(typeSet) - s.True(len(attrType) > 0) + s.NotEmpty(attrType) } // process close workflow task and assert search attributes is correct after workflow is closed @@ -1294,9 +1293,8 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() descResp, err = s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), descRequest) s.NoError(err) s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, descResp.WorkflowExecutionInfo.Status) - s.Equal( - len(expectedSearchAttributes.GetIndexedFields()), - len(descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields()), + s.Len( + descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields(), len(expectedSearchAttributes.GetIndexedFields()), ) for attrName, expectedPayload := range expectedSearchAttributes.GetIndexedFields() { respAttr, ok := descResp.WorkflowExecutionInfo.GetSearchAttributes().GetIndexedFields()[attrName] @@ -1304,7 +1302,7 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecutionSearchAttributes() s.Equal(expectedPayload.GetData(), respAttr.GetData()) attrType, typeSet := respAttr.GetMetadata()[searchattribute.MetadataType] s.True(typeSet) - s.True(len(attrType) > 0) + s.NotEmpty(attrType) } } @@ -1410,7 +1408,7 @@ func (s *AdvancedVisibilitySuite) TestModifyWorkflowExecutionProperties() { s.NotNil(newTask.WorkflowTask) s.Equal(int64(3), newTask.WorkflowTask.GetPreviousStartedEventId()) s.Equal(int64(7), newTask.WorkflowTask.GetStartedEventId()) - s.Equal(4, len(newTask.WorkflowTask.History.Events)) + s.Len(newTask.WorkflowTask.History.Events, 4) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, newTask.WorkflowTask.History.Events[0].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED, newTask.WorkflowTask.History.Events[1].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, newTask.WorkflowTask.History.Events[2].GetEventType()) @@ -1434,7 +1432,7 @@ func (s *AdvancedVisibilitySuite) TestModifyWorkflowExecutionProperties() { Query: fmt.Sprintf(`WorkflowType = '%s' and ExecutionStatus = 'Running'`, wt), } verified := false - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -1456,7 +1454,7 @@ func (s *AdvancedVisibilitySuite) TestModifyWorkflowExecutionProperties() { newTask = res.NewTask s.NotNil(newTask) s.NotNil(newTask.WorkflowTask) - s.Equal(4, len(newTask.WorkflowTask.History.Events)) + s.Len(newTask.WorkflowTask.History.Events, 4) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, newTask.WorkflowTask.History.Events[0].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED, newTask.WorkflowTask.History.Events[1].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, newTask.WorkflowTask.History.Events[2].GetEventType()) @@ -1480,7 +1478,7 @@ func (s *AdvancedVisibilitySuite) TestModifyWorkflowExecutionProperties() { Query: fmt.Sprintf(`WorkflowType = '%s' and ExecutionStatus = 'Running'`, wt), } verified = false - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -1519,7 +1517,7 @@ func (s *AdvancedVisibilitySuite) TestModifyWorkflowExecutionProperties() { func (s *AdvancedVisibilitySuite) testListResultForUpsertSearchAttributes(listRequest *workflowservice.ListWorkflowExecutionsRequest) { verified := false - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { resp, err := s.FrontendClient().ListWorkflowExecutions(testcore.NewContext(), listRequest) s.NoError(err) if len(resp.GetExecutions()) == 1 { @@ -1568,7 +1566,7 @@ func (s *AdvancedVisibilitySuite) testListResultForUpsertSearchAttributes(listRe } func (s *AdvancedVisibilitySuite) createSearchAttributes() *commonpb.SearchAttributes { - searchAttributes, err := searchattribute.Encode(map[string]interface{}{ + searchAttributes, err := searchattribute.Encode(map[string]any{ "CustomTextField": "another string", "CustomIntField": 123, "CustomDoubleField": 22.0878, @@ -1637,21 +1635,13 @@ func (s *AdvancedVisibilitySuite) TestUpsertWorkflowExecution_InvalidKey() { WorkflowId: id, RunId: we.RunId, }) - if !testcore.UseSQLVisibility() { - s.ErrorContains(err, "BadSearchAttributes: search attribute INVALIDKEY is not defined") - s.EqualHistoryEvents(` + s.ErrorContains(err, fmt.Sprintf("BadSearchAttributes: Namespace %s has no mapping defined for search attribute INVALIDKEY", s.Namespace().String())) + s.EqualHistoryEvents(fmt.Sprintf(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed {"Cause":23,"Failure":{"Message":"BadSearchAttributes: search attribute INVALIDKEY is not defined"}}`, historyEvents) - } else { - s.ErrorContains(err, fmt.Sprintf("BadSearchAttributes: Namespace %s has no mapping defined for search attribute INVALIDKEY", s.Namespace().String())) - s.EqualHistoryEvents(fmt.Sprintf(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskFailed {"Cause":23,"Failure":{"Message":"BadSearchAttributes: Namespace %s has no mapping defined for search attribute INVALIDKEY"}}`, s.Namespace().String()), historyEvents) - } + 4 WorkflowTaskFailed {"Cause":23,"Failure":{"Message":"BadSearchAttributes: Namespace %s has no mapping defined for search attribute INVALIDKEY"}} + 5 WorkflowTaskScheduled`, s.Namespace().String()), historyEvents) } func (s *AdvancedVisibilitySuite) TestChildWorkflow_ParentWorkflow() { @@ -1675,8 +1665,8 @@ func (s *AdvancedVisibilitySuite) TestChildWorkflow_ParentWorkflow() { Get(ctx, nil) } - s.Worker().RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: wfType}) - s.Worker().RegisterWorkflowWithOptions(childWf, workflow.RegisterOptions{Name: childWfType}) + s.SdkWorker().RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: wfType}) + s.SdkWorker().RegisterWorkflowWithOptions(childWf, workflow.RegisterOptions{Name: childWfType}) startOptions := sdkclient.StartWorkflowOptions{ ID: wfID, @@ -1767,7 +1757,7 @@ func (s *AdvancedVisibilitySuite) Test_BuildIdIndexedOnCompletion_UnversionedWor pollRequest := &workflowservice.PollWorkflowTaskQueueRequest{Namespace: s.Namespace().String(), TaskQueue: request.TaskQueue, Identity: id} task, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, pollRequest) s.NoError(err) - s.Greater(len(task.TaskToken), 0) + s.NotEmpty(task.TaskToken) _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Namespace: s.Namespace().String(), Identity: id, @@ -1787,7 +1777,7 @@ func (s *AdvancedVisibilitySuite) Test_BuildIdIndexedOnCompletion_UnversionedWor task, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, pollRequest) s.NoError(err) - s.Greater(len(task.TaskToken), 0) + s.NotEmpty(task.TaskToken) _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Namespace: s.Namespace().String(), Identity: id, @@ -1814,7 +1804,7 @@ func (s *AdvancedVisibilitySuite) Test_BuildIdIndexedOnCompletion_UnversionedWor task, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, pollRequest) s.NoError(err) - s.Greater(len(task.TaskToken), 0) + s.NotEmpty(task.TaskToken) buildIDs = s.getBuildIds(ctx, task.WorkflowExecution) s.Equal([]string{}, buildIDs) @@ -2469,7 +2459,7 @@ func (s *AdvancedVisibilitySuite) TestBuildIdScavenger_DeletesUnusedBuildId() { TaskQueue: tq, }) s.Require().NoError(err) - s.Require().Equal(1, len(compatibility.Sets)) + s.Require().Len(compatibility.Sets, 1) s.Require().Equal([]string{buildIdv1}, compatibility.Sets[0].BuildIDs) // Make sure the build ID was removed from the build ID->task queue mapping res, err := s.SdkClient().WorkflowService().GetWorkerTaskReachability(ctx, &workflowservice.GetWorkerTaskReachabilityRequest{ @@ -2477,7 +2467,7 @@ func (s *AdvancedVisibilitySuite) TestBuildIdScavenger_DeletesUnusedBuildId() { BuildIds: []string{buildIdv0}, }) s.Require().NoError(err) - s.Require().Equal(0, len(res.BuildIdReachability[0].TaskQueueReachability)) + s.Require().Empty(res.BuildIdReachability[0].TaskQueueReachability) } func (s *AdvancedVisibilitySuite) TestListWorkflow_ExternalPayloadSearchAttributes() { @@ -2685,10 +2675,10 @@ func (s *AdvancedVisibilitySuite) updateMaxResultWindow() { s.Require().NoError(err) s.Require().True(acknowledged) - for i := 0; i < numOfRetry; i++ { + for range numOfRetry { settings, err := esClient.IndexGetSettings(context.Background(), esConfig.GetVisibilityIndex()) s.Require().NoError(err) - if settings[esConfig.GetVisibilityIndex()].Settings["index"].(map[string]interface{})["max_result_window"].(string) == strconv.Itoa(testcore.DefaultPageSize) { //nolint:revive // unchecked-type-assertion + if settings[esConfig.GetVisibilityIndex()].Settings["index"].(map[string]any)["max_result_window"].(string) == strconv.Itoa(testcore.DefaultPageSize) { //nolint:revive // unchecked-type-assertion return } time.Sleep(waitTimeInMs * time.Millisecond) //nolint:forbidigo diff --git a/tests/archival_test.go b/tests/archival_test.go index 4125a729540..24a9e5e40dd 100644 --- a/tests/archival_test.go +++ b/tests/archival_test.go @@ -2,9 +2,11 @@ package tests import ( "bytes" + "context" "encoding/binary" "fmt" "strconv" + "sync/atomic" "testing" "time" @@ -17,9 +19,11 @@ import ( workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/api/adminservice/v1" + archiverspb "go.temporal.io/server/api/archiver/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common" "go.temporal.io/server/common/archiver" + "go.temporal.io/server/common/archiver/provider" "go.temporal.io/server/common/convert" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log/tag" @@ -33,20 +37,71 @@ import ( "google.golang.org/protobuf/types/known/durationpb" ) +const ( + // Custom scheme for testing custom archiver implementation + customArchiverScheme = "customtest" +) + type ( ArchivalSuite struct { testcore.FunctionalTestBase archivalNamespace namespace.Name archivalNamespaceID namespace.ID + + // Namespace for testing custom archiver + customArchiverNamespace namespace.Name + customArchiverNamespaceID namespace.ID + + // Counters to verify custom archivers are being called + customHistoryArchiveCalled atomic.Int32 + customVisibilityArchiveCalled atomic.Int32 } archivalWorkflowInfo struct { execution *commonpb.WorkflowExecution branchToken []byte } + + // customHistoryArchiver wraps a built-in history archiver and tracks Archive calls + customHistoryArchiver struct { + counter *atomic.Int32 + } + + // customVisibilityArchiver wraps a built-in visibility archiver and tracks Archive calls + customVisibilityArchiver struct { + counter *atomic.Int32 + } ) +// customHistoryArchiver method implementations +func (c *customHistoryArchiver) Archive(ctx context.Context, uri archiver.URI, request *archiver.ArchiveHistoryRequest, opts ...archiver.ArchiveOption) error { + c.counter.Add(1) + return nil +} + +func (c *customHistoryArchiver) Get(ctx context.Context, uri archiver.URI, request *archiver.GetHistoryRequest) (*archiver.GetHistoryResponse, error) { + return nil, nil +} + +func (c *customHistoryArchiver) ValidateURI(uri archiver.URI) error { + return nil +} + +// customVisibilityArchiver method implementations +func (c *customVisibilityArchiver) Archive(ctx context.Context, uri archiver.URI, request *archiverspb.VisibilityRecord, opts ...archiver.ArchiveOption) error { + c.counter.Add(1) + return nil +} + +func (c *customVisibilityArchiver) Query(ctx context.Context, uri archiver.URI, request *archiver.QueryVisibilityRequest, saTypeMap searchattribute.NameTypeMap) (*archiver.QueryVisibilityResponse, error) { + return nil, nil +} + +func (c *customVisibilityArchiver) ValidateURI(uri archiver.URI) error { + return nil +} + func TestArchivalSuite(t *testing.T) { t.Parallel() // This suite can work in parallel as long as it is the only one that use testcore.WithArchivalEnabled() option. suite.Run(t, new(ArchivalSuite)) @@ -57,12 +112,44 @@ func (s *ArchivalSuite) SetupSuite() { dynamicconfig.ArchivalProcessorArchiveDelay.Key(): time.Duration(0), } + // Create custom history archiver factory for custom scheme + customHistoryArchiverFactory := provider.CustomHistoryArchiverFactoryFunc( + func(params provider.NewCustomHistoryArchiverParams) (archiver.HistoryArchiver, error) { + // Only handle custom scheme, return ErrUnknownScheme for others (including filestore) + if params.Scheme != customArchiverScheme { + return nil, provider.ErrUnknownScheme + } + // Return a wrapper that delegates to filestore but tracks Archive calls + return &customHistoryArchiver{ + counter: &s.customHistoryArchiveCalled, + }, nil + }, + ) + + // Create custom visibility archiver factory for custom scheme + customVisibilityArchiverFactory := provider.CustomVisibilityArchiverFactoryFunc( + func(params provider.NewCustomVisibilityArchiverParams) (archiver.VisibilityArchiver, error) { + // Only handle custom scheme, return ErrUnknownScheme for others (including filestore) + if params.Scheme != customArchiverScheme { + return nil, provider.ErrUnknownScheme + } + // Return a wrapper that delegates to filestore but tracks Archive calls + return &customVisibilityArchiver{ + counter: &s.customVisibilityArchiveCalled, + }, nil + }, + ) + s.FunctionalTestBase.SetupSuiteWithCluster( testcore.WithDynamicConfigOverrides(dynamicConfigOverrides), testcore.WithArchivalEnabled(), + testcore.WithCustomHistoryArchiverFactory(customHistoryArchiverFactory), + testcore.WithCustomVisibilityArchiverFactory(customVisibilityArchiverFactory), ) var err error + + // Register namespace using built-in filestore archiver s.archivalNamespace = namespace.Name(testcore.RandomizeStr("archival-enabled-namespace")) s.archivalNamespaceID, err = s.RegisterNamespace( s.archivalNamespace, @@ -72,10 +159,24 @@ func (s *ArchivalSuite) SetupSuite() { s.GetTestCluster().ArchiverBase().VisibilityURI(), ) s.Require().NoError(err) + + // Register namespace using custom archiver with custom scheme + s.customArchiverNamespace = namespace.Name(testcore.RandomizeStr("custom-archiver-namespace")) + customHistoryURI := customArchiverScheme + "://custom-history-archiver" + customVisibilityURI := customArchiverScheme + "://custom-visibility-archiver" + s.customArchiverNamespaceID, err = s.RegisterNamespace( + s.customArchiverNamespace, + 0, // Archive right away. + enumspb.ARCHIVAL_STATE_ENABLED, + customHistoryURI, + customVisibilityURI, + ) + s.Require().NoError(err) } func (s *ArchivalSuite) TearDownSuite() { s.Require().NoError(s.MarkNamespaceAsDeleted(s.archivalNamespace)) + s.Require().NoError(s.MarkNamespaceAsDeleted(s.customArchiverNamespace)) s.FunctionalTestBase.TearDownCluster() } @@ -175,6 +276,33 @@ func (s *ArchivalSuite) TestVisibilityArchival() { } } +func (s *ArchivalSuite) TestCustomArchiver() { + s.True(s.GetTestCluster().ArchiverBase().Metadata().GetHistoryConfig().ClusterConfiguredForArchival()) + + workflowID := "custom-history-archiver-workflow-id" + workflowType := "custom-history-archiver-type" + taskQueue := "custom-history-archiver-task-queue" + numActivities := 1 + numRuns := 1 + + // Reset counter before test + s.customHistoryArchiveCalled.Store(0) + s.customVisibilityArchiveCalled.Store(0) + + // Use custom archiver namespace to trigger custom archiver + s.startAndFinishWorkflow(workflowID, workflowType, taskQueue, s.customArchiverNamespace, numActivities, numRuns) + + // Verify custom archiver's Archive method was called at least once + s.Eventually(func() bool { + called := s.customHistoryArchiveCalled.Load() + return called > 0 + }, 10*time.Second, 500*time.Millisecond, "Custom history archiver Archive method should have been called") + s.Eventually(func() bool { + called := s.customVisibilityArchiveCalled.Load() + return called > 0 + }, 10*time.Second, 500*time.Millisecond, "Custom visibility archiver Archive method should have been called") +} + // workflowIsArchived asserts that both the workflow history and workflow visibility are archived. func (s *ArchivalSuite) workflowIsArchived(namespaceID namespace.ID, execution *commonpb.WorkflowExecution) { historyURI, err := archiver.NewURI(s.GetTestCluster().ArchiverBase().HistoryURI()) @@ -322,7 +450,7 @@ func (s *ArchivalSuite) startAndFinishWorkflow( if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ @@ -383,8 +511,8 @@ func (s *ArchivalSuite) startAndFinishWorkflow( Logger: s.Logger, T: s.T(), } - for run := 0; run < numRuns; run++ { - for i := 0; i < numActivities; i++ { + for range numRuns { + for i := range numActivities { _, err := poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) s.NoError(err) diff --git a/tests/callbacks_migration_test.go b/tests/callbacks_migration_test.go index 97ac7088619..02498b151bb 100644 --- a/tests/callbacks_migration_test.go +++ b/tests/callbacks_migration_test.go @@ -40,8 +40,6 @@ func (s *CallbacksMigrationSuite) SetupTest() { // Start with CHASM disabled by default for migration tests s.OverrideDynamicConfig(dynamicconfig.EnableChasm, false) s.OverrideDynamicConfig(dynamicconfig.EnableCHASMCallbacks, false) - // Enable Nexus for callbacks - s.OverrideDynamicConfig(dynamicconfig.EnableNexus, true) } // TODO (seankane): This test can be removed once CHASM callbacks are the default diff --git a/tests/callbacks_test.go b/tests/callbacks_test.go index 5247903a7fd..d53158e5965 100644 --- a/tests/callbacks_test.go +++ b/tests/callbacks_test.go @@ -86,49 +86,36 @@ func (s *CallbacksSuite) TestWorkflowCallbacks_InvalidArgument() { urls []string header map[string]string message string - allow bool }{ - { - name: "disabled", - urls: []string{"http://some-ignored-address"}, - allow: false, - message: "attaching workflow callbacks is disabled for this namespace", - }, { name: "invalid-scheme", urls: []string{"invalid"}, - allow: true, message: "invalid url: unknown scheme: invalid", }, { name: "url-length-too-long", urls: []string{"http://some-very-very-very-very-very-very-very-long-url"}, - allow: true, message: "invalid url: url length longer than max length allowed of 50", }, { name: "header-size-too-large", urls: []string{"http://some-ignored-address"}, header: map[string]string{"too": "long"}, - allow: true, message: "invalid header: header size longer than max allowed size of 6", }, { name: "too many callbacks", urls: []string{"http://url-1", "http://url-2", "http://url-3"}, - allow: true, message: "cannot attach more than 2 callbacks to a workflow", }, { name: "url not configured", urls: []string{"http://some-unconfigured-address"}, - allow: true, message: "invalid url: url does not match any configured callback address: http://some-unconfigured-address", }, { name: "https required", urls: []string{"http://some-secure-address"}, - allow: true, message: "invalid url: callback address does not allow insecure connections: http://some-secure-address", }, } @@ -143,7 +130,6 @@ func (s *CallbacksSuite) TestWorkflowCallbacks_InvalidArgument() { for _, tc := range cases { s.Run(tc.name, func() { - s.OverrideDynamicConfig(dynamicconfig.EnableNexus, tc.allow) cbs := make([]*commonpb.Callback, 0, len(tc.urls)) for _, url := range tc.urls { cbs = append(cbs, &commonpb.Callback{ @@ -392,7 +378,7 @@ func (s *CallbacksSuite) TestWorkflowNexusCallbacks_CarriedOver() { s.EventuallyWithT(func(col *assert.CollectT) { description, err := sdkClient.DescribeWorkflowExecution(ctx, workflowID, "") require.NoError(col, err) - require.Equal(col, len(cbs), len(description.Callbacks)) + require.Len(col, description.Callbacks, len(cbs)) descCbs := make([]*commonpb.Callback, 0, len(description.Callbacks)) for _, callbackInfo := range description.Callbacks { protorequire.ProtoEqual( @@ -563,7 +549,7 @@ func (s *CallbacksSuite) TestNexusResetWorkflowWithCallback() { s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, description.WorkflowExecutionInfo.Status) // Should not be invoked during a reset - s.Equal(len(cbs), len(description.Callbacks)) + s.Len(description.Callbacks, len(cbs)) descCbs := make([]*commonpb.Callback, 0, len(description.Callbacks)) for _, callbackInfo := range description.Callbacks { s.Equal(enumspb.CALLBACK_STATE_STANDBY, callbackInfo.State) @@ -602,7 +588,7 @@ func (s *CallbacksSuite) TestNexusResetWorkflowWithCallback() { description.WorkflowExecutionInfo.Status, ) - require.Equal(t, len(cbs), len(description.Callbacks)) + require.Len(t, description.Callbacks, len(cbs)) descCbs = make([]*commonpb.Callback, 0, len(description.Callbacks)) for _, callbackInfo := range description.Callbacks { require.Equal(t, enumspb.CALLBACK_STATE_SUCCEEDED, callbackInfo.State) diff --git a/tests/cancel_workflow_test.go b/tests/cancel_workflow_test.go index 1029698a6dd..33ced39da7a 100644 --- a/tests/cancel_workflow_test.go +++ b/tests/cancel_workflow_test.go @@ -616,7 +616,8 @@ func (s *CancelWorkflowSuite) TestImmediateChildCancellation_WorkflowTaskFailed( 2 WorkflowTaskScheduled 3 WorkflowExecutionCancelRequested 4 WorkflowTaskStarted - 5 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ + 5 WorkflowTaskFailed + 6 WorkflowTaskScheduled`, s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: id, })) diff --git a/tests/chasm_test.go b/tests/chasm_test.go index d4c767c0207..7d68ab6bf72 100644 --- a/tests/chasm_test.go +++ b/tests/chasm_test.go @@ -2,11 +2,14 @@ package tests import ( "context" + "crypto/rand" "fmt" "strconv" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" @@ -225,16 +228,12 @@ func (s *ChasmTestSuite) TestListExecutions() { ) s.NoError(err) - archetypeID, ok := s.FunctionalTestBase.GetTestCluster().Host().GetCHASMRegistry().ComponentIDFor(&tests.PayloadStore{}) - s.True(ok) - - visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND PayloadStoreId = '%s'", archetypeID, storeID) + visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND PayloadStoreId = '%s'", tests.ArchetypeID, storeID) - var visRecord *chasm.ExecutionInfo[*testspb.TestPayloadStore] + var visRecord *chasm.VisibilityExecutionInfo[*testspb.TestPayloadStore] s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: string(s.Namespace()), PageSize: 10, Query: visQuery, @@ -272,7 +271,7 @@ func (s *ChasmTestSuite) TestListExecutions() { s.NoError(payload.Decode(visRecord.CustomSearchAttributes[sadefs.TemporalNamespaceDivision], &archetypeIDStr)) parsedArchetypeID, err := strconv.ParseUint(archetypeIDStr, 10, 32) s.NoError(err) - s.Equal(archetypeID, chasm.ArchetypeID(parsedArchetypeID)) + s.Equal(tests.ArchetypeID, chasm.ArchetypeID(parsedArchetypeID)) addPayloadResp, err := tests.AddPayloadHandler( ctx, @@ -288,7 +287,6 @@ func (s *ChasmTestSuite) TestListExecutions() { s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: string(s.Namespace()), PageSize: 10, Query: visQuery + " AND PayloadTotalCount > 0", @@ -319,7 +317,6 @@ func (s *ChasmTestSuite) TestListExecutions() { s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: s.NamespaceID().String(), NamespaceName: s.Namespace().String(), PageSize: 10, Query: visQuery + " AND ExecutionStatus = 'Completed' AND PayloadTotalCount > 0", @@ -346,7 +343,7 @@ func (s *ChasmTestSuite) TestCountExecutions_GroupBy() { ctx, cancel := context.WithTimeout(s.chasmContext, chasmTestTimeout) defer cancel() - for i := 0; i < 3; i++ { + for range 3 { storeID := tv.Any().String() _, err := tests.NewPayloadStoreHandler( @@ -359,7 +356,7 @@ func (s *ChasmTestSuite) TestCountExecutions_GroupBy() { s.NoError(err) } - for i := 0; i < 2; i++ { + for range 2 { storeID := tv.Any().String() resp, err := tests.NewPayloadStoreHandler( @@ -382,9 +379,9 @@ func (s *ChasmTestSuite) TestCountExecutions_GroupBy() { ) s.NoError(err) - _, err = tests.ClosePayloadStoreHandler( + _, err = tests.CancelPayloadStoreHandler( s.chasmContext, - tests.ClosePayloadStoreRequest{ + tests.CancelPayloadStoreRequest{ NamespaceID: s.NamespaceID(), StoreID: storeID, }, @@ -400,7 +397,6 @@ func (s *ChasmTestSuite) TestCountExecutions_GroupBy() { countResp, err = chasm.CountExecutions[*tests.PayloadStore]( ctx, &chasm.CountExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: s.Namespace().String(), Query: "GROUP BY `ExecutionStatus`", }, @@ -422,7 +418,7 @@ func (s *ChasmTestSuite) TestCountExecutions_GroupBy() { totalCount += group.Count var groupValue string s.NoError(payload.Decode(group.Values[0], &groupValue)) - s.Contains([]string{"Running", "Completed"}, groupValue) + s.Contains([]string{"Running", "Canceled"}, groupValue) } s.Equal(int64(5), totalCount) @@ -430,7 +426,6 @@ func (s *ChasmTestSuite) TestCountExecutions_GroupBy() { _, err = chasm.CountExecutions[*tests.PayloadStore]( ctx, &chasm.CountExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: s.Namespace().String(), Query: "GROUP BY `PayloadTotalCount`", }, @@ -520,9 +515,7 @@ func (s *ChasmTestSuite) TestPayloadStoreForceDelete() { s.NoError(err) // Make sure visibility record is created, so that we can test its deletion later. - archetypeID, ok := s.FunctionalTestBase.GetTestCluster().Host().GetCHASMRegistry().ComponentIDFor(&tests.PayloadStore{}) - s.True(ok) - visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND WorkflowId = '%s'", archetypeID, storeID) + visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND WorkflowId = '%s'", tests.ArchetypeID, storeID) var executionInfo *workflowpb.WorkflowExecutionInfo s.Eventually( func() bool { @@ -546,17 +539,15 @@ func (s *ChasmTestSuite) TestPayloadStoreForceDelete() { s.NoError(payload.Decode(archetypePayload, &archetypeIDStr)) parsedArchetypeID, err := strconv.ParseUint(archetypeIDStr, 10, 32) s.NoError(err) - s.Equal(archetypeID, chasm.ArchetypeID(parsedArchetypeID)) + s.Equal(tests.ArchetypeID, chasm.ArchetypeID(parsedArchetypeID)) - archetype, ok := s.FunctionalTestBase.GetTestCluster().Host().GetCHASMRegistry().ComponentFqnByID(archetypeID) - s.True(ok) _, err = s.AdminClient().DeleteWorkflowExecution(testcore.NewContext(), &adminservice.DeleteWorkflowExecutionRequest{ Namespace: s.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: storeID, RunId: createResp.RunID, }, - Archetype: archetype, + Archetype: tests.Archetype, }) s.NoError(err) @@ -567,7 +558,7 @@ func (s *ChasmTestSuite) TestPayloadStoreForceDelete() { WorkflowId: storeID, RunId: createResp.RunID, }, - Archetype: archetype, + Archetype: tests.Archetype, }) var notFoundErr *serviceerror.NotFound s.ErrorAs(err, ¬FoundErr) @@ -576,7 +567,6 @@ func (s *ChasmTestSuite) TestPayloadStoreForceDelete() { s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: s.NamespaceID().String(), NamespaceName: s.Namespace().String(), PageSize: 10, Query: visQuery, @@ -589,7 +579,7 @@ func (s *ChasmTestSuite) TestPayloadStoreForceDelete() { ) } -func (s *ChasmTestSuite) TestListExecutions_ExecutionStatusAsAlias() { +func (s *ChasmTestSuite) TestDeletePayloadStore_RunningExecution() { tv := testvars.New(s.T()) ctx, cancel := context.WithTimeout(s.chasmContext, chasmTestTimeout) defer cancel() @@ -598,24 +588,81 @@ func (s *ChasmTestSuite) TestListExecutions_ExecutionStatusAsAlias() { _, err := tests.NewPayloadStoreHandler( ctx, tests.NewPayloadStoreRequest{ + NamespaceID: s.NamespaceID(), + StoreID: storeID, + IDReusePolicy: chasm.BusinessIDReusePolicyRejectDuplicate, + IDConflictPolicy: chasm.BusinessIDConflictPolicyFail, + }, + ) + s.NoError(err) + + visQuery := fmt.Sprintf("WorkflowId = '%s'", storeID) + + // Wait for visibility record to appear. + s.EventuallyWithT( + func(t *assert.CollectT) { + resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ + NamespaceName: s.Namespace().String(), + PageSize: 10, + Query: visQuery, + }) + require.NoError(t, err) + assert.Len(t, resp.Executions, 1) + }, + testcore.WaitForESToSettle, + 100*time.Millisecond, + ) + + err = tests.DeletePayloadStoreHandler( + ctx, + tests.DeletePayloadStoreRequest{ NamespaceID: s.NamespaceID(), StoreID: storeID, + Reason: "test deletion", + Identity: "test-identity", }, ) s.NoError(err) - archetypeID, ok := s.FunctionalTestBase.GetTestCluster().Host().GetCHASMRegistry().ComponentIDFor(&tests.PayloadStore{}) - s.True(ok) + // Validate execution is fully deleted (both mutable state and visibility record). + s.EventuallyWithT( + func(t *assert.CollectT) { + resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ + NamespaceName: s.Namespace().String(), + PageSize: 10, + Query: visQuery, + }) + require.NoError(t, err) + assert.Empty(t, resp.Executions) + }, + testcore.WaitForESToSettle, + 100*time.Millisecond, + ) +} + +func (s *ChasmTestSuite) TestListExecutions_ExecutionStatusAsAlias() { + tv := testvars.New(s.T()) + ctx, cancel := context.WithTimeout(s.chasmContext, chasmTestTimeout) + defer cancel() + + storeID := tv.Any().String() + _, err := tests.NewPayloadStoreHandler( + ctx, + tests.NewPayloadStoreRequest{ + NamespaceID: s.NamespaceID(), + StoreID: storeID, + }, + ) + s.NoError(err) // Query using "ExecutionStatus" as a CHASM alias (which maps to TemporalKeyword03) // This tests that CHASM components can use "ExecutionStatus" as an alias for their own search attribute - visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND ExecutionStatus = 'Running' AND PayloadStoreId = '%s'", archetypeID, storeID) + visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND ExecutionStatus = 'Running' AND PayloadStoreId = '%s'", tests.ArchetypeID, storeID) - var visRecord *chasm.ExecutionInfo[*testspb.TestPayloadStore] + var visRecord *chasm.VisibilityExecutionInfo[*testspb.TestPayloadStore] s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: string(s.Namespace()), PageSize: 10, Query: visQuery, @@ -638,25 +685,22 @@ func (s *ChasmTestSuite) TestListExecutions_ExecutionStatusAsAlias() { s.True(ok) s.Equal("Running", executionStatus) - // Close the store and verify the status changes - _, err = tests.ClosePayloadStoreHandler( + _, err = tests.CancelPayloadStoreHandler( ctx, - tests.ClosePayloadStoreRequest{ + tests.CancelPayloadStoreRequest{ NamespaceID: s.NamespaceID(), StoreID: storeID, }, ) s.NoError(err) - // Query for Completed status using ExecutionStatus as CHASM alias - visQueryCompleted := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND ExecutionStatus = 'Completed'", archetypeID) + visQueryCanceled := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND ExecutionStatus = 'Canceled' AND PayloadStoreId = '%s'", tests.ArchetypeID, storeID) s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: string(s.Namespace()), PageSize: 10, - Query: visQueryCompleted + fmt.Sprintf(" AND PayloadStoreId = '%s'", storeID), + Query: visQueryCanceled, }) s.NoError(err) return len(resp.Executions) == 1 @@ -683,17 +727,13 @@ func (s *ChasmTestSuite) TestTaskQueuePreallocatedSearchAttribute() { ) s.NoError(err) - archetypeID, ok := s.FunctionalTestBase.GetTestCluster().Host().GetCHASMRegistry().ComponentIDFor(&tests.PayloadStore{}) - s.True(ok) - // Query using TaskQueue as a CHASM preallocated search attribute - visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND TaskQueue = '%s' AND PayloadStoreId = '%s'", archetypeID, tests.DefaultPayloadStoreTaskQueue, storeID) + visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND TaskQueue = '%s' AND PayloadStoreId = '%s'", tests.ArchetypeID, tests.DefaultPayloadStoreTaskQueue, storeID) - var visRecord *chasm.ExecutionInfo[*testspb.TestPayloadStore] + var visRecord *chasm.VisibilityExecutionInfo[*testspb.TestPayloadStore] s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: string(s.Namespace()), PageSize: 10, Query: visQuery, @@ -735,16 +775,12 @@ func (s *ChasmTestSuite) TestMutableStateRebuilder() { s.NoError(err) // wait for the payload store to be created - archetypeID, ok := s.FunctionalTestBase.GetTestCluster().Host().GetCHASMRegistry().ComponentIDFor(&tests.PayloadStore{}) - s.True(ok) - s.Equal(archetypeID, chasm.ArchetypeID(archetypeID)) - visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND WorkflowId = '%s'", archetypeID, storeID) - var visRecord *chasm.ExecutionInfo[*testspb.TestPayloadStore] + visQuery := fmt.Sprintf("TemporalNamespaceDivision = '%d' AND WorkflowId = '%s'", tests.ArchetypeID, storeID) + var visRecord *chasm.VisibilityExecutionInfo[*testspb.TestPayloadStore] var runID string s.Eventually( func() bool { resp, err := chasm.ListExecutions[*tests.PayloadStore, *testspb.TestPayloadStore](ctx, &chasm.ListExecutionsRequest{ - NamespaceID: string(s.NamespaceID()), NamespaceName: string(s.Namespace()), PageSize: 10, Query: visQuery, @@ -764,8 +800,7 @@ func (s *ChasmTestSuite) TestMutableStateRebuilder() { s.Equal(storeID, visRecord.BusinessID) // payloadStore archetype is not the workflow archetype, should fail the rebuild. - archetype, _ := s.FunctionalTestBase.GetTestCluster().Host().GetCHASMRegistry().ComponentFqnByID(archetypeID) - s.NotEqual(archetype, chasm.WorkflowArchetype, "Archetype should not be the workflow archetype") + s.NotEqual(tests.Archetype, chasm.WorkflowArchetype, "Archetype should not be the workflow archetype") _, err = s.AdminClient().RebuildMutableState(testcore.NewContext(), &adminservice.RebuildMutableStateRequest{ Namespace: s.Namespace().String(), @@ -900,4 +935,69 @@ func (s *ChasmTestSuite) TestUpdateWithStartExecution_CreateNew() { s.Equal(int64(42), descResp.State.TotalCount) // Update was applied during creation. } +func (s *ChasmTestSuite) TestPayloadStore_ApproximateExecutionSize() { + tv := testvars.New(s.T()) + + ctx, cancel := context.WithTimeout(s.chasmContext, chasmTestTimeout) + defer cancel() + + storeID := tv.Any().String() + _, err := tests.NewPayloadStoreHandler( + ctx, + tests.NewPayloadStoreRequest{ + NamespaceID: s.NamespaceID(), + StoreID: storeID, + }, + ) + s.NoError(err) + + descResp, err := tests.DescribePayloadStoreHandler( + ctx, + tests.DescribePayloadStoreRequest{ + NamespaceID: s.NamespaceID(), + StoreID: storeID, + }, + ) + s.NoError(err) + initialApproxSize := descResp.ApproximateStateSize + + payloadSize := 100 * 1024 // 100KB + payloadData := make([]byte, payloadSize) + _, err = rand.Read(payloadData) + s.NoError(err) + + _, err = tests.AddPayloadHandler( + ctx, + tests.AddPayloadRequest{ + NamespaceID: s.NamespaceID(), + StoreID: storeID, + PayloadKey: "key1", + Payload: payload.EncodeBytes(payloadData), + }, + ) + s.NoError(err) + + descResp, err = tests.DescribePayloadStoreHandler( + ctx, + tests.DescribePayloadStoreRequest{ + NamespaceID: s.NamespaceID(), + StoreID: storeID, + }, + ) + s.NoError(err) + currentApproxSize := descResp.ApproximateStateSize + sizeDelta := float64(100) // Allow 100 bytes of variance due to overhead, encoding, etc. + s.InDelta(payloadSize, currentApproxSize-initialApproxSize, sizeDelta) + + adminDescResp, err := s.AdminClient().DescribeMutableState(testcore.NewContext(), &adminservice.DescribeMutableStateRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: storeID, + }, + ArchetypeId: tests.ArchetypeID, + }) + s.NoError(err) + s.InDelta(adminDescResp.DatabaseMutableState.Size(), currentApproxSize, sizeDelta) +} + // TODO: More tests here... diff --git a/tests/child_workflow_test.go b/tests/child_workflow_test.go index 6354598447b..9ab6475dd51 100644 --- a/tests/child_workflow_test.go +++ b/tests/child_workflow_test.go @@ -490,7 +490,7 @@ func (s *ChildWorkflowSuite) TestCronChildWorkflowExecution() { s.True(seenChildStarted) // Run through three executions of the child workflow - for i := 0; i < 3; i++ { + for i := range 3 { _, err = pollerChild.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err), tag.Counter(i)) s.NoError(err) @@ -514,7 +514,7 @@ func (s *ChildWorkflowSuite) TestCronChildWorkflowExecution() { WorkflowId: childID, }, }) - s.Nil(terminateErr) + s.NoError(terminateErr) // Process ChildExecution terminated event and complete parent execution _, err = pollerParent.PollAndProcessWorkflowTask() @@ -529,7 +529,7 @@ func (s *ChildWorkflowSuite) TestCronChildWorkflowExecution() { startFilter.EarliestTime = timestamppb.New(startParentWorkflowTS) startFilter.LatestTime = timestamppb.New(time.Now().UTC()) var closedExecutions []*workflowpb.WorkflowExecutionInfo - for i := 0; i < 10; i++ { + for range 10 { resp, err := s.FrontendClient().ListClosedWorkflowExecutions(testcore.NewContext(), &workflowservice.ListClosedWorkflowExecutionsRequest{ Namespace: s.Namespace().String(), MaximumPageSize: 100, @@ -944,7 +944,7 @@ func (s *ChildWorkflowSuite) TestRetryFailChildWorkflowExecution() { // Child failure should be present in completion event s.NotNil(completedEvent) attrs := completedEvent.GetChildWorkflowExecutionFailedEventAttributes() - s.Equal(attrs.Failure.Message, "Failed attempt 3") + s.Equal("Failed attempt 3", attrs.Failure.Message) } func (s *ChildWorkflowSuite) TestStartChildWorkflowWithInternalTaskQueue_Blocked() { @@ -1028,7 +1028,7 @@ func (s *ChildWorkflowSuite) TestStartChildWorkflowWithInternalTaskQueue_Blocked foundTaskFailed = true attrs := event.GetWorkflowTaskFailedEventAttributes() s.Equal(enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_START_CHILD_EXECUTION_ATTRIBUTES, attrs.GetCause()) - s.Contains(attrs.GetFailure().GetMessage(), "internal per namespace task queue") + s.Contains(attrs.GetFailure().GetMessage(), "internal per-namespace task queue") break } } diff --git a/tests/client_data_converter_test.go b/tests/client_data_converter_test.go index 6e8eebfb0b4..f2e92561411 100644 --- a/tests/client_data_converter_test.go +++ b/tests/client_data_converter_test.go @@ -153,12 +153,12 @@ func (s *ClientDataConverterTestSuite) TestClientDataConverter() { } ctx, cancel := rpc.NewContextWithTimeoutAndVersionHeaders(time.Minute) defer cancel() - s.Worker().RegisterWorkflow(testDataConverterWorkflow) - s.Worker().RegisterActivity(testActivity) + s.SdkWorker().RegisterWorkflow(testDataConverterWorkflow) + s.SdkWorker().RegisterActivity(testActivity) we, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, testDataConverterWorkflow, tl) s.NoError(err) s.NotNil(we) - s.True(we.GetRunID() != "") + s.NotEmpty(we.GetRunID()) var res string err = we.Get(ctx, &res) @@ -189,12 +189,12 @@ func (s *ClientDataConverterTestSuite) TestClientDataConverterFailed() { ctx, cancel := rpc.NewContextWithTimeoutAndVersionHeaders(time.Minute) defer cancel() - s.Worker().RegisterWorkflow(testDataConverterWorkflow) - s.Worker().RegisterActivity(testActivity) + s.SdkWorker().RegisterWorkflow(testDataConverterWorkflow) + s.SdkWorker().RegisterActivity(testActivity) we, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, testDataConverterWorkflow, tl) s.NoError(err) s.NotNil(we) - s.True(we.GetRunID() != "") + s.NotEmpty(we.GetRunID()) var res string err = we.Get(ctx, &res) @@ -237,13 +237,13 @@ func (s *ClientDataConverterTestSuite) TestClientDataConverterWithChild() { } ctx, cancel := rpc.NewContextWithTimeoutAndVersionHeaders(time.Minute) defer cancel() - s.Worker().RegisterWorkflow(testParentWorkflow) - s.Worker().RegisterWorkflow(testChildWorkflow) + s.SdkWorker().RegisterWorkflow(testParentWorkflow) + s.SdkWorker().RegisterWorkflow(testChildWorkflow) we, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, testParentWorkflow) s.NoError(err) s.NotNil(we) - s.True(we.GetRunID() != "") + s.NotEmpty(we.GetRunID()) var res string err = we.Get(ctx, &res) diff --git a/tests/client_misc_test.go b/tests/client_misc_test.go index 0070351a655..4f86345620a 100644 --- a/tests/client_misc_test.go +++ b/tests/client_misc_test.go @@ -64,13 +64,13 @@ func (s *ClientMiscTestSuite) TestTooManyChildWorkflows() { maxPendingChildWorkflows := testcore.ClientSuiteLimit parentWorkflow := func(ctx workflow.Context) error { childStarted := workflow.GetSignalChannel(ctx, "blocking-child-started") - for i := 0; i < maxPendingChildWorkflows; i++ { + for i := range maxPendingChildWorkflows { childOptions := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{ WorkflowID: fmt.Sprintf("child-%d", i+1), }) workflow.ExecuteChildWorkflow(childOptions, blockingChildWorkflow) } - for i := 0; i < maxPendingChildWorkflows; i++ { + for range maxPendingChildWorkflows { childStarted.Receive(ctx, nil) } return workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{ @@ -79,9 +79,9 @@ func (s *ClientMiscTestSuite) TestTooManyChildWorkflows() { } // register all the workflows - s.Worker().RegisterWorkflow(blockingChildWorkflow) - s.Worker().RegisterWorkflow(childWorkflow) - s.Worker().RegisterWorkflow(parentWorkflow) + s.SdkWorker().RegisterWorkflow(blockingChildWorkflow) + s.SdkWorker().RegisterWorkflow(childWorkflow) + s.SdkWorker().RegisterWorkflow(parentWorkflow) // start the parent workflow timeout := time.Minute * 5 @@ -96,9 +96,9 @@ func (s *ClientMiscTestSuite) TestTooManyChildWorkflows() { s.NoError(err) s.WaitForHistoryEventsSuffix(` - WorkflowTaskScheduled - WorkflowTaskStarted // 26 below is enumspb.WORKFLOW_TASK_FAILED_CAUSE_PENDING_CHILD_WORKFLOWS_LIMIT_EXCEEDED WorkflowTaskFailed {"Cause":26,"Failure":{"Message":"PendingChildWorkflowsLimitExceeded: the number of pending child workflow executions, 10, has reached the per-workflow limit of 10"}} + WorkflowTaskScheduled + WorkflowTaskStarted `, func() []*historypb.HistoryEvent { return s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: parentWorkflowID}) }, 10*time.Second, 500*time.Millisecond) @@ -130,15 +130,15 @@ func (s *ClientMiscTestSuite) TestTooManyPendingActivities() { pendingActivities <- activity.GetInfo(ctx) return activity.ErrResultPending } - s.Worker().RegisterActivity(pendingActivity) + s.SdkWorker().RegisterActivity(pendingActivity) lastActivity := func(ctx context.Context) error { return nil } - s.Worker().RegisterActivity(lastActivity) + s.SdkWorker().RegisterActivity(lastActivity) readyToScheduleLastActivity := "ready-to-schedule-last-activity" myWorkflow := func(ctx workflow.Context) error { - for i := 0; i < testcore.ClientSuiteLimit; i++ { + for i := range testcore.ClientSuiteLimit { workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ StartToCloseTimeout: time.Minute, ActivityID: fmt.Sprintf("pending-activity-%d", i), @@ -152,7 +152,7 @@ func (s *ClientMiscTestSuite) TestTooManyPendingActivities() { ActivityID: "last-activity", }), lastActivity).Get(ctx, nil) } - s.Worker().RegisterWorkflow(myWorkflow) + s.SdkWorker().RegisterWorkflow(myWorkflow) workflowID := uuid.NewString() workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ @@ -164,7 +164,7 @@ func (s *ClientMiscTestSuite) TestTooManyPendingActivities() { // wait until all of the activities are started (but not finished) before trying to schedule the last one var activityInfo activity.Info - for i := 0; i < testcore.ClientSuiteLimit; i++ { + for range testcore.ClientSuiteLimit { activityInfo = <-pendingActivities } s.NoError(s.SdkClient().SignalWorkflow(ctx, workflowID, "", readyToScheduleLastActivity, nil)) @@ -180,9 +180,9 @@ func (s *ClientMiscTestSuite) TestTooManyPendingActivities() { // verify that the workflow's history contains a task that failed because it would otherwise exceed the pending // child workflow limit s.WaitForHistoryEventsSuffix(` - 19 WorkflowTaskScheduled - 20 WorkflowTaskStarted // 27 below is enumspb.WORKFLOW_TASK_FAILED_CAUSE_PENDING_ACTIVITIES_LIMIT_EXCEEDED 21 WorkflowTaskFailed {"Cause":27,"Failure":{"Message":"PendingActivitiesLimitExceeded: the number of pending activities, 10, has reached the per-workflow limit of 10"}} + 22 WorkflowTaskScheduled + 23 WorkflowTaskStarted `, func() []*historypb.HistoryEvent { return s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: workflowRun.GetID(), RunId: workflowRun.GetRunID()}) }, 3*time.Second, 500*time.Millisecond) @@ -204,8 +204,8 @@ func (s *ClientMiscTestSuite) TestTooManyCancelRequests() { return false }) } - s.Worker().RegisterWorkflow(targetWorkflow) - for i := 0; i < numTargetWorkflows; i++ { + s.SdkWorker().RegisterWorkflow(targetWorkflow) + for i := range numTargetWorkflows { _, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ ID: fmt.Sprintf("workflow-%d", i), TaskQueue: s.TaskQueue(), @@ -227,7 +227,7 @@ func (s *ClientMiscTestSuite) TestTooManyCancelRequests() { } return nil } - s.Worker().RegisterWorkflow(cancelWorkflowsInRange) + s.SdkWorker().RegisterWorkflow(cancelWorkflowsInRange) // try to cancel all the workflows at once and verify that we can't because of the limit violation s.Run("CancelAllWorkflowsAtOnce", func() { @@ -243,6 +243,8 @@ func (s *ClientMiscTestSuite) TestTooManyCancelRequests() { 2 WorkflowTaskScheduled 3 WorkflowTaskStarted // 29 below is enumspb.WORKFLOW_TASK_FAILED_CAUSE_PENDING_REQUEST_CANCEL_LIMIT_EXCEEDED 4 WorkflowTaskFailed {"Cause":29,"Failure":{"Message":"PendingRequestCancelLimitExceeded: the number of pending requests to cancel external workflows, 10, has reached the per-workflow limit of 10"}} + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted `, func() []*historypb.HistoryEvent { return s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: run.GetID(), RunId: run.GetRunID()}) }, 5*time.Second, 500*time.Millisecond) @@ -285,7 +287,7 @@ func (s *ClientMiscTestSuite) TestTooManyPendingSignals() { signalName := "my-signal" sender := func(ctx workflow.Context, n int) error { var futures []workflow.Future - for i := 0; i < n; i++ { + for range n { future := workflow.SignalExternalWorkflow(ctx, receiverId, "", signalName, nil) futures = append(futures, future) } @@ -296,7 +298,7 @@ func (s *ClientMiscTestSuite) TestTooManyPendingSignals() { } return errs } - s.Worker().RegisterWorkflow(sender) + s.SdkWorker().RegisterWorkflow(sender) receiver := func(ctx workflow.Context) error { channel := workflow.GetSignalChannel(ctx, signalName) @@ -304,7 +306,7 @@ func (s *ClientMiscTestSuite) TestTooManyPendingSignals() { channel.Receive(ctx, nil) } } - s.Worker().RegisterWorkflow(receiver) + s.SdkWorker().RegisterWorkflow(receiver) _, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ TaskQueue: s.TaskQueue(), ID: receiverId, @@ -331,6 +333,8 @@ func (s *ClientMiscTestSuite) TestTooManyPendingSignals() { 2 WorkflowTaskScheduled 3 WorkflowTaskStarted // 28 below is enumspb.WORKFLOW_TASK_FAILED_CAUSE_PENDING_SIGNALS_LIMIT_EXCEEDED 4 WorkflowTaskFailed {"Cause":28,"Failure":{"Message":"PendingSignalsLimitExceeded: the number of pending signals to external workflows, 10, has reached the per-workflow limit of 10"}} + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted `, func() []*historypb.HistoryEvent { return s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: senderRun.GetID(), RunId: senderRun.GetRunID()}) }, 3*time.Second, 500*time.Millisecond) @@ -362,7 +366,7 @@ func continueAsNewTightLoop(ctx workflow.Context, currCount, maxCount int) (int, func (s *ClientMiscTestSuite) TestContinueAsNewTightLoop() { // Simulate continue as new tight loop, and verify server throttle the rate. workflowId := "continue_as_new_tight_loop" - s.Worker().RegisterWorkflow(continueAsNewTightLoop) + s.SdkWorker().RegisterWorkflow(continueAsNewTightLoop) ctx, cancel := rpc.NewContextWithTimeoutAndVersionHeaders(time.Minute) defer cancel() @@ -398,7 +402,7 @@ func (s *ClientMiscTestSuite) TestStickyAutoReset() { return msg, nil } - s.Worker().RegisterWorkflow(wfFn) + s.SdkWorker().RegisterWorkflow(wfFn) ctx, cancel := rpc.NewContextWithTimeoutAndVersionHeaders(time.Minute) defer cancel() @@ -428,7 +432,7 @@ func (s *ClientMiscTestSuite) TestStickyAutoReset() { }, 5*time.Second, 200*time.Millisecond) // stop worker - s.Worker().Stop() + s.SdkWorker().Stop() //nolint:forbidigo time.Sleep(time.Second * 11) // wait 11s (longer than 10s timeout), after this time, matching will detect StickyWorkerUnavailable resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{ @@ -473,7 +477,7 @@ func (s *ClientMiscTestSuite) TestStickyAutoReset() { s.NoError(err) s.NotNil(task) s.NotNil(task.History) - s.True(len(task.History.Events) > 0) + s.NotEmpty(task.History.Events) s.Equal(int64(1), task.History.Events[0].EventId) } @@ -513,7 +517,7 @@ func (s *ClientMiscTestSuite) TestWorkflowCanBeCompletedDespiteAdmittedUpdate() return workflow.ExecuteLocalActivity(laCtx, localActivityFn).Get(laCtx, nil) } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ ID: tv.WorkflowID(), @@ -538,7 +542,7 @@ func (s *ClientMiscTestSuite) TestWorkflowCanBeCompletedDespiteAdmittedUpdate() UpdateName: tv.HandlerName(), WorkflowID: tv.WorkflowID(), RunID: tv.RunID(), - Args: []interface{}{"update-value"}, + Args: []any{"update-value"}, WaitForStage: sdkclient.WorkflowUpdateStageCompleted, }) updateErrCh <- err @@ -608,7 +612,7 @@ func (s *ClientMiscTestSuite) Test_CancelActivityAndTimerBeforeComplete() { return nil } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) id := s.T().Name() workflowOptions := sdkclient.StartWorkflowOptions{ @@ -652,9 +656,9 @@ func (s *ClientMiscTestSuite) Test_FinishWorkflowWithDeferredCommands() { return nil } - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterWorkflow(childWorkflowFn) - s.Worker().RegisterActivity(activityFn) + s.SdkWorker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(childWorkflowFn) + s.SdkWorker().RegisterActivity(activityFn) id := "functional-test-finish-workflow-with-deffered-commands" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -668,7 +672,7 @@ func (s *ClientMiscTestSuite) Test_FinishWorkflowWithDeferredCommands() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) err = workflowRun.Get(ctx, nil) s.NoError(err) @@ -705,7 +709,7 @@ func (s *ClientMiscTestSuite) TestInvalidCommandAttribute() { // between server starts the workflow task and this code is executed. var currentAttemptStartedTime time.Time - err := workflow.SideEffect(ctx, func(_ workflow.Context) interface{} { + err := workflow.SideEffect(ctx, func(_ workflow.Context) any { rpcCtx := context.Background() if deadline, ok := ctx.Deadline(); ok { var cancel context.CancelFunc @@ -734,8 +738,8 @@ func (s *ClientMiscTestSuite) TestInvalidCommandAttribute() { return workflow.ExecuteActivity(ctx, activityFn).Get(ctx, nil) } - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFn) + s.SdkWorker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFn) id := "functional-test-invalid-command-attributes" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -753,7 +757,7 @@ func (s *ClientMiscTestSuite) TestInvalidCommandAttribute() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) // wait until workflow close (it will be timeout) err = workflowRun.Get(ctx, nil) @@ -771,10 +775,10 @@ func (s *ClientMiscTestSuite) TestInvalidCommandAttribute() { s.assertHistory(id, workflowRun.GetRunID(), expectedHistory) // assert workflow task retried 3 times - s.Equal(3, len(startedTime)) + s.Len(startedTime, 3) - s.True(startedTime[1].Sub(startedTime[0]) < time.Second) // retry immediately - s.True(startedTime[2].Sub(startedTime[1]) > time.Second*3) // retry after WorkflowTaskTimeout + s.Less(startedTime[1].Sub(startedTime[0]), time.Second) // retry immediately + s.Greater(startedTime[2].Sub(startedTime[1]), time.Second*3) // retry after WorkflowTaskTimeout } func (s *ClientMiscTestSuite) Test_BufferedQuery() { @@ -806,7 +810,7 @@ func (s *ClientMiscTestSuite) Test_BufferedQuery() { return multierr.Combine(err1, workflow.Sleep(ctx, 5*time.Second)) } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) id := "functional-test-buffered-query" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -820,11 +824,12 @@ func (s *ClientMiscTestSuite) Test_BufferedQuery() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) // wait until first wf task started wfStarted.Wait() + describeErrCh := make(chan error, 1) go func() { // sleep 2s to make sure DescribeMutableState is called after QueryWorkflow time.Sleep(2 * time.Second) //nolint:forbidigo @@ -837,7 +842,7 @@ func (s *ClientMiscTestSuite) Test_BufferedQuery() { }, Archetype: chasm.WorkflowArchetype, }) - s.Assert().NoError(err) + describeErrCh <- err }() // this query will be buffered in mutable state because workflow task is in-flight. @@ -851,6 +856,7 @@ func (s *ClientMiscTestSuite) Test_BufferedQuery() { err = workflowRun.Get(ctx, nil) s.NoError(err) + s.NoError(<-describeErrCh) // assert on test goroutine after workflow completes } func (s *ClientMiscTestSuite) assertHistory(wid, rid string, expected []enumspb.EventType) { @@ -914,7 +920,7 @@ func (s *ClientMiscTestSuite) TestBufferedSignalCausesUnhandledCommandAndSchedul return nil } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) workflowOptions := sdkclient.StartWorkflowOptions{ ID: tv.WorkflowID(), @@ -928,7 +934,7 @@ func (s *ClientMiscTestSuite) TestBufferedSignalCausesUnhandledCommandAndSchedul s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) tv = tv.WithRunID(workflowRun.GetRunID()) // block until first workflow task started @@ -1084,7 +1090,7 @@ func (s *ClientMiscTestSuite) TestBatchSignal() { workflow.GetSignalChannel(ctx, "my-signal").Receive(ctx, &receivedData) return receivedData, nil } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) workflowRun, err := s.SdkClient().ExecuteWorkflow(context.Background(), sdkclient.StartWorkflowOptions{ ID: uuid.NewString(), @@ -1146,8 +1152,8 @@ func (s *ClientMiscTestSuite) TestBatchReset() { err := workflow.ExecuteActivity(ctx, activityFn).Get(ctx, &result) return result, err } - s.Worker().RegisterWorkflow(workflowFn) - s.Worker().RegisterActivity(activityFn) + s.SdkWorker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFn) workflowRun, err := s.SdkClient().ExecuteWorkflow(context.Background(), sdkclient.StartWorkflowOptions{ ID: uuid.NewString(), @@ -1227,7 +1233,7 @@ func (s *ClientMiscTestSuite) TestBatchResetByBuildId() { // now do something bad in a loop. // (we want something that's visible in history, not just failing workflow tasks, // otherwise we wouldn't need a reset to "fix" it, just a new build would be enough.) - for i := 0; i < 1000; i++ { + for range 1000 { s.NoError(workflow.ExecuteActivity(ao, "badact").Get(ctx, nil)) _ = workflow.Sleep(ctx, time.Second) } diff --git a/tests/continue_as_new_test.go b/tests/continue_as_new_test.go index 2f789e72f74..f7545e5976b 100644 --- a/tests/continue_as_new_test.go +++ b/tests/continue_as_new_test.go @@ -93,7 +93,7 @@ func (s *ContinueAsNewTestSuite) TestContinueAsNewWorkflow() { previousRunID = currentRunID continueAsNewCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, continueAsNewCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, continueAsNewCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, @@ -134,7 +134,7 @@ func (s *ContinueAsNewTestSuite) TestContinueAsNewWorkflow() { T: s.T(), } - for i := 0; i < 10; i++ { + for i := range 10 { _, err := poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) s.NoError(err, strconv.Itoa(i)) @@ -208,7 +208,7 @@ func (s *ContinueAsNewTestSuite) TestContinueAsNewRunTimeout() { if continueAsNewCounter < continueAsNewCount { continueAsNewCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, continueAsNewCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, continueAsNewCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, @@ -256,7 +256,7 @@ func (s *ContinueAsNewTestSuite) TestContinueAsNewRunTimeout() { time.Sleep(1 * time.Second) // wait 1 second for timeout var historyEvents []*historypb.HistoryEvent - for i := 0; i < 20; i++ { + for range 20 { historyEvents = s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: id, }) @@ -439,18 +439,18 @@ func (s *ContinueAsNewTestSuite) TestWorkflowContinueAsNewTaskID() { _, err := poller.PollAndProcessWorkflowTask() s.NoError(err) events := s.GetHistory(s.Namespace().String(), executions[0]) - s.True(len(events) != 0) + s.NotEmpty(events) for _, event := range events { - s.True(event.GetTaskId() > minTaskID) + s.Greater(event.GetTaskId(), minTaskID) minTaskID = event.GetTaskId() } _, err = poller.PollAndProcessWorkflowTask() s.NoError(err) events = s.GetHistory(s.Namespace().String(), executions[1]) - s.True(len(events) != 0) + s.NotEmpty(events) for _, event := range events { - s.True(event.GetTaskId() > minTaskID) + s.Greater(event.GetTaskId(), minTaskID) minTaskID = event.GetTaskId() } } @@ -521,7 +521,7 @@ func (w *ParentWithChildContinueAsNew) workflow(task *workflowservice.PollWorkfl if w.continueAsNewCounter < w.continueAsNewCount { w.continueAsNewCounter++ buf := new(bytes.Buffer) - w.suite.Nil(binary.Write(buf, binary.LittleEndian, w.continueAsNewCounter)) + w.suite.NoError(binary.Write(buf, binary.LittleEndian, w.continueAsNewCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, @@ -550,7 +550,7 @@ func (w *ParentWithChildContinueAsNew) workflow(task *workflowservice.PollWorkfl w.suite.Logger.Info("Starting child execution") w.childExecutionStarted = true buf := new(bytes.Buffer) - w.suite.Nil(binary.Write(buf, binary.LittleEndian, w.childData)) + w.suite.NoError(binary.Write(buf, binary.LittleEndian, w.childData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, @@ -641,7 +641,7 @@ func (s *ContinueAsNewTestSuite) TestChildWorkflowWithContinueAsNew() { s.True(definition.childExecutionStarted) // Process ChildExecution Started event and all generations of child executions - for i := 0; i < 11; i++ { + for i := range 11 { s.Logger.Info("workflow task", tag.Counter(i)) _, err = poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) @@ -755,7 +755,7 @@ func (s *ContinueAsNewTestSuite) TestChildWorkflowWithContinueAsNewParentTermina s.True(definition.childExecutionStarted) // Process ChildExecution Started event and all generations of child executions - for i := 0; i < 11; i++ { + for i := range 11 { s.Logger.Info("workflow task", tag.Counter(i)) _, err = poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) @@ -793,7 +793,7 @@ func (s *ContinueAsNewTestSuite) TestChildWorkflowWithContinueAsNewParentTermina var childDescribeResp *workflowservice.DescribeWorkflowExecutionResponse // Retry 10 times to wait for child to be terminated due to transfer task processing to enforce parent close policy - for i := 0; i < 10; i++ { + for range 10 { childDescribeResp, err = s.FrontendClient().DescribeWorkflowExecution( testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ @@ -904,7 +904,7 @@ func (s *ContinueAsNewTestSuite) TestContinueAsNewWithInternalTaskQueue_Blocked( s.Error(err, "Expected error when continuing as new on internal task queue") var invalidArgument *serviceerror.InvalidArgument s.ErrorAs(err, &invalidArgument) - s.Contains(err.Error(), "internal per namespace task queue") + s.Contains(err.Error(), "internal per-namespace task queue") // Wait a bit for the workflow task failed event to be written to history time.Sleep(100 * time.Millisecond) //nolint:forbidigo @@ -921,7 +921,7 @@ func (s *ContinueAsNewTestSuite) TestContinueAsNewWithInternalTaskQueue_Blocked( foundTaskFailed = true attrs := event.GetWorkflowTaskFailedEventAttributes() s.Equal(enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_CONTINUE_AS_NEW_ATTRIBUTES, attrs.GetCause()) - s.Contains(attrs.GetFailure().GetMessage(), "internal per namespace task queue") + s.Contains(attrs.GetFailure().GetMessage(), "internal per-namespace task queue") break } } diff --git a/tests/cron_test.go b/tests/cron_test.go index f7887c44f7a..460358b9e02 100644 --- a/tests/cron_test.go +++ b/tests/cron_test.go @@ -239,7 +239,7 @@ func (s *CronTestSuite) TestCronWorkflow() { }}, }) s.NoError(err) - s.Equal(1, len(resp.GetExecutions())) + s.Len(resp.GetExecutions(), 1) executionInfo := resp.GetExecutions()[0] s.Equal(targetBackoffDuration, executionInfo.GetExecutionTime().AsTime().Sub(executionInfo.GetStartTime().AsTime())) @@ -249,8 +249,8 @@ func (s *CronTestSuite) TestCronWorkflow() { // Make sure the cron workflow start running at a proper time, in this case 3 seconds after the // startWorkflowExecution request backoffDuration := time.Now().UTC().Sub(startWorkflowTS) - s.True(backoffDuration > targetBackoffDuration) - s.True(backoffDuration < targetBackoffDuration+backoffDurationTolerance) + s.Greater(backoffDuration, targetBackoffDuration) + s.Less(backoffDuration, targetBackoffDuration+backoffDurationTolerance) _, err = poller.PollAndProcessWorkflowTask() s.NoError(err) @@ -258,7 +258,7 @@ func (s *CronTestSuite) TestCronWorkflow() { _, err = poller.PollAndProcessWorkflowTask() s.NoError(err) - s.Equal(3, len(executions)) + s.Len(executions, 3) _, terminateErr := s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ Namespace: s.Namespace().String(), @@ -269,7 +269,7 @@ func (s *CronTestSuite) TestCronWorkflow() { s.NoError(terminateErr) // first two should be failures - for i := 0; i < 2; i++ { + for i := range 2 { events := s.GetHistory(s.Namespace().String(), executions[i]) s.EqualHistoryEvents(fmt.Sprintf(` 1 WorkflowExecutionStarted {"Memo":{"Fields":{"memoKey":{"Data":"\"memoVal\""}}},"SearchAttributes":{"IndexedFields":{"CustomKeywordField":{"Data":"\"keyword-value\"","Metadata":{"type":"Keyword"}}}}} @@ -292,7 +292,7 @@ func (s *CronTestSuite) TestCronWorkflow() { startFilter.LatestTime = timestamppb.New(time.Now().UTC()) var closedExecutions []*workflowpb.WorkflowExecutionInfo - for i := 0; i < 10; i++ { + for range 10 { resp, err := s.FrontendClient().ListClosedWorkflowExecutions(testcore.NewContext(), &workflowservice.ListClosedWorkflowExecutionsRequest{ Namespace: s.Namespace().String(), MaximumPageSize: 100, @@ -384,45 +384,45 @@ func (s *CronTestClientSuite) TestCronWorkflowCompletionStates() { switch iteration { case 1: s.False(workflow.HasLastCompletionResult(ctx)) - s.Nil(workflow.GetLastError(ctx)) + s.NoError(workflow.GetLastError(ctx)) return "pass", nil case 2: s.True(workflow.HasLastCompletionResult(ctx)) s.NoError(workflow.GetLastCompletionResult(ctx, &lcr)) - s.Equal(lcr, "pass") - s.Nil(workflow.GetLastError(ctx)) + s.Equal("pass", lcr) + s.NoError(workflow.GetLastError(ctx)) return "", errors.New("second error") //nolint:err113 case 3: s.True(workflow.HasLastCompletionResult(ctx)) s.NoError(workflow.GetLastCompletionResult(ctx, &lcr)) - s.Equal(lcr, "pass") - s.NotNil(workflow.GetLastError(ctx)) - s.Equal(workflow.GetLastError(ctx).Error(), "second error") + s.Equal("pass", lcr) + s.Error(workflow.GetLastError(ctx)) + s.Equal("second error", workflow.GetLastError(ctx).Error()) s.NoError(workflow.Sleep(ctx, 10*time.Second)) // cause wft timeout panic("should have been timed out on server already") case 4: s.True(workflow.HasLastCompletionResult(ctx)) s.NoError(workflow.GetLastCompletionResult(ctx, &lcr)) - s.Equal(lcr, "pass") - s.NotNil(workflow.GetLastError(ctx)) - s.Equal(workflow.GetLastError(ctx).Error(), "workflow timeout (type: StartToClose)") + s.Equal("pass", lcr) + s.Error(workflow.GetLastError(ctx)) + s.Equal("workflow timeout (type: StartToClose)", workflow.GetLastError(ctx).Error()) return "pass again", nil case 5: s.True(workflow.HasLastCompletionResult(ctx)) s.NoError(workflow.GetLastCompletionResult(ctx, &lcr)) - s.Equal(lcr, "pass again") - s.Nil(workflow.GetLastError(ctx)) + s.Equal("pass again", lcr) + s.NoError(workflow.GetLastError(ctx)) return "final pass", nil } panic("shouldn't get here") } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) // Because of rounding in GetBackoffForNextSchedule, we'll tend to stay aligned to whatever // phase we start in relative to second boundaries, but drift slightly later within the second @@ -457,7 +457,7 @@ func (s *CronTestClientSuite) TestCronWorkflowCompletionStates() { s.DurationNear(attrs1.FirstWorkflowTaskBackoff.AsDuration(), targetBackoffDuration, tolerance) // wait for first run - s.Equal(<-wfCh, 1) + s.Equal(1, <-wfCh) s.DurationNear(time.Since(ts), targetBackoffDuration, tolerance) ts = time.Now() @@ -479,24 +479,24 @@ func (s *CronTestClientSuite) TestCronWorkflowCompletionStates() { s.DurationNear(attrs2.FirstWorkflowTaskBackoff.AsDuration(), targetBackoffDuration, tolerance) // wait for second run - s.Equal(<-wfCh, 2) + s.Equal(2, <-wfCh) s.DurationNear(time.Since(ts), targetBackoffDuration, tolerance) ts = time.Now() // don't bother checking started events for subsequent runs, we covered the important parts already // wait for third run - s.Equal(<-wfCh, 3) + s.Equal(3, <-wfCh) s.DurationNear(time.Since(ts), targetBackoffDuration, tolerance) ts = time.Now() // wait for fourth run (third one waits for timeout after 5s, so will run after 6s) - s.Equal(<-wfCh, 4) + s.Equal(4, <-wfCh) s.DurationNear(time.Since(ts), 2*targetBackoffDuration, tolerance) ts = time.Now() // wait for fifth run - s.Equal(<-wfCh, 5) + s.Equal(5, <-wfCh) s.DurationNear(time.Since(ts), targetBackoffDuration, tolerance) // let fifth run finish and sixth get scheduled diff --git a/tests/describe_test.go b/tests/describe_test.go index 450ab72f861..40a72f7e791 100644 --- a/tests/describe_test.go +++ b/tests/describe_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -19,20 +18,21 @@ import ( "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/payloads" "go.temporal.io/server/common/primitives/timestamp" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" ) type DescribeTestSuite struct { - testcore.FunctionalTestBase + parallelsuite.Suite[*DescribeTestSuite] } func TestDescribeTestSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(DescribeTestSuite)) + parallelsuite.Run(t, &DescribeTestSuite{}) } func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { + env := testcore.NewEnv(s.T()) id := "functional-describe-wfe-test" wt := "functional-describe-wfe-test-type" tq := "functional-describe-wfe-test-taskqueue" @@ -42,7 +42,7 @@ func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { requestID := uuid.NewString() request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: requestID, - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -52,14 +52,14 @@ func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { Identity: identity, } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + we, err0 := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) s.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) describeWorkflowExecution := func() (*workflowservice.DescribeWorkflowExecutionResponse, error) { - return s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + return env.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: id, RunId: we.RunId, @@ -74,7 +74,7 @@ func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { s.Equal(int64(2), wfInfo.HistoryLength) // WorkflowStarted, WorkflowTaskScheduled s.Equal(wfInfo.GetStartTime(), wfInfo.GetExecutionTime()) s.Equal(tq, wfInfo.TaskQueue) - s.Greater(wfInfo.GetHistorySizeBytes(), int64(0)) + s.Positive(wfInfo.GetHistorySizeBytes()) s.Empty(wfInfo.GetParentNamespaceId()) s.Nil(wfInfo.GetParentExecution()) s.NotNil(wfInfo.GetRootExecution()) @@ -135,19 +135,19 @@ func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { } poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: identity, WorkflowTaskHandler: wtHandler, ActivityTaskHandler: atHandler, - Logger: s.Logger, + Logger: env.Logger, T: s.T(), } // first workflow task to schedule new activity _, err = poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) s.NoError(err) dweResponse, err = describeWorkflowExecution() @@ -157,7 +157,7 @@ func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { s.Nil(wfInfo.CloseTime) s.Nil(wfInfo.ExecutionDuration) s.Equal(int64(5), wfInfo.HistoryLength) // WorkflowTaskStarted, WorkflowTaskCompleted, ActivityScheduled - s.Equal(1, len(dweResponse.PendingActivities)) + s.Len(dweResponse.PendingActivities, 1) s.Equal("test-activity-type", dweResponse.PendingActivities[0].ActivityType.GetName()) s.True(timestamp.TimeValue(dweResponse.PendingActivities[0].GetLastHeartbeatTime()).IsZero()) @@ -169,7 +169,7 @@ func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { wfInfo = dweResponse.WorkflowExecutionInfo s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, wfInfo.GetStatus()) s.Equal(int64(8), wfInfo.HistoryLength) // ActivityTaskStarted, ActivityTaskCompleted, WorkflowTaskScheduled - s.Equal(0, len(dweResponse.PendingActivities)) + s.Empty(dweResponse.PendingActivities) // Process signal in workflow _, err = poller.PollAndProcessWorkflowTask(testcore.WithDumpHistory) @@ -190,6 +190,7 @@ func (s *DescribeTestSuite) TestDescribeWorkflowExecution() { } func (s *DescribeTestSuite) TestDescribeTaskQueue() { + env := testcore.NewEnv(s.T()) workflowID := "functional-get-poller-history" wt := "functional-get-poller-history-type" tl := "functional-get-poller-history-taskqueue" @@ -199,7 +200,7 @@ func (s *DescribeTestSuite) TestDescribeTaskQueue() { // Start workflow execution request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: workflowID, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -209,10 +210,10 @@ func (s *DescribeTestSuite) TestDescribeTaskQueue() { Identity: identity, } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + we, err0 := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) s.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) // workflow logic activityScheduled := false @@ -222,7 +223,7 @@ func (s *DescribeTestSuite) TestDescribeTaskQueue() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -252,19 +253,19 @@ func (s *DescribeTestSuite) TestDescribeTaskQueue() { } poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: identity, WorkflowTaskHandler: wtHandler, ActivityTaskHandler: atHandler, - Logger: s.Logger, + Logger: env.Logger, T: s.T(), } // this function poll events from history side testDescribeTaskQueue := func(namespace string, taskqueue *taskqueuepb.TaskQueue, taskqueueType enumspb.TaskQueueType) []*taskqueuepb.PollerInfo { - responseInner, errInner := s.FrontendClient().DescribeTaskQueue(testcore.NewContext(), &workflowservice.DescribeTaskQueueRequest{ + responseInner, errInner := env.FrontendClient().DescribeTaskQueue(testcore.NewContext(), &workflowservice.DescribeTaskQueueRequest{ Namespace: namespace, TaskQueue: taskqueue, TaskQueueType: taskqueueType, @@ -278,30 +279,30 @@ func (s *DescribeTestSuite) TestDescribeTaskQueue() { // when no one polling on the taskqueue (activity or workflow), there shall be no poller information tq := &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} - pollerInfos := testDescribeTaskQueue(s.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_ACTIVITY) + pollerInfos := testDescribeTaskQueue(env.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_ACTIVITY) s.Empty(pollerInfos) - pollerInfos = testDescribeTaskQueue(s.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW) + pollerInfos = testDescribeTaskQueue(env.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW) s.Empty(pollerInfos) _, errWorkflowTask := poller.PollAndProcessWorkflowTask() s.NoError(errWorkflowTask) - pollerInfos = testDescribeTaskQueue(s.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_ACTIVITY) + pollerInfos = testDescribeTaskQueue(env.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_ACTIVITY) s.Empty(pollerInfos) - pollerInfos = testDescribeTaskQueue(s.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW) - s.Equal(1, len(pollerInfos)) + pollerInfos = testDescribeTaskQueue(env.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW) + s.Len(pollerInfos, 1) s.Equal(identity, pollerInfos[0].GetIdentity()) s.True(pollerInfos[0].GetLastAccessTime().AsTime().After(before)) s.NotEmpty(pollerInfos[0].GetLastAccessTime()) errActivity := poller.PollAndProcessActivityTask(false) s.NoError(errActivity) - pollerInfos = testDescribeTaskQueue(s.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_ACTIVITY) - s.Equal(1, len(pollerInfos)) + pollerInfos = testDescribeTaskQueue(env.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_ACTIVITY) + s.Len(pollerInfos, 1) s.Equal(identity, pollerInfos[0].GetIdentity()) s.True(pollerInfos[0].GetLastAccessTime().AsTime().After(before)) s.NotEmpty(pollerInfos[0].GetLastAccessTime()) - pollerInfos = testDescribeTaskQueue(s.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW) - s.Equal(1, len(pollerInfos)) + pollerInfos = testDescribeTaskQueue(env.Namespace().String(), tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW) + s.Len(pollerInfos, 1) s.Equal(identity, pollerInfos[0].GetIdentity()) s.True(pollerInfos[0].GetLastAccessTime().AsTime().After(before)) s.NotEmpty(pollerInfos[0].GetLastAccessTime()) diff --git a/tests/dlq_test.go b/tests/dlq_test.go index 2a9f8feb95c..79a9a2867e4 100644 --- a/tests/dlq_test.go +++ b/tests/dlq_test.go @@ -51,7 +51,7 @@ type ( writer bytes.Buffer sdkClientFactory sdk.ClientFactory tdbgApp *cli.App - deleteBlockCh chan interface{} + deleteBlockCh chan any failingWorkflowIDPrefix atomic.Pointer[string] } @@ -142,9 +142,9 @@ func myWorkflow(workflow.Context) (string, error) { func (s *DLQSuite) SetupTest() { s.FunctionalTestBase.SetupTest() - s.Worker().RegisterWorkflow(myWorkflow) + s.SdkWorker().RegisterWorkflow(myWorkflow) - s.deleteBlockCh = make(chan interface{}) + s.deleteBlockCh = make(chan any) close(s.deleteBlockCh) } @@ -172,7 +172,7 @@ func (s *DLQSuite) TestReadArtificialDLQTasks() { QueueKey: queueKey, }) s.NoError(err) - for i := 0; i < 4; i++ { + for i := range 4 { task := &tasks.WorkflowTask{ WorkflowKey: workflowKey, TaskID: int64(42 + i), @@ -288,7 +288,7 @@ func (s *DLQSuite) TestPurgeRealWorkflow() { // Try to cancel completed workflow cancelResponse := s.cancelJob(ctx, token) - s.Equal(false, cancelResponse.Canceled) + s.False(cancelResponse.Canceled) } // This test executes actual workflows for which we've set up an executor wrapper to return a terminal error. This @@ -308,7 +308,7 @@ func (s *DLQSuite) TestMergeRealWorkflow() { numWorkflows := 3 var dlqMessageID int64 var runs []sdkclient.WorkflowRun - for i := 0; i < numWorkflows; i++ { + for range numWorkflows { run, dlqMessageID = s.executeDoomedWorkflow(ctx) runs = append(runs, run) } @@ -323,7 +323,7 @@ func (s *DLQSuite) TestMergeRealWorkflow() { s.Empty(dlqTasks) // Verify that the workflows now eventually complete successfully. - for i := 0; i < numWorkflows; i++ { + for i := range numWorkflows { s.validateWorkflowRun(ctx, runs[i]) } @@ -337,11 +337,11 @@ func (s *DLQSuite) TestMergeRealWorkflow() { // Try to cancel completed workflow cancelResponse := s.cancelJob(ctx, token) - s.Equal(false, cancelResponse.Canceled) + s.False(cancelResponse.Canceled) } func (s *DLQSuite) TestCancelRunningMerge() { - s.deleteBlockCh = make(chan interface{}) + s.deleteBlockCh = make(chan any) ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, dlqTestTimeout) defer cancel() @@ -353,7 +353,7 @@ func (s *DLQSuite) TestCancelRunningMerge() { // Try to cancel running workflow cancelResponse := s.cancelJob(ctx, token) - s.Equal(true, cancelResponse.Canceled) + s.True(cancelResponse.Canceled) // Unblock waiting tests on Delete close(s.deleteBlockCh) // Delete the workflow task from the DLQ. diff --git a/tests/eager_workflow_start_test.go b/tests/eager_workflow_start_test.go index 07af95f94e3..4ed0be6c7c7 100644 --- a/tests/eager_workflow_start_test.go +++ b/tests/eager_workflow_start_test.go @@ -70,7 +70,7 @@ func (s *EagerWorkflowTestSuite) startEagerWorkflow(baseOptions *workflowservice return response } -func (s *EagerWorkflowTestSuite) respondWorkflowTaskCompleted(task *workflowservice.PollWorkflowTaskQueueResponse, result interface{}) { +func (s *EagerWorkflowTestSuite) respondWorkflowTaskCompleted(task *workflowservice.PollWorkflowTaskQueueResponse, result any) { dataConverter := converter.GetDefaultDataConverter() payloads, err := dataConverter.ToPayloads(result) s.Require().NoError(err) diff --git a/tests/gethistory_test.go b/tests/gethistory_test.go index 2df29d3a970..b0f0c67e0d7 100644 --- a/tests/gethistory_test.go +++ b/tests/gethistory_test.go @@ -127,7 +127,7 @@ func (s *GetHistoryFunctionalSuite) TestGetWorkflowExecutionHistory_All() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -300,7 +300,7 @@ func (s *GetHistoryFunctionalSuite) TestGetWorkflowExecutionHistory_Close() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -400,7 +400,7 @@ func (s *GetHistoryFunctionalSuite) TestGetWorkflowExecutionHistory_Close() { // since we are only interested in close event if token == nil { - s.Equal(1, len(events)) + s.Len(events, 1) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED, events[0].EventType) } else { s.Empty(events) @@ -415,7 +415,7 @@ func (s *GetHistoryFunctionalSuite) TestGetWorkflowExecutionHistory_Close() { break } } - s.Equal(1, len(events)) + s.Len(events, 1) s.Logger.Info("Done TestGetWorkflowExecutionHistory_Close") } @@ -444,7 +444,7 @@ func (s *RawHistorySuite) TestGetWorkflowExecutionHistory_GetRawHistoryData() { } we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.Nil(err0) + s.NoError(err0) s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) @@ -457,7 +457,7 @@ func (s *RawHistorySuite) TestGetWorkflowExecutionHistory_GetRawHistoryData() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -515,7 +515,7 @@ func (s *RawHistorySuite) TestGetWorkflowExecutionHistory_GetRawHistoryData() { WaitNewEvent: isLongPoll, NextPageToken: token, }) - s.Nil(err) + s.NoError(err) return responseInner.RawHistory, responseInner.NextPageToken } @@ -528,14 +528,14 @@ func (s *RawHistorySuite) TestGetWorkflowExecutionHistory_GetRawHistoryData() { MaximumPageSize: int32(100), NextPageToken: token, }) - s.Nil(err) + s.NoError(err) return responseInner.RawHistory, responseInner.NextPageToken } convertBlob := func(blobs []*commonpb.DataBlob) []*historypb.HistoryEvent { events := []*historypb.HistoryEvent{} for _, blob := range blobs { - s.True(blob.GetEncodingType() == enumspb.ENCODING_TYPE_PROTO3) + s.Equal(enumspb.ENCODING_TYPE_PROTO3, blob.GetEncodingType()) blobEvents, err := serialization.DefaultDecoder.DeserializeEvents(&commonpb.DataBlob{ EncodingType: enumspb.ENCODING_TYPE_PROTO3, Data: blob.Data, @@ -655,8 +655,8 @@ func (s *RawHistoryClientSuite) TestGetHistoryReverse() { return nil } - s.Worker().RegisterActivity(activityFn) - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFn) + s.SdkWorker().RegisterWorkflow(workflowFn) wfId := "functional-test-gethistoryreverse" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -670,27 +670,27 @@ func (s *RawHistoryClientSuite) TestGetHistoryReverse() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) err = workflowRun.Get(ctx, nil) s.NoError(err) wfeResponse, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) - s.Nil(err) + s.NoError(err) eventDefaultOrder := s.GetHistory(s.Namespace().String(), wfeResponse.WorkflowExecutionInfo.Execution) eventDefaultOrder = reverseSlice(eventDefaultOrder) events := s.getHistoryReverse(s.Namespace().String(), wfeResponse.WorkflowExecutionInfo.Execution, 100) - s.Equal(len(eventDefaultOrder), len(events)) + s.Len(events, len(eventDefaultOrder)) s.Equal(eventDefaultOrder, events) events = s.getHistoryReverse(s.Namespace().String(), wfeResponse.WorkflowExecutionInfo.Execution, 3) - s.Equal(len(eventDefaultOrder), len(events)) + s.Len(events, len(eventDefaultOrder)) s.Equal(eventDefaultOrder, events) events = s.getHistoryReverse(s.Namespace().String(), wfeResponse.WorkflowExecutionInfo.Execution, 1) - s.Equal(len(eventDefaultOrder), len(events)) + s.Len(events, len(eventDefaultOrder)) s.Equal(eventDefaultOrder, events) } @@ -730,8 +730,8 @@ func (s *RawHistoryClientSuite) TestGetHistoryReverse_MultipleBranches() { return nil } - s.Worker().RegisterActivity(activityFn) - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterActivity(activityFn) + s.SdkWorker().RegisterWorkflow(workflowFn) wfId := "functional-test-wf-gethistory-reverse-multiple-branches" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -745,7 +745,7 @@ func (s *RawHistoryClientSuite) TestGetHistoryReverse_MultipleBranches() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) // we want to reset workflow in the middle of execution time.Sleep(time.Second) //nolint:forbidigo @@ -774,15 +774,15 @@ func (s *RawHistoryClientSuite) TestGetHistoryReverse_MultipleBranches() { eventsDefaultOrder = reverseSlice(eventsDefaultOrder) events := s.getHistoryReverse(s.Namespace().String(), resetWfeResponse.WorkflowExecutionInfo.Execution, 100) - s.Equal(len(eventsDefaultOrder), len(events)) + s.Len(events, len(eventsDefaultOrder)) s.Equal(eventsDefaultOrder, events) events = s.getHistoryReverse(s.Namespace().String(), resetWfeResponse.WorkflowExecutionInfo.Execution, 3) - s.Equal(len(eventsDefaultOrder), len(events)) + s.Len(events, len(eventsDefaultOrder)) s.Equal(eventsDefaultOrder, events) events = s.getHistoryReverse(s.Namespace().String(), resetWfeResponse.WorkflowExecutionInfo.Execution, 1) - s.Equal(len(eventsDefaultOrder), len(events)) + s.Len(events, len(eventsDefaultOrder)) s.Equal(eventsDefaultOrder, events) } diff --git a/tests/http_api_test.go b/tests/http_api_test.go index ea98fcfd3ed..3440d5d915b 100644 --- a/tests/http_api_test.go +++ b/tests/http_api_test.go @@ -80,7 +80,7 @@ func (s *HttpApiTestSuite) runHTTPAPIBasicsTest( } return arg, nil } - s.Worker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: "http-basic-workflow"}) + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: "http-basic-workflow"}) // Capture metrics capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() @@ -426,7 +426,7 @@ func (s *HttpApiTestSuite) TestHTTPAPI_OperatorService_ListSearchAttributes() { // we just check that a few defaults exist. We don't want to check for all // of them as that's brittle and will break the tests if we ever add a new type s.Require().Contains(searchAttrsResp.CustomAttributes, "CustomIntField") - s.Require().Equal(searchAttrsResp.CustomAttributes["CustomIntField"], "INDEXED_VALUE_TYPE_INT") + s.Require().Equal("INDEXED_VALUE_TYPE_INT", searchAttrsResp.CustomAttributes["CustomIntField"]) } func (s *HttpApiTestSuite) TestHTTPAPI_Serves_OpenAPIv2_Docs() { @@ -435,7 +435,7 @@ func (s *HttpApiTestSuite) TestHTTPAPI_Serves_OpenAPIv2_Docs() { "/swagger.json", "", ) - var spec map[string]interface{} + var spec map[string]any // We're not going to validate it here, just verify that it's valid s.Require().NoError(json.Unmarshal(respBody, &spec), string(respBody)) } @@ -446,7 +446,7 @@ func (s *HttpApiTestSuite) TestHTTPAPI_Serves_OpenAPIv3_Docs() { "/openapi.yaml", "", ) - var spec map[string]interface{} + var spec map[string]any // We're not going to validate it here, just verify that it's valid s.Require().NoError(yaml.Unmarshal(respBody, &spec), string(respBody)) } diff --git a/tests/links_test.go b/tests/links_test.go index 84bc3441982..210808219b1 100644 --- a/tests/links_test.go +++ b/tests/links_test.go @@ -6,22 +6,22 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/testing/protorequire" "go.temporal.io/server/tests/testcore" ) type LinksSuite struct { - testcore.FunctionalTestBase + parallelsuite.Suite[*LinksSuite] } func TestLinksTestSuite(t *testing.T) { - suite.Run(t, new(LinksSuite)) + parallelsuite.Run(t, &LinksSuite{}) } var links = []*commonpb.Link{ @@ -37,9 +37,10 @@ var links = []*commonpb.Link{ } func (s *LinksSuite) TestTerminateWorkflow_LinksAttachedToEvent() { + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() - run, err := s.SdkClient().ExecuteWorkflow( + run, err := env.SdkClient().ExecuteWorkflow( ctx, client.StartWorkflowOptions{ TaskQueue: "dont-care", @@ -49,8 +50,8 @@ func (s *LinksSuite) TestTerminateWorkflow_LinksAttachedToEvent() { s.NoError(err) // TODO(bergundy): Use SdkClient if and when it exposes links on TerminateWorkflow. - _, err = s.FrontendClient().TerminateWorkflowExecution(ctx, &workflowservice.TerminateWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateWorkflowExecution(ctx, &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{ WorkflowId: run.GetID(), }, @@ -59,16 +60,17 @@ func (s *LinksSuite) TestTerminateWorkflow_LinksAttachedToEvent() { }) s.NoError(err) - history := s.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT) + history := env.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT) event, err := history.Next() s.NoError(err) protorequire.ProtoSliceEqual(s.T(), links, event.Links) } func (s *LinksSuite) TestRequestCancelWorkflow_LinksAttachedToEvent() { + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() - run, err := s.SdkClient().ExecuteWorkflow( + run, err := env.SdkClient().ExecuteWorkflow( ctx, client.StartWorkflowOptions{ TaskQueue: "dont-care", @@ -78,8 +80,8 @@ func (s *LinksSuite) TestRequestCancelWorkflow_LinksAttachedToEvent() { s.NoError(err) // TODO(bergundy): Use SdkClient if and when it exposes links on CancelWorkflow. - _, err = s.FrontendClient().RequestCancelWorkflowExecution(ctx, &workflowservice.RequestCancelWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RequestCancelWorkflowExecution(ctx, &workflowservice.RequestCancelWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{ WorkflowId: run.GetID(), }, @@ -88,7 +90,7 @@ func (s *LinksSuite) TestRequestCancelWorkflow_LinksAttachedToEvent() { }) s.NoError(err) - history := s.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + history := env.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) foundEvent := false for history.HasNext() { event, err := history.Next() @@ -103,9 +105,10 @@ func (s *LinksSuite) TestRequestCancelWorkflow_LinksAttachedToEvent() { } func (s *LinksSuite) TestSignalWorkflowExecution_LinksAttachedToEvent() { + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() - run, err := s.SdkClient().ExecuteWorkflow( + run, err := env.SdkClient().ExecuteWorkflow( ctx, client.StartWorkflowOptions{ TaskQueue: "dont-care", @@ -115,8 +118,8 @@ func (s *LinksSuite) TestSignalWorkflowExecution_LinksAttachedToEvent() { s.NoError(err) // TODO(bergundy): Use SdkClient if and when it exposes links on SignalWorkflow. - _, err = s.FrontendClient().SignalWorkflowExecution(ctx, &workflowservice.SignalWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().SignalWorkflowExecution(ctx, &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{ WorkflowId: run.GetID(), }, @@ -127,7 +130,7 @@ func (s *LinksSuite) TestSignalWorkflowExecution_LinksAttachedToEvent() { }) s.NoError(err) - history := s.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + history := env.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) foundEvent := false for history.HasNext() { event, err := history.Next() @@ -142,6 +145,7 @@ func (s *LinksSuite) TestSignalWorkflowExecution_LinksAttachedToEvent() { } func (s *LinksSuite) TestSignalWithStartWorkflowExecution_LinksAttachedToRelevantEvents() { + env := testcore.NewEnv(s.T()) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() @@ -149,7 +153,7 @@ func (s *LinksSuite) TestSignalWithStartWorkflowExecution_LinksAttachedToRelevan // TODO(bergundy): Use SdkClient if and when it exposes links on SignalWithStartWorkflow. request := &workflowservice.SignalWithStartWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: workflowID, WorkflowType: &commonpb.WorkflowType{ Name: "dont-care", @@ -162,15 +166,15 @@ func (s *LinksSuite) TestSignalWithStartWorkflowExecution_LinksAttachedToRelevan RequestId: uuid.NewString(), Links: links, } - _, err := s.FrontendClient().SignalWithStartWorkflowExecution(ctx, request) + _, err := env.FrontendClient().SignalWithStartWorkflowExecution(ctx, request) s.NoError(err) // Send a second request and verify that the new signal has links attached to it too. request.RequestId = uuid.NewString() - _, err = s.FrontendClient().SignalWithStartWorkflowExecution(ctx, request) + _, err = env.FrontendClient().SignalWithStartWorkflowExecution(ctx, request) s.NoError(err) - history := s.SdkClient().GetWorkflowHistory(ctx, workflowID, "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + history := env.SdkClient().GetWorkflowHistory(ctx, workflowID, "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) foundStartEvent := false foundFirstSignal := false foundSecondSignal := false diff --git a/tests/matching_utils.go b/tests/matching_utils.go new file mode 100644 index 00000000000..74cdf6d6b0b --- /dev/null +++ b/tests/matching_utils.go @@ -0,0 +1,77 @@ +package tests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + enumspb "go.temporal.io/api/enums/v1" + deploymentspb "go.temporal.io/server/api/deployment/v1" + "go.temporal.io/server/api/matchingservice/v1" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/primitives/timestamp" + "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/tests/testcore" +) + +// runWithMatchingBehaviors runs a test with all combinations of matching behaviors. +func runWithMatchingBehaviors( + t *testing.T, + baseOpts []testcore.TestOption, + subtest func(s *testcore.TestEnv, behavior testcore.MatchingBehavior), +) { + for _, behavior := range testcore.AllMatchingBehaviors() { + t.Run(behavior.Name(), func(t *testing.T) { + opts := append([]testcore.TestOption{}, baseOpts...) + opts = append(opts, behavior.Options()...) + + env := testcore.NewEnv(t, opts...) + behavior.InjectHooks(env) + + subtest(env, behavior) + }) + } +} + +// syncDeploymentVersionToTaskQueues sends a SyncDeploymentUserData request to the matching service +// to register a deployment version as current for the specified task queue types, then waits for +// the data to propagate to all partitions using CheckTaskQueueUserDataPropagation. +func syncDeploymentVersionToTaskQueues( + t testing.TB, + matchingClient matchingservice.MatchingServiceClient, + namespaceID namespace.ID, + tv *testvars.TestVars, + tqTypes ...enumspb.TaskQueueType, +) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + now := timestamp.TimePtr(time.Now()) + resp, err := matchingClient.SyncDeploymentUserData( + ctx, &matchingservice.SyncDeploymentUserDataRequest{ + NamespaceId: namespaceID.String(), + TaskQueue: tv.TaskQueue().GetName(), + TaskQueueTypes: tqTypes, + Operation: &matchingservice.SyncDeploymentUserDataRequest_UpdateVersionData{ + UpdateVersionData: &deploymentspb.DeploymentVersionData{ + Version: tv.DeploymentVersion(), + RoutingUpdateTime: now, + CurrentSinceTime: now, + }, + }, + }, + ) + require.NoError(t, err) + + // Wait for the data to propagate to all partitions. + _, err = matchingClient.CheckTaskQueueUserDataPropagation( + ctx, &matchingservice.CheckTaskQueueUserDataPropagationRequest{ + NamespaceId: namespaceID.String(), + TaskQueue: tv.TaskQueue().GetName(), + Version: resp.GetVersion(), + }, + ) + require.NoError(t, err) +} diff --git a/tests/max_buffered_event_test.go b/tests/max_buffered_event_test.go index 7c047084194..4062e0c6b4a 100644 --- a/tests/max_buffered_event_test.go +++ b/tests/max_buffered_event_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/client" @@ -16,30 +15,32 @@ import ( "go.temporal.io/server/common" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" ) type MaxBufferedEventSuite struct { - testcore.FunctionalTestBase + parallelsuite.Suite[*MaxBufferedEventSuite] } func TestMaxBufferedEventSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(MaxBufferedEventSuite)) + parallelsuite.Run(t, &MaxBufferedEventSuite{}) } -func (s *MaxBufferedEventSuite) SetupSuite() { - dynamicConfigOverrides := map[dynamicconfig.Key]any{ - // Set MaximumBufferedEventsSizeInBytes high so we don't hit that limit - dynamicconfig.MaximumBufferedEventsSizeInBytes.Key(): 10 * 1024 * 1024, // 10MB - // Set MutableStateSizeLimitError low so buffered events exhaust mutable state size - dynamicconfig.MutableStateSizeLimitWarn.Key(): 200, - dynamicconfig.MutableStateSizeLimitError.Key(): 410 * 1024, // 410KB +func (s *MaxBufferedEventSuite) opts() []testcore.TestOption { + return []testcore.TestOption{ + testcore.WithSdkWorker(), + // Set MaximumBufferedEventsSizeInBytes high so we don't hit that limit. + testcore.WithDynamicConfig(dynamicconfig.MaximumBufferedEventsSizeInBytes, 10*1024*1024), // 10MB + testcore.WithDynamicConfig(dynamicconfig.MutableStateSizeLimitWarn, 200), + // Set MutableStateSizeLimitError low so buffered events exhaust mutable state size. + testcore.WithDynamicConfig(dynamicconfig.MutableStateSizeLimitError, 410*1024), // 410KB } - s.SetupSuiteWithCluster(testcore.WithDynamicConfigOverrides(dynamicConfigOverrides)) } func (s *MaxBufferedEventSuite) TestMaxBufferedEventsLimit() { + env := testcore.NewEnv(s.T(), s.opts()...) + /* This test starts a workflow, and block its workflow task, then sending signals to it which will be buffered. The default max buffered event @@ -80,15 +81,15 @@ func (s *MaxBufferedEventSuite) TestMaxBufferedEventsLimit() { return sigCount, nil } - s.Worker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterWorkflow(workflowFn) testCtx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() wid := "test-max-buffered-events-limit" - wf1, err1 := s.SdkClient().ExecuteWorkflow(testCtx, client.StartWorkflowOptions{ + wf1, err1 := env.SdkClient().ExecuteWorkflow(testCtx, client.StartWorkflowOptions{ ID: wid, - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), WorkflowTaskTimeout: time.Second * 20, }, workflowFn) @@ -98,13 +99,13 @@ func (s *MaxBufferedEventSuite) TestMaxBufferedEventsLimit() { <-waitStartChan // now send 100 signals, all of them will be buffered - for i := 0; i < 100; i++ { - err := s.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", i) + for i := range 100 { + err := env.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", i) s.NoError(err) } // send 101 signal, this will fail the started workflow task - err := s.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", 100) + err := env.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", 100) s.NoError(err) // unblock goroutine that runs local activity @@ -115,7 +116,7 @@ func (s *MaxBufferedEventSuite) TestMaxBufferedEventsLimit() { s.NoError(err) s.Equal(101, sigCount) - historyEvents := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: wf1.GetID()}) + historyEvents := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: wf1.GetID()}) // Not using historyrequire here because history is not deterministic. var failedCause enumspb.WorkflowTaskFailedCause var failedCount int @@ -130,6 +131,8 @@ func (s *MaxBufferedEventSuite) TestMaxBufferedEventsLimit() { } func (s *MaxBufferedEventSuite) TestBufferedEventsMutableStateSizeLimit() { + env := testcore.NewEnv(s.T(), s.opts()...) + /* This test starts a workflow, and blocks its workflow task, then sends signals to it which will be buffered. The test is configured with @@ -172,15 +175,15 @@ func (s *MaxBufferedEventSuite) TestBufferedEventsMutableStateSizeLimit() { return sigCount, nil } - s.Worker().RegisterWorkflow(workflowFn) + env.SdkWorker().RegisterWorkflow(workflowFn) testCtx, cancel := context.WithTimeout(context.Background(), 40*time.Second) defer cancel() wid := "test-max-buffered-events-limit" - wf1, err1 := s.SdkClient().ExecuteWorkflow(testCtx, client.StartWorkflowOptions{ + wf1, err1 := env.SdkClient().ExecuteWorkflow(testCtx, client.StartWorkflowOptions{ ID: wid, - TaskQueue: s.TaskQueue(), + TaskQueue: env.WorkerTaskQueue(), WorkflowTaskTimeout: time.Second * 20, }, workflowFn) @@ -197,16 +200,16 @@ func (s *MaxBufferedEventSuite) TestBufferedEventsMutableStateSizeLimit() { s.NoError(err) largePayload := payloads.EncodeBytes(buf) - // Send signals until mutable state size limit is exceeded - // With 410KB limit and 100KB payloads, the first 3 signals succeed but the 4th exceeds the limit - // First three signals should succeed - for i := 0; i < 3; i++ { - err = s.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", largePayload) + // Send signals until mutable state size limit is exceeded. + // With 410KB limit and 100KB payloads, the first 3 signals succeed but the 4th exceeds the limit. + // First three signals should succeed. + for i := range 3 { + err = env.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", largePayload) s.NoError(err, "Signal %d should succeed", i+1) } // Fourth signal should fail due to mutable state size limit - err = s.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", largePayload) + err = env.SdkClient().SignalWorkflow(testCtx, wid, "", "test-signal", largePayload) s.Error(err, "Fourth signal should fail due to mutable state size limit") s.Contains(err.Error(), "mutable state size exceeds limit", "Expected mutable state size limit error") @@ -218,7 +221,7 @@ func (s *MaxBufferedEventSuite) TestBufferedEventsMutableStateSizeLimit() { // The workflow should be terminated, so we expect an error s.Error(err) - historyEvents := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: wf1.GetID()}) + historyEvents := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: wf1.GetID()}) // Verify that the workflow was terminated due to mutable state size limit var terminated bool diff --git a/tests/mixedbrain/build_util.go b/tests/mixedbrain/build_util.go new file mode 100644 index 00000000000..eeded327b21 --- /dev/null +++ b/tests/mixedbrain/build_util.go @@ -0,0 +1,130 @@ +package mixedbrain + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/blang/semver/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.temporal.io/server/common/headers" +) + +const ( + retryTimeout = 30 * time.Second + temporalRepo = "https://github.com/temporalio/temporal.git" + omesRepo = "https://github.com/temporalio/omes" + omesCommit = "8e4c1f54f3b0fb5e39d131f859c56fb2236395b1" +) + +func sourceRoot() string { + _, filename, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(filename), "..", "..") +} + +func cloneRepo(t *testing.T, url, destDir, ref string) { + t.Helper() + t.Logf("Cloning %s at %s...", url, ref) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + _ = os.RemoveAll(destDir) + out, err := exec.CommandContext(t.Context(), "git", "clone", "--filter=blob:none", url, destDir).CombinedOutput() + require.NoError(collect, err, "git clone failed:\n%s", out) + }, retryTimeout, 2*time.Second, "git clone "+filepath.Base(url)) + + out, err := exec.CommandContext(t.Context(), "git", "-C", destDir, "checkout", ref).CombinedOutput() + require.NoError(t, err, "git checkout %s failed:\n%s", ref, out) +} + +func buildServer(t *testing.T, srcDir, outputPath string) { + t.Helper() + t.Logf("Building server binary from %s...", srcDir) + cmd := exec.CommandContext(t.Context(), "go", + "build", + "-tags", "disable_grpc_modules", + "-o", outputPath, + "./cmd/server", + ) + cmd.Dir = srcDir + out, err := cmd.CombinedOutput() + require.NoError(t, err, "build server binary failed:\n%s", out) +} + +// resolveReleaseVersion returns the highest version for the previous minor. +// Stable releases are preferred over pre-releases per semver ordering. +// Pre-release tags (e.g. v1.30.1-184.3) serve as a fallback when no stable +// release exists yet. Returns the zero value if no matching tag is found. +func resolveReleaseVersion(serverVersion string, tags []string) semver.Version { + current := semver.MustParse(serverVersion) + targetMajor := current.Major + targetMinor := current.Minor - 1 + + var best semver.Version + for _, tag := range tags { + v, err := semver.ParseTolerant(tag) + if err != nil { + continue + } + if v.Major == targetMajor && v.Minor == targetMinor && v.GT(best) { + best = v + } + } + return best +} + +// downloadAndBuildReleaseServer finds the highest patch of the previous minor +// version, clones the repo at that tag, and builds the server binary. For +// example, if ServerVersion is 1.31.x, it will look for the highest 1.30.x +// tag. Falls back to pre-release tags (cloud versions) if no stable release +// exists yet. +func downloadAndBuildReleaseServer(t *testing.T, outputPath string) string { + t.Helper() + + t.Log("Resolving release tags...") + var version semver.Version + require.EventuallyWithT(t, func(collect *assert.CollectT) { + out, err := exec.CommandContext(t.Context(), "git", "ls-remote", "--tags", "--refs", temporalRepo).CombinedOutput() + require.NoError(collect, err, "git ls-remote failed:\n%s", out) + + var tags []string + for _, line := range strings.Split(string(out), "\n") { + parts := strings.Fields(line) + if len(parts) == 2 { + tags = append(tags, strings.TrimPrefix(parts[1], "refs/tags/")) + } + } + + version = resolveReleaseVersion(headers.ServerVersion, tags) + require.NotEqual(collect, semver.Version{}, version, "no tags found for previous minor") + }, retryTimeout, 2*time.Second, "fetch release tags") + + tag := "v" + version.String() + repoDir := filepath.Join(filepath.Dir(outputPath), "temporal-release") + cloneRepo(t, temporalRepo, repoDir, tag) + + t.Log("Building release server binary...") + buildServer(t, repoDir, outputPath) + return tag +} + +func downloadAndBuildOmes(t *testing.T, workDir string) { + t.Helper() + + repoDir := filepath.Join(workDir, "omes") + cloneRepo(t, omesRepo, repoDir, omesCommit) + + t.Log("Building Omes...") + omesBinary := filepath.Join(workDir, "omes-bin") + buildCmd := exec.CommandContext(t.Context(), "go", + "build", + "-o", omesBinary, + "./cmd", + ) + buildCmd.Dir = repoDir + out, err := buildCmd.CombinedOutput() + require.NoError(t, err, "build Omes failed:\n%s", out) +} diff --git a/tests/mixedbrain/build_util_test.go b/tests/mixedbrain/build_util_test.go new file mode 100644 index 00000000000..135ca533e76 --- /dev/null +++ b/tests/mixedbrain/build_util_test.go @@ -0,0 +1,63 @@ +package mixedbrain + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestResolveReleaseVersion(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + serverVersion string + tags []string + want string + }{ + { + name: "prefers stable over pre-release", + serverVersion: "1.31.0", + tags: []string{"v1.30.0", "v1.30.1-184.3", "v1.30.1"}, + want: "1.30.1", + }, + { + name: "falls back to pre-release when no stable", + serverVersion: "1.31.0", + tags: []string{"v1.30.1-184.3", "v1.30.0-100.1", "v1.29.5"}, + want: "1.30.1-184.3", + }, + { + name: "filters to previous minor only", + serverVersion: "1.31.0", + tags: []string{"v1.30.2", "v1.29.5", "v1.31.0", "v2.30.0"}, + want: "1.30.2", + }, + { + name: "picks highest patch", + serverVersion: "1.31.0", + tags: []string{"v1.30.0", "v1.30.3", "v1.30.1"}, + want: "1.30.3", + }, + { + name: "skips invalid tags", + serverVersion: "1.31.0", + tags: []string{"v1.30.0", "not-a-version", "v1.30.1"}, + want: "1.30.1", + }, + { + name: "zero when no matching tags", + serverVersion: "1.31.0", + tags: []string{"v1.29.0", "v1.28.0"}, + want: "0.0.0", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := resolveReleaseVersion(tc.serverVersion, tc.tags) + require.Equal(t, tc.want, got.String()) + }) + } +} diff --git a/tests/mixedbrain/config_util.go b/tests/mixedbrain/config_util.go new file mode 100644 index 00000000000..2f05388afcf --- /dev/null +++ b/tests/mixedbrain/config_util.go @@ -0,0 +1,212 @@ +package mixedbrain + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.temporal.io/server/common/cluster" + "go.temporal.io/server/common/config" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/primitives" + "go.temporal.io/server/common/testing/freeport" + "gopkg.in/yaml.v3" +) + +var ( + // Fixed ports for CI to avoid exceeding the cluster_membership.rpc_port + // SMALLINT column (max 32767). Linux ephemeral ports start at 32768. + portSetA = newPortSet(7230) + portSetB = newPortSet(8240) +) + +type portSet struct { + FrontendGRPC int + FrontendMembership int + FrontendHTTP int + HistoryGRPC int + HistoryMembership int + MatchingGRPC int + MatchingMembership int + WorkerGRPC int + WorkerMembership int +} + +func newPortSet(base int) portSet { + return portSet{ + FrontendGRPC: base, + FrontendMembership: base + 1, + FrontendHTTP: base + 2, + HistoryGRPC: base + 3, + HistoryMembership: base + 4, + MatchingGRPC: base + 5, + MatchingMembership: base + 6, + WorkerGRPC: base + 7, + WorkerMembership: base + 8, + } +} + +func newRandPortSet() portSet { + return portSet{ + FrontendGRPC: freeport.MustGetFreePort(), + FrontendMembership: freeport.MustGetFreePort(), + FrontendHTTP: freeport.MustGetFreePort(), + HistoryGRPC: freeport.MustGetFreePort(), + HistoryMembership: freeport.MustGetFreePort(), + MatchingGRPC: freeport.MustGetFreePort(), + MatchingMembership: freeport.MustGetFreePort(), + WorkerGRPC: freeport.MustGetFreePort(), + WorkerMembership: freeport.MustGetFreePort(), + } +} + +func (p portSet) frontendAddr() string { + return fmt.Sprintf("127.0.0.1:%d", p.FrontendGRPC) +} + +func (p portSet) membershipPorts() []int { + return []int{ + p.FrontendMembership, + p.HistoryMembership, + p.MatchingMembership, + p.WorkerMembership, + } +} + +func generateConfig(t *testing.T, tmpDir string, ports portSet, activePorts portSet) string { + t.Helper() + + configDir := filepath.Join(tmpDir, fmt.Sprintf("config-%d", ports.FrontendGRPC)) + require.NoError(t, os.MkdirAll(configDir, 0755)) + + dynConfigPath := filepath.Join(sourceRoot(), "config", "dynamicconfig", "development-sql.yaml") + + driver := os.Getenv("PERSISTENCE_DRIVER") + if driver == "" { + driver = "sqlite" + } + + var dataStores map[string]config.DataStore + switch driver { + case "sqlite": + newStore := func(dbName string) config.DataStore { + return config.DataStore{ + SQL: &config.SQL{ + PluginName: "sqlite", + DatabaseName: filepath.Join(tmpDir, dbName), + ConnectAddr: "localhost", + ConnectProtocol: "tcp", + ConnectAttributes: map[string]string{ + "cache": "private", + "setup": "true", + "journal_mode": "wal", + "synchronous": "2", + "busy_timeout": "10000", + }, + MaxConns: 1, + MaxIdleConns: 1, + }, + } + } + dataStores = map[string]config.DataStore{ + "default": newStore("temporal_default.db"), + "visibility": newStore("temporal_visibility.db"), + } + case "postgres12", "postgres12_pgx": + newStore := func(dbName string) config.DataStore { + return config.DataStore{ + SQL: &config.SQL{ + PluginName: driver, + DatabaseName: dbName, + ConnectAddr: "127.0.0.1:5432", + ConnectProtocol: "tcp", + User: "temporal", + Password: "temporal", + MaxConns: 20, + MaxIdleConns: 20, + MaxConnLifetime: time.Hour, + }, + } + } + dataStores = map[string]config.DataStore{ + "default": newStore("temporal"), + "visibility": newStore("temporal_visibility"), + } + default: + t.Fatalf("unsupported persistence driver: %s", driver) + } + + cfg := config.Config{ + Log: log.Config{ + Stdout: true, + Level: "info", + }, + Persistence: config.Persistence{ + DefaultStore: "default", + VisibilityStore: "visibility", + NumHistoryShards: 4, + DataStores: dataStores, + }, + Global: config.Global{ + Membership: config.Membership{ + MaxJoinDuration: 30 * time.Second, + BroadcastAddress: "127.0.0.1", + }, + }, + Services: map[string]config.Service{ + string(primitives.FrontendService): { + RPC: config.RPC{ + GRPCPort: ports.FrontendGRPC, + MembershipPort: ports.FrontendMembership, + BindOnLocalHost: true, + HTTPPort: ports.FrontendHTTP, + }}, + string(primitives.MatchingService): { + RPC: config.RPC{ + GRPCPort: ports.MatchingGRPC, + MembershipPort: ports.MatchingMembership, + BindOnLocalHost: true, + }}, + string(primitives.HistoryService): { + RPC: config.RPC{ + GRPCPort: ports.HistoryGRPC, + MembershipPort: ports.HistoryMembership, + BindOnLocalHost: true, + }}, + string(primitives.WorkerService): { + RPC: config.RPC{ + GRPCPort: ports.WorkerGRPC, + MembershipPort: ports.WorkerMembership, + BindOnLocalHost: true, + }}, + }, + ClusterMetadata: &cluster.Config{ + FailoverVersionIncrement: 10, + MasterClusterName: "active", + CurrentClusterName: "active", + ClusterInformation: map[string]cluster.ClusterInformation{ + "active": { + Enabled: true, + InitialFailoverVersion: 1, + RPCAddress: fmt.Sprintf("localhost:%d", activePorts.FrontendGRPC), + HTTPAddress: fmt.Sprintf("localhost:%d", activePorts.FrontendHTTP), + }, + }, + }, + DynamicConfigClient: &dynamicconfig.FileBasedClientConfig{ + Filepath: dynConfigPath, + PollInterval: 10 * time.Second, + }, + } + + data, err := yaml.Marshal(cfg) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(configDir, "development.yaml"), data, 0644) + require.NoError(t, err) + return configDir +} diff --git a/tests/mixedbrain/mixed_brain_test.go b/tests/mixedbrain/mixed_brain_test.go new file mode 100644 index 00000000000..1c5b5d8ab3e --- /dev/null +++ b/tests/mixedbrain/mixed_brain_test.go @@ -0,0 +1,187 @@ +package mixedbrain + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func testDuration() time.Duration { + if v := os.Getenv("MIXED_BRAIN_TEST_DURATION"); v != "" { + d, err := time.ParseDuration(v) + if err != nil { + panic(fmt.Sprintf("invalid MIXED_BRAIN_TEST_DURATION %q: %v", v, err)) + } + return d + } + return 30 * time.Second // locally we want only a smoke test to ensure it works +} + +func logDir(t *testing.T) string { + t.Helper() + dir := os.Getenv("TEST_OUTPUT_ROOT") + if dir == "" { + dir = filepath.Join(os.TempDir(), "temporal-test-output") + } + require.NoError(t, os.MkdirAll(dir, 0755)) + return dir +} + +// TestMixedBrain starts two servers in parallel, one using the current branch's binary +// and the other using the latest release binary. It then runs Omes throughput_stress +// to ensure that the mixed brain works correctly. +// Uses SQLite locally; and a dedicated database in CI for better concurrency. +func TestMixedBrain(t *testing.T) { + tmpDir := t.TempDir() + logRoot := logDir(t) + + currentBinary := filepath.Join(tmpDir, "temporal-server-current") + releaseBinary := filepath.Join(tmpDir, "temporal-server-release") + omesBinary := filepath.Join(tmpDir, "omes-bin") + + t.Run("setup", func(t *testing.T) { + t.Run("build current server", func(t *testing.T) { + t.Parallel() + buildServer(t, sourceRoot(), currentBinary) + }) + t.Run("download and build release server", func(t *testing.T) { + t.Parallel() + downloadAndBuildReleaseServer(t, releaseBinary) + }) + t.Run("download and build Omes", func(t *testing.T) { + t.Parallel() + downloadAndBuildOmes(t, tmpDir) + }) + }) + if t.Failed() { + return + } + + var portsCurrent, portsRelease portSet + if os.Getenv("CI") != "" { + portsCurrent = portSetA + portsRelease = portSetB + } else { + portsCurrent = newRandPortSet() + portsRelease = newRandPortSet() + } + + configCurrent := generateConfig(t, tmpDir, portsCurrent, portsCurrent) + configRelease := generateConfig(t, tmpDir, portsRelease, portsCurrent) + + var procCurrent, procRelease *serverProcess + var conn *grpc.ClientConn + var proxy *frontendProxy + runID := fmt.Sprintf("mixed-brain-%d", time.Now().Unix()) + nexusEndpoint := "mixed-brain-nexus" + + t.Run("start current server", func(st *testing.T) { + // Server processes use the parent t so their context survives this sub-test. + procCurrent = startServerProcess(t, "current", currentBinary, configCurrent, filepath.Join(logRoot, "mixedbrain_process-current.log")) + + var err error + conn, err = grpc.NewClient(portsCurrent.frontendAddr(), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(st, err) + + // This ensures the current server is fully booted before starting the release server. + registerDefaultNamespace(st, conn) + }) + if t.Failed() { + return + } + t.Cleanup(procCurrent.stop) + defer func() { _ = conn.Close() }() + + t.Run("start release server", func(_ *testing.T) { + procRelease = startServerProcess(t, "release", releaseBinary, configRelease, filepath.Join(logRoot, "mixedbrain_process-release.log")) + }) + if t.Failed() { + return + } + t.Cleanup(procRelease.stop) + + t.Run("form cluster", func(st *testing.T) { + waitForClusterFormation(st, conn, 90*time.Second, portsCurrent, portsRelease) + }) + if t.Failed() { + return + } + + t.Run("run omes", func(st *testing.T) { + createNexusEndpoint(st, conn, nexusEndpoint, "default", "omes-"+runID) + + proxy = startFrontendProxy(st, portsCurrent.frontendAddr(), portsRelease.frontendAddr()) + + runOmes(st, omesBinary, proxy.addr(), filepath.Join(logRoot, "mixedbrain_omes.log"), testDuration(), runID, nexusEndpoint) + }) + if t.Failed() { + return + } + t.Cleanup(proxy.stop) + + t.Run("verify", func(st *testing.T) { + procCurrent.requireAlive(st) + procRelease.requireAlive(st) + + for i, backend := range []string{"current", "release"} { + count := proxy.connCount[i].Load() + st.Logf("Proxy connections to %s: %d", backend, count) + require.Positive(st, count, "expected proxy to route traffic to %s server", backend) + } + }) +} + +// runOmes runs Omes throughput stress scenario. +// Retries if Omes fails due to search attribute not being ready yet. +// Deducts elapsed time from duration on retry so total wall time stays bounded. +func runOmes(t *testing.T, binary, serverAddr, logPath string, duration time.Duration, runID, nexusEndpoint string) { + t.Helper() + t.Logf("Running Omes throughput_stress for %v against %s", duration, serverAddr) + + started := time.Now() + for { + remaining := duration - time.Since(started) + require.Greater(t, remaining, 10*time.Second, "Omes never started successfully, check %s", logPath) + + logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + require.NoError(t, err) + + var buf bytes.Buffer + cmd := exec.CommandContext(t.Context(), binary, + "run-scenario-with-worker", + "--scenario", "throughput_stress", + "--language", "go", + "--server-address", serverAddr, + "--duration", remaining.String(), + "--timeout", (remaining + 2*time.Minute).String(), // with grace period to complete + "--run-id", runID, + "--max-concurrent", "5", + "--option", "internal-iterations=10", + "--option", "nexus-endpoint="+nexusEndpoint, + ) + cmd.Stdout = logFile + cmd.Stderr = io.MultiWriter(logFile, &buf) + cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) } + cmd.WaitDelay = 15 * time.Second + + err = cmd.Run() + _ = logFile.Close() + if err != nil && strings.Contains(buf.String(), "no mapping defined for search attribute") { + t.Log("Omes failed due to search attributes not ready, retrying...") + continue + } + require.NoError(t, err, "Omes scenario failed, check %s", logPath) + return + } +} diff --git a/tests/mixedbrain/proxy_util.go b/tests/mixedbrain/proxy_util.go new file mode 100644 index 00000000000..ae3b57caac2 --- /dev/null +++ b/tests/mixedbrain/proxy_util.go @@ -0,0 +1,75 @@ +package mixedbrain + +import ( + "io" + "net" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// frontendProxy is a TCP proxy that distributes connections round-robin across +// multiple frontend backends, ensuring Omes exercises both servers. +type frontendProxy struct { + listener net.Listener + backends []string + connCount []atomic.Int64 + next atomic.Int64 + wg sync.WaitGroup +} + +func startFrontendProxy(t *testing.T, backends ...string) *frontendProxy { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + p := &frontendProxy{ + listener: listener, + backends: backends, + connCount: make([]atomic.Int64, len(backends)), + } + go p.serve() + return p +} + +func (p *frontendProxy) addr() string { + return p.listener.Addr().String() +} + +func (p *frontendProxy) stop() { + _ = p.listener.Close() + p.wg.Wait() +} + +func (p *frontendProxy) serve() { + for { + conn, err := p.listener.Accept() + if err != nil { + return + } + idx := int(p.next.Add(1)-1) % len(p.backends) + p.connCount[idx].Add(1) + p.wg.Add(1) + go func() { + defer p.wg.Done() + p.proxyConn(conn, p.backends[idx]) + }() + } +} + +func (p *frontendProxy) proxyConn(client net.Conn, backend string) { + server, err := net.DialTimeout("tcp", backend, 5*time.Second) + if err != nil { + _ = client.Close() + return + } + p.wg.Go(func() { + _, _ = io.Copy(server, client) + _ = server.Close() + }) + _, _ = io.Copy(client, server) + _ = client.Close() +} diff --git a/tests/mixedbrain/server_util.go b/tests/mixedbrain/server_util.go new file mode 100644 index 00000000000..02314c82926 --- /dev/null +++ b/tests/mixedbrain/server_util.go @@ -0,0 +1,170 @@ +package mixedbrain + +import ( + "context" + "net" + "os" + "os/exec" + "strconv" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" + nexuspb "go.temporal.io/api/nexus/v1" + "go.temporal.io/api/operatorservice/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/api/adminservice/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" +) + +type serverProcess struct { + name string + cmd *exec.Cmd + cancel context.CancelFunc + logFile *os.File + logPath string + done chan error +} + +func startServerProcess(t *testing.T, name, binary, configDir, logPath string) *serverProcess { + t.Helper() + t.Logf("Starting %s: %s", name, binary) + + logFile, err := os.Create(logPath) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(t.Context()) + cmd := exec.CommandContext(ctx, binary, + "--root", "/", + "--config", configDir, + "--allow-no-auth", + "start", + ) + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.Dir = sourceRoot() + cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) } + cmd.WaitDelay = 15 * time.Second + + require.NoError(t, cmd.Start()) + + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + + return &serverProcess{ + name: name, + cmd: cmd, + cancel: cancel, + logFile: logFile, + logPath: logPath, + done: done, + } +} + +func (p *serverProcess) stop() { + p.cancel() + <-p.done + _ = p.logFile.Close() +} + +func (p *serverProcess) requireAlive(t *testing.T) { + t.Helper() + select { + case err := <-p.done: + p.done <- err // put back so stop() doesn't deadlock + t.Fatalf("%s exited unexpectedly: %v (logs: %s)", p.name, err, p.logPath) + default: + } +} + +func registerDefaultNamespace(t *testing.T, conn *grpc.ClientConn) { + t.Helper() + + client := workflowservice.NewWorkflowServiceClient(conn) + + require.Eventually(t, func() bool { + _, err := client.RegisterNamespace(t.Context(), &workflowservice.RegisterNamespaceRequest{ + Namespace: "default", + WorkflowExecutionRetentionPeriod: durationpb.New(24 * time.Hour), + }) + if err == nil { + return true + } + st, ok := status.FromError(err) + return ok && st.Code() == codes.AlreadyExists + }, retryTimeout, time.Second, "failed to register default namespace") +} + +func createNexusEndpoint(t *testing.T, conn *grpc.ClientConn, endpointName, namespace, taskQueue string) { + t.Helper() + + client := operatorservice.NewOperatorServiceClient(conn) + + require.Eventually(t, func() bool { + _, err := client.CreateNexusEndpoint(t.Context(), &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: endpointName, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: namespace, + TaskQueue: taskQueue, + }, + }, + }, + }, + }) + if err == nil { + return true + } + st, ok := status.FromError(err) + return ok && st.Code() == codes.AlreadyExists + }, retryTimeout, time.Second, "failed to create nexus endpoint %s", endpointName) +} + +// waitForClusterFormation waits until the server's reachable members include +// all membership ports from all provided port sets, confirming the servers +// discovered each other. Reachable members use raw ringpop addresses (membership ports). +func waitForClusterFormation(t *testing.T, conn *grpc.ClientConn, timeout time.Duration, portSets ...portSet) { + t.Helper() + + client := adminservice.NewAdminServiceClient(conn) + + require.Eventually(t, func() bool { + resp, err := client.DescribeCluster(t.Context(), &adminservice.DescribeClusterRequest{}) + if err != nil { + return false + } + membership := resp.GetMembershipInfo() + if membership == nil { + return false + } + + seen := map[int]bool{} + for _, member := range membership.GetReachableMembers() { + _, portStr, err := net.SplitHostPort(member) + if err != nil { + continue + } + port, err := strconv.Atoi(portStr) + if err != nil { + continue + } + seen[port] = true + } + + for _, ps := range portSets { + for _, port := range ps.membershipPorts() { + if !seen[port] { + t.Logf("Waiting for cluster formation: port %d not yet visible", port) + return false + } + } + } + return true + }, timeout, time.Second, "cluster did not form within %v", timeout) +} diff --git a/tests/namespace_delete_test.go b/tests/namespace_delete_test.go index 3e5c8003a60..5de4b5d9959 100644 --- a/tests/namespace_delete_test.go +++ b/tests/namespace_delete_test.go @@ -232,7 +232,7 @@ func (s *namespaceTestSuite) Test_NamespaceDelete_WithWorkflows() { // Start few workflow executions. var executions []*commonpb.WorkflowExecution - for i := 0; i < 100; i++ { + for i := range 100 { wid := "wf_id_" + strconv.Itoa(i) resp, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), @@ -316,7 +316,7 @@ func (s *namespaceTestSuite) Test_NamespaceDelete_WithMissingWorkflows() { // Start few workflow executions. var executions []*commonpb.WorkflowExecution - for i := 0; i < 10; i++ { + for i := range 10 { wid := "wf_id_" + strconv.Itoa(i) resp, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), diff --git a/tests/ndc/ndc_test.go b/tests/ndc/ndc_test.go index 18303dac203..ace84317523 100644 --- a/tests/ndc/ndc_test.go +++ b/tests/ndc/ndc_test.go @@ -1960,11 +1960,11 @@ func (s *NDCFunctionalTestSuite) TestResend() { token, ) s.NoError(err) - s.True(len(resp.HistoryBatches) <= 1) + s.LessOrEqual(len(resp.HistoryBatches), 1) batchCount++ token = resp.NextPageToken } - s.Equal(batchCount, 4) + s.Equal(4, batchCount) // GetWorkflowExecutionRawHistoryV2 start and end not on the same branch token = nil @@ -1983,11 +1983,11 @@ func (s *NDCFunctionalTestSuite) TestResend() { token, ) s.NoError(err) - s.True(len(resp.HistoryBatches) <= 1) + s.LessOrEqual(len(resp.HistoryBatches), 1) batchCount++ token = resp.NextPageToken } - s.Equal(batchCount, 2) + s.Equal(2, batchCount) // GetWorkflowExecutionRawHistoryV2 start boundary token = nil @@ -2006,11 +2006,11 @@ func (s *NDCFunctionalTestSuite) TestResend() { token, ) s.NoError(err) - s.True(len(resp.HistoryBatches) <= 1) + s.LessOrEqual(len(resp.HistoryBatches), 1) batchCount++ token = resp.NextPageToken } - s.Equal(batchCount, 3) + s.Equal(3, batchCount) // GetWorkflowExecutionRawHistoryV2 end boundary token = nil @@ -2029,11 +2029,11 @@ func (s *NDCFunctionalTestSuite) TestResend() { token, ) s.NoError(err) - s.True(len(resp.HistoryBatches) <= 1) + s.LessOrEqual(len(resp.HistoryBatches), 1) batchCount++ token = resp.NextPageToken } - s.Equal(batchCount, 10) + s.Equal(10, batchCount) } func (s *NDCFunctionalTestSuite) registerNamespace() { @@ -2314,7 +2314,7 @@ func (s *NDCFunctionalTestSuite) verifyEventHistorySize( s.NoError(err) // NOTE: non current branch can contain force termination event // so calculation should be updated, for now only assert below - s.True(historySize <= describeWorkflow.WorkflowExecutionInfo.HistorySizeBytes) + s.LessOrEqual(historySize, describeWorkflow.WorkflowExecutionInfo.HistorySizeBytes) } func (s *NDCFunctionalTestSuite) verifyVersionHistory( diff --git a/tests/ndc/replication_migration_back_test.go b/tests/ndc/replication_migration_back_test.go index 38471b91cdf..47f9ecf01cc 100644 --- a/tests/ndc/replication_migration_back_test.go +++ b/tests/ndc/replication_migration_back_test.go @@ -252,12 +252,12 @@ func (s *ReplicationMigrationBackTestSuite) longRunningMigrationBackReplicationT currentHistoryIndex := res1.DatabaseMutableState.ExecutionInfo.VersionHistories.CurrentVersionHistoryIndex currentHistoryItems := res1.DatabaseMutableState.ExecutionInfo.VersionHistories.Histories[currentHistoryIndex].Items - s.Equal(2, len(currentHistoryItems)) + s.Len(currentHistoryItems, 2) s.Equal(&historyspb.VersionHistoryItem{EventId: 5, Version: 1}, currentHistoryItems[0]) s.Equal(&historyspb.VersionHistoryItem{EventId: 10, Version: 2}, currentHistoryItems[1]) // last imported event (event 10) is a timer started event, so it should have a timer in mutablestate - s.Equal(1, len(res1.DatabaseMutableState.TimerInfos)) + s.Len(res1.DatabaseMutableState.TimerInfos, 1) s.assertHistoryEvents(context.Background(), s.namespaceID.String(), workflowID, runID, 1, 1, 10, 2, eventBatches[0:7]) } @@ -324,7 +324,7 @@ func (s *ReplicationMigrationBackTestSuite) TestHistoryReplication_LongRunningMi currentHistoryIndex := res1.DatabaseMutableState.ExecutionInfo.VersionHistories.CurrentVersionHistoryIndex currentHistoryItems := res1.DatabaseMutableState.ExecutionInfo.VersionHistories.Histories[currentHistoryIndex].Items - s.Equal(3, len(currentHistoryItems)) + s.Len(currentHistoryItems, 3) s.Equal(&historyspb.VersionHistoryItem{EventId: 5, Version: 1}, currentHistoryItems[0]) s.Equal(&historyspb.VersionHistoryItem{EventId: 10, Version: 2}, currentHistoryItems[1]) s.Equal(&historyspb.VersionHistoryItem{EventId: 12, Version: 11}, currentHistoryItems[2]) diff --git a/tests/ndc/replication_test.go b/tests/ndc/replication_test.go index b8752368545..e5bc99b0142 100644 --- a/tests/ndc/replication_test.go +++ b/tests/ndc/replication_test.go @@ -56,7 +56,7 @@ func (s *NDCFunctionalTestSuite) TestReplicationMessageDLQ() { // Applying replication messages through fetcher is Async. // So we need to retry a couple of times. Loop: - for i := 0; i < 60; i++ { + for range 60 { time.Sleep(time.Second) actualDLQMsgs := map[int64]bool{} diff --git a/tests/ndc/test_data.go b/tests/ndc/test_data.go index e14a9806ca6..515af19d837 100644 --- a/tests/ndc/test_data.go +++ b/tests/ndc/test_data.go @@ -28,20 +28,20 @@ func GetEventBatchesFromTestEvents(fileName string, workflowId string) ([][]*his if err != nil { return nil, nil, err } - var result map[string]interface{} + var result map[string]any // Unmarshal the JSON data into a map err = json.Unmarshal(content, &result) if err != nil { return nil, nil, err } - batches, ok := result[workflowId].([]interface{}) + batches, ok := result[workflowId].([]any) if !ok { return nil, nil, errors.New("workflowId not found in test data") } var elements [][]byte for i, v := range batches { - subArray, ok := v.([]interface{}) + subArray, ok := v.([]any) if !ok { return nil, nil, fmt.Errorf("element %d is not a sub-array, is type %s", i, fmt.Sprintf("%T", v)) } diff --git a/tests/nexus_api_test.go b/tests/nexus_api_test.go index dcf4c32b844..90b3ae9bfa5 100644 --- a/tests/nexus_api_test.go +++ b/tests/nexus_api_test.go @@ -11,7 +11,6 @@ import ( "github.com/nexus-rpc/sdk-go/nexus" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" nexuspb "go.temporal.io/api/nexus/v1" @@ -20,13 +19,16 @@ import ( "go.temporal.io/sdk/temporal" "go.temporal.io/server/common/authorization" "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/headers" "go.temporal.io/server/common/metrics" "go.temporal.io/server/common/metrics/metricstest" commonnexus "go.temporal.io/server/common/nexus" "go.temporal.io/server/common/nexus/nexusrpc" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/components/nexusoperations" "go.temporal.io/server/service/frontend/configs" "go.temporal.io/server/tests/testcore" + "google.golang.org/grpc/metadata" ) type headerCapture struct { @@ -48,22 +50,18 @@ func newHeaderCaptureCaller() (func(*http.Request) (*http.Response, error), *hea var op = nexus.NewOperationReference[string, string]("my-operation") type NexusApiTestSuite struct { - NexusTestBaseSuite + parallelsuite.Suite[*NexusApiTestSuite] } func TestNexusApiTestSuiteWithLegacyErrorPaths(t *testing.T) { - t.Parallel() - suite.Run(t, new(NexusApiTestSuite)) + parallelsuite.Run(t, &NexusApiTestSuite{}, false) // useTemporalFailures = false } func TestNexusApiTestSuiteWithTemporalFailures(t *testing.T) { - t.Parallel() - s := new(NexusApiTestSuite) - s.useTemporalFailures = true - suite.Run(t, s) + parallelsuite.Run(t, &NexusApiTestSuite{}, true) // useTemporalFailures = true } -func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { +func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes(useTemporalFailures bool) { callerLink := &commonpb.Link_WorkflowEvent{ Namespace: "caller-ns", WorkflowId: "caller-wf-id", @@ -92,66 +90,67 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { asyncSuccessEndpoint := testcore.RandomizeStr("test-endpoint") operationErrorOutcome := "operation_error" - if s.useTemporalFailures { + if useTemporalFailures { operationErrorOutcome = "failure" } type testcase struct { name string outcome string - endpoint *nexuspb.Endpoint + endpointName string timeout time.Duration handler nexusTaskHandler - assertion func(*testing.T, *nexusrpc.ClientStartOperationResponse[string], error, http.Header) + assertion func(*NexusApiTestSuite, *nexusrpc.ClientStartOperationResponse[string], error, http.Header) onlyByEndpoint bool } testCases := []testcase{ { - name: "sync_success", - outcome: "sync_success", - endpoint: s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), testcore.RandomizeStr("task-queue")), - handler: nexusEchoHandler, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { - require.NoError(t, err) - require.Equal(t, "input", res.Successful) + name: "sync_success", + outcome: "sync_success", + endpointName: testcore.RandomizeStr("test-endpoint"), + handler: nexusEchoHandler, + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { + s.NoError(err) + s.Equal("input", res.Successful) }, }, { name: "async_success", outcome: "async_success", onlyByEndpoint: true, - endpoint: s.createNexusEndpoint(asyncSuccessEndpoint, testcore.RandomizeStr("task-queue")), - handler: func(res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + endpointName: asyncSuccessEndpoint, + handler: func(t *testing.T, res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { // Choose an arbitrary test case to assert that all of the input is delivered to the // poll response. - s.Equal(asyncSuccessEndpoint, res.Request.Endpoint) + require.Equal(t, asyncSuccessEndpoint, res.Request.Endpoint) start := res.Request.Variant.(*nexuspb.Request_StartOperation).StartOperation - s.Equal(op.Name(), start.Operation) - s.Equal("http://localhost/callback", start.Callback) - s.Equal("request-id", start.RequestId) - s.Equal("value", res.Request.Header["key"]) - s.Len(start.GetLinks(), 1) - s.Equal(callerNexusLink.URL.String(), start.Links[0].GetUrl()) - s.Equal(callerNexusLink.Type, start.Links[0].Type) + require.Equal(t, op.Name(), start.Operation) + require.Equal(t, "http://localhost/callback", start.Callback) + require.Equal(t, "request-id", start.RequestId) + require.Equal(t, "value", res.Request.Header["key"]) + require.NotContains(t, res.Request.Header, "temporal-nexus-failure-support") + require.Len(t, start.GetLinks(), 1) + require.Equal(t, callerNexusLink.URL.String(), start.Links[0].GetUrl()) + require.Equal(t, callerNexusLink.Type, start.Links[0].Type) return &nexusTaskResponse{ StartResult: &nexus.HandlerStartOperationResultAsync{OperationToken: "test-token"}, Links: []nexus.Link{handlerNexusLink}, }, nil }, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { - require.NoError(t, err) - require.Equal(t, "test-token", res.Pending.Token) - require.Len(t, res.Links, 1) - require.Equal(t, handlerNexusLink.URL.String(), res.Links[0].URL.String()) - require.Equal(t, handlerNexusLink.Type, res.Links[0].Type) + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { + s.NoError(err) + s.Equal("test-token", res.Pending.Token) + s.Len(res.Links, 1) + s.Equal(handlerNexusLink.URL.String(), res.Links[0].URL.String()) + s.Equal(handlerNexusLink.Type, res.Links[0].Type) }, }, { - name: "operation_error", - outcome: operationErrorOutcome, - endpoint: s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), testcore.RandomizeStr("task-queue")), - handler: func(_ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + name: "operation_error", + outcome: operationErrorOutcome, + endpointName: testcore.RandomizeStr("test-endpoint"), + handler: func(_ *testing.T, _ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { return nil, &nexus.OperationError{ State: nexus.OperationStateFailed, Cause: &nexus.FailureError{ @@ -163,43 +162,43 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { }, } }, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { var operationError *nexus.OperationError - require.ErrorAs(t, err, &operationError) - require.Equal(t, nexus.OperationStateFailed, operationError.State) - if s.useTemporalFailures { + s.ErrorAs(err, &operationError) + s.Equal(nexus.OperationStateFailed, operationError.State) + if useTemporalFailures { // Through the Temporal failure round-trip, the cause chain has an extra wrapper // for the OperationError's ApplicationFailureInfo. var failureErr *nexus.FailureError - require.ErrorAs(t, operationError.Cause, &failureErr) + s.ErrorAs(operationError.Cause, &failureErr) var innerErr *nexus.FailureError - require.ErrorAs(t, failureErr.Cause, &innerErr) + s.ErrorAs(failureErr.Cause, &innerErr) tFailure, err := commonnexus.NexusFailureToTemporalFailure(innerErr.Failure) - require.NoError(t, err) + s.NoError(err) convErr := temporal.GetDefaultFailureConverter().FailureToError(tFailure) var appErr *temporal.ApplicationError - require.ErrorAs(t, convErr, &appErr) - require.Equal(t, "deliberate test failure", appErr.Message()) + s.ErrorAs(convErr, &appErr) + s.Equal("deliberate test failure", appErr.Message()) var details nexus.Failure - require.NoError(t, appErr.Details(&details)) - require.Equal(t, "v", details.Metadata["k"]) + s.NoError(appErr.Details(&details)) + s.Equal("v", details.Metadata["k"]) } else { - require.Equal(t, "deliberate test failure", operationError.Cause.Error()) + s.Equal("deliberate test failure", operationError.Cause.Error()) var failureErr *nexus.FailureError - require.ErrorAs(t, operationError.Cause, &failureErr) - require.Equal(t, map[string]string{"k": "v"}, failureErr.Failure.Metadata) + s.ErrorAs(operationError.Cause, &failureErr) + s.Equal(map[string]string{"k": "v"}, failureErr.Failure.Metadata) var details string err = json.Unmarshal(failureErr.Failure.Details, &details) - require.NoError(t, err) - require.Equal(t, "details", details) + s.NoError(err) + s.Equal("details", details) } }, }, { - name: "handler_error", - outcome: "handler_error:INTERNAL", - endpoint: s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), testcore.RandomizeStr("task-queue")), - handler: func(_ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + name: "handler_error", + outcome: "handler_error:INTERNAL", + endpointName: testcore.RandomizeStr("test-endpoint"), + handler: func(_ *testing.T, _ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { return nil, &nexus.HandlerError{ Type: nexus.HandlerErrorTypeInternal, Cause: &nexus.FailureError{ @@ -207,22 +206,22 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { }, } }, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeInternal, handlerErr.Type) - require.Equal(t, nexus.HandlerErrorRetryBehaviorUnspecified, handlerErr.RetryBehavior) - require.Equal(t, "worker", headers.Get("Temporal-Nexus-Failure-Source")) - require.Empty(t, handlerErr.Message) - require.Error(t, handlerErr.Cause) - require.Equal(t, "deliberate internal failure", handlerErr.Cause.Error()) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeInternal, handlerErr.Type) + s.Equal(nexus.HandlerErrorRetryBehaviorUnspecified, handlerErr.RetryBehavior) + s.Equal("worker", headers.Get("Temporal-Nexus-Failure-Source")) + s.Empty(handlerErr.Message) + s.Error(handlerErr.Cause) + s.Equal("deliberate internal failure", handlerErr.Cause.Error()) }, }, { - name: "handler_error_non_retryable", - outcome: "handler_error:INTERNAL", - endpoint: s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), testcore.RandomizeStr("task-queue")), - handler: func(_ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + name: "handler_error_non_retryable", + outcome: "handler_error:INTERNAL", + endpointName: testcore.RandomizeStr("test-endpoint"), + handler: func(_ *testing.T, _ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { return nil, &nexus.HandlerError{ Type: nexus.HandlerErrorTypeInternal, RetryBehavior: nexus.HandlerErrorRetryBehaviorNonRetryable, @@ -231,45 +230,53 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { }, } }, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, headers http.Header) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeInternal, handlerErr.Type) - require.Equal(t, nexus.HandlerErrorRetryBehaviorNonRetryable, handlerErr.RetryBehavior) - require.Equal(t, "worker", headers.Get("Temporal-Nexus-Failure-Source")) - require.Empty(t, handlerErr.Message) - require.Error(t, handlerErr.Cause) - require.Equal(t, "deliberate internal failure", handlerErr.Cause.Error()) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeInternal, handlerErr.Type) + s.Equal(nexus.HandlerErrorRetryBehaviorNonRetryable, handlerErr.RetryBehavior) + s.Equal("worker", headers.Get("Temporal-Nexus-Failure-Source")) + s.Empty(handlerErr.Message) + s.Error(handlerErr.Cause) + s.Equal("deliberate internal failure", handlerErr.Cause.Error()) }, }, { - name: "handler_timeout", - outcome: "handler_timeout", - endpoint: s.createNexusEndpoint(testcore.RandomizeStr("test-service"), testcore.RandomizeStr("task-queue")), - timeout: 2 * time.Second, - handler: func(res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + name: "handler_timeout", + outcome: "handler_timeout", + endpointName: testcore.RandomizeStr("test-service"), + timeout: 2 * time.Second, + handler: func(t *testing.T, res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { timeoutStr, set := res.Request.Header[nexus.HeaderRequestTimeout] - s.True(set) + require.True(t, set) timeout, err := time.ParseDuration(timeoutStr) var dispatchTimeoutBuffer = nexusoperations.MinDispatchTaskTimeout.Get(dynamicconfig.NewNoopCollection())("test") expectedMaxTimeout := 2*time.Second - dispatchTimeoutBuffer - s.LessOrEqual(timeout, expectedMaxTimeout, "timeout should be buffered") + require.LessOrEqual(t, timeout, expectedMaxTimeout, "timeout should be buffered") - s.NoError(err) + require.NoError(t, err) time.Sleep(timeout) //nolint:forbidigo // Allow time.Sleep for timeout tests return nil, nil }, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, header http.Header) { + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, header http.Header) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeUpstreamTimeout, handlerErr.Type) - require.Equal(t, "upstream timeout", handlerErr.Message) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeUpstreamTimeout, handlerErr.Type) + s.Equal("upstream timeout", handlerErr.Message) }, }, } - testFn := func(t *testing.T, tc testcase, dispatchURL string) { + testFn := func(s *NexusApiTestSuite, tc testcase, dispatchOnlyByEndpoint bool) { + env := newNexusTestEnv(s.T(), useTemporalFailures, testcore.WithDedicatedCluster()) + endpoint := env.createNexusEndpoint(s.T(), tc.endpointName, testcore.RandomizeStr("task-queue")) + var dispatchURL string + if dispatchOnlyByEndpoint { + dispatchURL = getDispatchByEndpointURL(env.HttpAPIAddress(), endpoint.Id) + } else { + dispatchURL = getDispatchByNsAndTqURL(env.HttpAPIAddress(), env.Namespace().String(), endpoint.Spec.Target.GetWorker().TaskQueue) + } ctx, cancel := context.WithCancel(testcore.NewContext()) defer cancel() @@ -279,14 +286,14 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { Service: "test-service", HTTPCaller: httpCaller, }) - require.NoError(t, err) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + s.NoError(err) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - pollerErrCh := s.nexusTaskPoller(ctx, tc.endpoint.Spec.Target.GetWorker().TaskQueue, tc.handler) + pollerErrCh := env.nexusTaskPoller(ctx, s.T(), endpoint.Spec.Target.GetWorker().TaskQueue, tc.handler) eventuallyTick := 500 * time.Millisecond - header := nexus.Header{"key": "value"} + header := nexus.Header{"key": "value", "temporal-nexus-failure-support": "true"} if tc.timeout > 0 { eventuallyTick = tc.timeout + (100 * time.Millisecond) header[nexus.HeaderRequestTimeout] = tc.timeout.String() @@ -306,23 +313,23 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { return err == nil || !(errors.As(err, &handlerErr) && handlerErr.Type == nexus.HandlerErrorTypeNotFound) }, 10*time.Second, eventuallyTick) - tc.assertion(t, result, err, headerCapture.lastHeaders) + tc.assertion(s, result, err, headerCapture.lastHeaders) s.NoError(<-pollerErrCh) snap := capture.Snapshot() - require.Equal(t, 1, len(snap["nexus_requests"])) - require.Subset(t, snap["nexus_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "method": "StartNexusOperation", "outcome": tc.outcome}) - require.Contains(t, snap["nexus_requests"][0].Tags, "nexus_endpoint") - require.Equal(t, int64(1), snap["nexus_requests"][0].Value) - require.Equal(t, metrics.MetricUnit(""), snap["nexus_requests"][0].Unit) + s.Len(snap["nexus_requests"], 1) + s.Subset(snap["nexus_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "method": "StartNexusOperation", "outcome": tc.outcome}) + s.Contains(snap["nexus_requests"][0].Tags, "nexus_endpoint") + s.Equal(int64(1), snap["nexus_requests"][0].Value) + s.Equal(metrics.MetricUnit(""), snap["nexus_requests"][0].Unit) - require.Equal(t, 1, len(snap["nexus_latency"])) - require.Subset(t, snap["nexus_latency"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "method": "StartNexusOperation", "outcome": tc.outcome}) - require.Contains(t, snap["nexus_latency"][0].Tags, "nexus_endpoint") + s.Len(snap["nexus_latency"], 1) + s.Subset(snap["nexus_latency"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "method": "StartNexusOperation", "outcome": tc.outcome}) + s.Contains(snap["nexus_latency"][0].Tags, "nexus_endpoint") // Ensure that StartOperation request is tracked as part of normal service telemetry metrics - require.Condition(t, func() bool { + s.Condition(func() bool { for _, m := range snap["service_requests"] { if opTag, ok := m.Tags["operation"]; ok && opTag == "StartNexusOperation" { return true @@ -333,38 +340,33 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Outcomes() { } for _, tc := range testCases { - s.T().Run(tc.name, func(t *testing.T) { + s.Run(tc.name, func(s *NexusApiTestSuite) { if !tc.onlyByEndpoint { - t.Run("ByNamespaceAndTaskQueue", func(t *testing.T) { - testFn(t, tc, getDispatchByNsAndTqURL(s.HttpAPIAddress(), s.Namespace().String(), tc.endpoint.Spec.Target.GetWorker().TaskQueue)) - }) + s.Run("ByNamespaceAndTaskQueue", func(s *NexusApiTestSuite) { testFn(s, tc, false) }) } - t.Run("ByEndpoint", func(t *testing.T) { - testFn(t, tc, getDispatchByEndpointURL(s.HttpAPIAddress(), tc.endpoint.Id)) - }) + s.Run("ByEndpoint", func(s *NexusApiTestSuite) { testFn(s, tc, true) }) }) } } -func (s *NexusApiTestSuite) TestNexusStartOperation_Claims() { +func (s *NexusApiTestSuite) TestNexusStartOperation_Claims(useTemporalFailures bool) { taskQueue := testcore.RandomizeStr("task-queue") - testEndpoint := s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), taskQueue) type testcase struct { name string header nexus.Header handler nexusTaskHandler - assertion func(*testing.T, *nexusrpc.ClientStartOperationResponse[string], error, map[string][]*metricstest.CapturedRecording) + assertion func(*NexusApiTestSuite, *nexusrpc.ClientStartOperationResponse[string], error, map[string][]*metricstest.CapturedRecording) } testCases := []testcase{ { name: "no header", - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, snap map[string][]*metricstest.CapturedRecording) { + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, snap map[string][]*metricstest.CapturedRecording) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) - require.Equal(t, "permission denied", handlerErr.Message) - require.Equal(t, 0, len(snap["nexus_request_preprocess_errors"])) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) + s.Equal("permission denied", handlerErr.Message) + s.Empty(snap["nexus_request_preprocess_errors"]) }, }, { @@ -372,12 +374,12 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Claims() { header: nexus.Header{ "authorization": "Bearer invalid", }, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, snap map[string][]*metricstest.CapturedRecording) { + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, snap map[string][]*metricstest.CapturedRecording) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeUnauthenticated, handlerErr.Type) - require.Equal(t, "unauthorized", handlerErr.Message) - require.Equal(t, 1, len(snap["nexus_request_preprocess_errors"])) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeUnauthenticated, handlerErr.Type) + s.Equal("unauthorized", handlerErr.Message) + s.Len(snap["nexus_request_preprocess_errors"], 1) }, }, { @@ -386,34 +388,42 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Claims() { "authorization": "Bearer test", }, handler: nexusEchoHandler, - assertion: func(t *testing.T, res *nexusrpc.ClientStartOperationResponse[string], err error, snap map[string][]*metricstest.CapturedRecording) { - require.NoError(t, err) - require.Equal(t, "input", res.Successful) - require.Equal(t, 0, len(snap["nexus_request_preprocess_errors"])) + assertion: func(s *NexusApiTestSuite, res *nexusrpc.ClientStartOperationResponse[string], err error, snap map[string][]*metricstest.CapturedRecording) { + s.NoError(err) + s.Equal("input", res.Successful) + s.Empty(snap["nexus_request_preprocess_errors"]) }, }, } - s.GetTestCluster().Host().SetOnAuthorize(func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { - if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName && (c == nil || c.Subject != "test") { - return authorization.Result{Decision: authorization.DecisionDeny}, nil - } - if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName && (c == nil || c.Subject != "test") { - return authorization.Result{Decision: authorization.DecisionDeny}, nil - } - return authorization.Result{Decision: authorization.DecisionAllow}, nil - }) - defer s.GetTestCluster().Host().SetOnAuthorize(nil) - - s.GetTestCluster().Host().SetOnGetClaims(func(ai *authorization.AuthInfo) (*authorization.Claims, error) { - if ai.AuthToken != "Bearer test" { - return nil, errors.New("invalid auth token") + testFn := func(s *NexusApiTestSuite, tc testcase, dispatchOnlyByEndpoint bool) { + env := newNexusTestEnv(s.T(), useTemporalFailures, testcore.WithDedicatedCluster()) + env.GetTestCluster().Host().SetOnAuthorize(func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { + if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName && (c == nil || c.Subject != "test") { + return authorization.Result{Decision: authorization.DecisionDeny}, nil + } + if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName && (c == nil || c.Subject != "test") { + return authorization.Result{Decision: authorization.DecisionDeny}, nil + } + return authorization.Result{Decision: authorization.DecisionAllow}, nil + }) + defer env.GetTestCluster().Host().SetOnAuthorize(nil) + env.GetTestCluster().Host().SetOnGetClaims(func(ai *authorization.AuthInfo) (*authorization.Claims, error) { + if ai.AuthToken != "Bearer test" { + return nil, errors.New("invalid auth token") + } + return &authorization.Claims{Subject: "test"}, nil + }) + defer env.GetTestCluster().Host().SetOnGetClaims(nil) + + testEndpoint := env.createNexusEndpoint(s.T(), testcore.RandomizeStr("test-endpoint"), taskQueue) + var dispatchURL string + if dispatchOnlyByEndpoint { + dispatchURL = getDispatchByEndpointURL(env.HttpAPIAddress(), testEndpoint.Id) + } else { + dispatchURL = getDispatchByNsAndTqURL(env.HttpAPIAddress(), env.Namespace().String(), taskQueue) } - return &authorization.Claims{Subject: "test"}, nil - }) - defer s.GetTestCluster().Host().SetOnGetClaims(nil) - testFn := func(t *testing.T, tc testcase, dispatchURL string) { ctx, cancel := context.WithCancel(testcore.NewContext()) defer cancel() @@ -423,71 +433,67 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_Claims() { var pollerErrCh <-chan error if tc.handler != nil { // only set on valid request - pollerErrCh = s.nexusTaskPoller(ctx, taskQueue, tc.handler) + pollerErrCh = env.nexusTaskPoller(ctx, s.T(), taskQueue, tc.handler) } - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() result, err := nexusrpc.StartOperation(ctx, client, op, "input", nexus.StartOperationOptions{ Header: tc.header, }) snap := capture.Snapshot() - s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - tc.assertion(t, result, err, snap) + tc.assertion(s, result, err, snap) if pollerErrCh != nil { s.NoError(<-pollerErrCh) } } for _, tc := range testCases { - s.T().Run(tc.name, func(t *testing.T) { - t.Run("ByNamespaceAndTaskQueue", func(t *testing.T) { - testFn(t, tc, getDispatchByNsAndTqURL(s.HttpAPIAddress(), s.Namespace().String(), taskQueue)) - }) - t.Run("ByEndpoint", func(t *testing.T) { - testFn(t, tc, getDispatchByEndpointURL(s.HttpAPIAddress(), testEndpoint.Id)) - }) + s.Run(tc.name, func(s *NexusApiTestSuite) { + s.Run("ByNamespaceAndTaskQueue", func(s *NexusApiTestSuite) { testFn(s, tc, false) }) + s.Run("ByEndpoint", func(s *NexusApiTestSuite) { testFn(s, tc, true) }) }) } } -func (s *NexusApiTestSuite) TestNexusCancelOperation_Outcomes() { +func (s *NexusApiTestSuite) TestNexusCancelOperation_Outcomes(useTemporalFailures bool) { asyncSuccessEndpoint := testcore.RandomizeStr("async-success-endpoint") type testcase struct { outcome string onlyByEndpoint bool - endpoint *nexuspb.Endpoint + endpointName string timeout time.Duration handler nexusTaskHandler - assertion func(*testing.T, error, http.Header) + assertion func(*NexusApiTestSuite, error, http.Header) } testCases := []testcase{ { outcome: "success", onlyByEndpoint: true, - endpoint: s.createNexusEndpoint(asyncSuccessEndpoint, testcore.RandomizeStr("task-queue")), - handler: func(res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { - s.Equal(asyncSuccessEndpoint, res.Request.Endpoint) + endpointName: asyncSuccessEndpoint, + handler: func(t *testing.T, res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + require.Equal(t, asyncSuccessEndpoint, res.Request.Endpoint) // Choose an arbitrary test case to assert that all of the input is delivered to the // poll response. op, ok := res.Request.Variant.(*nexuspb.Request_CancelOperation) - s.True(ok) - s.Equal("test-service", op.CancelOperation.Service) - s.Equal("operation", op.CancelOperation.Operation) - s.Equal("token", op.CancelOperation.OperationToken) - s.Equal("value", res.Request.Header["key"]) + require.True(t, ok) + require.Equal(t, "test-service", op.CancelOperation.Service) + require.Equal(t, "operation", op.CancelOperation.Operation) + require.Equal(t, "token", op.CancelOperation.OperationToken) + require.Equal(t, "value", res.Request.Header["key"]) return &nexusTaskResponse{CancelResult: new(struct{})}, nil }, - assertion: func(t *testing.T, err error, headers http.Header) { - require.NoError(t, err) + assertion: func(s *NexusApiTestSuite, err error, headers http.Header) { + s.NoError(err) }, }, { - outcome: "handler_error:INTERNAL", - endpoint: s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), testcore.RandomizeStr("task-queue")), - handler: func(_ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + outcome: "handler_error:INTERNAL", + endpointName: testcore.RandomizeStr("test-endpoint"), + handler: func(_ *testing.T, _ *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { return nil, &nexus.HandlerError{ Type: nexus.HandlerErrorTypeInternal, Cause: &nexus.FailureError{ @@ -495,38 +501,46 @@ func (s *NexusApiTestSuite) TestNexusCancelOperation_Outcomes() { }, } }, - assertion: func(t *testing.T, err error, headers http.Header) { + assertion: func(s *NexusApiTestSuite, err error, headers http.Header) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeInternal, handlerErr.Type) - require.Equal(t, "worker", headers.Get("Temporal-Nexus-Failure-Source")) - require.Empty(t, handlerErr.Message) - require.Error(t, handlerErr.Cause) - require.Equal(t, "deliberate internal failure", handlerErr.Cause.Error()) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeInternal, handlerErr.Type) + s.Equal("worker", headers.Get("Temporal-Nexus-Failure-Source")) + s.Empty(handlerErr.Message) + s.Error(handlerErr.Cause) + s.Equal("deliberate internal failure", handlerErr.Cause.Error()) }, }, { - outcome: "handler_timeout", - endpoint: s.createNexusEndpoint(testcore.RandomizeStr("test-service"), testcore.RandomizeStr("task-queue")), - timeout: 2 * time.Second, - handler: func(res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + outcome: "handler_timeout", + endpointName: testcore.RandomizeStr("test-service"), + timeout: 2 * time.Second, + handler: func(t *testing.T, res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { timeoutStr, set := res.Request.Header[nexus.HeaderRequestTimeout] - s.True(set) + require.True(t, set) timeout, err := time.ParseDuration(timeoutStr) - s.NoError(err) + require.NoError(t, err) time.Sleep(timeout) //nolint:forbidigo // Allow time.Sleep for timeout tests return nil, nil }, - assertion: func(t *testing.T, err error, headers http.Header) { + assertion: func(s *NexusApiTestSuite, err error, headers http.Header) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeUpstreamTimeout, handlerErr.Type) - require.Equal(t, "upstream timeout", handlerErr.Message) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeUpstreamTimeout, handlerErr.Type) + s.Equal("upstream timeout", handlerErr.Message) }, }, } - testFn := func(t *testing.T, tc testcase, dispatchURL string) { + testFn := func(s *NexusApiTestSuite, tc testcase, dispatchOnlyByEndpoint bool) { + env := newNexusTestEnv(s.T(), useTemporalFailures, testcore.WithDedicatedCluster()) + endpoint := env.createNexusEndpoint(s.T(), tc.endpointName, testcore.RandomizeStr("task-queue")) + var dispatchURL string + if dispatchOnlyByEndpoint { + dispatchURL = getDispatchByEndpointURL(env.HttpAPIAddress(), endpoint.Id) + } else { + dispatchURL = getDispatchByNsAndTqURL(env.HttpAPIAddress(), env.Namespace().String(), endpoint.Spec.Target.GetWorker().TaskQueue) + } ctx, cancel := context.WithCancel(testcore.NewContext()) defer cancel() @@ -536,14 +550,14 @@ func (s *NexusApiTestSuite) TestNexusCancelOperation_Outcomes() { Service: "test-service", HTTPCaller: httpCaller, }) - require.NoError(t, err) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + s.NoError(err) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - pollerErrCh := s.nexusTaskPoller(ctx, tc.endpoint.Spec.Target.GetWorker().TaskQueue, tc.handler) + pollerErrCh := env.nexusTaskPoller(ctx, s.T(), endpoint.Spec.Target.GetWorker().TaskQueue, tc.handler) handle, err := client.NewOperationHandle("operation", "token") - require.NoError(t, err) + s.NoError(err) eventuallyTick := 500 * time.Millisecond header := nexus.Header{"key": "value"} @@ -559,23 +573,23 @@ func (s *NexusApiTestSuite) TestNexusCancelOperation_Outcomes() { return err == nil || !(errors.As(err, &handlerErr) && handlerErr.Type == nexus.HandlerErrorTypeNotFound) }, 10*time.Second, eventuallyTick) - tc.assertion(t, err, headerCapture.lastHeaders) + tc.assertion(s, err, headerCapture.lastHeaders) s.NoError(<-pollerErrCh) snap := capture.Snapshot() - require.Equal(t, 1, len(snap["nexus_requests"])) - require.Subset(t, snap["nexus_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "method": "CancelNexusOperation", "outcome": tc.outcome}) - require.Contains(t, snap["nexus_requests"][0].Tags, "nexus_endpoint") - require.Equal(t, int64(1), snap["nexus_requests"][0].Value) - require.Equal(t, metrics.MetricUnit(""), snap["nexus_requests"][0].Unit) + s.Len(snap["nexus_requests"], 1) + s.Subset(snap["nexus_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "method": "CancelNexusOperation", "outcome": tc.outcome}) + s.Contains(snap["nexus_requests"][0].Tags, "nexus_endpoint") + s.Equal(int64(1), snap["nexus_requests"][0].Value) + s.Equal(metrics.MetricUnit(""), snap["nexus_requests"][0].Unit) - require.Equal(t, 1, len(snap["nexus_latency"])) - require.Subset(t, snap["nexus_latency"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "method": "CancelNexusOperation", "outcome": tc.outcome}) - require.Contains(t, snap["nexus_latency"][0].Tags, "nexus_endpoint") + s.Len(snap["nexus_latency"], 1) + s.Subset(snap["nexus_latency"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "method": "CancelNexusOperation", "outcome": tc.outcome}) + s.Contains(snap["nexus_latency"][0].Tags, "nexus_endpoint") // Ensure that CancelOperation request is tracked as part of normal service telemetry metrics - require.Condition(t, func() bool { + s.Condition(func() bool { for _, m := range snap["service_requests"] { if opTag, ok := m.Tags["operation"]; ok && opTag == "CancelNexusOperation" { return true @@ -586,39 +600,37 @@ func (s *NexusApiTestSuite) TestNexusCancelOperation_Outcomes() { } for _, tc := range testCases { - s.T().Run(tc.outcome, func(t *testing.T) { + s.Run(tc.outcome, func(s *NexusApiTestSuite) { if !tc.onlyByEndpoint { - t.Run("ByNamespaceAndTaskQueue", func(t *testing.T) { - testFn(t, tc, getDispatchByNsAndTqURL(s.HttpAPIAddress(), s.Namespace().String(), tc.endpoint.Spec.Target.GetWorker().TaskQueue)) - }) + s.Run("ByNamespaceAndTaskQueue", func(s *NexusApiTestSuite) { testFn(s, tc, false) }) } - t.Run("ByEndpoint", func(t *testing.T) { - testFn(t, tc, getDispatchByEndpointURL(s.HttpAPIAddress(), tc.endpoint.Id)) - }) + s.Run("ByEndpoint", func(s *NexusApiTestSuite) { testFn(s, tc, true) }) }) } } -func (s *NexusApiTestSuite) TestNexusStartOperation_WithNamespaceAndTaskQueue_SupportsVersioning() { +func (s *NexusApiTestSuite) TestNexusStartOperation_WithNamespaceAndTaskQueue_SupportsVersioning(useTemporalFailures bool) { + env := newNexusTestEnv(s.T(), useTemporalFailures, testcore.WithDedicatedCluster()) + env.OverrideDynamicConfig(dynamicconfig.FrontendEnableWorkerVersioningRuleAPIs, true) ctx, cancel := context.WithCancel(testcore.NewContext()) defer cancel() taskQueue := testcore.RandomizeStr("task-queue") - err := s.SdkClient().UpdateWorkerBuildIdCompatibility(ctx, &sdkclient.UpdateWorkerBuildIdCompatibilityOptions{ + err := env.SdkClient().UpdateWorkerBuildIdCompatibility(ctx, &sdkclient.UpdateWorkerBuildIdCompatibilityOptions{ //nolint:staticcheck // SA1019 deprecated TaskQueue: taskQueue, Operation: &sdkclient.BuildIDOpAddNewIDInNewDefaultSet{BuildID: "old-build-id"}, }) s.NoError(err) - err = s.SdkClient().UpdateWorkerBuildIdCompatibility(ctx, &sdkclient.UpdateWorkerBuildIdCompatibilityOptions{ + err = env.SdkClient().UpdateWorkerBuildIdCompatibility(ctx, &sdkclient.UpdateWorkerBuildIdCompatibilityOptions{ //nolint:staticcheck // SA1019 deprecated TaskQueue: taskQueue, Operation: &sdkclient.BuildIDOpAddNewIDInNewDefaultSet{BuildID: "new-build-id"}, }) s.NoError(err) - u := getDispatchByNsAndTqURL(s.HttpAPIAddress(), s.Namespace().String(), taskQueue) + u := getDispatchByNsAndTqURL(env.HttpAPIAddress(), env.Namespace().String(), taskQueue) client, err := nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{BaseURL: u, Service: "test-service"}) s.NoError(err) // Versioned poller gets task - pollerErrCh1 := s.versionedNexusTaskPoller(ctx, taskQueue, "new-build-id", nexusEchoHandler) + pollerErrCh1 := env.versionedNexusTaskPoller(ctx, s.T(), taskQueue, "new-build-id", nexusEchoHandler) result, err := nexusrpc.StartOperation(ctx, client, op, "input", nexus.StartOperationOptions{}) s.NoError(err) @@ -626,9 +638,9 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_WithNamespaceAndTaskQueue_Su s.NoError(<-pollerErrCh1) // Unversioned poller doesn't get a task - pollerErrCh2 := s.nexusTaskPoller(ctx, taskQueue, nexusEchoHandler) + pollerErrCh2 := env.nexusTaskPoller(ctx, s.T(), taskQueue, nexusEchoHandler) // Versioned poller gets task with wrong build ID - pollerErrCh3 := s.versionedNexusTaskPoller(ctx, taskQueue, "old-build-id", nexusEchoHandler) + pollerErrCh3 := env.versionedNexusTaskPoller(ctx, s.T(), taskQueue, "old-build-id", nexusEchoHandler) timeoutCtx, timeoutCancel := context.WithTimeout(ctx, time.Second*2) defer timeoutCancel() @@ -645,7 +657,61 @@ func (s *NexusApiTestSuite) TestNexusStartOperation_WithNamespaceAndTaskQueue_Su s.NoError(<-pollerErrCh3) } -func nexusEchoHandler(res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { +// TestNexusClientNameMetricPropagation verifies that when an SDK worker polls for Nexus tasks +// with client-name in gRPC metadata, the matching service emits nexus_task_requests with a +// client_name tag. This proves the header propagates e2e: SDK → frontend → matching. +func (s *NexusApiTestSuite) TestNexusClientNameMetricPropagation(useTemporalFailures bool) { + env := newNexusTestEnv(s.T(), useTemporalFailures, testcore.WithDedicatedCluster()) + const expectedClientName = "temporal-go" + taskQueue := testcore.RandomizeStr("tq") + endpoint := env.createNexusEndpoint(s.T(), testcore.RandomizeStr("endpoint"), taskQueue) + + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + + ctx, cancel := context.WithCancel(testcore.NewContext()) + defer cancel() + + // Start a poller that simulates an SDK worker with a specific client-name. + // We build the outgoing metadata from scratch (instead of using NewContext which + // sets client-name=temporal-server) so the SDK name is the only value. + pollerCtx := metadata.NewOutgoingContext(ctx, metadata.Pairs( + "client-name", expectedClientName, + "client-version", "1.0.0", + "supported-server-versions", headers.SupportedServerVersions, + "supported-features", headers.AllFeatures, + )) + pollerErrCh := env.nexusTaskPoller(pollerCtx, s.T(), taskQueue, nexusEchoHandler) + + // Trigger a Nexus start operation via HTTP to unblock the poller. + client, err := nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{ + BaseURL: getDispatchByEndpointURL(env.HttpAPIAddress(), endpoint.Id), + Service: "test-service", + }) + s.NoError(err) + + s.Eventually(func() bool { + _, err = nexusrpc.StartOperation(ctx, client, op, "input", nexus.StartOperationOptions{}) + var handlerErr *nexus.HandlerError + return err == nil || (!errors.As(err, &handlerErr) || handlerErr.Type != nexus.HandlerErrorTypeNotFound) + }, 10*time.Second, 500*time.Millisecond) + s.NoError(err) + s.NoError(<-pollerErrCh) + + // Verify that the matching service emitted nexus_task_requests with client_name tag. + snap := capture.Snapshot() + var found bool + for _, rec := range snap["nexus_task_requests"] { + if rec.Tags["client_name"] == expectedClientName { + found = true + break + } + } + s.True(found, "expected nexus_task_requests metric with client_name=%s, got entries: %v", + expectedClientName, snap["nexus_task_requests"]) +} + +func nexusEchoHandler(_ *testing.T, res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { return &nexusTaskResponse{StartResult: &nexus.HandlerStartOperationResultSync[*commonpb.Payload]{Value: res.Request.GetStartOperation().GetPayload()}}, nil } diff --git a/tests/nexus_api_validation_test.go b/tests/nexus_api_validation_test.go index 83bcc0224d7..bcd26ea96e5 100644 --- a/tests/nexus_api_validation_test.go +++ b/tests/nexus_api_validation_test.go @@ -10,8 +10,6 @@ import ( "github.com/google/uuid" "github.com/nexus-rpc/sdk-go/nexus" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" @@ -19,29 +17,30 @@ import ( "go.temporal.io/server/common/authorization" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/nexus/nexusrpc" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/service/frontend/configs" "go.temporal.io/server/tests/testcore" ) type NexusAPIValidationTestSuite struct { - NexusTestBaseSuite + parallelsuite.Suite[*NexusAPIValidationTestSuite] } func TestNexusAPIValidationTestSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(NexusAPIValidationTestSuite)) + parallelsuite.Run(t, &NexusAPIValidationTestSuite{}) } func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_WithNamespaceAndTaskQueue_NamespaceNotFound() { + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) // Also use this test to verify that namespaces are unescaped in the path. taskQueue := testcore.RandomizeStr("task-queue") namespace := "namespace not/found" - u := getDispatchByNsAndTqURL(s.HttpAPIAddress(), namespace, taskQueue) + u := getDispatchByNsAndTqURL(env.HttpAPIAddress(), namespace, taskQueue) client, err := nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{BaseURL: u, Service: "test-service"}) s.NoError(err) ctx := testcore.NewContext() - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) _, err = nexusrpc.StartOperation(ctx, client, op, "input", nexus.StartOperationOptions{}) var handlerError *nexus.HandlerError s.ErrorAs(err, &handlerError) @@ -56,19 +55,20 @@ func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_WithNamespaceAndTa } func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_WithNamespaceAndTaskQueue_NamespaceTooLong() { + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) taskQueue := testcore.RandomizeStr("task-queue") var namespace string - for i := 0; i < 500; i++ { + for range 500 { namespace += "namespace-is-a-very-long-string" } - u := getDispatchByNsAndTqURL(s.HttpAPIAddress(), namespace, taskQueue) + u := getDispatchByNsAndTqURL(env.HttpAPIAddress(), namespace, taskQueue) client, err := nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{BaseURL: u, Service: "test-service"}) s.NoError(err) ctx := testcore.NewContext() - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) _, err = nexusrpc.StartOperation(ctx, client, op, "input", nexus.StartOperationOptions{}) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) @@ -82,112 +82,131 @@ func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_WithNamespaceAndTa } func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_Forbidden() { - taskQueue := testcore.RandomizeStr("task-queue") - testEndpoint := s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), taskQueue) - type testcase struct { name string - onAuthorize func(context.Context, *authorization.Claims, *authorization.CallTarget) (authorization.Result, error) - checkFailure func(t *testing.T, handlerErr *nexus.HandlerError) + onAuthorize func(endpointName string) func(context.Context, *authorization.Claims, *authorization.CallTarget) (authorization.Result, error) + checkFailure func(s *NexusAPIValidationTestSuite, handlerErr *nexus.HandlerError) exposeAuthorizerErrors bool expectedOutcomeMetric string } testCases := []testcase{ { name: "deny with reason", - onAuthorize: func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { - if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { - return authorization.Result{Decision: authorization.DecisionDeny, Reason: "unauthorized in test"}, nil - } - if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { - if ct.NexusEndpointName != testEndpoint.Spec.Name { - panic("expected nexus endpoint name") + onAuthorize: func(endpointName string) func(context.Context, *authorization.Claims, *authorization.CallTarget) (authorization.Result, error) { + return func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { + if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { + return authorization.Result{Decision: authorization.DecisionDeny, Reason: "unauthorized in test"}, nil } - return authorization.Result{Decision: authorization.DecisionDeny, Reason: "unauthorized in test"}, nil + if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { + if ct.NexusEndpointName != endpointName { + panic("expected nexus endpoint name") + } + return authorization.Result{Decision: authorization.DecisionDeny, Reason: "unauthorized in test"}, nil + } + return authorization.Result{Decision: authorization.DecisionAllow}, nil } - return authorization.Result{Decision: authorization.DecisionAllow}, nil }, - checkFailure: func(t *testing.T, handlerErr *nexus.HandlerError) { - require.Equal(t, nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) - require.Equal(t, "permission denied: unauthorized in test", handlerErr.Message) + checkFailure: func(s *NexusAPIValidationTestSuite, handlerErr *nexus.HandlerError) { + s.Equal(nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) + s.Equal("permission denied: unauthorized in test", handlerErr.Message) }, expectedOutcomeMetric: "unauthorized", exposeAuthorizerErrors: false, }, { name: "deny without reason", - onAuthorize: func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { - if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { - return authorization.Result{Decision: authorization.DecisionDeny}, nil - } - if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { - if ct.NexusEndpointName != testEndpoint.Spec.Name { - panic("expected nexus endpoint name") + onAuthorize: func(endpointName string) func(context.Context, *authorization.Claims, *authorization.CallTarget) (authorization.Result, error) { + return func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { + if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { + return authorization.Result{Decision: authorization.DecisionDeny}, nil + } + if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { + if ct.NexusEndpointName != endpointName { + panic("expected nexus endpoint name") + } + return authorization.Result{Decision: authorization.DecisionDeny}, nil } - return authorization.Result{Decision: authorization.DecisionDeny}, nil + return authorization.Result{Decision: authorization.DecisionAllow}, nil } - return authorization.Result{Decision: authorization.DecisionAllow}, nil }, - checkFailure: func(t *testing.T, handlerErr *nexus.HandlerError) { - require.Equal(t, nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) - require.Equal(t, "permission denied", handlerErr.Message) + checkFailure: func(s *NexusAPIValidationTestSuite, handlerErr *nexus.HandlerError) { + s.Equal(nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) + s.Equal("permission denied", handlerErr.Message) }, expectedOutcomeMetric: "unauthorized", exposeAuthorizerErrors: false, }, { name: "deny with generic error", - onAuthorize: func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { - if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { - return authorization.Result{}, errors.New("some generic error") - } - if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { - if ct.NexusEndpointName != testEndpoint.Spec.Name { - panic("expected nexus endpoint name") + onAuthorize: func(endpointName string) func(context.Context, *authorization.Claims, *authorization.CallTarget) (authorization.Result, error) { + return func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { + if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { + return authorization.Result{}, errors.New("some generic error") + } + if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { + if ct.NexusEndpointName != endpointName { + panic("expected nexus endpoint name") + } + return authorization.Result{}, errors.New("some generic error") } - return authorization.Result{}, errors.New("some generic error") + return authorization.Result{Decision: authorization.DecisionAllow}, nil } - return authorization.Result{Decision: authorization.DecisionAllow}, nil }, - checkFailure: func(t *testing.T, handlerErr *nexus.HandlerError) { - require.Equal(t, nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) - require.Equal(t, "permission denied", handlerErr.Message) + checkFailure: func(s *NexusAPIValidationTestSuite, handlerErr *nexus.HandlerError) { + s.Equal(nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) + s.Equal("permission denied", handlerErr.Message) }, expectedOutcomeMetric: "unauthorized", exposeAuthorizerErrors: false, }, { name: "deny with exposed error", - onAuthorize: func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { - if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { - return authorization.Result{}, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeUnavailable, "exposed error") - } - if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { - if ct.NexusEndpointName != testEndpoint.Spec.Name { - panic("expected nexus endpoint name") + onAuthorize: func(endpointName string) func(context.Context, *authorization.Claims, *authorization.CallTarget) (authorization.Result, error) { + return func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { + if ct.APIName == configs.DispatchNexusTaskByNamespaceAndTaskQueueAPIName { + return authorization.Result{}, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeUnavailable, "exposed error") } - return authorization.Result{}, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeUnavailable, "exposed error") + if ct.APIName == configs.DispatchNexusTaskByEndpointAPIName { + if ct.NexusEndpointName != endpointName { + panic("expected nexus endpoint name") + } + return authorization.Result{}, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeUnavailable, "exposed error") + } + return authorization.Result{Decision: authorization.DecisionAllow}, nil } - return authorization.Result{Decision: authorization.DecisionAllow}, nil }, - checkFailure: func(t *testing.T, handlerErr *nexus.HandlerError) { - require.Equal(t, nexus.HandlerErrorTypeUnavailable, handlerErr.Type) - require.Equal(t, "exposed error", handlerErr.Message) + checkFailure: func(s *NexusAPIValidationTestSuite, handlerErr *nexus.HandlerError) { + s.Equal(nexus.HandlerErrorTypeUnavailable, handlerErr.Type) + s.Equal("exposed error", handlerErr.Message) }, expectedOutcomeMetric: "internal_auth_error", exposeAuthorizerErrors: true, }, } - testFn := func(t *testing.T, tc testcase, dispatchURL string) { + testFn := func(s *NexusAPIValidationTestSuite, tc testcase, dispatchOnlyByEndpoint bool) { + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) + taskQueue := testcore.RandomizeStr("task-queue") + testEndpoint := env.createNexusEndpoint(s.T(), testcore.RandomizeStr("test-endpoint"), taskQueue) + + env.GetTestCluster().Host().SetOnAuthorize(tc.onAuthorize(testEndpoint.Spec.Name)) + s.T().Cleanup(func() { env.GetTestCluster().Host().SetOnAuthorize(nil) }) + + env.OverrideDynamicConfig(dynamicconfig.ExposeAuthorizerErrors, tc.exposeAuthorizerErrors) + + var dispatchURL string + if dispatchOnlyByEndpoint { + dispatchURL = getDispatchByEndpointURL(env.HttpAPIAddress(), testEndpoint.Id) + } else { + dispatchURL = getDispatchByNsAndTqURL(env.HttpAPIAddress(), env.Namespace().String(), taskQueue) + } + client, err := nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{BaseURL: dispatchURL, Service: "test-service"}) - require.NoError(t, err) + s.NoError(err) ctx := testcore.NewContext() - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - - s.OverrideDynamicConfig(dynamicconfig.ExposeAuthorizerErrors, tc.exposeAuthorizerErrors) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) // Wait until the endpoint is loaded into the registry. s.Eventually(func() bool { @@ -197,47 +216,48 @@ func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_Forbidden() { }, 10*time.Second, 1*time.Second) var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - tc.checkFailure(t, handlerErr) + s.ErrorAs(err, &handlerErr) + tc.checkFailure(s, handlerErr) snap := capture.Snapshot() - require.Len(t, snap["nexus_requests"], 1) - require.Subset(t, snap["nexus_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "method": "StartNexusOperation", "outcome": tc.expectedOutcomeMetric}) - require.Equal(t, int64(1), snap["nexus_requests"][0].Value) + s.Len(snap["nexus_requests"], 1) + s.Subset(snap["nexus_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "method": "StartNexusOperation", "outcome": tc.expectedOutcomeMetric}) + s.Equal(int64(1), snap["nexus_requests"][0].Value) } for _, tc := range testCases { - s.Run(tc.name, func() { - s.GetTestCluster().Host().SetOnAuthorize(tc.onAuthorize) - defer s.GetTestCluster().Host().SetOnAuthorize(nil) - - s.Run("ByNamespaceAndTaskQueue", func() { - testFn(s.T(), tc, getDispatchByNsAndTqURL(s.HttpAPIAddress(), s.Namespace().String(), taskQueue)) - }) - s.Run("ByEndpoint", func() { - testFn(s.T(), tc, getDispatchByEndpointURL(s.HttpAPIAddress(), testEndpoint.Id)) - }) + s.Run(tc.name, func(s *NexusAPIValidationTestSuite) { + s.Run("ByNamespaceAndTaskQueue", func(s *NexusAPIValidationTestSuite) { testFn(s, tc, false) }) + s.Run("ByEndpoint", func(s *NexusAPIValidationTestSuite) { testFn(s, tc, true) }) }) } } func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_PayloadSizeLimit() { - taskQueue := testcore.RandomizeStr("task-queue") - testEndpoint := s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint"), taskQueue) - // Use -10 to avoid hitting MaxNexusAPIRequestBodyBytes. Actual payload will still exceed limit because of // additional Content headers. See common/rpc/grpc.go:66 input := strings.Repeat("a", (2*1024*1024)-10) - testFn := func(t *testing.T, dispatchURL string) { + testFn := func(s *NexusAPIValidationTestSuite, dispatchOnlyByEndpoint bool) { + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) + taskQueue := testcore.RandomizeStr("task-queue") + testEndpoint := env.createNexusEndpoint(s.T(), testcore.RandomizeStr("test-endpoint"), taskQueue) + + var dispatchURL string + if dispatchOnlyByEndpoint { + dispatchURL = getDispatchByEndpointURL(env.HttpAPIAddress(), testEndpoint.Id) + } else { + dispatchURL = getDispatchByNsAndTqURL(env.HttpAPIAddress(), env.Namespace().String(), taskQueue) + } + ctx, cancel := context.WithCancel(testcore.NewContext()) defer cancel() client, err := nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{BaseURL: dispatchURL, Service: "test-service"}) - require.NoError(t, err) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + s.NoError(err) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) var result *nexusrpc.ClientStartOperationResponse[string] @@ -251,42 +271,39 @@ func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_PayloadSizeLimit() return err == nil || (!errors.As(err, &handlerErr) || handlerErr.Type != nexus.HandlerErrorTypeNotFound) }, 10*time.Second, 500*time.Millisecond) - require.Nil(t, result) + s.Nil(result) var handlerErr *nexus.HandlerError - require.ErrorAs(t, err, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeBadRequest, handlerErr.Type) - require.Equal(t, "input exceeds size limit", handlerErr.Message) + s.ErrorAs(err, &handlerErr) + s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) + s.Equal("input exceeds size limit", handlerErr.Message) } - s.Run("ByNamespaceAndTaskQueue", func() { - testFn(s.T(), getDispatchByNsAndTqURL(s.HttpAPIAddress(), s.Namespace().String(), taskQueue)) - }) - s.Run("ByEndpoint", func() { - testFn(s.T(), getDispatchByEndpointURL(s.HttpAPIAddress(), testEndpoint.Id)) - }) + s.Run("ByNamespaceAndTaskQueue", func(s *NexusAPIValidationTestSuite) { testFn(s, false) }) + s.Run("ByEndpoint", func(s *NexusAPIValidationTestSuite) { testFn(s, true) }) } func (s *NexusAPIValidationTestSuite) TestNexus_RespondNexusTaskMethods_VerifiesTaskTokenMatchesRequestNamespace() { + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() tt := tokenspb.NexusTask{ - NamespaceId: s.NamespaceID().String(), + NamespaceId: env.NamespaceID().String(), TaskQueue: "test", TaskId: uuid.NewString(), } ttBytes, err := tt.Marshal() s.NoError(err) - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ - Namespace: s.ExternalNamespace().String(), + _, err = env.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: env.ExternalNamespace().String(), Identity: uuid.NewString(), TaskToken: ttBytes, Response: &nexuspb.Response{}, }) s.ErrorContains(err, "Operation requested with a token from a different namespace.") - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ - Namespace: s.ExternalNamespace().String(), + _, err = env.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ + Namespace: env.ExternalNamespace().String(), Identity: uuid.NewString(), TaskToken: ttBytes, Error: &nexuspb.HandlerError{}, @@ -295,18 +312,19 @@ func (s *NexusAPIValidationTestSuite) TestNexus_RespondNexusTaskMethods_Verifies } func (s *NexusAPIValidationTestSuite) TestNexus_RespondNexusTaskCompleted_ValidateOperationTokenLength() { + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() tt := tokenspb.NexusTask{ - NamespaceId: s.NamespaceID().String(), + NamespaceId: env.NamespaceID().String(), TaskQueue: "test", TaskId: uuid.NewString(), } ttBytes, err := tt.Marshal() s.NoError(err) - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: ttBytes, Response: &nexuspb.Response{ @@ -327,18 +345,19 @@ func (s *NexusAPIValidationTestSuite) TestNexus_RespondNexusTaskCompleted_Valida } func (s *NexusAPIValidationTestSuite) TestNexus_RespondNexusTaskMethods_ValidateFailureDetailsJSON() { + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() tt := tokenspb.NexusTask{ - NamespaceId: s.NamespaceID().String(), + NamespaceId: env.NamespaceID().String(), TaskQueue: "test", TaskId: uuid.NewString(), } ttBytes, err := tt.Marshal() s.NoError(err) - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: ttBytes, Response: &nexuspb.Response{ @@ -360,8 +379,8 @@ func (s *NexusAPIValidationTestSuite) TestNexus_RespondNexusTaskMethods_Validate s.ErrorAs(err, &invalidArgumentErr) s.Equal("failure details must be JSON serializable", invalidArgumentErr.Message) - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: ttBytes, Error: &nexuspb.HandlerError{ @@ -375,12 +394,13 @@ func (s *NexusAPIValidationTestSuite) TestNexus_RespondNexusTaskMethods_Validate } func (s *NexusAPIValidationTestSuite) TestNexusStartOperation_ByEndpoint_EndpointNotFound() { - u := getDispatchByEndpointURL(s.HttpAPIAddress(), uuid.NewString()) + env := newNexusTestEnv(s.T(), false, testcore.WithDedicatedCluster()) + u := getDispatchByEndpointURL(env.HttpAPIAddress(), uuid.NewString()) client, err := nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{BaseURL: u, Service: "test-service"}) s.NoError(err) ctx := testcore.NewContext() - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) _, err = nexusrpc.StartOperation(ctx, client, op, "input", nexus.StartOperationOptions{}) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) diff --git a/tests/nexus_endpoint_test.go b/tests/nexus_endpoint_test.go index 7f5f354b3de..4a5a709c82f 100644 --- a/tests/nexus_endpoint_test.go +++ b/tests/nexus_endpoint_test.go @@ -1,6 +1,7 @@ package tests import ( + "context" "fmt" "strings" "testing" @@ -55,21 +56,22 @@ func (s *CommonSuite) TestListOrdering() { // create some endpoints numEndpoints := 40 // minimum number of endpoints to test, there may be more in DB from other tests - for i := 0; i < numEndpoints; i++ { - s.createNexusEndpoint(testcore.RandomizeStr("test-endpoint-name")) + ctx := testcore.NewContext() + for range numEndpoints { + s.createNexusEndpoint(ctx, testcore.RandomizeStr("test-endpoint-name")) } tableVersion := initialTableVersion + int64(numEndpoints) // list from persistence manager level persistence := s.GetTestCluster().TestBase().NexusEndpointManager - persistenceResp1, err := persistence.ListNexusEndpoints(testcore.NewContext(), &p.ListNexusEndpointsRequest{ + persistenceResp1, err := persistence.ListNexusEndpoints(ctx, &p.ListNexusEndpointsRequest{ LastKnownTableVersion: tableVersion, PageSize: numEndpoints / 2, }) s.NoError(err) s.Len(persistenceResp1.Entries, numEndpoints/2) s.NotNil(persistenceResp1.NextPageToken) - persistenceResp2, err := persistence.ListNexusEndpoints(testcore.NewContext(), &p.ListNexusEndpointsRequest{ + persistenceResp2, err := persistence.ListNexusEndpoints(ctx, &p.ListNexusEndpointsRequest{ LastKnownTableVersion: tableVersion, PageSize: numEndpoints / 2, NextPageToken: persistenceResp1.NextPageToken, @@ -79,14 +81,14 @@ func (s *CommonSuite) TestListOrdering() { // list from matching level matchingClient := s.GetTestCluster().MatchingClient() - matchingResp1, err := matchingClient.ListNexusEndpoints(testcore.NewContext(), &matchingservice.ListNexusEndpointsRequest{ + matchingResp1, err := matchingClient.ListNexusEndpoints(ctx, &matchingservice.ListNexusEndpointsRequest{ LastKnownTableVersion: tableVersion, PageSize: int32(numEndpoints / 2), }) s.NoError(err) s.Len(matchingResp1.Entries, numEndpoints/2) s.NotNil(matchingResp1.NextPageToken) - matchingResp2, err := matchingClient.ListNexusEndpoints(testcore.NewContext(), &matchingservice.ListNexusEndpointsRequest{ + matchingResp2, err := matchingClient.ListNexusEndpoints(ctx, &matchingservice.ListNexusEndpointsRequest{ LastKnownTableVersion: tableVersion, PageSize: int32(numEndpoints / 2), NextPageToken: matchingResp1.NextPageToken, @@ -95,13 +97,13 @@ func (s *CommonSuite) TestListOrdering() { s.Len(matchingResp2.Entries, numEndpoints/2) // list from operator level - operatorResp1, err := s.OperatorClient().ListNexusEndpoints(testcore.NewContext(), &operatorservice.ListNexusEndpointsRequest{ + operatorResp1, err := s.OperatorClient().ListNexusEndpoints(ctx, &operatorservice.ListNexusEndpointsRequest{ PageSize: int32(numEndpoints / 2), }) s.NoError(err) s.Len(operatorResp1.Endpoints, numEndpoints/2) s.NotNil(operatorResp1.NextPageToken) - operatorResp2, err := s.OperatorClient().ListNexusEndpoints(testcore.NewContext(), &operatorservice.ListNexusEndpointsRequest{ + operatorResp2, err := s.OperatorClient().ListNexusEndpoints(ctx, &operatorservice.ListNexusEndpointsRequest{ PageSize: int32(numEndpoints / 2), NextPageToken: operatorResp1.NextPageToken, }) @@ -123,8 +125,9 @@ type MatchingSuite struct { } func (s *MatchingSuite) TestCreate() { + ctx := testcore.NewContext() endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - entry := s.createNexusEndpoint(endpointName) + entry := s.createNexusEndpoint(ctx, endpointName) s.Equal(int64(1), entry.Version) s.NotNil(entry.Endpoint.Clock) s.NotNil(entry.Endpoint.CreatedTime) @@ -132,7 +135,7 @@ func (s *MatchingSuite) TestCreate() { s.Equal(entry.Endpoint.Spec.Name, endpointName) s.Equal(entry.Endpoint.Spec.Target.GetWorker().NamespaceId, s.NamespaceID().String()) - _, err := s.GetTestCluster().MatchingClient().CreateNexusEndpoint(testcore.NewContext(), &matchingservice.CreateNexusEndpointRequest{ + _, err := s.GetTestCluster().MatchingClient().CreateNexusEndpoint(ctx, &matchingservice.CreateNexusEndpointRequest{ Spec: &persistencespb.NexusEndpointSpec{ Name: endpointName, Target: &persistencespb.NexusEndpointTarget{ @@ -152,7 +155,8 @@ func (s *MatchingSuite) TestCreate() { func (s *MatchingSuite) TestUpdate() { endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) updatedName := testcore.RandomizedNexusEndpoint(s.T().Name() + "-updated") - endpoint := s.createNexusEndpoint(endpointName) + ctx := testcore.NewContext() + endpoint := s.createNexusEndpoint(ctx, endpointName) type testcase struct { name string request *matchingservice.UpdateNexusEndpointRequest @@ -241,7 +245,8 @@ func (s *MatchingSuite) TestUpdate() { func (s *MatchingSuite) TestDelete() { endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - endpoint := s.createNexusEndpoint(endpointName) + ctx := testcore.NewContext() + endpoint := s.createNexusEndpoint(ctx, endpointName) type testcase struct { name string endpointID string @@ -279,15 +284,16 @@ func (s *MatchingSuite) TestDelete() { } func (s *MatchingSuite) TestList() { + ctx := testcore.NewContext() // initialize some endpoints - s.createNexusEndpoint("list-test-endpoint0") - s.createNexusEndpoint("list-test-endpoint1") - s.createNexusEndpoint("list-test-endpoint2") + s.createNexusEndpoint(ctx, "list-test-endpoint0") + s.createNexusEndpoint(ctx, "list-test-endpoint1") + s.createNexusEndpoint(ctx, "list-test-endpoint2") // get expected table version and endpoints for the course of the tests matchingClient := s.GetTestCluster().MatchingClient() resp, err := matchingClient.ListNexusEndpoints( - testcore.NewContext(), + ctx, &matchingservice.ListNexusEndpointsRequest{ PageSize: 100, LastKnownTableVersion: 0, @@ -406,16 +412,21 @@ func (s *MatchingSuite) TestList() { for _, tc := range testCases { s.T().Run(tc.name, func(t *testing.T) { + ctx := testcore.NewContext() listReqDone := make(chan struct{}) go func() { defer close(listReqDone) - resp, err := matchingClient.ListNexusEndpoints(testcore.NewContext(), tc.request) //nolint:revive + resp, err := matchingClient.ListNexusEndpoints(ctx, tc.request) //nolint:revive tc.assertion(resp, err) }() if tc.request.Wait && tc.request.NextPageToken == nil && tc.request.LastKnownTableVersion != 0 { - s.createNexusEndpoint("new-endpoint") + s.createNexusEndpoint(ctx, "new-endpoint") + } + select { + case <-listReqDone: + case <-ctx.Done(): + s.Fail("timed out waiting for list nexus endpoints request to complete") } - <-listReqDone }) } } @@ -726,7 +737,8 @@ func (s *OperatorSuite) TestCreate() { func (s *OperatorSuite) TestUpdate() { endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) updatedName := testcore.RandomizedNexusEndpoint(s.T().Name() + "-updated") - endpoint := s.createNexusEndpoint(endpointName) + ctx := testcore.NewContext() + endpoint := s.createNexusEndpoint(ctx, endpointName) type testcase struct { name string request *operatorservice.UpdateNexusEndpointRequest @@ -813,7 +825,8 @@ func (s *OperatorSuite) TestUpdate() { } func (s *OperatorSuite) TestDelete() { - endpoint := s.createNexusEndpoint("endpoint-to-delete-operator") + ctx := testcore.NewContext() + endpoint := s.createNexusEndpoint(ctx, "endpoint-to-delete-operator") type testcase struct { name string serviceId string @@ -851,18 +864,19 @@ func (s *OperatorSuite) TestDelete() { } func (s *OperatorSuite) TestList() { + ctx := testcore.NewContext() // initialize some endpoints - s.createNexusEndpoint("operator-list-test-service0") - s.createNexusEndpoint("operator-list-test-service1") - entryToFilter := s.createNexusEndpoint("operator-list-test-service2") + s.createNexusEndpoint(ctx, "operator-list-test-service0") + s.createNexusEndpoint(ctx, "operator-list-test-service1") + entryToFilter := s.createNexusEndpoint(ctx, "operator-list-test-service2") // get ordered endpoints for the course of the tests - resp, err := s.OperatorClient().ListNexusEndpoints(testcore.NewContext(), &operatorservice.ListNexusEndpointsRequest{}) + resp, err := s.OperatorClient().ListNexusEndpoints(ctx, &operatorservice.ListNexusEndpointsRequest{}) s.NoError(err) s.NotNil(resp) endpointsOrdered := resp.Endpoints - resp, err = s.OperatorClient().ListNexusEndpoints(testcore.NewContext(), &operatorservice.ListNexusEndpointsRequest{PageSize: 2}) + resp, err = s.OperatorClient().ListNexusEndpoints(ctx, &operatorservice.ListNexusEndpointsRequest{PageSize: 2}) s.NoError(err) s.NotNil(resp) nextPageToken := resp.NextPageToken @@ -954,7 +968,8 @@ func (s *OperatorSuite) TestList() { func (s *OperatorSuite) TestGet() { endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - endpoint := s.createNexusEndpoint(endpointName) + ctx := testcore.NewContext() + endpoint := s.createNexusEndpoint(ctx, endpointName) type testcase struct { name string @@ -1009,9 +1024,9 @@ func (s *NexusEndpointFunctionalSuite) defaultTaskQueue() *taskqueuepb.TaskQueue return &taskqueuepb.TaskQueue{Name: name, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} } -func (s *NexusEndpointFunctionalSuite) createNexusEndpoint(name string) *persistencespb.NexusEndpointEntry { +func (s *NexusEndpointFunctionalSuite) createNexusEndpoint(ctx context.Context, name string) *persistencespb.NexusEndpointEntry { resp, err := s.GetTestCluster().MatchingClient().CreateNexusEndpoint( - testcore.NewContext(), + ctx, &matchingservice.CreateNexusEndpointRequest{ Spec: &persistencespb.NexusEndpointSpec{ Name: name, diff --git a/tests/nexus_test_base.go b/tests/nexus_test_base.go index 178784cbf28..4c6aefc57ff 100644 --- a/tests/nexus_test_base.go +++ b/tests/nexus_test_base.go @@ -3,9 +3,11 @@ package tests import ( "context" "errors" + "testing" "github.com/google/uuid" "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" nexuspb "go.temporal.io/api/nexus/v1" @@ -13,39 +15,38 @@ import ( "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" - "go.temporal.io/sdk/converter" cnexus "go.temporal.io/server/common/nexus" "go.temporal.io/server/common/nexus/nexusrpc" "go.temporal.io/server/tests/testcore" ) -type NexusTestBaseSuite struct { - testcore.FunctionalTestBase +type NexusTestEnv struct { + *testcore.TestEnv useTemporalFailures bool } -func (s *NexusTestBaseSuite) mustToPayload(v any) *commonpb.Payload { - conv := converter.GetDefaultDataConverter() - payload, err := conv.ToPayload(v) - s.NoError(err) - return payload +func newNexusTestEnv(t *testing.T, useTemporalFailures bool, opts ...testcore.TestOption) *NexusTestEnv { + return &NexusTestEnv{ + TestEnv: testcore.NewEnv(t, opts...), + useTemporalFailures: useTemporalFailures, + } } -func (s *NexusTestBaseSuite) createNexusEndpoint(name string, taskQueue string) *nexuspb.Endpoint { - resp, err := s.OperatorClient().CreateNexusEndpoint(testcore.NewContext(), &operatorservice.CreateNexusEndpointRequest{ +func (env *NexusTestEnv) createNexusEndpoint(t *testing.T, name string, taskQueue string) *nexuspb.Endpoint { + resp, err := env.OperatorClient().CreateNexusEndpoint(testcore.NewContext(), &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: name, Target: &nexuspb.EndpointTarget{ Variant: &nexuspb.EndpointTarget_Worker_{ Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: taskQueue, }, }, }, }, }) - s.NoError(err) + require.NoError(t, err) return resp.Endpoint } @@ -62,21 +63,21 @@ type nexusTaskResponse struct { Links []nexus.Link } -type nexusTaskHandler func(res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) +type nexusTaskHandler func(t *testing.T, res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) -func (s *NexusTestBaseSuite) nexusTaskPoller(ctx context.Context, taskQueue string, handler nexusTaskHandler) <-chan error { - return s.versionedNexusTaskPoller(ctx, taskQueue, "", handler) +func (env *NexusTestEnv) nexusTaskPoller(ctx context.Context, t *testing.T, taskQueue string, handler nexusTaskHandler) <-chan error { + return env.versionedNexusTaskPoller(ctx, t, taskQueue, "", handler) } -func (s *NexusTestBaseSuite) versionedNexusTaskPoller(ctx context.Context, taskQueue, buildID string, handler nexusTaskHandler) <-chan error { +func (env *NexusTestEnv) versionedNexusTaskPoller(ctx context.Context, t *testing.T, taskQueue, buildID string, handler nexusTaskHandler) <-chan error { errCh := make(chan error, 1) go func() { - errCh <- s.versionedNexusTaskPollerDo(ctx, taskQueue, buildID, handler) + errCh <- env.versionedNexusTaskPollerDo(ctx, t, taskQueue, buildID, handler) }() return errCh } -func (s *NexusTestBaseSuite) versionedNexusTaskPollerDo(ctx context.Context, taskQueue, buildID string, handler nexusTaskHandler) error { +func (env *NexusTestEnv) versionedNexusTaskPollerDo(ctx context.Context, t *testing.T, taskQueue, buildID string, handler nexusTaskHandler) error { var vc *commonpb.WorkerVersionCapabilities if buildID != "" { vc = &commonpb.WorkerVersionCapabilities{ @@ -84,8 +85,8 @@ func (s *NexusTestBaseSuite) versionedNexusTaskPollerDo(ctx context.Context, tas UseVersioning: true, } } - res, err := s.GetTestCluster().FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ - Namespace: s.Namespace().String(), + res, err := env.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, @@ -106,14 +107,14 @@ func (s *NexusTestBaseSuite) versionedNexusTaskPollerDo(ctx context.Context, tas if res.Request.GetStartOperation().GetService() != "test-service" && res.Request.GetCancelOperation().GetService() != "test-service" { return errors.New("expected service to be test-service") } - result, handlerErr := handler(res) + result, handlerErr := handler(t, res) if handlerErr != nil { var opErr *nexus.OperationError var he *nexus.HandlerError if errors.As(handlerErr, &opErr) { - return s.respondNexusTaskCompletedWithOperationError(ctx, res.TaskToken, opErr) + return env.respondNexusTaskCompletedWithOperationError(ctx, res.TaskToken, opErr) } else if errors.As(handlerErr, &he) { - return s.respondNexusTaskFailed(ctx, res.TaskToken, he) + return env.respondNexusTaskFailed(ctx, res.TaskToken, he) } return handlerErr } @@ -171,8 +172,8 @@ func (s *NexusTestBaseSuite) versionedNexusTaskPollerDo(ctx context.Context, tas panic("unreachable") // nolint:revive // all implementations of HandlerStartOperationResult must be covered here, so this should be unreachable. } } - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: res.TaskToken, Response: response, @@ -183,8 +184,8 @@ func (s *NexusTestBaseSuite) versionedNexusTaskPollerDo(ctx context.Context, tas return nil } -func (s *NexusTestBaseSuite) respondNexusTaskFailed(ctx context.Context, taskToken []byte, he *nexus.HandlerError) error { - if s.useTemporalFailures { +func (env *NexusTestEnv) respondNexusTaskFailed(ctx context.Context, taskToken []byte, he *nexus.HandlerError) error { + if env.useTemporalFailures { nexusFailure, err := nexusrpc.DefaultFailureConverter().ErrorToFailure(he) if err != nil { return err @@ -193,8 +194,8 @@ func (s *NexusTestBaseSuite) respondNexusTaskFailed(ctx context.Context, taskTok if err != nil { return err } - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: taskToken, Failure: temporalFailure, @@ -227,8 +228,8 @@ func (s *NexusTestBaseSuite) respondNexusTaskFailed(ctx context.Context, taskTok protoError.RetryBehavior = enumspb.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE default: } - _, err := s.GetTestCluster().FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: taskToken, Error: protoError, @@ -239,8 +240,8 @@ func (s *NexusTestBaseSuite) respondNexusTaskFailed(ctx context.Context, taskTok return nil } -func (s *NexusTestBaseSuite) respondNexusTaskCompletedWithOperationError(ctx context.Context, taskToken []byte, opErr *nexus.OperationError) error { - if s.useTemporalFailures { +func (env *NexusTestEnv) respondNexusTaskCompletedWithOperationError(ctx context.Context, taskToken []byte, opErr *nexus.OperationError) error { + if env.useTemporalFailures { nexusFailure, err := nexusrpc.DefaultFailureConverter().ErrorToFailure(opErr) if err != nil { return err @@ -258,8 +259,8 @@ func (s *NexusTestBaseSuite) respondNexusTaskCompletedWithOperationError(ctx con }, }, } - _, err = s.GetTestCluster().FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: taskToken, Response: response, @@ -292,8 +293,8 @@ func (s *NexusTestBaseSuite) respondNexusTaskCompletedWithOperationError(ctx con }, }, } - _, err := s.GetTestCluster().FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: taskToken, Response: response, diff --git a/tests/nexus_workflow_test.go b/tests/nexus_workflow_test.go index 6c8c19952fe..2c36b38042d 100644 --- a/tests/nexus_workflow_test.go +++ b/tests/nexus_workflow_test.go @@ -14,7 +14,6 @@ import ( "github.com/nexus-rpc/sdk-go/nexus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -36,11 +35,11 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/common" "go.temporal.io/server/common/authorization" - "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/metrics/metricstest" commonnexus "go.temporal.io/server/common/nexus" "go.temporal.io/server/common/nexus/nexusrpc" "go.temporal.io/server/common/nexus/nexustest" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/testing/protorequire" "go.temporal.io/server/components/nexusoperations" "go.temporal.io/server/service/frontend/configs" @@ -49,19 +48,15 @@ import ( ) type NexusWorkflowTestSuite struct { - NexusTestBaseSuite + parallelsuite.Suite[*NexusWorkflowTestSuite] } func TestNexusWorkflowTestSuite(t *testing.T) { - t.Parallel() - suite.Run(t, &NexusWorkflowTestSuite{ - NexusTestBaseSuite: NexusTestBaseSuite{ - useTemporalFailures: true, - }, - }) + parallelsuite.Run(t, &NexusWorkflowTestSuite{}) } func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -70,7 +65,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { h := nexustest.Handler{ OnStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) { if service != "service" { - return nil, nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, `expected service to equal "service"`) + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, `expected service to equal "service"`) } return &nexus.HandlerStartOperationResultAsync{OperationToken: "test"}, nil }, @@ -78,7 +73,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { if !firstCancelSeen { // Fail cancel request once to test NexusOperationCancelRequestFailed event is recorded and request is retried. firstCancelSeen = true - return nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "intentional non-retyrable cancel error for test") + return nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "intentional non-retyrable cancel error for test") } return nil }, @@ -86,7 +81,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -100,15 +95,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, WorkflowTaskTimeout: time.Second, }, "workflow") s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -116,7 +111,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { Identity: "test", }) require.NoError(t, err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -127,7 +122,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -137,8 +132,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { }, time.Second*20, time.Millisecond*200) // Poll and wait for the "started" event to be recorded. - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -150,15 +145,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { startedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationStartedEventAttributes() != nil }) - s.Greater(startedEventIdx, 0) + s.Positive(startedEventIdx) // Get the scheduleEventId to issue the cancel command. scheduledEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationScheduledEventAttributes() != nil }) - s.Greater(scheduledEventIdx, 0) + s.Positive(scheduledEventIdx) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -175,8 +170,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { s.NoError(err) // Poll and verify first cancel request failed and allowed workflow to make progress. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -187,10 +182,10 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { cancelFailedIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCancelRequestFailedEventAttributes() != nil }) - s.Greater(cancelFailedIdx, 0) + s.Positive(cancelFailedIdx) // Start new operation to successfully cancel. - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -201,7 +196,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -209,8 +204,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { }) s.NoError(err) // Poll and wait for the "started" event to be recorded. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -226,8 +221,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { break } } - s.Greater(secondScheduledEventID, int64(0)) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + s.Positive(secondScheduledEventID) + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -245,9 +240,9 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { // Poll and wait for the cancelation request to go through. s.EventuallyWithT(func(t *assert.CollectT) { - desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) + desc, err := env.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) require.NoError(t, err) - require.Equal(t, 2, len(desc.PendingNexusOperations)) + require.Len(t, desc.PendingNexusOperations, 2) op1 := desc.PendingNexusOperations[0] require.Equal(t, pollResp.History.Events[scheduledEventIdx].EventId, op1.ScheduledEventId) require.Equal(t, endpointName, op1.Endpoint) @@ -264,10 +259,10 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_SUCCEEDED, op2.CancellationInfo.State) }, time.Second*5, time.Millisecond*30) - err = s.SdkClient().TerminateWorkflow(ctx, run.GetID(), run.GetRunID(), "test") + err = env.SdkClient().TerminateWorkflow(ctx, run.GetID(), run.GetRunID(), "test") s.NoError(err) - hist := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ + hist := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: run.GetID(), RunId: run.GetRunID(), }) @@ -276,6 +271,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelation() { } func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -301,7 +297,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -315,14 +311,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -330,7 +326,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { Identity: "test", }) require.NoError(t, err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -341,7 +337,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -350,8 +346,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { require.NoError(t, err) }, time.Second*20, time.Millisecond*200) - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -362,11 +358,11 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { completedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCompletedEventAttributes() != nil }) - s.Greater(completedEventIdx, 0) + s.Positive(completedEventIdx) s.Len(pollResp.History.Events[completedEventIdx].GetLinks(), 1) protorequire.ProtoEqual(s.T(), handlerLink, pollResp.History.Events[completedEventIdx].GetLinks()[0].GetWorkflowEvent()) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -392,18 +388,19 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion() { // Use this test case to verify that the state machine is actually deleted, the workflowservice // DescribeWorkflowExecution API filters out operations in terminal state in case they complete in a server version // without state machine deletion enabled, hence the use of the adminservice API here. - desc, err := s.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{ - Namespace: s.Namespace().String(), + desc, err := env.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: run.GetID(), }, Archetype: chasm.WorkflowArchetype, }) s.NoError(err) - s.Len(desc.DatabaseMutableState.GetExecutionInfo().SubStateMachinesByType, 0) + s.Empty(desc.DatabaseMutableState.GetExecutionInfo().SubStateMachinesByType) } func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -418,7 +415,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -432,14 +429,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -447,7 +444,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() Identity: "test", }) require.NoError(t, err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -458,7 +455,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -467,8 +464,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() require.NoError(t, err) }, time.Second*20, time.Millisecond*200) - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -479,9 +476,9 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() failedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationFailedEventAttributes() != nil }) - s.Greater(failedEventIdx, 0) + s.Positive(failedEventIdx) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -491,7 +488,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ Result: &commonpb.Payloads{ Payloads: []*commonpb.Payload{ - s.mustToPayload(pollResp.History.Events[failedEventIdx].GetNexusOperationFailedEventAttributes().Failure.Cause.Message), + testcore.MustToPayload(s.T(), pollResp.History.Events[failedEventIdx].GetNexusOperationFailedEventAttributes().Failure.Cause.Message), }, }, }, @@ -507,14 +504,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncCompletion_LargePayload() } func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - testClusterInfo, err := s.FrontendClient().GetClusterInfo(ctx, &workflowservice.GetClusterInfoRequest{}) + testClusterInfo, err := env.FrontendClient().GetClusterInfo(ctx, &workflowservice.GetClusterInfoRequest{}) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) @@ -557,7 +555,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { } s.NotNil(links[0].GetWorkflowEvent()) protorequire.ProtoEqual(s.T(), &commonpb.Link_WorkflowEvent{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: run.GetID(), RunId: run.GetRunID(), Reference: &commonpb.Link_WorkflowEvent_EventRef{ @@ -585,7 +583,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err = s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err = env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -599,8 +597,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { }) s.NoError(err) - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -608,7 +606,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -619,7 +617,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -628,8 +626,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { s.NoError(err) // Poll and verify that the "started" event was recorded. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -637,7 +635,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, }) @@ -650,7 +648,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { startedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationStartedEventAttributes() != nil }) - s.Greater(startedEventIdx, 0) + s.Positive(startedEventIdx) s.Len(pollResp.History.Events[startedEventIdx].Links, 1) l := pollResp.History.Events[startedEventIdx].Links[0].GetWorkflowEvent() @@ -660,32 +658,32 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { largeCompletion := nexusrpc.CompleteOperationOptions{ // Use -10 to avoid hitting MaxNexusAPIRequestBodyBytes. Actual payload will still exceed limit because of // additional Content headers. See common/rpc/grpc.go:66 - Result: s.mustToPayload(strings.Repeat("a", (2*1024*1024)-10)), + Result: testcore.MustToPayload(s.T(), strings.Repeat("a", (2*1024*1024)-10)), Header: nexus.Header{commonnexus.CallbackTokenHeader: callbackToken}, } s.NoError(err) - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, largeCompletion) + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, largeCompletion) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "error_bad_request"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "error_bad_request"}) invalidNamespace := testcore.RandomizeStr("ns") - _, err = s.FrontendClient().RegisterNamespace(ctx, &workflowservice.RegisterNamespaceRequest{ + _, err = env.FrontendClient().RegisterNamespace(ctx, &workflowservice.RegisterNamespaceRequest{ Namespace: invalidNamespace, WorkflowExecutionRetentionPeriod: durationpb.New(time.Hour * 24), }) s.NoError(err) // Send an invalid completion request and verify that we get an error that the namespace in the URL doesn't match the namespace in the token. - invalidCallbackURL := "http://" + s.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(invalidNamespace) + invalidCallbackURL := "http://" + env.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(invalidNamespace) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{commonnexus.CallbackTokenHeader: callbackToken}, } - _, err = s.sendNexusCompletionRequest(ctx, invalidCallbackURL, completion) + _, err = s.sendNexusCompletionRequest(ctx, env, invalidCallbackURL, completion) // Verify we get the correct error response s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) @@ -705,11 +703,11 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { s.NoError(err) completion.Header = nexus.Header{commonnexus.CallbackTokenHeader: callbackToken} - snap, err = s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err = s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "error_not_found"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "error_not_found"}) // Request fails if the state machine reference is stale. staleToken := common.CloneProto(completionToken) @@ -718,20 +716,20 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { s.NoError(err) completion.Header = nexus.Header{commonnexus.CallbackTokenHeader: callbackToken} - snap, err = s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err = s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "error_not_found"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "error_not_found"}) callbackToken, err = gen.Tokenize(completionToken) s.NoError(err) completion.Header = nexus.Header{commonnexus.CallbackTokenHeader: callbackToken} - snap, err = s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err = s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) s.NoError(err) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "success"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "success"}) // Ensure that CompleteOperation request is tracked as part of normal service telemetry metrics idx := slices.IndexFunc(snap["service_requests"], func(m *metricstest.CapturedRecording) bool { opTag, ok := m.Tags["operation"] @@ -740,15 +738,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { s.Greater(idx, -1) // Resend the request and verify we get a not found error since the operation has already completed. - snap, err = s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err = s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "error_not_found"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "error_not_found"}) // Poll again and verify the completion is recorded and triggers workflow progress. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -759,9 +757,9 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { completedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCompletedEventAttributes() != nil }) - s.Greater(completedEventIdx, 0) + s.Positive(completedEventIdx) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -785,8 +783,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { s.Equal("result", result) // Reset the workflow and check that the completion event has been reapplied. - resp, err := s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: pollResp.WorkflowExecution, Reason: "test", RequestId: uuid.NewString(), @@ -794,7 +792,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { }) s.NoError(err) - hist := s.SdkClient().GetWorkflowHistory(ctx, run.GetID(), resp.RunId, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + hist := env.SdkClient().GetWorkflowHistory(ctx, run.GetID(), resp.RunId, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) seenCompletedEvent := false for hist.HasNext() { @@ -809,8 +807,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { // Reset the workflow again to the same point with enumspb.RESET_REAPPLY_EXCLUDE_TYPE_NEXUS option // and verify that the completion event has been excluded. - resp, err = s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + resp, err = env.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: pollResp.WorkflowExecution, Reason: "test", RequestId: uuid.NewString(), @@ -819,7 +817,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { }) s.NoError(err) - hist = s.SdkClient().GetWorkflowHistory(ctx, run.GetID(), resp.RunId, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + hist = env.SdkClient().GetWorkflowHistory(ctx, run.GetID(), resp.RunId, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) seenCompletedEvent = false for hist.HasNext() { @@ -834,6 +832,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletion() { } func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueues := []string{testcore.RandomizeStr(s.T().Name()), testcore.RandomizeStr(s.T().Name())} wfRuns := []client.WorkflowRun{} @@ -843,7 +842,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() completionWFID := testcore.RandomizeStr(s.T().Name()) completionWFTaskQueue := testcore.RandomizeStr(s.T().Name()) completionWFStartReq := &workflowservice.StartWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: completionWFID, WorkflowType: &commonpb.WorkflowType{Name: completionWFType}, TaskQueue: &taskqueuepb.TaskQueue{Name: completionWFTaskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -864,13 +863,13 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() // The second workflow will have its callback attached to the running workflow. for _, tq := range taskQueues { endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ Variant: &nexuspb.EndpointTarget_Worker_{ Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: tq, }, }, @@ -879,15 +878,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: tq, }, "workflow") s.NoError(err) wfRuns = append(wfRuns, run) // Poll workflow task, and schedule Nexus operation. - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -895,7 +894,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -906,7 +905,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() Endpoint: endpointName, Service: "test-service", Operation: "my-operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -915,8 +914,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() s.NoError(err) // Poll Nexus task - nexusTask, err := s.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ - Namespace: s.Namespace().String(), + nexusTask, err := env.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskQueue: &taskqueuepb.TaskQueue{ Name: tq, @@ -945,7 +944,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() completionWFStartRequestIDs = append(completionWFStartRequestIDs, completionWFStartReq.RequestId) // Start workflow (first request) or attach callback (second request) - completionRun, err := s.FrontendClient().StartWorkflowExecution(ctx, completionWFStartReq) + completionRun, err := env.FrontendClient().StartWorkflowExecution(ctx, completionWFStartReq) s.NoError(err) completionWfRunIDs = append(completionWfRunIDs, completionRun.RunId) } @@ -959,8 +958,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() s.Equal(completionWfRunIDs[0], completionWfRunIDs[1]) // Complete workflow containing callback - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: completionWFTaskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -968,7 +967,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -978,7 +977,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ Result: &commonpb.Payloads{ Payloads: []*commonpb.Payload{ - s.mustToPayload("result"), + testcore.MustToPayload(s.T(), "result"), }, }, }, @@ -990,7 +989,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() expectedLinks := []*commonpb.Link_WorkflowEvent{ { - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: completionWFID, RunId: completionWfRunIDs[0], Reference: &commonpb.Link_WorkflowEvent_EventRef{ @@ -1001,7 +1000,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() }, }, { - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: completionWFID, RunId: completionWfRunIDs[1], Reference: &commonpb.Link_WorkflowEvent_RequestIdRef{ @@ -1015,8 +1014,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() for i, tq := range taskQueues { // Poll and verify the fabricated start event and completion event are recorded and triggers workflow progress. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1035,11 +1034,11 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() completedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCompletedEventAttributes() != nil }) - s.Greater(completedEventIdx, 0) + s.Positive(completedEventIdx) // Complete start request to verify response is ignored. - _, err = s.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: env.Namespace().String(), Identity: uuid.NewString(), TaskToken: nexusTasks[i].TaskToken, Response: &nexuspb.Response{ @@ -1057,7 +1056,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() s.NoErrorf(err, "Duplicate start response should be ignored.") // Complete caller workflow - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1084,6 +1083,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionBeforeStart() } func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -1100,7 +1100,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -1114,14 +1114,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1129,7 +1129,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { Identity: "test", }) require.NoError(t, err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1140,7 +1140,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -1150,8 +1150,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { }, time.Second*20, time.Millisecond*200) // Poll and verify that the "started" event was recorded. - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1159,7 +1159,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, }) @@ -1168,21 +1168,21 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { startedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationStartedEventAttributes() != nil }) - s.Greater(startedEventIdx, 0) + s.Positive(startedEventIdx) // Send a valid - failed completion request. completion := nexusrpc.CompleteOperationOptions{ Error: nexus.NewOperationFailedErrorf("test operation failed"), Header: nexus.Header{commonnexus.CallbackTokenHeader: callbackToken}, } - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) s.NoError(err) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "success"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "success"}) // Poll again and verify the completion is recorded and triggers workflow progress. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1193,9 +1193,9 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { completedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationFailedEventAttributes() != nil }) - s.Greater(completedEventIdx, 0) + s.Positive(completedEventIdx) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1223,118 +1223,97 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncFailure() { func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionErrors() { ctx := testcore.NewContext() - s.Run("ConfigDisabled", func() { - s.OverrideDynamicConfig(dynamicconfig.EnableNexus, false) - completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), - } - publicCallbackURL := "http://" + s.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(s.Namespace().String()) - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) - var handlerErr *nexus.HandlerError - s.ErrorAs(err, &handlerErr) - s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_request_preprocess_errors"])) - }) - - s.Run("ConfigDisabledNoIdentifier", func() { - s.OverrideDynamicConfig(dynamicconfig.EnableNexus, false) - completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), - } - publicCallbackURL := "http://" + s.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) - var handlerErr *nexus.HandlerError - s.ErrorAs(err, &handlerErr) - s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_request_preprocess_errors"])) - }) - - s.Run("NamespaceNotFound", func() { + s.Run("NamespaceNotFound", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) // Generate a token with a non-existent namespace ID tokenWithBadNamespace, err := s.generateValidCallbackToken("namespace-doesnt-exist-id", testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) - publicCallbackURL := "http://" + s.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path("namespace-doesnt-exist") + publicCallbackURL := "http://" + env.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path("namespace-doesnt-exist") completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{commonnexus.CallbackTokenHeader: tokenWithBadNamespace}, } - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_request_preprocess_errors"])) + s.Len(snap["nexus_completion_request_preprocess_errors"], 1) }) - s.Run("NamespaceNotFoundNoIdentifier", func() { + s.Run("NamespaceNotFoundNoIdentifier", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) // Generate a token with a non-existent namespace ID tokenWithBadNamespace, err := s.generateValidCallbackToken("namespace-doesnt-exist-id", testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) - publicCallbackURL := "http://" + s.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier + publicCallbackURL := "http://" + env.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{commonnexus.CallbackTokenHeader: tokenWithBadNamespace}, } - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_request_preprocess_errors"])) + s.Len(snap["nexus_completion_request_preprocess_errors"], 1) }) - s.Run("OperationTokenTooLong", func() { - publicCallbackURL := "http://" + s.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(s.Namespace().String()) + s.Run("OperationTokenTooLong", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) + publicCallbackURL := "http://" + env.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(env.Namespace().String()) // Generate a valid callback token to get past initial validation - namespaceID := s.GetNamespaceID(s.Namespace().String()) + namespaceID := env.NamespaceID().String() validToken, err := s.generateValidCallbackToken(namespaceID, testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), OperationToken: strings.Repeat("long", 2000), Header: nexus.Header{commonnexus.CallbackTokenHeader: validToken}, } - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) - s.Equal(0, len(snap["nexus_completion_request_preprocess_errors"])) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "error_bad_request"}) + s.Empty(snap["nexus_completion_request_preprocess_errors"]) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "error_bad_request"}) }) - s.Run("OperationTokenTooLongNoIdentifier", func() { - publicCallbackURL := "http://" + s.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier + s.Run("OperationTokenTooLongNoIdentifier", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) + publicCallbackURL := "http://" + env.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier // Generate a valid callback token to get past initial validation - namespaceID := s.GetNamespaceID(s.Namespace().String()) + namespaceID := env.NamespaceID().String() validToken, err := s.generateValidCallbackToken(namespaceID, testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), OperationToken: strings.Repeat("long", 2000), Header: nexus.Header{commonnexus.CallbackTokenHeader: validToken}, } - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) - s.Equal(0, len(snap["nexus_completion_request_preprocess_errors"])) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "error_bad_request"}) + s.Empty(snap["nexus_completion_request_preprocess_errors"]) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "error_bad_request"}) }) - s.Run("InvalidCallbackToken", func() { + s.Run("InvalidCallbackToken", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), } - publicCallbackURL := "http://" + s.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(s.Namespace().String()) + publicCallbackURL := "http://" + env.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(env.Namespace().String()) // metrics collection is not initialized before callback validation // Send request without callback token, helper does not add token if blank - _, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + _, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) // Verify we get the correct error response var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) @@ -1342,14 +1321,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionErrors() { s.Contains(handlerErr.Error(), "invalid callback token", "Response should indicate invalid callback token") }) - s.Run("InvalidCallbackTokenNoIdentifier", func() { + s.Run("InvalidCallbackTokenNoIdentifier", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), } - publicCallbackURL := "http://" + s.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier + publicCallbackURL := "http://" + env.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier // metrics collection is not initialized before callback validation // Send request without callback token, helper does not add token if blank - _, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + _, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) // Verify we get the correct error response var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) @@ -1357,18 +1337,19 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionErrors() { s.Contains(handlerErr.Error(), "invalid callback token", "Response should indicate invalid callback token") }) - s.Run("InvalidClientVersion", func() { - publicCallbackURL := "http://" + s.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(s.Namespace().String()) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + s.Run("InvalidClientVersion", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) + publicCallbackURL := "http://" + env.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(env.Namespace().String()) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) // Generate a valid callback token to get past initial validation - namespaceID := s.GetNamespaceID(s.Namespace().String()) + namespaceID := env.NamespaceID().String() validToken, err := s.generateValidCallbackToken(namespaceID, testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{ commonnexus.CallbackTokenHeader: validToken, "user-agent": "Nexus-go-sdk/v99.0.0", @@ -1382,22 +1363,23 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionErrors() { var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "unsupported_client"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "unsupported_client"}) }) - s.Run("InvalidClientVersionNoIdentifier", func() { - publicCallbackURL := "http://" + s.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + s.Run("InvalidClientVersionNoIdentifier", func(s *NexusWorkflowTestSuite) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) + publicCallbackURL := "http://" + env.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) // Generate a valid callback token to get past initial validation - namespaceID := s.GetNamespaceID(s.Namespace().String()) + namespaceID := env.NamespaceID().String() validToken, err := s.generateValidCallbackToken(namespaceID, testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{ commonnexus.CallbackTokenHeader: validToken, "user-agent": "Nexus-go-sdk/v99.0.0", @@ -1412,12 +1394,13 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionErrors() { var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "unsupported_client"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "unsupported_client"}) }) } func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAuthErrors() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() onAuthorize := func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { @@ -1426,29 +1409,30 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAuthErrors() { } return authorization.Result{Decision: authorization.DecisionAllow}, nil } - s.GetTestCluster().Host().SetOnAuthorize(onAuthorize) - defer s.GetTestCluster().Host().SetOnAuthorize(nil) + env.GetTestCluster().Host().SetOnAuthorize(onAuthorize) + defer env.GetTestCluster().Host().SetOnAuthorize(nil) // Generate a valid callback token for testing - namespaceID := s.GetNamespaceID(s.Namespace().String()) + namespaceID := env.NamespaceID().String() callbackToken, err := s.generateValidCallbackToken(namespaceID, testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{commonnexus.CallbackTokenHeader: callbackToken}, } - publicCallbackURL := "http://" + s.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(s.Namespace().String()) - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + publicCallbackURL := "http://" + env.HttpAPIAddress() + "/" + commonnexus.RouteCompletionCallback.Path(env.Namespace().String()) + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "unauthorized"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "unauthorized"}) } func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAuthErrorsNoIdentifier() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() onAuthorize := func(ctx context.Context, c *authorization.Claims, ct *authorization.CallTarget) (authorization.Result, error) { @@ -1457,30 +1441,31 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAuthErrorsNoId } return authorization.Result{Decision: authorization.DecisionAllow}, nil } - s.GetTestCluster().Host().SetOnAuthorize(onAuthorize) - defer s.GetTestCluster().Host().SetOnAuthorize(nil) + env.GetTestCluster().Host().SetOnAuthorize(onAuthorize) + defer env.GetTestCluster().Host().SetOnAuthorize(nil) // Generate a valid callback token for testing - namespaceID := s.GetNamespaceID(s.Namespace().String()) + namespaceID := env.NamespaceID().String() callbackToken, err := s.generateValidCallbackToken(namespaceID, testcore.RandomizeStr("workflow"), uuid.NewString()) s.NoError(err) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{commonnexus.CallbackTokenHeader: callbackToken}, } - publicCallbackURL := "http://" + s.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier - snap, err := s.sendNexusCompletionRequest(ctx, publicCallbackURL, completion) + publicCallbackURL := "http://" + env.HttpAPIAddress() + commonnexus.PathCompletionCallbackNoIdentifier + snap, err := s.sendNexusCompletionRequest(ctx, env, publicCallbackURL, completion) var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeUnauthorized, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) - s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "outcome": "unauthorized"}) + s.Len(snap["nexus_completion_requests"], 1) + s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "outcome": "unauthorized"}) } func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) // Set URL template with invalid host - s.OverrideDynamicConfig( + env.OverrideDynamicConfig( nexusoperations.CallbackURLTemplate, "http://INTERNAL/namespaces/{{.NamespaceName}}/nexus/callback") @@ -1488,13 +1473,13 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ Variant: &nexuspb.EndpointTarget_Worker_{ Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: taskQueue, }, }, @@ -1503,7 +1488,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) @@ -1512,7 +1497,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() completionWFTaskQueue := testcore.RandomizeStr(s.T().Name()) completionWFStartReq := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: testcore.RandomizeStr(s.T().Name()), WorkflowType: &commonpb.WorkflowType{Name: completionWFType}, TaskQueue: &taskqueuepb.TaskQueue{Name: completionWFTaskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -1521,9 +1506,9 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() Identity: "test", } - pollerErrCh := s.nexusTaskPoller(ctx, taskQueue, func(res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { + pollerErrCh := env.nexusTaskPoller(ctx, s.T(), taskQueue, func(t *testing.T, res *workflowservice.PollNexusTaskQueueResponse) (*nexusTaskResponse, error) { start := res.Request.Variant.(*nexuspb.Request_StartOperation).StartOperation - s.Equal(op.Name(), start.Operation) + require.Equal(t, op.Name(), start.Operation) completionWFStartReq.CompletionCallbacks = []*commonpb.Callback{ { @@ -1536,7 +1521,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() }, } - _, err := s.FrontendClient().StartWorkflowExecution(ctx, completionWFStartReq) + _, err := env.FrontendClient().StartWorkflowExecution(ctx, completionWFStartReq) if err != nil { return nil, err } @@ -1544,8 +1529,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() return &nexusTaskResponse{StartResult: &nexus.HandlerStartOperationResultAsync{OperationToken: "test-token"}}, nil }) - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1553,7 +1538,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1564,7 +1549,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() Endpoint: endpointName, Service: "test-service", Operation: "my-operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -1573,8 +1558,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() s.NoError(err) // Poll and verify that the "started" event was recorded. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1582,7 +1567,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, }) @@ -1590,11 +1575,11 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() startedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationStartedEventAttributes() != nil }) - s.Greater(startedEventIdx, 0) + s.Positive(startedEventIdx) // Complete workflow containing callback - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: completionWFTaskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1602,7 +1587,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1612,7 +1597,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ Result: &commonpb.Payloads{ Payloads: []*commonpb.Payload{ - s.mustToPayload("result"), + testcore.MustToPayload(s.T(), "result"), }, }, }, @@ -1623,8 +1608,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() s.NoError(err) // Poll again and verify the completion is recorded and triggers workflow progress. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1635,9 +1620,9 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() completedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCompletedEventAttributes() != nil }) - s.Greater(completedEventIdx, 0) + s.Positive(completedEventIdx) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1664,6 +1649,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionInternalAuth() } func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_CancelationEventuallyDelivered() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -1688,7 +1674,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_Cancelati listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -1702,13 +1688,13 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_Cancelati }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1716,7 +1702,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_Cancelati Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1727,7 +1713,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_Cancelati Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -1746,8 +1732,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_Cancelati s.NoError(err) // Poll and cancel the operation. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1760,10 +1746,10 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_Cancelati scheduledEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationScheduledEventAttributes() != nil }) - s.Greater(scheduledEventIdx, 0) + s.Positive(scheduledEventIdx) scheduledEventID := pollResp.History.Events[scheduledEventIdx].EventId - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1779,15 +1765,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationCancelBeforeStarted_Cancelati }) s.NoError(err) - canStartCh <- struct{}{} - s.WaitForChannel(ctx, cancelSentCh) + env.SendToChannel(ctx, canStartCh) + env.WaitForChannel(ctx, cancelSentCh) // Terminate the workflow for good measure. - err = s.SdkClient().TerminateWorkflow(ctx, run.GetID(), run.GetRunID(), "test") + err = env.SdkClient().TerminateWorkflow(ctx, run.GetID(), run.GetRunID(), "test") s.NoError(err) // Assert that we did not send a cancel request until after the operation was started. - hist := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ + hist := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: run.GetID(), RunId: run.GetRunID(), }) @@ -1797,6 +1783,7 @@ NexusOperationStarted`, hist) } func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -1813,7 +1800,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -1827,13 +1814,13 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1841,7 +1828,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1852,7 +1839,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -1861,8 +1848,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { s.NoError(err) // Poll and verify that the "started" event was recorded. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1870,7 +1857,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, }) @@ -1879,15 +1866,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { startedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationStartedEventAttributes() != nil }) - s.Greater(startedEventIdx, 0) + s.Positive(startedEventIdx) // Remember the workflow task completed event ID (next after the last WFT started), we'll use it to test reset // below. wftCompletedEventID := int64(len(pollResp.History.Events)) // Reset the workflow and check that the started event has been reapplied. - resetResp, err := s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + resetResp, err := env.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: pollResp.WorkflowExecution, Reason: "test", RequestId: uuid.NewString(), @@ -1895,7 +1882,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { }) s.NoError(err) - hist := s.SdkClient().GetWorkflowHistory(ctx, run.GetID(), resetResp.RunId, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + hist := env.SdkClient().GetWorkflowHistory(ctx, run.GetID(), resetResp.RunId, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) seenStartedEvent := false for hist.HasNext() { @@ -1907,15 +1894,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { } s.True(seenStartedEvent) completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload("result"), + Result: testcore.MustToPayload(s.T(), "result"), Header: nexus.Header{commonnexus.CallbackTokenHeader: callbackToken}, } - _, err = s.sendNexusCompletionRequest(ctx, publicCallbackUrl, completion) + _, err = s.sendNexusCompletionRequest(ctx, env, publicCallbackUrl, completion) s.NoError(err) // Poll again and verify the completion is recorded and triggers workflow progress. - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -1926,9 +1913,9 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { completedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCompletedEventAttributes() != nil }) - s.Greater(completedEventIdx, 0) + s.Positive(completedEventIdx) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -1948,12 +1935,13 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationAsyncCompletionAfterReset() { }) s.NoError(err) var result string - run = s.SdkClient().GetWorkflow(ctx, run.GetID(), resetResp.RunId) + run = env.SdkClient().GetWorkflow(ctx, run.GetID(), resetResp.RunId) s.NoError(run.Get(ctx, &result)) s.Equal("result", result) } func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationWithNilIO() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() callerTaskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) @@ -1961,13 +1949,13 @@ func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationWithNilIO() { endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) handlerWorkflowID := testcore.RandomizeStr(s.T().Name()) - _, err := s.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ Variant: &nexuspb.EndpointTarget_Worker_{ Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: callerTaskQueue, }, }, @@ -1977,7 +1965,7 @@ func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationWithNilIO() { s.NoError(err) w := worker.New( - s.SdkClient(), + env.SdkClient(), callerTaskQueue, worker.Options{}, ) @@ -2010,21 +1998,21 @@ func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationWithNilIO() { w.Start() defer w.Stop() - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: callerTaskQueue, }, callerWF, nil) s.NoError(err) - pollRes, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollRes, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: handlerWorkflowTaskQueue, }, Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), TaskToken: pollRes.TaskToken, Identity: "test", Commands: []*commandpb.Command{ @@ -2041,370 +2029,370 @@ func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationWithNilIO() { s.NoError(err) s.NoError(run.Get(ctx, nil)) - history := s.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + history := env.SdkClient().GetWorkflowHistory(ctx, run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) for history.HasNext() { ev, err := history.Next() s.NoError(err) if attr := ev.GetNexusOperationCompletedEventAttributes(); attr != nil { - protorequire.ProtoEqual(s.T(), s.mustToPayload(nil), attr.GetResult()) + protorequire.ProtoEqual(s.T(), testcore.MustToPayload(s.T(), nil), attr.GetResult()) break } } } func (s *NexusWorkflowTestSuite) TestNexusSyncOperationErrorRehydration() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) - defer cancel() - taskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) - endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - converter := temporal.NewDefaultFailureConverter(temporal.DefaultFailureConverterOptions{}) - - _, err := s.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ - Spec: &nexuspb.EndpointSpec{ - Name: endpointName, - Target: &nexuspb.EndpointTarget{ - Variant: &nexuspb.EndpointTarget_Worker_{ - Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), - TaskQueue: taskQueue, - }, - }, - }, - }, - }) - s.NoError(err) - - w := worker.New( - s.SdkClient(), - taskQueue, - worker.Options{}, - ) - - svc := nexus.NewService("test") - op := nexus.NewSyncOperation("op", func(ctx context.Context, outcome string, soo nexus.StartOperationOptions) (nexus.NoValue, error) { - switch outcome { - case "fail-handler-internal": - return nil, nexus.HandlerErrorf(nexus.HandlerErrorTypeInternal, "intentional internal error") - case "fail-handler-app-error": - return nil, temporal.NewApplicationError("app error", "TestError", "details") - case "fail-handler-bad-request": - return nil, nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "bad request") - case "fail-operation": - return nil, nexus.NewOperationFailedError("some error") - case "fail-operation-app-error": - return nil, temporal.NewNonRetryableApplicationError("app error", "TestError", nil, "details") - } - return nil, nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "unexpected outcome: %s", outcome) - }) - s.NoError(svc.Register(op)) - - callerWF := func(ctx workflow.Context, outcome string) (nexus.NoValue, error) { - c := workflow.NewNexusClient(endpointName, svc.Name) - fut := c.ExecuteOperation(ctx, op, outcome, workflow.NexusOperationOptions{}) - return nil, fut.Get(ctx, nil) - } - - w.RegisterNexusService(svc) - w.RegisterWorkflow(callerWF) - s.NoError(w.Start()) - s.T().Cleanup(w.Stop) - - cases := []struct { + type testcase struct { outcome string metricsOutcome string - checkPendingError func(t *testing.T, pendingErr error) - checkWorkflowError func(t *testing.T, wfErr error) - }{ + checkPendingError func(s *NexusWorkflowTestSuite, pendingErr error) + checkWorkflowError func(s *NexusWorkflowTestSuite, wfErr error) + } + cases := []testcase{ { outcome: "fail-handler-internal", metricsOutcome: "handler-error:INTERNAL", - checkPendingError: func(t *testing.T, pendingErr error) { + checkPendingError: func(s *NexusWorkflowTestSuite, pendingErr error) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, pendingErr, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeInternal, handlerErr.Type) - var appErr *temporal.ApplicationError - require.ErrorAs(t, handlerErr.Cause, &appErr) - require.Equal(t, "intentional internal error", appErr.Message()) + s.ErrorAs(pendingErr, &handlerErr) + s.Equal(nexus.HandlerErrorTypeInternal, handlerErr.Type) + s.Equal("intentional internal error", handlerErr.Message) }, }, { outcome: "fail-handler-app-error", metricsOutcome: "handler-error:INTERNAL", - checkPendingError: func(t *testing.T, pendingErr error) { + checkPendingError: func(s *NexusWorkflowTestSuite, pendingErr error) { var handlerErr *nexus.HandlerError - require.ErrorAs(t, pendingErr, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeInternal, handlerErr.Type) + s.ErrorAs(pendingErr, &handlerErr) + s.Equal(nexus.HandlerErrorTypeInternal, handlerErr.Type) var appErr *temporal.ApplicationError - require.ErrorAs(t, handlerErr.Cause, &appErr) - require.Equal(t, "app error", appErr.Message()) - require.Equal(t, "TestError", appErr.Type()) + s.ErrorAs(handlerErr.Cause, &appErr) + s.Equal("app error", appErr.Message()) + s.Equal("TestError", appErr.Type()) var details string - require.NoError(t, appErr.Details(&details)) - require.Equal(t, "details", details) + s.NoError(appErr.Details(&details)) + s.Equal("details", details) }, }, { outcome: "fail-handler-bad-request", metricsOutcome: "handler-error:BAD_REQUEST", - checkWorkflowError: func(t *testing.T, wfErr error) { + checkWorkflowError: func(s *NexusWorkflowTestSuite, wfErr error) { var opErr *temporal.NexusOperationError - require.ErrorAs(t, wfErr, &opErr) + s.ErrorAs(wfErr, &opErr) var handlerErr *nexus.HandlerError - require.ErrorAs(t, opErr, &handlerErr) - require.Equal(t, nexus.HandlerErrorTypeBadRequest, handlerErr.Type) - var appErr *temporal.ApplicationError - require.ErrorAs(t, handlerErr.Cause, &appErr) - require.Equal(t, "bad request", appErr.Message()) + s.ErrorAs(opErr, &handlerErr) + s.Equal(nexus.HandlerErrorTypeBadRequest, handlerErr.Type) + s.Equal("bad request", handlerErr.Message) }, }, { outcome: "fail-operation", metricsOutcome: "operation-unsuccessful:failed", - checkWorkflowError: func(t *testing.T, wfErr error) { + checkWorkflowError: func(s *NexusWorkflowTestSuite, wfErr error) { var opErr *temporal.NexusOperationError - require.ErrorAs(t, wfErr, &opErr) - require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message) + s.ErrorAs(wfErr, &opErr) + s.Equal("nexus operation completed unsuccessfully", opErr.Message) var appErr *temporal.ApplicationError - require.ErrorAs(t, opErr.Cause, &appErr) - require.Equal(t, "some error", appErr.Message()) + s.ErrorAs(opErr.Cause, &appErr) + s.Equal("some error", appErr.Message()) }, }, { outcome: "fail-operation-app-error", metricsOutcome: "handler-error:INTERNAL", - checkWorkflowError: func(t *testing.T, wfErr error) { + checkWorkflowError: func(s *NexusWorkflowTestSuite, wfErr error) { var opErr *temporal.NexusOperationError - require.ErrorAs(t, wfErr, &opErr) + s.ErrorAs(wfErr, &opErr) var appErr *temporal.ApplicationError - require.ErrorAs(t, opErr, &appErr) - require.Equal(t, "app error", appErr.Message()) - require.Equal(t, "TestError", appErr.Type()) + s.ErrorAs(opErr, &appErr) + s.Equal("app error", appErr.Message()) + s.Equal("TestError", appErr.Type()) var details string - require.NoError(t, appErr.Details(&details)) - require.Equal(t, "details", details) + s.NoError(appErr.Details(&details)) + s.Equal("details", details) }, }, } - for _, tc := range cases { - s.T().Run(tc.outcome, func(t *testing.T) { - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ - TaskQueue: taskQueue, - }, callerWF, tc.outcome) - s.NoError(err) - - if tc.checkPendingError != nil { - var f *failurepb.Failure - require.EventuallyWithT(t, func(t *assert.CollectT) { - desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) - require.NoError(t, err) - require.Len(t, desc.PendingNexusOperations, 1) - f = desc.PendingNexusOperations[0].LastAttemptFailure - require.NotNil(t, f) - - }, 10*time.Second, 100*time.Millisecond) - s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - tc.checkPendingError(t, converter.FailureToError(f)) - s.NoError(s.SdkClient().TerminateWorkflow(ctx, run.GetID(), run.GetRunID(), "test cleanup")) - } else { - wfErr := run.Get(ctx, nil) - s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - tc.checkWorkflowError(t, wfErr) - } - - snap := capture.Snapshot() - require.Len(t, snap["nexus_outbound_requests"], 1) - require.Subset( - t, - snap["nexus_outbound_requests"][0].Tags, - map[string]string{ - "namespace": s.Namespace().String(), - "method": "StartOperation", - "failure_source": "worker", - "outcome": tc.metricsOutcome, - }, - ) - }) - - } -} - -func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationErrorRehydration() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) - defer cancel() - testCtx := ctx - taskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) - endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - handlerWorkflowID := testcore.RandomizeStr(s.T().Name()) + testFn := func(s *NexusWorkflowTestSuite, tc testcase) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + taskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) + endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) + converter := temporal.NewDefaultFailureConverter(temporal.DefaultFailureConverterOptions{}) - _, err := s.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ - Spec: &nexuspb.EndpointSpec{ - Name: endpointName, - Target: &nexuspb.EndpointTarget{ - Variant: &nexuspb.EndpointTarget_Worker_{ - Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), - TaskQueue: taskQueue, + _, err := env.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: endpointName, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: env.Namespace().String(), + TaskQueue: taskQueue, + }, }, }, }, - }, - }) - s.NoError(err) - - w := worker.New( - s.SdkClient(), - taskQueue, - worker.Options{}, - ) + }) + s.NoError(err) - svc := nexus.NewService("test") + w := worker.New(env.SdkClient(), taskQueue, worker.Options{}) + svc := nexus.NewService("test") + op := nexus.NewSyncOperation("op", func(ctx context.Context, outcome string, soo nexus.StartOperationOptions) (nexus.NoValue, error) { + switch outcome { + case "fail-handler-internal": + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeInternal, "intentional internal error") + case "fail-handler-app-error": + return nil, temporal.NewApplicationError("app error", "TestError", "details") + case "fail-handler-bad-request": + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "bad request") + case "fail-operation": + return nil, nexus.NewOperationFailedErrorf("some error") + case "fail-operation-app-error": + return nil, temporal.NewNonRetryableApplicationError("app error", "TestError", nil, "details") + default: + } + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "unexpected outcome: %s", outcome) + }) + s.NoError(svc.Register(op)) - handlerWF := func(ctx workflow.Context, outcome string) (nexus.NoValue, error) { - switch outcome { - case "wait", "timeout": - // Wait for the workflow to be canceled. - return nil, workflow.Await(ctx, func() bool { return false }) - case "fail": - return nil, temporal.NewApplicationError("app error", "TestError", "details") + callerWF := func(ctx workflow.Context, outcome string) (nexus.NoValue, error) { + c := workflow.NewNexusClient(endpointName, svc.Name) + fut := c.ExecuteOperation(ctx, op, outcome, workflow.NexusOperationOptions{}) + return nil, fut.Get(ctx, nil) } - return nil, fmt.Errorf("unexpected outcome: %s", outcome) - } - op := temporalnexus.NewWorkflowRunOperation("op", handlerWF, func(ctx context.Context, outcome string, soo nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { - var workflowExecutionTimeout time.Duration - if outcome == "timeout" { - workflowExecutionTimeout = time.Second - } - return client.StartWorkflowOptions{ID: handlerWorkflowID, WorkflowExecutionTimeout: workflowExecutionTimeout}, nil - }) - s.NoError(svc.Register(op)) + w.RegisterNexusService(svc) + w.RegisterWorkflow(callerWF) + s.NoError(w.Start()) + defer w.Stop() - callerWF := func(ctx workflow.Context, outcome, action string) (nexus.NoValue, error) { - opCtx, cancel := workflow.WithCancel(ctx) - defer cancel() - c := workflow.NewNexusClient(endpointName, svc.Name) - fut := c.ExecuteOperation(opCtx, op, outcome, workflow.NexusOperationOptions{}) - var exec workflow.NexusOperationExecution - if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { - return nil, err - } - switch action { - case "terminate": - // Lazy man's version of a local activity, don't try this at home. - workflow.SideEffect(ctx, func(ctx workflow.Context) any { - err := s.SdkClient().TerminateWorkflow(testCtx, handlerWorkflowID, "", "") - if err != nil { - panic(err) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: taskQueue, + }, callerWF, tc.outcome) + s.NoError(err) + + if tc.checkPendingError != nil { + var f *failurepb.Failure + s.EventuallyWithT(func(t *assert.CollectT) { + desc, err := env.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) + assert.NoError(t, err) + assert.Len(t, desc.PendingNexusOperations, 1) + if len(desc.PendingNexusOperations) > 0 { + f = desc.PendingNexusOperations[0].LastAttemptFailure + assert.NotNil(t, f) } - return nil - }) - case "cancel": - cancel() - err := fut.Get(ctx, nil) - // The Go SDK unwraps CanceledErrors when an error is returned from the workflow, assert in-workflow. - var opErr *temporal.NexusOperationError - if !errors.As(err, &opErr) { - return nil, fmt.Errorf("expected NexusOperationError, got %w", err) - } - var canceledErr *temporal.CanceledError - if !errors.As(opErr, &canceledErr) { - return nil, fmt.Errorf("expected CanceledError, got %w", err) - } + }, 10*time.Second, 100*time.Millisecond) + env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + tc.checkPendingError(s, converter.FailureToError(f)) + s.NoError(env.SdkClient().TerminateWorkflow(ctx, run.GetID(), run.GetRunID(), "test cleanup")) + } else { + wfErr := run.Get(ctx, nil) + env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + tc.checkWorkflowError(s, wfErr) } - return nil, fut.Get(ctx, nil) + + snap := capture.Snapshot() + s.Len(snap["nexus_outbound_requests"], 1) + s.Subset( + snap["nexus_outbound_requests"][0].Tags, + map[string]string{ + "namespace": env.Namespace().String(), + "method": "StartOperation", + "failure_source": "worker", + "outcome": tc.metricsOutcome, + }, + ) } - w.RegisterNexusService(svc) - w.RegisterWorkflow(callerWF) - w.RegisterWorkflow(handlerWF) - s.NoError(w.Start()) - s.T().Cleanup(w.Stop) + for _, tc := range cases { + s.Run(tc.outcome, func(s *NexusWorkflowTestSuite) { + testFn(s, tc) + }) + } +} - cases := []struct { +func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationErrorRehydration() { + type testcase struct { outcome, action string - checkWorkflowError func(t *testing.T, wfErr error) - }{ + checkWorkflowError func(s *NexusWorkflowTestSuite, wfErr error) + } + cases := []testcase{ { outcome: "fail", - checkWorkflowError: func(t *testing.T, wfErr error) { + checkWorkflowError: func(s *NexusWorkflowTestSuite, wfErr error) { var opErr *temporal.NexusOperationError - require.ErrorAs(t, wfErr, &opErr) + s.ErrorAs(wfErr, &opErr) var appErr *temporal.ApplicationError - require.ErrorAs(t, opErr, &appErr) - require.Equal(t, "app error", appErr.Message()) - require.Equal(t, "TestError", appErr.Type()) + s.ErrorAs(opErr, &appErr) + s.Equal("app error", appErr.Message()) + s.Equal("TestError", appErr.Type()) var details string - require.NoError(t, appErr.Details(&details)) - require.Equal(t, "details", details) + s.NoError(appErr.Details(&details)) + s.Equal("details", details) }, }, { outcome: "wait", action: "terminate", - checkWorkflowError: func(t *testing.T, wfErr error) { + checkWorkflowError: func(s *NexusWorkflowTestSuite, wfErr error) { var opErr *temporal.NexusOperationError - require.ErrorAs(t, wfErr, &opErr) + s.ErrorAs(wfErr, &opErr) var termErr *temporal.TerminatedError - require.ErrorAs(t, opErr, &termErr) + s.ErrorAs(opErr, &termErr) }, }, { outcome: "wait", action: "cancel", - checkWorkflowError: func(t *testing.T, wfErr error) { + checkWorkflowError: func(s *NexusWorkflowTestSuite, wfErr error) { // The Go SDK loses the NexusOperationError (as well as any other error if it wraps a CanceledError), // assertions done in workflow. var canceledErr *temporal.CanceledError - require.ErrorAs(t, wfErr, &canceledErr) + s.ErrorAs(wfErr, &canceledErr) }, }, { outcome: "timeout", - checkWorkflowError: func(t *testing.T, wfErr error) { + checkWorkflowError: func(s *NexusWorkflowTestSuite, wfErr error) { var opErr *temporal.NexusOperationError - require.ErrorAs(t, wfErr, &opErr) + s.ErrorAs(wfErr, &opErr) var timeoutErr *temporal.TimeoutError - require.ErrorAs(t, opErr, &timeoutErr) + s.ErrorAs(opErr, &timeoutErr) }, }, } - for _, tc := range cases { - s.T().Run(tc.outcome+"-"+tc.action, func(t *testing.T) { - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ - TaskQueue: taskQueue, - }, callerWF, tc.outcome, tc.action) - s.NoError(err) + testFn := func(s *NexusWorkflowTestSuite, tc testcase) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + testCtx := ctx + taskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) + endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) + handlerWorkflowID := testcore.RandomizeStr(s.T().Name()) - wfErr := run.Get(ctx, nil) - s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - tc.checkWorkflowError(t, wfErr) + _, err := env.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: endpointName, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: env.Namespace().String(), + TaskQueue: taskQueue, + }, + }, + }, + }, + }) + s.NoError(err) - snap := capture.Snapshot() - require.GreaterOrEqual(t, len(snap["nexus_outbound_requests"]), 1) - require.Subset(t, snap["nexus_outbound_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "method": "StartOperation", "failure_source": "_unknown_", "outcome": "pending"}) + w := worker.New(env.SdkClient(), taskQueue, worker.Options{}) + svc := nexus.NewService("test") + + handlerWF := func(ctx workflow.Context, outcome string) (nexus.NoValue, error) { + switch outcome { + case "wait", "timeout": + // Wait for the workflow to be canceled. + return nil, workflow.Await(ctx, func() bool { return false }) + case "fail": + return nil, temporal.NewApplicationError("app error", "TestError", "details") + default: + } + return nil, fmt.Errorf("unexpected outcome: %s", outcome) + } + + op := temporalnexus.NewWorkflowRunOperation("op", handlerWF, func(ctx context.Context, outcome string, soo nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + var workflowExecutionTimeout time.Duration + if outcome == "timeout" { + workflowExecutionTimeout = time.Second + } + return client.StartWorkflowOptions{ID: handlerWorkflowID, WorkflowExecutionTimeout: workflowExecutionTimeout}, nil }) + s.NoError(svc.Register(op)) + + callerWF := func(ctx workflow.Context, outcome, action string) (nexus.NoValue, error) { + opCtx, cancel := workflow.WithCancel(ctx) + defer cancel() + c := workflow.NewNexusClient(endpointName, svc.Name) + fut := c.ExecuteOperation(opCtx, op, outcome, workflow.NexusOperationOptions{}) + var exec workflow.NexusOperationExecution + if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { + return nil, err + } + switch action { + case "terminate": + // Lazy man's version of a local activity, don't try this at home. + workflow.SideEffect(ctx, func(ctx workflow.Context) any { + err := env.SdkClient().TerminateWorkflow(testCtx, handlerWorkflowID, "", "") + if err != nil { + panic(err) + } + return nil + }) + case "cancel": + cancel() + err := fut.Get(ctx, nil) + // The Go SDK unwraps CanceledErrors when an error is returned from the workflow, assert in-workflow. + var opErr *temporal.NexusOperationError + if !errors.As(err, &opErr) { + return nil, fmt.Errorf("expected NexusOperationError, got %w", err) + } + var canceledErr *temporal.CanceledError + if !errors.As(opErr, &canceledErr) { + return nil, fmt.Errorf("expected CanceledError, got %w", err) + } + default: + } + return nil, fut.Get(ctx, nil) + } + + w.RegisterNexusService(svc) + w.RegisterWorkflow(callerWF) + w.RegisterWorkflow(handlerWF) + s.NoError(w.Start()) + defer w.Stop() + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: taskQueue, + }, callerWF, tc.outcome, tc.action) + s.NoError(err) + + wfErr := run.Get(ctx, nil) + env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + tc.checkWorkflowError(s, wfErr) + + snap := capture.Snapshot() + s.GreaterOrEqual(len(snap["nexus_outbound_requests"]), 1) + s.Subset(snap["nexus_outbound_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "method": "StartOperation", "failure_source": "_unknown_", "outcome": "pending"}) + } + + for _, tc := range cases { + s.Run(tc.outcome+"-"+tc.action, func(s *NexusWorkflowTestSuite) { + testFn(s, tc) + }) } } func (s *NexusWorkflowTestSuite) TestNexusCallbackAfterCallerComplete() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() taskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) handlerWorkflowID := testcore.RandomizeStr(s.T().Name()) - _, err := s.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ Variant: &nexuspb.EndpointTarget_Worker_{ Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: taskQueue, }, }, @@ -2414,7 +2402,7 @@ func (s *NexusWorkflowTestSuite) TestNexusCallbackAfterCallerComplete() { s.NoError(err) w := worker.New( - s.SdkClient(), + env.SdkClient(), taskQueue, worker.Options{}, ) @@ -2447,18 +2435,18 @@ func (s *NexusWorkflowTestSuite) TestNexusCallbackAfterCallerComplete() { s.NoError(w.Start()) s.T().Cleanup(w.Stop) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, callerWF) s.NoError(err) s.NoError(run.Get(ctx, nil)) - err = s.SdkClient().SignalWorkflow(ctx, handlerWorkflowID, "", "test-signal", nil) + err = env.SdkClient().SignalWorkflow(ctx, handlerWorkflowID, "", "test-signal", nil) s.NoError(err) s.EventuallyWithT(func(ct *assert.CollectT) { - resp, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: handlerWorkflowID, }, @@ -2472,6 +2460,7 @@ func (s *NexusWorkflowTestSuite) TestNexusCallbackAfterCallerComplete() { } func (s *NexusWorkflowTestSuite) TestNexusOperationSyncNexusFailure() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -2493,7 +2482,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncNexusFailure() { listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -2508,7 +2497,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncNexusFailure() { s.NoError(err) w := worker.New( - s.SdkClient(), + env.SdkClient(), taskQueue, worker.Options{}, ) @@ -2523,13 +2512,13 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncNexusFailure() { s.NoError(w.Start()) s.T().Cleanup(w.Stop) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, callerWF) s.NoError(err) wfErr := run.Get(ctx, nil) - s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) var handlerErr *nexus.HandlerError s.ErrorAs(wfErr, &handlerErr) @@ -2537,7 +2526,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncNexusFailure() { // Old SDK path var appErr *temporal.ApplicationError s.ErrorAs(handlerErr.Cause, &appErr) - s.Equal(appErr.Message(), "fail me") + s.Equal("fail me", appErr.Message()) var failure nexus.Failure s.NoError(appErr.Details(&failure)) s.Equal(map[string]string{"key": "val"}, failure.Metadata) @@ -2548,181 +2537,184 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSyncNexusFailure() { snap := capture.Snapshot() s.Len(snap["nexus_outbound_requests"], 1) // Confirming that requests which do not go through our frontend are not tagged with `failure_source` - s.Subset(snap["nexus_outbound_requests"][0].Tags, map[string]string{"namespace": s.Namespace().String(), "method": "StartOperation", "failure_source": "_unknown_", "outcome": "handler-error:BAD_REQUEST"}) + s.Subset(snap["nexus_outbound_requests"][0].Tags, map[string]string{"namespace": env.Namespace().String(), "method": "StartOperation", "failure_source": "_unknown_", "outcome": "handler-error:BAD_REQUEST"}) } func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationWithMultipleCallers() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) - defer cancel() - callerTaskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) - endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - handlerWorkflowID := testcore.RandomizeStr(s.T().Name()) - // number of concurrent Nexus operation calls numCalls := 5 - - _, err := s.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ - Spec: &nexuspb.EndpointSpec{ - Name: endpointName, - Target: &nexuspb.EndpointTarget{ - Variant: &nexuspb.EndpointTarget_Worker_{ - Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), - TaskQueue: callerTaskQueue, - }, - }, - }, - }, - }) - s.NoError(err) - - w := worker.New(s.SdkClient(), callerTaskQueue, worker.Options{}) - svc := nexus.NewService("test") handlerWf := func(ctx workflow.Context, input string) (string, error) { workflow.GetSignalChannel(ctx, "terminate").Receive(ctx, nil) return "hello " + input, nil } - - op := temporalnexus.NewWorkflowRunOperation( - "op", - handlerWf, - func(ctx context.Context, input string, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { - var conflictPolicy enumspb.WorkflowIdConflictPolicy - if input == "conflict-policy-use-existing" { - conflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING - } - return client.StartWorkflowOptions{ - ID: handlerWorkflowID, - WorkflowIDConflictPolicy: conflictPolicy, - }, nil - }, - ) - svc.MustRegister(op) + handlerWorkflowID := testcore.RandomizeStr(s.T().Name()) + callerTaskQueue := testcore.RandomizeStr("caller_" + s.T().Name()) type CallerWfOutput struct { CntOk int CntErr int } + type CallerWfFn = func(ctx workflow.Context, input string) (CallerWfOutput, error) - callerWf := func(ctx workflow.Context, input string) (CallerWfOutput, error) { - output := CallerWfOutput{} - var retError error + buildNexusEnvFn := func(ctx context.Context, s *NexusWorkflowTestSuite) (*NexusTestEnv, CallerWfFn) { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) + endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - c := workflow.NewNexusClient(endpointName, svc.Name) + _, err := env.SdkClient().OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: endpointName, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: env.Namespace().String(), + TaskQueue: callerTaskQueue, + }, + }, + }, + }, + }) + s.NoError(err) - nexusFutures := []workflow.NexusOperationFuture{} - for i := 0; i < numCalls; i++ { - fut := c.ExecuteOperation(ctx, op, input, workflow.NexusOperationOptions{}) - nexusFutures = append(nexusFutures, fut) - } + w := worker.New(env.SdkClient(), callerTaskQueue, worker.Options{}) + svc := nexus.NewService("test") + op := temporalnexus.NewWorkflowRunOperation( + "op", + handlerWf, + func(ctx context.Context, input string, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + var conflictPolicy enumspb.WorkflowIdConflictPolicy + if input == "conflict-policy-use-existing" { + conflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + } + return client.StartWorkflowOptions{ + ID: handlerWorkflowID, + WorkflowIDConflictPolicy: conflictPolicy, + }, nil + }, + ) + svc.MustRegister(op) - nexusOpStartedFutures := []workflow.NexusOperationFuture{} - for _, fut := range nexusFutures { - var exec workflow.NexusOperationExecution - err := fut.GetNexusOperationExecution().Get(ctx, &exec) - if err == nil { - output.CntOk++ - nexusOpStartedFutures = append(nexusOpStartedFutures, fut) - continue + callerWf := func(ctx workflow.Context, input string) (CallerWfOutput, error) { + output := CallerWfOutput{} + var retError error + + c := workflow.NewNexusClient(endpointName, svc.Name) + + nexusFutures := []workflow.NexusOperationFuture{} + for range numCalls { + fut := c.ExecuteOperation(ctx, op, input, workflow.NexusOperationOptions{}) + nexusFutures = append(nexusFutures, fut) } - output.CntErr++ - var handlerErr *nexus.HandlerError - var appErr *temporal.ApplicationError - if !errors.As(err, &handlerErr) { - retError = err - } else if !errors.As(handlerErr, &appErr) { - retError = err - } else if appErr.Type() != "WorkflowExecutionAlreadyStarted" { - retError = err + + nexusOpStartedFutures := []workflow.NexusOperationFuture{} + for _, fut := range nexusFutures { + var exec workflow.NexusOperationExecution + err := fut.GetNexusOperationExecution().Get(ctx, &exec) + if err == nil { + output.CntOk++ + nexusOpStartedFutures = append(nexusOpStartedFutures, fut) + continue + } + output.CntErr++ + var handlerErr *nexus.HandlerError + var appErr *temporal.ApplicationError + if !errors.As(err, &handlerErr) || !errors.As(handlerErr, &appErr) || appErr.Type() != "WorkflowExecutionAlreadyStarted" { + retError = err + } } - } - if output.CntOk > 0 { - // signal handler workflow so it will complete - err = workflow.SignalExternalWorkflow(ctx, handlerWorkflowID, "", "terminate", nil).Get(ctx, nil) - if err != nil { - return output, err + if output.CntOk > 0 { + // signal handler workflow so it will complete + err = workflow.SignalExternalWorkflow(ctx, handlerWorkflowID, "", "terminate", nil).Get(ctx, nil) + if err != nil { + return output, err + } } - } - for _, fut := range nexusOpStartedFutures { - var res string - err := fut.Get(ctx, &res) - if err != nil { - retError = err - } else if res != "hello "+input { - retError = fmt.Errorf("unexpected result from handler workflow: %q", res) + for _, fut := range nexusOpStartedFutures { + var res string + err := fut.Get(ctx, &res) + if err != nil { + retError = err + } else if res != "hello "+input { + retError = fmt.Errorf("unexpected result from handler workflow: %q", res) + } } + + return output, retError } - return output, retError - } + w.RegisterNexusService(svc) + w.RegisterWorkflow(handlerWf) + w.RegisterWorkflowWithOptions(callerWf, workflow.RegisterOptions{Name: "caller-wf"}) + s.NoError(w.Start()) - w.RegisterNexusService(svc) - w.RegisterWorkflow(handlerWf) - w.RegisterWorkflowWithOptions(callerWf, workflow.RegisterOptions{Name: "caller-wf"}) - s.NoError(w.Start()) - defer w.Stop() + // s.T().Cleanup(...) runs after the s.T()'s test finishes, not after this function returns + s.T().Cleanup(func() { w.Stop() }) + return env, callerWf + } testCases := []struct { input string - checkOutput func(t *testing.T, res CallerWfOutput, err error) + checkOutput func(s *NexusWorkflowTestSuite, env *NexusTestEnv, res CallerWfOutput, err error) }{ { input: "conflict-policy-fail", - checkOutput: func(t *testing.T, res CallerWfOutput, err error) { - require.NoError(t, err) - require.EqualValues(t, 1, res.CntOk) - require.EqualValues(t, numCalls-1, res.CntErr) + checkOutput: func(s *NexusWorkflowTestSuite, env *NexusTestEnv, res CallerWfOutput, err error) { + s.NoError(err) + s.Equal(1, res.CntOk) + s.Equal(numCalls-1, res.CntErr) // check the handler workflow has the request ID infos map correct - descResp, err := s.SdkClient().DescribeWorkflowExecution(context.Background(), handlerWorkflowID, "") - require.NoError(t, err) + descResp, err := env.SdkClient().DescribeWorkflowExecution(context.Background(), handlerWorkflowID, "") + s.NoError(err) requestIDInfos := descResp.GetWorkflowExtendedInfo().GetRequestIdInfos() - require.NotNil(t, requestIDInfos) - require.Len(t, requestIDInfos, 1) + s.NotNil(requestIDInfos) + s.Len(requestIDInfos, 1) for _, info := range requestIDInfos { - require.False(t, info.Buffered) - require.GreaterOrEqual(t, info.EventId, common.FirstEventID) - require.Equal(t, enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED, info.EventType) + s.False(info.Buffered) + s.GreaterOrEqual(info.EventId, common.FirstEventID) + s.Equal(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED, info.EventType) } }, }, { input: "conflict-policy-use-existing", - checkOutput: func(t *testing.T, res CallerWfOutput, err error) { - require.NoError(t, err) - require.EqualValues(t, numCalls, res.CntOk) - require.EqualValues(t, 0, res.CntErr) + checkOutput: func(s *NexusWorkflowTestSuite, env *NexusTestEnv, res CallerWfOutput, err error) { + s.NoError(err) + s.Equal(numCalls, res.CntOk) + s.Equal(0, res.CntErr) // check the handler workflow has the request ID infos map correct - descResp, err := s.SdkClient().DescribeWorkflowExecution(context.Background(), handlerWorkflowID, "") - require.NoError(t, err) + descResp, err := env.SdkClient().DescribeWorkflowExecution(context.Background(), handlerWorkflowID, "") + s.NoError(err) requestIDInfos := descResp.GetWorkflowExtendedInfo().GetRequestIdInfos() - require.NotNil(t, requestIDInfos) + s.NotNil(requestIDInfos) cntStarted := 0 cntAttached := 0 for _, info := range requestIDInfos { - require.False(t, info.Buffered) - require.GreaterOrEqual(t, info.EventId, common.FirstEventID) + s.False(info.Buffered) + s.GreaterOrEqual(info.EventId, common.FirstEventID) switch info.EventType { case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: cntStarted++ case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED: cntAttached++ default: - require.Fail(t, "Unexpected event type in request ID info") + s.Fail("Unexpected event type in request ID info") } } - require.Equal(t, 1, cntStarted) - require.Equal(t, numCalls-1, cntAttached) + s.Equal(1, cntStarted) + s.Equal(numCalls-1, cntAttached) }, }, } for _, tc := range testCases { - s.Run(tc.input, func() { - run, err := s.SdkClient().ExecuteWorkflow( + s.Run(tc.input, func(s *NexusWorkflowTestSuite) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + env, callerWf := buildNexusEnvFn(ctx, s) + run, err := env.SdkClient().ExecuteWorkflow( ctx, client.StartWorkflowOptions{ TaskQueue: callerTaskQueue, @@ -2733,23 +2725,24 @@ func (s *NexusWorkflowTestSuite) TestNexusAsyncOperationWithMultipleCallers() { s.NoError(err) var res CallerWfOutput err = run.Get(ctx, &res) - tc.checkOutput(s.T(), res, err) + tc.checkOutput(s, env, res, err) }) } } func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToCloseTimeout() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ Variant: &nexuspb.EndpointTarget_Worker_{ Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: "unreachable-for-test", }, }, @@ -2758,14 +2751,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToCloseTimeout() { }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) // Schedule the operation with a short schedule-to-close timeout - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -2773,7 +2766,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToCloseTimeout() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -2784,7 +2777,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToCloseTimeout() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), ScheduleToCloseTimeout: durationpb.New(2 * time.Second), }, }, @@ -2793,14 +2786,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToCloseTimeout() { }) s.NoError(err) - descResp, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) + descResp, err := env.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) s.Len(descResp.PendingNexusOperations, 1) s.Equal(2*time.Second, descResp.PendingNexusOperations[0].ScheduleToCloseTimeout.AsDuration()) // Now wait for the timeout event - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -2819,7 +2812,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToCloseTimeout() { timedOutEvent.GetNexusOperationTimedOutEventAttributes().GetFailure().GetCause().GetTimeoutFailureInfo().GetTimeoutType()) // Complete the workflow - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -2836,17 +2829,18 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToCloseTimeout() { } func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToStartTimeout() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ Variant: &nexuspb.EndpointTarget_Worker_{ Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: "unreachable-for-test", }, }, @@ -2855,14 +2849,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToStartTimeout() { }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) // Schedule the operation with a short schedule-to-close timeout - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -2870,7 +2864,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToStartTimeout() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -2881,7 +2875,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToStartTimeout() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), ScheduleToStartTimeout: durationpb.New(2 * time.Second), }, }, @@ -2890,14 +2884,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToStartTimeout() { }) s.NoError(err) - descResp, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) + descResp, err := env.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) s.Len(descResp.PendingNexusOperations, 1) s.Equal(2*time.Second, descResp.PendingNexusOperations[0].ScheduleToStartTimeout.AsDuration()) // Now wait for the timeout event - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -2916,7 +2910,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToStartTimeout() { timedOutEvent.GetNexusOperationTimedOutEventAttributes().GetFailure().GetCause().GetTimeoutFailureInfo().GetTimeoutType()) // Complete the workflow - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -2933,6 +2927,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationScheduleToStartTimeout() { } func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) @@ -2947,7 +2942,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { listenAddr := nexustest.AllocListenAddress() nexustest.NewNexusServer(s.T(), listenAddr, h) - _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ Spec: &nexuspb.EndpointSpec{ Name: endpointName, Target: &nexuspb.EndpointTarget{ @@ -2961,14 +2956,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { }) s.NoError(err) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) // Schedule the operation with a short start-to-close timeout - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -2976,7 +2971,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -2987,7 +2982,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), StartToCloseTimeout: durationpb.New(2 * time.Second), }, }, @@ -2996,14 +2991,14 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { }) s.NoError(err) - descResp, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) + descResp, err := env.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) s.Len(descResp.PendingNexusOperations, 1) s.Equal(2*time.Second, descResp.PendingNexusOperations[0].StartToCloseTimeout.AsDuration()) // Wait for the started event first - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -3019,15 +3014,15 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { s.Positive(startedEventIdx) // Respond to acknowledge the started event - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, }) s.NoError(err) // Now wait for the timeout event - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -3047,7 +3042,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationStartToCloseTimeout() { s.Contains(timedOutEvent.GetNexusOperationTimedOutEventAttributes().GetFailure().GetCause().GetMessage(), "operation timed out") // Complete the workflow - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -3092,11 +3087,12 @@ func (s *NexusWorkflowTestSuite) generateValidCallbackToken(namespaceID, workflo func (s *NexusWorkflowTestSuite) sendNexusCompletionRequest( ctx context.Context, + env *NexusTestEnv, url string, completion nexusrpc.CompleteOperationOptions, ) (map[string][]*metricstest.CapturedRecording, error) { - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + capture := env.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() + defer env.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) c := nexusrpc.NewCompletionHTTPClient(nexusrpc.CompletionHTTPClientOptions{ Serializer: commonnexus.PayloadSerializer, @@ -3108,16 +3104,17 @@ func (s *NexusWorkflowTestSuite) sendNexusCompletionRequest( // NOTE: This test cannot use the SDK workflow package because there is a restriction that prevents setting the // __temporal_system endpoint. func (s *NexusWorkflowTestSuite) TestNexusOperationSystemEndpoint() { + env := newNexusTestEnv(s.T(), true, testcore.WithDedicatedCluster()) ctx := testcore.NewContext() taskQueue := testcore.RandomizeStr(s.T().Name()) - run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ TaskQueue: taskQueue, }, "workflow") s.NoError(err) - pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -3125,7 +3122,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSystemEndpoint() { Identity: "test", }) s.NoError(err) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ @@ -3136,7 +3133,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSystemEndpoint() { Endpoint: commonnexus.SystemEndpoint, Service: "TestService", Operation: "TestOperation", - Input: s.mustToPayload("Temporal"), + Input: testcore.MustToPayload(s.T(), "Temporal"), }, }, }, @@ -3145,8 +3142,8 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSystemEndpoint() { s.NoError(err) // Poll for the completion - pollResp, err = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), + pollResp, err = env.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL, @@ -3167,7 +3164,7 @@ func (s *NexusWorkflowTestSuite) TestNexusOperationSystemEndpoint() { s.NotNil(result) // Complete the workflow - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", TaskToken: pollResp.TaskToken, Commands: []*commandpb.Command{ diff --git a/tests/nil_search_attribute_test.go b/tests/nil_search_attribute_test.go new file mode 100644 index 00000000000..760377507a6 --- /dev/null +++ b/tests/nil_search_attribute_test.go @@ -0,0 +1,375 @@ +package tests + +import ( + "testing" + "time" + + "github.com/google/uuid" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + historypb "go.temporal.io/api/history/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/payload" + "go.temporal.io/server/tests/testcore" + "google.golang.org/protobuf/types/known/durationpb" +) + +func TestWorkflowStart_NilSearchAttributesFiltered(t *testing.T) { + s := testcore.NewEnv(t) + workflowID := "nil-sa-filter-" + uuid.NewString() + workflowType := &commonpb.WorkflowType{Name: "nil-sa-filter-workflow-type"} + taskQueue := &taskqueuepb.TaskQueue{Name: s.Tv().TaskQueue().Name, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + nilPayload, err := payload.Encode(nil) + s.Require().NoError(err) + validPayload := payload.EncodeString("valid-value") + + searchAttributes := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": validPayload, + "CustomTextField": nilPayload, + }, + } + + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + WorkflowType: workflowType, + TaskQueue: taskQueue, + WorkflowRunTimeout: durationpb.New(10 * time.Second), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), + Identity: "test-identity", + SearchAttributes: searchAttributes, + } + + we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + s.Require().NoError(err) + s.Require().NotNil(we.GetRunId()) + + historyResp, err := s.FrontendClient().GetWorkflowExecutionHistory(testcore.NewContext(), &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, + RunId: we.GetRunId(), + }, + }) + s.Require().NoError(err) + + var startedEvent *historypb.HistoryEvent + for _, event := range historyResp.History.Events { + if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + startedEvent = event + break + } + } + + s.Require().NotNil(startedEvent) + attrs := startedEvent.GetWorkflowExecutionStartedEventAttributes() + s.Require().NotNil(attrs) + + if attrs.SearchAttributes != nil { + s.NotNil(attrs.SearchAttributes.IndexedFields["CustomKeywordField"]) + _, hasNilKey := attrs.SearchAttributes.IndexedFields["CustomTextField"] + s.False(hasNilKey, "nil search attribute key should be filtered out from history event") + } + + _, err = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, + Reason: "test cleanup", + }) + s.Require().NoError(err) +} + +func TestWorkflowStart_AllNilSearchAttributesFiltered(t *testing.T) { + s := testcore.NewEnv(t) + workflowID := "nil-sa-filter-all-" + uuid.NewString() + workflowType := &commonpb.WorkflowType{Name: "nil-sa-filter-workflow-type"} + taskQueue := &taskqueuepb.TaskQueue{Name: s.Tv().TaskQueue().Name, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + nilPayload, err := payload.Encode(nil) + s.Require().NoError(err) + + searchAttributes := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": nilPayload, + "CustomTextField": nilPayload, + }, + } + + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + WorkflowType: workflowType, + TaskQueue: taskQueue, + WorkflowRunTimeout: durationpb.New(10 * time.Second), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), + Identity: "test-identity", + SearchAttributes: searchAttributes, + } + + we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + s.Require().NoError(err) + s.Require().NotNil(we.GetRunId()) + + historyResp, err := s.FrontendClient().GetWorkflowExecutionHistory(testcore.NewContext(), &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, + RunId: we.GetRunId(), + }, + }) + s.Require().NoError(err) + + var startedEvent *historypb.HistoryEvent + for _, event := range historyResp.History.Events { + if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + startedEvent = event + break + } + } + + s.Require().NotNil(startedEvent) + attrs := startedEvent.GetWorkflowExecutionStartedEventAttributes() + s.Require().NotNil(attrs) + s.Nil(attrs.SearchAttributes, "SearchAttributes should be nil when all values are nil") + + _, err = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, + Reason: "test cleanup", + }) + s.Require().NoError(err) +} + +func TestDescribeWorkflow_NilSearchAttributesNotVisible(t *testing.T) { + s := testcore.NewEnv(t) + workflowID := "nil-sa-filter-describe-" + uuid.NewString() + workflowType := &commonpb.WorkflowType{Name: "nil-sa-filter-workflow-type"} + taskQueue := &taskqueuepb.TaskQueue{Name: s.Tv().TaskQueue().Name, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + nilPayload, err := payload.Encode(nil) + s.Require().NoError(err) + validPayload := payload.EncodeString("valid-value") + + searchAttributes := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": validPayload, + "CustomTextField": nilPayload, + }, + } + + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + WorkflowType: workflowType, + TaskQueue: taskQueue, + WorkflowRunTimeout: durationpb.New(10 * time.Second), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), + Identity: "test-identity", + SearchAttributes: searchAttributes, + } + + we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + s.Require().NoError(err) + s.Require().NotNil(we.GetRunId()) + + descResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, + RunId: we.GetRunId(), + }, + }) + s.Require().NoError(err) + s.Require().NotNil(descResp) + + if descResp.WorkflowExecutionInfo.SearchAttributes != nil { + s.NotNil(descResp.WorkflowExecutionInfo.SearchAttributes.IndexedFields["CustomKeywordField"]) + _, hasNilKey := descResp.WorkflowExecutionInfo.SearchAttributes.IndexedFields["CustomTextField"] + s.False(hasNilKey, "nil search attribute key should not be visible in mutable state") + } + + _, err = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, + Reason: "test cleanup", + }) + s.Require().NoError(err) +} + +func TestWorkflowStart_NilMemoFiltered(t *testing.T) { + s := testcore.NewEnv(t) + workflowID := "nil-memo-filter-" + uuid.NewString() + workflowType := &commonpb.WorkflowType{Name: "nil-memo-filter-workflow-type"} + taskQueue := &taskqueuepb.TaskQueue{Name: s.Tv().TaskQueue().Name, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + nilPayload, err := payload.Encode(nil) + s.Require().NoError(err) + validPayload := payload.EncodeString("valid-value") + + memo := &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "ValidKey": validPayload, + "NilKey": nilPayload, + }, + } + + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + WorkflowType: workflowType, + TaskQueue: taskQueue, + WorkflowRunTimeout: durationpb.New(10 * time.Second), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), + Identity: "test-identity", + Memo: memo, + } + + we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + s.Require().NoError(err) + s.Require().NotNil(we.GetRunId()) + + historyResp, err := s.FrontendClient().GetWorkflowExecutionHistory(testcore.NewContext(), &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, + RunId: we.GetRunId(), + }, + }) + s.Require().NoError(err) + + var startedEvent *historypb.HistoryEvent + for _, event := range historyResp.History.Events { + if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + startedEvent = event + break + } + } + + s.Require().NotNil(startedEvent) + attrs := startedEvent.GetWorkflowExecutionStartedEventAttributes() + s.Require().NotNil(attrs) + + if attrs.Memo != nil { + s.NotNil(attrs.Memo.Fields["ValidKey"]) + _, hasNilKey := attrs.Memo.Fields["NilKey"] + s.False(hasNilKey, "nil memo key should be filtered out from history event") + } + + _, _ = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, Reason: "test cleanup", + }) +} + +func TestWorkflowStart_AllNilMemoFiltered(t *testing.T) { + s := testcore.NewEnv(t) + workflowID := "nil-memo-filter-all-" + uuid.NewString() + workflowType := &commonpb.WorkflowType{Name: "nil-memo-filter-workflow-type"} + taskQueue := &taskqueuepb.TaskQueue{Name: s.Tv().TaskQueue().Name, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + nilPayload, err := payload.Encode(nil) + s.Require().NoError(err) + + memo := &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "NilKey1": nilPayload, + "NilKey2": nilPayload, + }, + } + + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + WorkflowType: workflowType, + TaskQueue: taskQueue, + WorkflowRunTimeout: durationpb.New(10 * time.Second), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), + Identity: "test-identity", + Memo: memo, + } + + we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + s.Require().NoError(err) + s.Require().NotNil(we.GetRunId()) + + historyResp, err := s.FrontendClient().GetWorkflowExecutionHistory(testcore.NewContext(), &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: we.GetRunId()}, + }) + s.Require().NoError(err) + + var startedEvent *historypb.HistoryEvent + for _, event := range historyResp.History.Events { + if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + startedEvent = event + break + } + } + + s.Require().NotNil(startedEvent) + attrs := startedEvent.GetWorkflowExecutionStartedEventAttributes() + s.Require().NotNil(attrs) + s.Nil(attrs.Memo, "Memo should be nil when all values are nil") + + _, _ = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, Reason: "test cleanup", + }) +} + +func TestDescribeWorkflow_NilMemoNotVisible(t *testing.T) { + s := testcore.NewEnv(t) + workflowID := "nil-memo-filter-describe-" + uuid.NewString() + workflowType := &commonpb.WorkflowType{Name: "nil-memo-filter-workflow-type"} + taskQueue := &taskqueuepb.TaskQueue{Name: s.Tv().TaskQueue().Name, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + + nilPayload, err := payload.Encode(nil) + s.Require().NoError(err) + validPayload := payload.EncodeString("valid-value") + + memo := &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "ValidKey": validPayload, + "NilKey": nilPayload, + }, + } + + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + WorkflowType: workflowType, + TaskQueue: taskQueue, + WorkflowRunTimeout: durationpb.New(10 * time.Second), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), + Identity: "test-identity", + Memo: memo, + } + + we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + s.Require().NoError(err) + s.Require().NotNil(we.GetRunId()) + + descResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: we.GetRunId()}, + }) + s.Require().NoError(err) + s.Require().NotNil(descResp) + + if descResp.WorkflowExecutionInfo.Memo != nil { + s.NotNil(descResp.WorkflowExecutionInfo.Memo.Fields["ValidKey"]) + _, hasNilKey := descResp.WorkflowExecutionInfo.Memo.Fields["NilKey"] + s.False(hasNilKey, "nil memo key should not be visible in mutable state / describe") + } + + _, _ = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: workflowID}, Reason: "test cleanup", + }) +} diff --git a/tests/pause_workflow_execution_test.go b/tests/pause_workflow_execution_test.go index 3fe8f0974e5..58d081d5247 100644 --- a/tests/pause_workflow_execution_test.go +++ b/tests/pause_workflow_execution_test.go @@ -101,14 +101,14 @@ func (s *PauseWorkflowExecutionSuite) SetupTest() { return "activity", nil } - s.Worker().RegisterWorkflow(s.workflowFn) - s.Worker().RegisterWorkflow(s.childWorkflowFn) - s.Worker().RegisterActivity(s.activityFn) + s.SdkWorker().RegisterWorkflow(s.workflowFn) + s.SdkWorker().RegisterWorkflow(s.childWorkflowFn) + s.SdkWorker().RegisterActivity(s.activityFn) // Setup for TestPauseWorkflowAndActivity s.activityShouldSucceed.Store(false) - s.Worker().RegisterWorkflow(s.workflowWithFailingActivity) - s.Worker().RegisterActivity(s.failingActivity) + s.SdkWorker().RegisterWorkflow(s.workflowWithFailingActivity) + s.SdkWorker().RegisterActivity(s.failingActivity) } // failingActivity is an activity that fails until activityShouldSucceed is set to true. @@ -168,6 +168,9 @@ func (s *PauseWorkflowExecutionSuite) TestPauseUnpauseWorkflowExecution() { info := desc.GetWorkflowExecutionInfo() require.NotNil(t, info) require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus()) + // Wait for the workflow task to be processed and the activity to be scheduled, + // so that a subsequent pause request is applied to a fully initialized workflow. + require.NotEmpty(t, desc.PendingActivities) }, 5*time.Second, 100*time.Millisecond) pauseRequest := &workflowservice.PauseWorkflowExecutionRequest{ @@ -230,7 +233,7 @@ func (s *PauseWorkflowExecutionSuite) TestPauseUnpauseWorkflowExecution() { }, 5*time.Second, 200*time.Millisecond) // Unblock the activity to complete the workflow. - s.activityCompletedCh <- struct{}{} + s.SendToChannel(ctx, s.activityCompletedCh) // assert that the workflow completes now. s.EventuallyWithT(func(t *assert.CollectT) { @@ -411,6 +414,109 @@ func (s *PauseWorkflowExecutionSuite) TestPauseWorkflowAndActivity() { }, 10*time.Second, 200*time.Millisecond) } +// TestUnpauseWorkflowKeepsActivityPaused tests that unpausing a workflow does not unpause its paused activities. +// 1. Start a workflow with a failing activity +// 2. Pause the activity +// 3. Pause the workflow +// 4. Unpause the workflow (while the activity is still paused) +// 5. Verify the activity remains paused and the workflow is running +func (s *PauseWorkflowExecutionSuite) TestUnpauseWorkflowKeepsActivityPaused() { + ctx := testcore.NewContext() + + s.activityShouldSucceed.Store(false) + + activityID := "failing-activity" + + workflowOptions := sdkclient.StartWorkflowOptions{ + ID: testcore.RandomizeStr("unpause-wf-keeps-activity-paused-" + s.T().Name()), + TaskQueue: s.TaskQueue(), + } + + workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, s.workflowWithFailingActivity) + s.NoError(err) + workflowID := workflowRun.GetID() + runID := workflowRun.GetRunID() + + // Wait for activity to fail at least once + s.EventuallyWithT(func(t *assert.CollectT) { + desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID) + require.NoError(t, err) + require.Len(t, desc.PendingActivities, 1) + require.NotNil(t, desc.PendingActivities[0].LastFailure) + }, 5*time.Second, 200*time.Millisecond) + + // Pause the activity + _, err = s.FrontendClient().PauseActivity(ctx, &workflowservice.PauseActivityRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + Activity: &workflowservice.PauseActivityRequest_Id{Id: activityID}, + Identity: s.pauseIdentity, + Reason: "pausing activity for test", + }) + s.NoError(err) + + s.EventuallyWithT(func(t *assert.CollectT) { + desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID) + require.NoError(t, err) + require.Len(t, desc.PendingActivities, 1) + require.True(t, desc.PendingActivities[0].Paused) + }, 5*time.Second, 200*time.Millisecond) + + // Pause the workflow + _, err = s.FrontendClient().PauseWorkflowExecution(ctx, &workflowservice.PauseWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + RunId: runID, + Identity: s.pauseIdentity, + Reason: s.pauseReason, + RequestId: uuid.NewString(), + }) + s.NoError(err) + + s.EventuallyWithT(func(t *assert.CollectT) { + s.assertWorkflowIsPaused(ctx, t, workflowID, runID) + }, 5*time.Second, 200*time.Millisecond) + + // Unpause the workflow only + _, err = s.FrontendClient().UnpauseWorkflowExecution(ctx, &workflowservice.UnpauseWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowId: workflowID, + RunId: runID, + Identity: s.pauseIdentity, + Reason: s.pauseReason, + RequestId: uuid.NewString(), + }) + s.NoError(err) + + // Verify the workflow is running but the activity remains paused + s.EventuallyWithT(func(t *assert.CollectT) { + desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID) + require.NoError(t, err) + info := desc.GetWorkflowExecutionInfo() + require.NotNil(t, info) + require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus(), + "workflow should be running after unpause, got: %s", info.GetStatus()) + require.Len(t, desc.PendingActivities, 1) + require.True(t, desc.PendingActivities[0].Paused, "activity should still be paused after workflow unpause") + }, 5*time.Second, 200*time.Millisecond) + + // Cleanup: unblock and unpause the activity so the workflow can complete + s.activityShouldSucceed.Store(true) + _, err = s.FrontendClient().UnpauseActivity(ctx, &workflowservice.UnpauseActivityRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + Activity: &workflowservice.UnpauseActivityRequest_Id{Id: activityID}, + Identity: s.pauseIdentity, + }) + s.NoError(err) + + s.EventuallyWithT(func(t *assert.CollectT) { + desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID) + require.NoError(t, err) + require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, desc.GetWorkflowExecutionInfo().GetStatus()) + }, 10*time.Second, 200*time.Millisecond) +} + func (s *PauseWorkflowExecutionSuite) TestQueryWorkflowWhenPaused() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -425,12 +531,15 @@ func (s *PauseWorkflowExecutionSuite) TestQueryWorkflowWhenPaused() { workflowID := workflowRun.GetID() runID := workflowRun.GetRunID() + // Wait for the workflow task to be processed and the activity to be scheduled, + // so that a subsequent pause request is applied to a fully initialized workflow. s.EventuallyWithT(func(t *assert.CollectT) { desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, workflowID, runID) require.NoError(t, err) info := desc.GetWorkflowExecutionInfo() require.NotNil(t, info) require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus()) + require.NotEmpty(t, desc.PendingActivities) }, 5*time.Second, 100*time.Millisecond) // Pause the workflow. @@ -482,7 +591,7 @@ func (s *PauseWorkflowExecutionSuite) TestQueryWorkflowWhenPaused() { s.NotNil(unpauseResp) // Unblock the activity and send the signal to complete the workflow. - s.activityCompletedCh <- struct{}{} + s.SendToChannel(ctx, s.activityCompletedCh) err = s.SdkClient().SignalWorkflow(ctx, workflowID, runID, s.testEndSignal, "test end signal") s.NoError(err) @@ -654,6 +763,9 @@ func (s *PauseWorkflowExecutionSuite) TestPauseWorkflowExecutionAlreadyPaused() info := desc.GetWorkflowExecutionInfo() require.NotNil(t, info) require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, info.GetStatus()) + // Wait for the workflow task to be processed and the activity to be scheduled, + // so that a subsequent pause request is applied to a fully initialized workflow. + require.NotEmpty(t, desc.PendingActivities) }, 5*time.Second, 100*time.Millisecond) // 1st pause request should succeed. @@ -706,7 +818,7 @@ func (s *PauseWorkflowExecutionSuite) TestPauseWorkflowExecutionAlreadyPaused() }, 5*time.Second, 200*time.Millisecond) // Unblock the activity and send the signal to complete the workflow. - s.activityCompletedCh <- struct{}{} + s.SendToChannel(ctx, s.activityCompletedCh) err = s.SdkClient().SignalWorkflow(ctx, workflowID, runID, s.testEndSignal, "test end signal") s.NoError(err) diff --git a/tests/poller_scaling_test.go b/tests/poller_scaling_test.go index d271175d6ec..14a1e9a4862 100644 --- a/tests/poller_scaling_test.go +++ b/tests/poller_scaling_test.go @@ -18,7 +18,6 @@ import ( taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" sdkclient "go.temporal.io/sdk/client" - "go.temporal.io/sdk/converter" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/payloads" "go.temporal.io/server/common/util" @@ -31,13 +30,6 @@ type PollerScalingIntegSuite struct { testcore.FunctionalTestBase } -func (s *PollerScalingIntegSuite) mustToPayload(v any) *commonpb.Payload { - conv := converter.GetDefaultDataConverter() - payload, err := conv.ToPayload(v) - s.NoError(err) - return payload -} - func TestPollerScalingFunctionalSuite(t *testing.T) { t.Parallel() suite.Run(t, new(PollerScalingIntegSuite)) @@ -79,7 +71,7 @@ func (s *PollerScalingIntegSuite) TestPollerScalingSimpleBacklog() { s.NoError(err) // Queue up a couple workflows - for i := 0; i < 5; i++ { + for range 5 { _, err := s.SdkClient().ExecuteWorkflow( ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf") s.NoError(err) @@ -99,7 +91,7 @@ func (s *PollerScalingIntegSuite) TestPollerScalingSimpleBacklog() { // Start enough activities / nexus tasks to ensure we will see scale up decisions commands := make([]*commandpb.Command, 0, 5) - for i := 0; i < 5; i++ { + for i := range 5 { commands = append(commands, &commandpb.Command{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ @@ -117,7 +109,7 @@ func (s *PollerScalingIntegSuite) TestPollerScalingSimpleBacklog() { Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -152,7 +144,7 @@ func (s *PollerScalingIntegSuite) TestPollerScalingSimpleBacklog() { }) s.NoError(err) s.NotNil(actResp.PollerScalingDecision) - s.Assert().GreaterOrEqual(int32(1), actResp.PollerScalingDecision.PollRequestDeltaSuggestion) + s.GreaterOrEqual(int32(1), actResp.PollerScalingDecision.PollRequestDeltaSuggestion) nexusResp, err := feClient.PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ Namespace: s.Namespace().String(), @@ -189,7 +181,7 @@ func (s *PollerScalingIntegSuite) TestPollerScalingDecisionsAreSeenProbabilistic }() allScaleDecisions := make([]*taskqueuepb.PollerScalingDecision, 0, 15) - for i := 0; i < 15; i++ { + for range 15 { resp, _ := s.FrontendClient().PollWorkflowTaskQueue(longctx, &workflowservice.PollWorkflowTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -204,7 +196,7 @@ func (s *PollerScalingIntegSuite) TestPollerScalingDecisionsAreSeenProbabilistic // We must have seen at least a handful of non-nil scaling decisions nonNilDecisions := util.FilterSlice(allScaleDecisions, func(d *taskqueuepb.PollerScalingDecision) bool { return d != nil }) - s.Assert().GreaterOrEqual(len(nonNilDecisions), 3) + s.GreaterOrEqual(len(nonNilDecisions), 3) } // The following tests verify poller scaling decisions work with worker-versioning based concepts. @@ -267,7 +259,7 @@ func (s *PollerScalingIntegSuite) testPollerScalingOnPromotedVersionConsidersUnv deploymentName := testcore.RandomizeStr(deploymentNamePrefix) // Queueing up unversioned workflows - for i := 0; i < 5; i++ { + for range 5 { _, err := s.SdkClient().ExecuteWorkflow( ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf") s.NoError(err) @@ -352,7 +344,7 @@ func (s *PollerScalingIntegSuite) testPollerScalingOnPromotedVersionConsidersUnv // Start enough activities to ensure we will see scale up decisions. These are scheduled by an unversioned poller. commands := make([]*commandpb.Command, 0, 10) - for i := 0; i < 10; i++ { + for i := range 10 { commands = append(commands, &commandpb.Command{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ diff --git a/tests/premature_eos_test.go b/tests/premature_eos_test.go new file mode 100644 index 00000000000..9f72fafefa6 --- /dev/null +++ b/tests/premature_eos_test.go @@ -0,0 +1,173 @@ +package tests + +import ( + "context" + "testing" + "time" + + commonpb "go.temporal.io/api/common/v1" + historypb "go.temporal.io/api/history/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/testing/parallelsuite" + "go.temporal.io/server/tests/testcore" +) + +type PrematureEosTestSuite struct { + parallelsuite.Suite[*PrematureEosTestSuite] +} + +func TestPrematureEosTestSuite(t *testing.T) { + parallelsuite.Run(t, &PrematureEosTestSuite{}) +} + +// Test_SpeculativeWFTEventsLostAfterSignalMidHistoryPagination demonstrates the +// "premature end of stream" bug in a scenario mimicking SDK workflow cache eviction: +// the SDK uses GetWorkflowExecutionHistory (not PollWorkflowTaskQueue) to replay +// history, fetching page 1 while a speculative WFT is active, then a signal arrives +// before page 2 is fetched. +// +// Root cause (same underlying bug as the shard-reload variant): +// +// GetWorkflowExecutionHistory page 1 sets continuationToken.NextEventId=8 when a +// speculative WFT (event 8 in memory) exists. The signal triggers +// convertSpeculativeWorkflowTaskToNormal, committing event 8 (WFT_SCHEDULED) and +// event 9 (WorkflowExecutionSignaled) to persistence. When page 2 is fetched with +// the stale token (NextEventId=8), the DB range [6, 8) returns only events 6–7. +// +// Without the gap-detection fix in GetWorkflowExecutionHistory: +// appendTransientTasks finds no transient events (speculative was committed), +// assembled history = events 1..7 (N-2, missing events 8 and 9), causing premature EOS. +// +// With the gap-detection fix: +// freshNextEventId (10) > continuationToken.NextEventId (8) → gap fetch [8, 10) +// returns events 8 and 9; assembled history has 9 events (no premature EOS). +// +// This test asserts the FIXED behavior. +func (s *PrematureEosTestSuite) Test_SpeculativeWFTEventsLostAfterSignalMidHistoryPagination() { + // MaximumPageSize controls the number of DB event batches per page, not individual + // events. The 7 persisted events are stored in 5 batches: + // [1,2] StartWorkflow, [3] WFTStarted, [4,5] WFTCompleted+WFTScheduled, + // [6] WFTStarted, [7] WFTCompleted + // A page size of 3 batches returns events 1..5 on page 1 (batches [1,2]+[3]+[4,5]), + // leaving batches [6] and [7] for the second page. + const maxBatchesPerPage = 3 + + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + tv := env.Tv() + runID := mustStartWorkflow(env, tv) + wfExecution := &commonpb.WorkflowExecution{WorkflowId: tv.WorkflowID(), RunId: runID} + + // Build 7 persisted events: + // 1: WorkflowExecutionStarted + // 2: WorkflowTaskScheduled + // 3: WorkflowTaskStarted + // 4: WorkflowTaskCompleted (ForceCreateNewWorkflowTask=true) + // 5: WorkflowTaskScheduled (force-created) + // 6: WorkflowTaskStarted + // 7: WorkflowTaskCompleted + _, err := env.TaskPoller().PollAndHandleWorkflowTask(tv, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + ForceCreateNewWorkflowTask: true, + }, nil + }) + s.NoError(err) + + _, err = env.TaskPoller().PollAndHandleWorkflowTask(tv, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{}, nil + }) + s.NoError(err) + + // Send an update to create a speculative WFT (event 8 in memory, scheduled but not polled). + ctx, cancel := context.WithCancel(testcore.NewContext()) + defer cancel() + updateCh := sendUpdate(ctx, env, tv) + defer func() { go func() { <-updateCh }() }() + + // Wait until the speculative WFT is scheduled before fetching page 1. + // This ensures the signal (sent later) arrives while the speculative WFT exists in + // mutable state, so convertSpeculativeWorkflowTaskToNormal commits both event 8 + // (WFT_SCHEDULED) and event 9 (WorkflowExecutionSignaled), giving freshNextEventId=10. + // Without this wait there is a race: if the update hasn't been processed yet, the signal + // would only add event 8 (SignalReceived) with freshNextEventId=9, producing 8 events + // instead of the expected 9 and causing a false test failure. + s.Eventually(func() bool { + desc, descErr := env.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), + &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + Execution: wfExecution, + }) + return descErr == nil && desc.GetPendingWorkflowTask() != nil + }, 5*time.Second, 250*time.Millisecond, "speculative WFT should be scheduled after sending update") + + // Fetch page 1 via GetWorkflowExecutionHistory — mimicking what the SDK does when a + // workflow is evicted from its sticky cache and must replay history from scratch. + // queryMutableState returns nextEventId=8 (speculative WFT scheduled; speculative events + // do NOT advance hBuilder.NextEventID). With maxBatchesPerPage=3, only the first 3 DB + // batches are returned ([1,2]+[3]+[4,5] = events 1..5), leaving batches [6] and [7] for + // the next page. The continuation token encodes NextEventId=8 and PersistenceToken + // pointing to the next DB batch — this is the "stale token" that exercises the bug. + histPage1, err := env.FrontendClient().GetWorkflowExecutionHistory( + testcore.NewContext(), + &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: env.Namespace().String(), + Execution: wfExecution, + MaximumPageSize: maxBatchesPerPage, + }, + ) + s.NoError(err) + s.NotNil(histPage1.NextPageToken, + "NextPageToken must be set: with maxBatchesPerPage=3 and 5 total batches, page 1 must not be the last page") + s.T().Logf("NEXTPAGETOKEN: %s", histPage1.NextPageToken) + + firstPageEvents := histPage1.History.Events + staleNextPageToken := histPage1.NextPageToken + + // Send the signal. Since the speculative WFT was cleared by the shard reload, signal + // processing finds the pending update in the registry and schedules a normal WFT: + // 8: WorkflowTaskScheduled (normal WFT scheduled to handle the pending update) + // 9: WorkflowExecutionSignaled (flushed immediately: HasStartedWorkflowTask=false) + // After this transaction, freshNextEventId=10. + _, signalErr := env.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), + &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + WorkflowExecution: wfExecution, + SignalName: tv.Any().String(), + }) + s.NoError(signalErr) + + // Fetch remaining history pages using the stale token obtained before the signal. + allEvents := make([]*historypb.HistoryEvent, len(firstPageEvents)) + copy(allEvents, firstPageEvents) + for nextPageToken := staleNextPageToken; nextPageToken != nil; { + histResp, histErr := env.FrontendClient().GetWorkflowExecutionHistory(testcore.NewContext(), + &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: env.Namespace().String(), + Execution: wfExecution, + NextPageToken: nextPageToken, + MaximumPageSize: maxBatchesPerPage, + }) + s.NoError(histErr) + allEvents = append(allEvents, histResp.History.Events...) + nextPageToken = histResp.NextPageToken + } + + // With the gap-detection fix: freshNextEventId (10) > stale NextEventId (8), so the + // gap [8..10) is fetched from DB (events 8 and 9 are now persisted after the signal). + // 9 events are assembled correctly — no premature EOS. + // + // Without the fix: DB range [6, 8) returns only events 6–7; appendTransientTasks finds + // no transient events (speculative was committed); assembled history = 7 events (N-2), + // causing premature EOS. + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowTaskScheduled + 9 WorkflowExecutionSignaled`, allEvents) +} diff --git a/tests/priority_fairness_test.go b/tests/priority_fairness_test.go index 77df9e8d349..09112d552ef 100644 --- a/tests/priority_fairness_test.go +++ b/tests/priority_fairness_test.go @@ -43,7 +43,6 @@ func TestPrioritySuite(t *testing.T) { func (s *PrioritySuite) SetupSuite() { dynamicConfigOverrides := map[dynamicconfig.Key]any{ - dynamicconfig.MatchingUseNewMatcher.Key(): true, dynamicconfig.MatchingGetTasksBatchSize.Key(): 20, dynamicconfig.MatchingGetTasksReloadAt.Key(): 5, } @@ -451,7 +450,6 @@ func (s *FairnessSuite) SetupSuite() { } if s.doAutoEnable { dynamicConfigOverrides[dynamicconfig.MatchingAutoEnableV2.Key()] = true - dynamicConfigOverrides[dynamicconfig.MatchingEnableMigration.Key()] = true dynamicConfigOverrides[dynamicconfig.MatchingUseNewMatcher.Key()] = false dynamicConfigOverrides[dynamicconfig.MatchingEnableFairness.Key()] = false } else { @@ -636,7 +634,6 @@ func (s *FairnessSuite) testMigration(newMatcher, fairness bool) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - s.OverrideDynamicConfig(dynamicconfig.MatchingEnableMigration, true) // Speed up periodic sync so drain completion is detected faster s.OverrideDynamicConfig(dynamicconfig.MatchingUpdateAckInterval, 100*time.Millisecond) @@ -663,16 +660,24 @@ func (s *FairnessSuite) testMigration(newMatcher, fairness bool) { waitForTasks := func(tp enumspb.TaskQueueType, onDraining, onActive int64) { s.T().Helper() s.EventuallyWithT(func(c *assert.CollectT) { - tasksOnDraining, tasksOnActive, _, err := s.countTasksByDrainingActive(ctx, tv, tp) + tasksOnDraining, tasksOnActive, loadedOnDraining, loadedOnActive, _, err := s.countTasksByDrainingActive(ctx, tv, tp) require.NoError(c, err) require.Equal(c, onDraining, tasksOnDraining) require.Equal(c, onActive, tasksOnActive) + // ensure that expected tasks are actually loaded to avoid poller getting regular + // task before draining loads + if tasksOnDraining > 0 { + require.NotZero(c, loadedOnDraining) + } + if tasksOnActive > 0 { + require.NotZero(c, loadedOnActive) + } }, 15*time.Second, 250*time.Millisecond) } waitForNoDraining := func(tp enumspb.TaskQueueType) { s.T().Helper() s.EventuallyWithT(func(c *assert.CollectT) { - _, _, hasDraining, err := s.countTasksByDrainingActive(ctx, tv, tp) + _, _, _, _, hasDraining, err := s.countTasksByDrainingActive(ctx, tv, tp) require.NoError(c, err) require.False(c, hasDraining, "draining queue should be unloaded after drain completes") }, 15*time.Second, 250*time.Millisecond) @@ -803,7 +808,7 @@ func (s *FairnessSuite) testMigration(newMatcher, fairness bool) { } func (s *FairnessSuite) countTasksByDrainingActive(ctx context.Context, tv *testvars.TestVars, tp enumspb.TaskQueueType) ( - tasksOnDraining, tasksOnActive int64, hasDraining bool, retErr error, + tasksOnDraining, tasksOnActive, loadedOnDraining, loadedOnActive int64, hasDraining bool, retErr error, ) { for i := range s.partitions { res, err := s.AdminClient().DescribeTaskQueuePartition(ctx, &adminservice.DescribeTaskQueuePartitionRequest{ @@ -816,15 +821,17 @@ func (s *FairnessSuite) countTasksByDrainingActive(ctx context.Context, tv *test BuildIds: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true}, }) if err != nil { - return 0, 0, false, err + return 0, 0, 0, 0, false, err } for _, versionInfoInternal := range res.VersionsInfoInternal { for _, st := range versionInfoInternal.PhysicalTaskQueueInfo.InternalTaskQueueStatus { if st.Draining { hasDraining = true tasksOnDraining += st.ApproximateBacklogCount + loadedOnDraining += st.LoadedTasks } else { tasksOnActive += st.ApproximateBacklogCount + loadedOnActive += st.LoadedTasks } } } diff --git a/tests/purge_dlq_tasks_api_test.go b/tests/purge_dlq_tasks_api_test.go index bba3f94b51a..f9afeb264ef 100644 --- a/tests/purge_dlq_tasks_api_test.go +++ b/tests/purge_dlq_tasks_api_test.go @@ -180,7 +180,7 @@ func (s *PurgeDLQTasksSuite) TestPurgeDLQTasks() { }) s.NoError(err) s.Len(readRawTasksResponse.Tasks, 1) - s.Assert().Equal(int64(persistence.FirstQueueMessageID+2), readRawTasksResponse.Tasks[0].MessageMetadata.ID) + s.Equal(int64(persistence.FirstQueueMessageID+2), readRawTasksResponse.Tasks[0].MessageMetadata.ID) }) } } @@ -201,7 +201,7 @@ func (s *PurgeDLQTasksSuite) enqueueTasks(ctx context.Context, queueKey persiste }) s.NoError(err) - for i := 0; i < 3; i++ { + for range 3 { _, err := s.dlq.EnqueueTask(ctx, &persistence.EnqueueTaskRequest{ QueueType: queueKey.QueueType, SourceCluster: queueKey.SourceCluster, diff --git a/tests/query_workflow_test.go b/tests/query_workflow_test.go index ea94c61e68a..c883664b7c1 100644 --- a/tests/query_workflow_test.go +++ b/tests/query_workflow_test.go @@ -57,7 +57,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_Sticky() { return msg, nil } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) id := "test-query-sticky" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -71,7 +71,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_Sticky() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) queryResult, err := s.SdkClient().QueryWorkflow(ctx, id, "", "test", "test") s.NoError(err) @@ -107,7 +107,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_Consistent_PiggybackQuery() { } } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) id := "test-query-consistent" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -121,7 +121,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_Consistent_PiggybackQuery() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) err = s.SdkClient().SignalWorkflow(ctx, id, "", "test", "pause") s.NoError(err) @@ -149,7 +149,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryWhileBackoff() { }) return nil } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) testCases := []struct { testName string @@ -207,7 +207,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryWhileBackoff() { func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryBeforeStart() { // stop the worker, so the workflow won't be started before query - s.Worker().Stop() + s.SdkWorker().Stop() workflowFn := func(ctx workflow.Context) (string, error) { status := "initialized" @@ -232,8 +232,11 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryBeforeStart() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) + var queryErr, getErr error + var queryResultStr string + var queryDuration time.Duration wg := sync.WaitGroup{} wg.Add(1) go func() { @@ -241,16 +244,11 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryBeforeStart() { startTime := time.Now() queryResult, err := s.SdkClient().QueryWorkflow(ctx, id, "", "test") - endTime := time.Now() - s.NoError(err) - var queryResultStr string - err = queryResult.Get(&queryResultStr) - s.NoError(err) - - // verify query sees all signals before it - s.Equal("started", queryResultStr) - - s.True(endTime.Sub(startTime) > time.Second) + queryDuration = time.Since(startTime) + queryErr = err + if err == nil { + getErr = queryResult.Get(&queryResultStr) + } }() // delay 2s to start worker, this will block query for 2s @@ -264,6 +262,12 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryBeforeStart() { // wait query wg.Wait() + + s.NoError(queryErr) + s.NoError(getErr) + // verify query sees all signals before it + s.Equal("started", queryResultStr) + s.Greater(queryDuration, time.Second) } func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryFailedWorkflowTask() { @@ -282,7 +286,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryFailedWorkflowTask() { panic("Workflow failed") } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) id := "test-query-failed-workflow-task" workflowOptions := sdkclient.StartWorkflowOptions{ @@ -297,7 +301,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_QueryFailedWorkflowTask() { s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) s.Eventually(func() bool { // wait for workflow task to fail 3 times @@ -325,7 +329,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_ClosedWithoutWorkflowTaskStarted( workflowRun, err := s.SdkClient().ExecuteWorkflow(ctx, workflowOptions, workflowFn) s.NoError(err) s.NotNil(workflowRun) - s.True(workflowRun.GetRunID() != "") + s.NotEmpty(workflowRun.GetRunID()) err = s.SdkClient().TerminateWorkflow(ctx, id, "", "terminating to make sure query fails") s.NoError(err) @@ -340,7 +344,7 @@ func (s *QueryWorkflowSuite) TestQueryWorkflow_WithRawHistoryBytesToMatchingServ s.OverrideDynamicConfig(dynamicconfig.SendRawHistoryBytesToMatchingService, true) // Stop the default worker, so we can control sticky behavior - s.Worker().Stop() + s.SdkWorker().Stop() workflowFn := func(ctx workflow.Context) (string, error) { status := "initialized" diff --git a/tests/relay_task_test.go b/tests/relay_task_test.go index 0bc4f7a2a85..c4fb878e0d5 100644 --- a/tests/relay_task_test.go +++ b/tests/relay_task_test.go @@ -95,9 +95,9 @@ func (s *RelayTaskTestSuite) TestRelayWorkflowTaskTimeout() { //nolint:forbidigo time.Sleep(time.Second * 2) // wait 2s for relay workflow task to timeout workflowTaskTimeout := false - for i := 0; i < 3; i++ { + for range 3 { events := s.GetHistory(s.Namespace().String(), workflowExecution) - if len(events) == 8 { + if len(events) >= 8 { s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled @@ -106,7 +106,8 @@ func (s *RelayTaskTestSuite) TestRelayWorkflowTaskTimeout() { 5 MarkerRecorded 6 WorkflowTaskScheduled 7 WorkflowTaskStarted - 8 WorkflowTaskTimedOut {"ScheduledEventId":6,"StartedEventId":7,"TimeoutType":1} // TIMEOUT_TYPE_START_TO_CLOSE`, events) + 8 WorkflowTaskTimedOut {"ScheduledEventId":6,"StartedEventId":7,"TimeoutType":1} // TIMEOUT_TYPE_START_TO_CLOSE + 9 WorkflowTaskScheduled`, events) workflowTaskTimeout = true break } diff --git a/tests/reset_workflow_test.go b/tests/reset_workflow_test.go index 1fe11ab3cd0..09c8dcd8200 100644 --- a/tests/reset_workflow_test.go +++ b/tests/reset_workflow_test.go @@ -83,7 +83,7 @@ func (s *ResetWorkflowTestSuite) TestResetWorkflow() { // Schedule 3 activities on first workflow task isFirstTaskProcessed = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) var scheduleActivityCommands []*commandpb.Command for i := 1; i <= activityCount; i++ { @@ -950,7 +950,7 @@ func (s *ResetWorkflowTestSuite) TestResetWorkflow_ResetAfterContinueAsNew() { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - s.Worker().RegisterWorkflow(CaNOnceWorkflow) + s.SdkWorker().RegisterWorkflow(CaNOnceWorkflow) run, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: s.TaskQueue()}, CaNOnceWorkflow, "") s.NoError(err) @@ -1152,3 +1152,7 @@ func (s *ResetWorkflowTestSuite) TestResetWorkflowWithExternalPayloads() { s.Equal(int64(1), descResp.WorkflowExecutionInfo.ExternalPayloadCount) s.Equal(workflowExternalPayloadSize, descResp.WorkflowExecutionInfo.ExternalPayloadSizeBytes) } + +func (s *ResetWorkflowTestSuite) Context() context.Context { + return s.T().Context() +} diff --git a/tests/schedule_migration_test.go b/tests/schedule_migration_test.go new file mode 100644 index 00000000000..91a4cfeef29 --- /dev/null +++ b/tests/schedule_migration_test.go @@ -0,0 +1,1305 @@ +package tests + +import ( + "encoding/binary" + "errors" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + schedulepb "go.temporal.io/api/schedule/v1" + "go.temporal.io/api/serviceerror" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + workflowpb "go.temporal.io/api/workflow/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/workflow" + "go.temporal.io/server/api/adminservice/v1" + "go.temporal.io/server/api/historyservice/v1" + schedulespb "go.temporal.io/server/api/schedule/v1" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/primitives" + "go.temporal.io/server/common/sdk" + "go.temporal.io/server/common/testing/parallelsuite" + "go.temporal.io/server/service/worker/scheduler" + "go.temporal.io/server/tests/testcore" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type ScheduleMigrationTestSuite struct { + parallelsuite.Suite[*ScheduleMigrationTestSuite] +} + +func TestScheduleMigrationTestSuite(t *testing.T) { + parallelsuite.Run(t, &ScheduleMigrationTestSuite{}) +} + +func (s *ScheduleMigrationTestSuite) TestScheduleMigrationV2AlreadyExists() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-migrate-v2-exists") + wid := testcore.RandomizeStr("sched-migrate-v2-exists-wf") + wt := testcore.RandomizeStr("sched-migrate-v2-exists-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create CHASM Schedule directly + _, err := env.GetTestCluster().SchedulerClient().CreateSchedule( + ctx, + &schedulerpb.CreateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: sched, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }, + }, + ) + s.NoError(err) + + _, err = env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + s.NoError(err) + + // Directly calling CreateFromMigrationState when a CHASM schedule already + // exists should return AlreadyExists, matching CreateSchedule's behavior. + _, err = env.GetTestCluster().SchedulerClient().CreateFromMigrationState( + ctx, + &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: nsID, + State: &schedulerpb.SchedulerMigrationState{ + SchedulerState: &schedulerpb.SchedulerState{ + Namespace: nsName, + NamespaceId: nsID, + ScheduleId: sid, + Schedule: sched, + }, + GeneratorState: &schedulerpb.GeneratorState{}, + InvokerState: &schedulerpb.InvokerState{}, + }, + }, + ) + var alreadyExists *serviceerror.AlreadyExists + s.ErrorAs(err, &alreadyExists) + s.Contains(alreadyExists.Error(), sid) + + // Create the V1 (workflow-backed) scheduler directly + startArgs := &schedulespb.StartScheduleArgs{ + Schedule: sched, + State: &schedulespb.InternalState{ + Namespace: nsName, + NamespaceId: nsID, + ScheduleId: sid, + ConflictToken: scheduler.InitialConflictToken, + }, + } + inputPayloads, err := sdk.PreferProtoDataConverter.ToPayloads(startArgs) + s.NoError(err) + v1WorkflowID := scheduler.WorkflowIDPrefix + sid + startReq := &workflowservice.StartWorkflowExecutionRequest{ + Namespace: nsName, + WorkflowId: v1WorkflowID, + WorkflowType: &commonpb.WorkflowType{Name: scheduler.WorkflowType}, + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + Input: inputPayloads, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } + _, err = env.GetTestCluster().HistoryClient().StartWorkflowExecution( + ctx, + common.CreateHistoryStartWorkflowRequest(nsID, startReq, nil, nil, time.Now().UTC()), + ) + s.NoError(err) + + _, err = env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + s.NoError(err) + + // Issue migration. The CHASM handler will return AlreadyStarted, + // and the V1 activity treats that as success (logs warning, returns nil). + // The V1 workflow terminates, but the pre-existing V2 schedule retains + // its original state -- the V1 state is not applied. + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_CHASM, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + s.Eventually(func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 10*time.Second, 500*time.Millisecond) + + // The V2 schedule should still exist and be describable after migration. + _, err = env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + s.NoError(err) +} + +func (s *ScheduleMigrationTestSuite) TestScheduleMigrationDynamicConfig() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerMigration, true), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-migrate-dc") + wid := testcore.RandomizeStr("sched-migrate-dc-wf") + wt := testcore.RandomizeStr("sched-migrate-dc-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create the V1 (workflow-backed) scheduler directly. + startArgs := &schedulespb.StartScheduleArgs{ + Schedule: sched, + State: &schedulespb.InternalState{ + Namespace: nsName, + NamespaceId: nsID, + ScheduleId: sid, + ConflictToken: scheduler.InitialConflictToken, + }, + } + inputPayloads, err := sdk.PreferProtoDataConverter.ToPayloads(startArgs) + s.NoError(err) + v1WorkflowID := scheduler.WorkflowIDPrefix + sid + startReq := &workflowservice.StartWorkflowExecutionRequest{ + Namespace: nsName, + WorkflowId: v1WorkflowID, + WorkflowType: &commonpb.WorkflowType{Name: scheduler.WorkflowType}, + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + Input: inputPayloads, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } + _, err = env.GetTestCluster().HistoryClient().StartWorkflowExecution( + ctx, + common.CreateHistoryStartWorkflowRequest(nsID, startReq, nil, nil, time.Now().UTC()), + ) + s.NoError(err) + + // Wait for the per-namespace worker to pick up the V1 workflow. + s.Eventually(func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetHistoryLength() > 3 + }, 10*time.Second, 500*time.Millisecond) + + // V1 workflow should automatically migrate due to dynamic config and complete. + s.Eventually(func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 30*time.Second, 500*time.Millisecond) + + // V2 schedule should now exist. + _, err = env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + s.NoError(err) +} + +func (s *ScheduleMigrationTestSuite) TestScheduleMigrationV1ToV2() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-migrate-v1-to-v2") + wid := testcore.RandomizeStr("sched-migrate-v1-to-v2-wf") + wt := testcore.RandomizeStr("sched-migrate-v1-to-v2-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create the V1 (workflow-backed) scheduler directly. + startArgs := &schedulespb.StartScheduleArgs{ + Schedule: sched, + State: &schedulespb.InternalState{ + Namespace: nsName, + NamespaceId: nsID, + ScheduleId: sid, + ConflictToken: scheduler.InitialConflictToken, + }, + } + inputPayloads, err := sdk.PreferProtoDataConverter.ToPayloads(startArgs) + s.NoError(err) + v1WorkflowID := scheduler.WorkflowIDPrefix + sid + startReq := &workflowservice.StartWorkflowExecutionRequest{ + Namespace: nsName, + WorkflowId: v1WorkflowID, + WorkflowType: &commonpb.WorkflowType{Name: scheduler.WorkflowType}, + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + Input: inputPayloads, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } + _, err = env.GetTestCluster().HistoryClient().StartWorkflowExecution( + ctx, + common.CreateHistoryStartWorkflowRequest(nsID, startReq, nil, nil, time.Now().UTC()), + ) + s.NoError(err) + + // Wait for the per-namespace worker to pick up the V1 workflow. + s.Eventually(func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetHistoryLength() > 3 + }, 10*time.Second, 500*time.Millisecond) + + // Issue migration from V1 to V2. + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_CHASM, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + // Wait for V1 workflow to complete. + s.Eventually(func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 10*time.Second, 500*time.Millisecond) + + // V2 schedule should now exist. + _, err = env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + s.NoError(err) +} + +func (s *ScheduleMigrationTestSuite) TestScheduleMigrationV2ToV1() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerCreation, false), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerRouting, false), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-migrate-v2-to-v1") + wid := testcore.RandomizeStr("sched-migrate-v2-to-v1-wf") + wt := testcore.RandomizeStr("sched-migrate-v2-to-v1-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + Policies: &schedulepb.SchedulePolicies{ + OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, + CatchupWindow: durationpb.New(time.Minute), + }, + State: &schedulepb.ScheduleState{ + Notes: "original notes", + }, + } + + // Create CHASM schedule directly. + _, err := env.GetTestCluster().SchedulerClient().CreateSchedule( + ctx, + &schedulerpb.CreateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: sched, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }, + }, + ) + s.NoError(err) + + // Describe the CHASM schedule before migration to capture its state. + v2Desc, err := env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + s.NoError(err) + v2Schedule := v2Desc.GetFrontendResponse().GetSchedule() + v2ConflictToken := v2Desc.GetFrontendResponse().GetConflictToken() + + // Migrate from V2 (CHASM) to V1 (workflow). + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + // Wait for the CHASM scheduler to be closed after migration. + var failedPreconditionErr *serviceerror.FailedPrecondition + s.Eventually(func() bool { + _, chasmErr := env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + return errors.As(chasmErr, &failedPreconditionErr) + }, 10*time.Second, 500*time.Millisecond) + + // Wait for the V1 system scheduler workflow to be running. + sysWorkflowID := scheduler.WorkflowIDPrefix + sid + s.Eventually(func() bool { + _, descErr := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: sysWorkflowID}, + }, + }, + ) + return descErr == nil + }, 10*time.Second, 500*time.Millisecond) + + // Describe the V1 schedule via the frontend. With routing disabled, this + // goes directly to the V1 path. The per-namespace worker must pick up + // the workflow and register query handlers before this succeeds. + var v1Desc *workflowservice.DescribeScheduleResponse + s.Eventually(func() bool { + v1Desc, err = env.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + }) + return err == nil + }, 30*time.Second, 500*time.Millisecond) + + v1Schedule := v1Desc.GetSchedule() + + // Validate the schedule spec is preserved across migration. + s.Len(v1Schedule.GetSpec().GetInterval(), len(v2Schedule.GetSpec().GetInterval())) + s.Equal( + v2Schedule.GetSpec().GetInterval()[0].GetInterval().AsDuration(), + v1Schedule.GetSpec().GetInterval()[0].GetInterval().AsDuration(), + ) + + // Validate the action is preserved. + v2Action := v2Schedule.GetAction().GetStartWorkflow() + v1Action := v1Schedule.GetAction().GetStartWorkflow() + s.Equal(v2Action.GetWorkflowId(), v1Action.GetWorkflowId()) + s.Equal(v2Action.GetWorkflowType().GetName(), v1Action.GetWorkflowType().GetName()) + s.Equal(v2Action.GetTaskQueue().GetName(), v1Action.GetTaskQueue().GetName()) + + // Validate policies are preserved. + s.Equal( + v2Schedule.GetPolicies().GetOverlapPolicy(), + v1Schedule.GetPolicies().GetOverlapPolicy(), + ) + s.Equal( + v2Schedule.GetPolicies().GetCatchupWindow().AsDuration(), + v1Schedule.GetPolicies().GetCatchupWindow().AsDuration(), + ) + + // Validate the paused state is correctly restored (not the migration-imposed pause). + s.Equal(v2Schedule.GetState().GetPaused(), v1Schedule.GetState().GetPaused()) + s.Equal(v2Schedule.GetState().GetNotes(), v1Schedule.GetState().GetNotes()) + + // Validate the conflict token value is preserved across migration. + // V2 (CHASM) serializes as LittleEndian, V1 (workflow) as BigEndian, so decode both to int64. + s.Len(v2ConflictToken, 8) + v2Token := int64(binary.LittleEndian.Uint64(v2ConflictToken)) + v1ConflictToken := v1Desc.GetConflictToken() + s.Len(v1ConflictToken, 8) + v1Token := int64(binary.BigEndian.Uint64(v1ConflictToken)) + s.Equal(v2Token, v1Token) + + // Validate ListSchedules returns exactly one entry once the V1 workflow + // has written its visibility records (no duplicates from V1+V2). + var listResp *workflowservice.ListSchedulesResponse + s.Eventually(func() bool { + listResp, err = env.FrontendClient().ListSchedules(ctx, &workflowservice.ListSchedulesRequest{ + Namespace: nsName, + MaximumPageSize: 10, + }) + return err == nil && len(listResp.GetSchedules()) == 1 + }, 30*time.Second, 500*time.Millisecond) + s.Equal(sid, listResp.GetSchedules()[0].GetScheduleId()) +} + +func (s *ScheduleMigrationTestSuite) TestScheduleMigrationV2ToV1Idempotent() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerCreation, false), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerRouting, false), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-migrate-v2-to-v1-idem") + wid := testcore.RandomizeStr("sched-migrate-v2-to-v1-idem-wf") + wt := testcore.RandomizeStr("sched-migrate-v2-to-v1-idem-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create CHASM schedule. + _, err := env.GetTestCluster().SchedulerClient().CreateSchedule( + ctx, + &schedulerpb.CreateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: sched, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }, + }, + ) + s.NoError(err) + + // First migration call. + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + // Second migration call should also succeed (idempotent). + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) +} + +func (s *ScheduleMigrationTestSuite) TestCHASMScheduleDescribeAfterDisablingCreationAndMigration() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerCreation, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerMigration, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerRouting, true), + ) + + ctx := testcore.NewContext() + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sid := testcore.RandomizeStr("sched-routing-after-disable") + wid := testcore.RandomizeStr("sched-routing-after-disable-wf") + wt := testcore.RandomizeStr("sched-routing-after-disable-wt") + tq := testcore.RandomizeStr("tq") + + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{{Interval: durationpb.New(1 * time.Hour)}}, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + _, err := env.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: sched, + Identity: "test", + RequestId: uuid.NewString(), + }) + s.NoError(err) + + firstDescribe, err := env.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + }) + s.NoError(err) + s.NotNil(firstDescribe.GetSchedule()) + s.Eventually(func() bool { + listResp, listErr := env.FrontendClient().ListSchedules(ctx, &workflowservice.ListSchedulesRequest{Namespace: nsName}) + if listErr != nil { + return false + } + for _, schedule := range listResp.GetSchedules() { + if schedule.GetScheduleId() == sid { + return true + } + } + return false + }, 10*time.Second, 200*time.Millisecond) + + // Verify the schedule exists in CHASM by describing it directly through the + // scheduler client (history-only path that only goes to CHASM). + _, err = env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + s.NoError(err) + + env.OverrideDynamicConfig(dynamicconfig.EnableCHASMSchedulerCreation, false) + env.OverrideDynamicConfig(dynamicconfig.EnableCHASMSchedulerMigration, false) + + s.Eventually(func() bool { + describeResp, describeErr := env.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + }) + if describeErr != nil { + return false + } + if describeResp.GetSchedule() == nil { + return false + } + listResp, listErr := env.FrontendClient().ListSchedules(ctx, &workflowservice.ListSchedulesRequest{Namespace: nsName}) + if listErr != nil { + return false + } + for _, schedule := range listResp.GetSchedules() { + if schedule.GetScheduleId() == sid { + return true + } + } + return false + }, 10*time.Second, 200*time.Millisecond) +} + +// TestScheduleMigrationV2ToV1RoutingFallback verifies that after migrating a +// CHASM schedule to V1, frontend operations with CHASM routing enabled fall +// through to the V1 workflow stack when the CHASM scheduler returns ErrClosed. +func (s *ScheduleMigrationTestSuite) TestScheduleMigrationV2ToV1RoutingFallback() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerCreation, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerRouting, true), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-v2-to-v1-routing") + wid := testcore.RandomizeStr("sched-v2-to-v1-routing-wf") + wt := testcore.RandomizeStr("sched-v2-to-v1-routing-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create CHASM schedule directly. + _, err := env.GetTestCluster().SchedulerClient().CreateSchedule( + ctx, + &schedulerpb.CreateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: sched, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }, + }, + ) + s.NoError(err) + + // Migrate from V2 (CHASM) to V1 (workflow). + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + // Wait for the CHASM scheduler to be closed after migration. + var failedPreconditionErr *serviceerror.FailedPrecondition + s.Eventually(func() bool { + _, chasmErr := env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + return errors.As(chasmErr, &failedPreconditionErr) + }, 10*time.Second, 500*time.Millisecond) + + // Wait for the V1 workflow to be running and query handlers registered. + s.Eventually(func() bool { + _, descErr := env.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + }) + return descErr == nil + }, 30*time.Second, 500*time.Millisecond) + + // With CHASM routing still enabled, DescribeSchedule through the frontend + // should succeed by falling through from the closed CHASM schedule to V1. + descResp, err := env.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + }) + s.NoError(err) + s.NotNil(descResp.GetSchedule()) + s.Equal(wt, descResp.GetSchedule().GetAction().GetStartWorkflow().GetWorkflowType().GetName()) + // The schedule was created unpaused; migration should preserve that state + // (not the temporary migration-imposed pause). + s.False(descResp.GetSchedule().GetState().GetPaused()) + + // ListScheduleMatchingTimes should also fall through to V1. + now := time.Now().UTC() + matchResp, err := env.FrontendClient().ListScheduleMatchingTimes(ctx, &workflowservice.ListScheduleMatchingTimesRequest{ + Namespace: nsName, + ScheduleId: sid, + StartTime: timestamppb.New(now), + EndTime: timestamppb.New(now.Add(5 * time.Hour)), + }) + s.NoError(err) + s.NotEmpty(matchResp.GetStartTime()) + + // PatchSchedule (pause) should also fall through to V1. + _, err = env.FrontendClient().PatchSchedule(ctx, &workflowservice.PatchScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Patch: &schedulepb.SchedulePatch{ + Pause: "pausing via routing fallback test", + }, + Identity: "test", + }) + s.NoError(err) + + // Verify the pause took effect on V1. The patch is delivered as a signal, + // so the workflow needs time to process it. + s.Eventually(func() bool { + descResp, err = env.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + }) + return err == nil && descResp.GetSchedule().GetState().GetPaused() + }, 10*time.Second, 500*time.Millisecond) + + // DeleteSchedule should also fall through to V1. + _, err = env.FrontendClient().DeleteSchedule(ctx, &workflowservice.DeleteScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Identity: "test", + }) + s.NoError(err) +} + +func (s *ScheduleMigrationTestSuite) TestScheduleUpdateAfterDelete() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerCreation, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerRouting, true), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-update-after-delete") + wid := testcore.RandomizeStr("sched-update-after-delete-wf") + wt := testcore.RandomizeStr("sched-update-after-delete-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create CHASM schedule. + _, err := env.GetTestCluster().SchedulerClient().CreateSchedule( + ctx, + &schedulerpb.CreateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }, + }, + ) + s.NoError(err) + + // Delete via scheduler client. + _, err = env.GetTestCluster().SchedulerClient().DeleteSchedule( + ctx, + &schedulerpb.DeleteScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DeleteScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Identity: "test", + }, + }, + ) + s.NoError(err) + + // Update via scheduler client should fail on the closed schedule. + _, err = env.GetTestCluster().SchedulerClient().UpdateSchedule( + ctx, + &schedulerpb.UpdateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.UpdateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + }, + }, + ) + var failedPreconditionErr *serviceerror.FailedPrecondition + s.ErrorAs(err, &failedPreconditionErr) + + // Patch via scheduler client should also fail on the closed schedule. + _, err = env.GetTestCluster().SchedulerClient().PatchSchedule( + ctx, + &schedulerpb.PatchScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.PatchScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Patch: &schedulepb.SchedulePatch{Pause: "test"}, + Identity: "test", + }, + }, + ) + s.ErrorAs(err, &failedPreconditionErr) + + // Delete again is idempotent in CHASM — sets Closed=true again. + _, err = env.GetTestCluster().SchedulerClient().DeleteSchedule( + ctx, + &schedulerpb.DeleteScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DeleteScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Identity: "test", + }, + }, + ) + s.NoError(err) +} + +func (s *ScheduleMigrationTestSuite) TestScheduleMigrationV1ToV2WithClosedV2() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-migrate-v1-v2-closed") + wid := testcore.RandomizeStr("sched-migrate-v1-v2-closed-wf") + wt := testcore.RandomizeStr("sched-migrate-v1-v2-closed-wt") + tq := testcore.RandomizeStr("tq") + + nsName := env.Namespace().String() + nsID := env.NamespaceID().String() + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create a CHASM schedule and then delete it. + _, err := env.GetTestCluster().SchedulerClient().CreateSchedule( + ctx, + &schedulerpb.CreateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: sched, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }, + }, + ) + s.NoError(err) + + _, err = env.GetTestCluster().SchedulerClient().DeleteSchedule( + ctx, + &schedulerpb.DeleteScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DeleteScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Identity: "test", + }, + }, + ) + s.NoError(err) + + // Create a V1 (workflow-backed) scheduler with the same ID. + startArgs := &schedulespb.StartScheduleArgs{ + Schedule: sched, + State: &schedulespb.InternalState{ + Namespace: nsName, + NamespaceId: nsID, + ScheduleId: sid, + ConflictToken: scheduler.InitialConflictToken, + }, + } + inputPayloads, err := sdk.PreferProtoDataConverter.ToPayloads(startArgs) + s.NoError(err) + v1WorkflowID := scheduler.WorkflowIDPrefix + sid + startReq := &workflowservice.StartWorkflowExecutionRequest{ + Namespace: nsName, + WorkflowId: v1WorkflowID, + WorkflowType: &commonpb.WorkflowType{Name: scheduler.WorkflowType}, + TaskQueue: &taskqueuepb.TaskQueue{Name: primitives.PerNSWorkerTaskQueue}, + Input: inputPayloads, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + WorkflowIdReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } + _, err = env.GetTestCluster().HistoryClient().StartWorkflowExecution( + ctx, + common.CreateHistoryStartWorkflowRequest(nsID, startReq, nil, nil, time.Now().UTC()), + ) + s.NoError(err) + + // Wait for the per-namespace worker to pick up the V1 workflow. + s.Eventually(func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetHistoryLength() > 3 + }, 10*time.Second, 500*time.Millisecond) + + // Issue migration from V1 to V2. The previously deleted CHASM execution + // does not block creation of a new one -- StartExecution succeeds because + // closed executions allow reuse of the business ID. + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_CHASM, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + // Wait for the V1 workflow to complete (migration activity ran). + s.Eventually(func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: nsID, + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 10*time.Second, 500*time.Millisecond) + + // The new V2 schedule should be describable. + _, err = env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + s.NoError(err) +} + +// TestScheduleMigrationV1ToV2NoDuplicateRecentActions verifies that migrating +// a V1 schedule with a running workflow to V2 does not produce duplicate entries +// in RecentActions. In V1, recordAction puts the same workflow in both +// RunningWorkflows and RecentActions. The migration must deduplicate these. +func TestScheduleMigrationV1ToV2NoDuplicateRecentActions(t *testing.T) { + // Create the env without EnableChasm so that CreateSchedule does not write + // a CHASM sentinel (which would block the migration activity). + env := testcore.NewEnv( + t, + testcore.WithSdkWorker(), + ) + + ctx := testcore.NewContext() + sid := testcore.RandomizeStr("sched-migrate-no-dup") + wid := testcore.RandomizeStr("sched-migrate-no-dup-wf") + wt := testcore.RandomizeStr("sched-migrate-no-dup-wt") + + nsName := env.Namespace().String() + + // Register a workflow that blocks until signaled, so it stays running + // during migration. + resumeSignal := "resume" + workflowFn := func(ctx workflow.Context) error { + ch := workflow.GetSignalChannel(ctx, resumeSignal) + ch.Receive(ctx, nil) + return nil + } + env.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + + // Create a V1 schedule with an immediate trigger. + sched := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: env.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + _, err := env.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Schedule: sched, + InitialPatch: &schedulepb.SchedulePatch{ + TriggerImmediately: &schedulepb.TriggerImmediatelyRequest{}, + }, + Identity: "test", + RequestId: uuid.NewString(), + }) + require.NoError(t, err) + + // Wait for the V1 scheduler to start the workflow and record it as running. + var runningWfID string + require.Eventually(t, func() bool { + descResp, err := env.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + }) + if err != nil || len(descResp.GetInfo().GetRecentActions()) == 0 { + return false + } + a := descResp.Info.RecentActions[0] + if a.GetStartWorkflowStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING { + return false + } + runningWfID = a.GetStartWorkflowResult().GetWorkflowId() + return true + }, 15*time.Second, 500*time.Millisecond) + + // Enable CHASM now so the migration activity can create the V2 schedule. + env.OverrideDynamicConfig(dynamicconfig.EnableChasm, true) + + // Migrate from V1 to V2 while the workflow is still running. + _, err = env.AdminClient().MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: nsName, + ScheduleId: sid, + Target: adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_CHASM, + Identity: "test", + RequestId: testcore.RandomizeStr("request-id"), + }) + require.NoError(t, err) + + // Wait for the V1 scheduler workflow to complete (migration done). + v1WorkflowID := scheduler.WorkflowIDPrefix + sid + require.Eventually(t, func() bool { + desc, err := env.GetTestCluster().HistoryClient().DescribeWorkflowExecution( + ctx, + &historyservice.DescribeWorkflowExecutionRequest{ + NamespaceId: env.NamespaceID().String(), + Request: &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{WorkflowId: v1WorkflowID}, + }, + }, + ) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 10*time.Second, 500*time.Millisecond) + + // Describe the V2 schedule and verify no duplicate RunIds in RecentActions. + v2Desc, err := env.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: env.NamespaceID().String(), + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: nsName, ScheduleId: sid}, + }, + ) + require.NoError(t, err) + + recentActions := v2Desc.GetFrontendResponse().GetInfo().GetRecentActions() + assertRecentActionsNoDuplicateRunIDs(t, recentActions) + + // The running workflow should appear exactly once. + var count int + for _, action := range recentActions { + if strings.HasPrefix(action.GetStartWorkflowResult().GetWorkflowId(), wid) { + count++ + } + } + require.Equal(t, 1, count, "running workflow should appear exactly once in RecentActions, got %d", count) + + // Clean up: signal the running workflow to complete. + _, err = env.FrontendClient().SignalWorkflowExecution(ctx, &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: nsName, + WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: runningWfID}, + SignalName: resumeSignal, + }) + require.NoError(t, err) +} diff --git a/tests/schedule_test.go b/tests/schedule_test.go index 279aae1b509..3c336e6a4ca 100644 --- a/tests/schedule_test.go +++ b/tests/schedule_test.go @@ -4,15 +4,16 @@ import ( "context" "errors" "fmt" + "net/http/httptest" "strings" "sync/atomic" "testing" "time" "github.com/google/uuid" + "github.com/nexus-rpc/sdk-go/nexus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" schedulepb "go.temporal.io/api/schedule/v1" @@ -21,16 +22,20 @@ import ( workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" sdkclient "go.temporal.io/sdk/client" - "go.temporal.io/sdk/converter" - "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" + schedulespb "go.temporal.io/server/api/schedule/v1" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/nexus/nexusrpc" "go.temporal.io/server/common/payload" "go.temporal.io/server/common/payloads" "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/searchattribute/sadefs" "go.temporal.io/server/common/testing/protorequire" + "go.temporal.io/server/components/callbacks" + "go.temporal.io/server/service/worker/dummy" "go.temporal.io/server/service/worker/scheduler" "go.temporal.io/server/tests/testcore" "google.golang.org/grpc/metadata" @@ -39,98 +44,133 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -/* -more tests to write: +// contextFactory wraps a base context for CHASM vs V1 differences. +type contextFactory func(context.Context) context.Context -various validation errors - -overlap policies, esp. buffer - -worker restart/long-poll activity failure: - get it in a state where it's waiting for a wf to exit, say with bufferone - restart the worker/force activity to fail - terminate the wf - check that new one starts immediately -*/ - -type ( - scheduleFunctionalSuiteBase struct { - testcore.FunctionalTestBase - - sdkClient sdkclient.Client - worker worker.Worker - taskQueue string - dataConverter converter.DataConverter - newContext func() context.Context - } - - ScheduleCHASMFunctionalSuite struct { - scheduleFunctionalSuiteBase +var ( + chasmContextFactory contextFactory = func(ctx context.Context) context.Context { + return metadata.NewOutgoingContext(ctx, metadata.Pairs( + headers.ExperimentHeaderName, "chasm-scheduler", + )) } - - ScheduleV1FunctionalSuite struct { - scheduleFunctionalSuiteBase + v1ContextFactory contextFactory = func(ctx context.Context) context.Context { + return ctx } ) -func TestScheduleFunctionalSuite(t *testing.T) { - t.Parallel() +func scheduleCommonOpts() []testcore.TestOption { + return []testcore.TestOption{ + testcore.WithSdkWorker(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.FrontendAllowedExperiments, []string{"*"}), + } +} - // CHASM tests must run as a separate suite, with a separate cluster/functional environment, because the tests - // assume a fully clean state. For example, TestBasics has assertions on visibility entries for workflow runs - // started by the scheduler, which would not be cleaned up even when the associated scheduler has been deleted. - suite.Run(t, new(ScheduleCHASMFunctionalSuite)) - suite.Run(t, new(ScheduleV1FunctionalSuite)) +func TestScheduleCHASM(t *testing.T) { + runSharedScheduleTests(t, chasmContextFactory) + + // CHASM-only tests + newContext := chasmContextFactory + t.Run("TestCreateScheduleAlreadyExists", func(t *testing.T) { testCreateScheduleAlreadyExists(t, newContext) }) + t.Run("TestCreateScheduleDuplicateSdkError", func(t *testing.T) { testCreateScheduleDuplicateSdkError(t, true) }) + t.Run("TestPatchRejectsExcessBackfillers", func(t *testing.T) { testPatchRejectsExcessBackfillers(t, newContext) }) + t.Run("TestDoubleReset_HSMCallbacks", func(t *testing.T) { testScheduledWorkflowDoubleReset(t, newContext, false) }) + t.Run("TestDoubleReset_ChasmCallbacks", func(t *testing.T) { testScheduledWorkflowDoubleReset(t, newContext, true) }) + t.Run("TestResetWithAdditionalCallback_HSMCallbacks", func(t *testing.T) { testResetWithAdditionalCallback(t, newContext, false) }) + t.Run("TestResetWithAdditionalCallback_ChasmCallbacks", func(t *testing.T) { testResetWithAdditionalCallback(t, newContext, true) }) + t.Run("TestMigrationCallbackAttach", func(t *testing.T) { testMigrationCallbackAttach(t, newContext) }) + t.Run("TestCreatesWorkflowSentinel", func(t *testing.T) { testCreatesWorkflowSentinel(t, newContext) }) + t.Run("TestUpdateScheduleMemo", func(t *testing.T) { testUpdateScheduleMemo(t, newContext) }) + t.Run("TestUpdateScheduleMemoOnly", func(t *testing.T) { testUpdateScheduleMemoOnly(t, newContext) }) } -func (s *ScheduleCHASMFunctionalSuite) SetupTest() { - s.newContext = func() context.Context { - baseCtx := testcore.NewContext() - return metadata.NewOutgoingContext(baseCtx, metadata.Pairs( - headers.ExperimentHeaderName, "chasm-scheduler", - )) - } - s.scheduleFunctionalSuiteBase.SetupTest() +func TestScheduleV1(t *testing.T) { + runSharedScheduleTests(t, v1ContextFactory) + + // V1-only tests + newContext := v1ContextFactory + t.Run("TestCreateScheduleDuplicateSdkError", func(t *testing.T) { testCreateScheduleDuplicateSdkError(t, false) }) + t.Run("TestCHASMCanListV1Schedules", func(t *testing.T) { testCHASMCanListV1Schedules(t, newContext) }) + t.Run("TestRefresh", func(t *testing.T) { testRefresh(t, newContext) }) + t.Run("TestListBeforeRun", func(t *testing.T) { testListBeforeRun(t, newContext) }) + t.Run("TestRateLimit", func(t *testing.T) { testRateLimit(t, newContext) }) + t.Run("TestNextTimeCache", func(t *testing.T) { testNextTimeCache(t, newContext) }) + t.Run("TestCreatesCHASMSentinel", func(t *testing.T) { testCreatesCHASMSentinel(t, newContext) }) + t.Run("TestUpdateScheduleMemoRejected", func(t *testing.T) { testUpdateScheduleMemoRejected(t, newContext) }) } -func (s *ScheduleV1FunctionalSuite) SetupTest() { - s.newContext = func() context.Context { - return testcore.NewContext() - } - s.scheduleFunctionalSuiteBase.SetupTest() +func runSharedScheduleTests(t *testing.T, newContext contextFactory) { + t.Run("TestBasics", func(t *testing.T) { testBasics(t, newContext) }) + t.Run("TestInput", func(t *testing.T) { testInput(t, newContext) }) + t.Run("TestLastCompletionAndError", func(t *testing.T) { testLastCompletionAndError(t, newContext) }) + t.Run("TestListSchedulesReturnsWorkflowStatus", func(t *testing.T) { testListSchedulesReturnsWorkflowStatus(t, newContext) }) + t.Run("TestUpdateIntervalTakesEffect", func(t *testing.T) { testUpdateIntervalTakesEffect(t, newContext) }) + t.Run("TestListScheduleMatchingTimes", func(t *testing.T) { testListScheduleMatchingTimes(t, newContext) }) + t.Run("TestLimitMemoSpecSize", func(t *testing.T) { testLimitMemoSpecSize(t, newContext) }) + t.Run("TestCountSchedules", func(t *testing.T) { testCountSchedules(t, newContext) }) + t.Run("TestSchedule_InternalTaskQueue", func(t *testing.T) { testScheduleInternalTaskQueue(t, newContext) }) + t.Run("TestDeletedScheduleOperations", func(t *testing.T) { testDeletedScheduleOperations(t, newContext) }) } -func (s *scheduleFunctionalSuiteBase) SetupTest() { - s.OverrideDynamicConfig(dynamicconfig.EnableChasm, true) - s.OverrideDynamicConfig(dynamicconfig.FrontendAllowedExperiments, []string{"*"}) - s.FunctionalTestBase.SetupTest() - s.dataConverter = testcore.NewTestDataConverter() +func testDeletedScheduleOperations(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-deleted-ops" + wid := "sched-test-deleted-ops-wf" + wt := "sched-test-deleted-ops-wt" + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } - var err error - s.sdkClient, err = sdkclient.Dial(sdkclient.Options{ - HostPort: s.FrontendGRPCAddress(), - Namespace: s.Namespace().String(), - DataConverter: s.dataConverter, + // Create a schedule. + _, err := s.FrontendClient().CreateSchedule(newContext(s.Context()), &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), }) s.NoError(err) - s.taskQueue = testcore.RandomizeStr("tq") - s.worker = worker.New(s.sdkClient, s.taskQueue, worker.Options{}) - err = s.worker.Start() + // Delete the schedule. + _, err = s.FrontendClient().DeleteSchedule(newContext(s.Context()), &workflowservice.DeleteScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Identity: "test", + }) s.NoError(err) -} -func (s *scheduleFunctionalSuiteBase) TearDownTest() { - if s.worker != nil { - s.worker.Stop() - } - if s.sdkClient != nil { - s.sdkClient.Close() - } - s.FunctionalTestBase.TearDownTest() + // Describe should return NotFound. + var notFoundErr *serviceerror.NotFound + s.Eventually(func() bool { + _, descErr := s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + }) + return errors.As(descErr, ¬FoundErr) + }, 10*time.Second, 200*time.Millisecond) + + // Update, Patch, and Delete behave differently across CHASM and V1, + // so they are not tested here. See TestScheduleUpdateAfterDelete. } -func (s *scheduleFunctionalSuiteBase) TestBasics() { +func testBasics(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + sid := "sched-test-basics" wid := "sched-test-basics-wf" wt := "sched-test-basics-wt" @@ -163,7 +203,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ WorkflowId: wid, WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Memo: &commonpb.Memo{ Fields: map[string]*commonpb.Payload{"wfmemo1": wfMemo}, }, @@ -200,7 +240,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { }) return nil } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) workflow2Fn := func(ctx workflow.Context) error { workflow.SideEffect(ctx, func(ctx workflow.Context) any { atomic.AddInt32(&runs2, 1) @@ -208,15 +248,14 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { }) return nil } - s.worker.RegisterWorkflowWithOptions(workflow2Fn, workflow.RegisterOptions{Name: wt2}) + s.SdkWorker().RegisterWorkflowWithOptions(workflow2Fn, workflow.RegisterOptions{Name: wt2}) // create - ctx := s.newContext() + ctx := newContext(s.Context()) createTime := time.Now() _, err := s.FrontendClient().CreateSchedule(ctx, req) s.NoError(err) - s.cleanup(sid) // describe immediately after create and verify FutureActionTimes describeRespAfterCreate, err := s.FrontendClient().DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ @@ -240,12 +279,12 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { // wait for visibility to stabilize on completed before calling describe, // otherwise their recent actions may flake and differ - visibilityResponse := s.getScheduleEntryFomVisibility(sid, func(ent *schedulepb.ScheduleListEntry) bool { + visibilityResponse := getScheduleEntryFromVisibility(s, sid, newContext, func(ent *schedulepb.ScheduleListEntry) bool { recentActions := ent.GetInfo().GetRecentActions() return len(recentActions) >= 2 && recentActions[1].GetStartWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED }) - describeResp, err := s.FrontendClient().DescribeSchedule(s.newContext(), &workflowservice.DescribeScheduleRequest{ + describeResp, err := s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, }) @@ -294,7 +333,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { s.Equal(wfMemo.Data, describeResp.Schedule.Action.GetStartWorkflow().Memo.Fields["wfmemo1"].Data) // GreaterOrEqual is used as we may have had other runs start while waiting for visibility - s.DurationNear(describeResp.Info.CreateTime.AsTime().Sub(createTime), 0, 3*time.Second) + durationNear(t, describeResp.Info.CreateTime.AsTime().Sub(createTime), 0) s.GreaterOrEqual(describeResp.Info.ActionCount, int64(2)) s.EqualValues(0, describeResp.Info.MissedCatchupWindow) s.EqualValues(0, describeResp.Info.OverlapSkipped) @@ -302,8 +341,8 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { s.GreaterOrEqual(len(describeResp.Info.RecentActions), 2) action0 := describeResp.Info.RecentActions[0] s.WithinRange(action0.ScheduleTime.AsTime(), createTime, time.Now()) - s.True(action0.ScheduleTime.AsTime().UnixNano()%int64(5*time.Second) == 0) - s.DurationNear(action0.ActualTime.AsTime().Sub(action0.ScheduleTime.AsTime()), 0, 3*time.Second) + s.Equal(int64(0), action0.ScheduleTime.AsTime().UnixNano()%int64(5*time.Second)) + durationNear(t, action0.ActualTime.AsTime().Sub(action0.ScheduleTime.AsTime()), 0) // validate list response @@ -318,11 +357,12 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { checkSpec(visibilityResponse.Info.Spec) s.Equal(wt, visibilityResponse.Info.WorkflowType.Name) s.False(visibilityResponse.Info.Paused) - s.assertSameRecentActions(describeResp, visibilityResponse) + assertSameRecentActions(s.T(), describeResp, visibilityResponse) + assertRecentActionsNoDuplicateRunIDs(s.T(), describeResp.Info.RecentActions) // list workflows - wfResp, err := s.FrontendClient().ListWorkflowExecutions(s.newContext(), &workflowservice.ListWorkflowExecutionsRequest{ + wfResp, err := s.FrontendClient().ListWorkflowExecutions(newContext(s.Context()), &workflowservice.ListWorkflowExecutionsRequest{ Namespace: s.Namespace().String(), PageSize: 5, Query: "", @@ -350,11 +390,11 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { var ex0StartTime time.Time s.NoError(payload.Decode(ex0.SearchAttributes.IndexedFields[sadefs.TemporalScheduledStartTime], &ex0StartTime)) s.WithinRange(ex0StartTime, createTime, time.Now()) - s.True(ex0StartTime.UnixNano()%int64(5*time.Second) == 0) + s.Equal(int64(0), ex0StartTime.UnixNano()%int64(5*time.Second)) // list schedules with search attribute filter - listResp, err := s.FrontendClient().ListSchedules(s.newContext(), &workflowservice.ListSchedulesRequest{ + listResp, err := s.FrontendClient().ListSchedules(newContext(s.Context()), &workflowservice.ListSchedulesRequest{ Namespace: s.Namespace().String(), MaximumPageSize: 5, Query: "CustomKeywordField = 'schedule sa value' AND TemporalSchedulePaused = false", @@ -366,7 +406,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { // list schedules with invalid search attribute filter - _, err = s.FrontendClient().ListSchedules(s.newContext(), &workflowservice.ListSchedulesRequest{ + _, err = s.FrontendClient().ListSchedules(newContext(s.Context()), &workflowservice.ListSchedulesRequest{ Namespace: s.Namespace().String(), MaximumPageSize: 5, Query: "ExecutionDuration > '1s'", @@ -379,7 +419,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { schedule.Action.GetStartWorkflow().WorkflowType.Name = wt2 updateTime := time.Now() - _, err = s.FrontendClient().UpdateSchedule(s.newContext(), &workflowservice.UpdateScheduleRequest{ + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, Schedule: schedule, @@ -397,7 +437,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { // describe again describeResp, err = s.FrontendClient().DescribeSchedule( - s.newContext(), + newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, @@ -413,9 +453,9 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { s.Equal(wfSAValue.Data, describeResp.Schedule.Action.GetStartWorkflow().SearchAttributes.IndexedFields[csaKeyword].Data) s.Equal(wfMemo.Data, describeResp.Schedule.Action.GetStartWorkflow().Memo.Fields["wfmemo1"].Data) - s.DurationNear(describeResp.Info.UpdateTime.AsTime().Sub(updateTime), 0, 3*time.Second) + durationNear(t, describeResp.Info.UpdateTime.AsTime().Sub(updateTime), 0) lastAction := describeResp.Info.RecentActions[len(describeResp.Info.RecentActions)-1] - s.True(lastAction.ScheduleTime.AsTime().UnixNano()%int64(5*time.Second) == 1000000000, lastAction.ScheduleTime.AsTime().UnixNano()) + s.Equal(int64(1000000000), lastAction.ScheduleTime.AsTime().UnixNano()%int64(5*time.Second), lastAction.ScheduleTime.AsTime().UnixNano()) // update schedule and search attributes @@ -425,7 +465,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { csaDouble := "CustomDoubleField" schSADoubleValue, _ := payload.Encode(3.14) schSAIntValue, _ = payload.Encode(321) - _, err = s.FrontendClient().UpdateSchedule(s.newContext(), &workflowservice.UpdateScheduleRequest{ + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, Schedule: schedule, @@ -446,7 +486,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { s.EventuallyWithT( func(c *assert.CollectT) { describeResp, err = s.FrontendClient().DescribeSchedule( - s.newContext(), + newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, @@ -468,7 +508,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { schedule.Spec.Interval[0].Phase = durationpb.New(1 * time.Second) schedule.Action.GetStartWorkflow().WorkflowType.Name = wt2 - _, err = s.FrontendClient().UpdateSchedule(s.newContext(), &workflowservice.UpdateScheduleRequest{ + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, Schedule: schedule, @@ -482,7 +522,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { s.EventuallyWithT( func(c *assert.CollectT) { describeResp, err = s.FrontendClient().DescribeSchedule( - s.newContext(), + newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, @@ -497,7 +537,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { // pause - _, err = s.FrontendClient().PatchSchedule(s.newContext(), &workflowservice.PatchScheduleRequest{ + _, err = s.FrontendClient().PatchSchedule(newContext(s.Context()), &workflowservice.PatchScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, Patch: &schedulepb.SchedulePatch{ @@ -511,7 +551,7 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { time.Sleep(7 * time.Second) //nolint:forbidigo s.EqualValues(1, atomic.LoadInt32(&runs2), "has not run again") - describeResp, err = s.FrontendClient().DescribeSchedule(s.newContext(), &workflowservice.DescribeScheduleRequest{ + describeResp, err = s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, }) @@ -521,12 +561,12 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { s.Equal("because I said so", describeResp.Schedule.State.Notes) // don't loop to wait for visibility, we already waited 7s from the patch - listResp, err = s.FrontendClient().ListSchedules(s.newContext(), &workflowservice.ListSchedulesRequest{ + listResp, err = s.FrontendClient().ListSchedules(newContext(s.Context()), &workflowservice.ListSchedulesRequest{ Namespace: s.Namespace().String(), MaximumPageSize: 5, }) s.NoError(err) - s.Equal(1, len(listResp.Schedules)) + s.Len(listResp.Schedules, 1) entry = listResp.Schedules[0] s.Equal(sid, entry.ScheduleId) s.True(entry.Info.Paused) @@ -534,21 +574,22 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { // finally delete - _, err = s.FrontendClient().DeleteSchedule(s.newContext(), &workflowservice.DeleteScheduleRequest{ + _, err = s.FrontendClient().DeleteSchedule(newContext(s.Context()), &workflowservice.DeleteScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, Identity: "test", }) s.NoError(err) - describeResp, err = s.FrontendClient().DescribeSchedule(s.newContext(), &workflowservice.DescribeScheduleRequest{ + describeResp, err = s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, }) - s.Error(err) + var notFoundErr *serviceerror.NotFound + s.ErrorAs(err, ¬FoundErr) s.Eventually(func() bool { // wait for visibility - listResp, err := s.FrontendClient().ListSchedules(s.newContext(), &workflowservice.ListSchedulesRequest{ + listResp, err := s.FrontendClient().ListSchedules(newContext(s.Context()), &workflowservice.ListSchedulesRequest{ Namespace: s.Namespace().String(), MaximumPageSize: 5, }) @@ -557,7 +598,9 @@ func (s *scheduleFunctionalSuiteBase) TestBasics() { }, 10*time.Second, 1*time.Second) } -func (s *scheduleFunctionalSuiteBase) TestInput() { +func testInput(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + sid := "sched-test-input" wid := "sched-test-input-wf" wt := "sched-test-input-wt" @@ -572,7 +615,7 @@ func (s *scheduleFunctionalSuiteBase) TestInput() { Things: []int{7, 8, 9}, } input2 := map[int]float64{11: 1.4375} - inputPayloads, err := s.dataConverter.ToPayloads(input1, input2) + inputPayloads, err := payloads.Encode(input1, input2) s.NoError(err) schedule := &schedulepb.Schedule{ @@ -586,7 +629,7 @@ func (s *scheduleFunctionalSuiteBase) TestInput() { StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ WorkflowId: wid, WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Input: inputPayloads, }, }, @@ -610,17 +653,18 @@ func (s *scheduleFunctionalSuiteBase) TestInput() { }) return nil } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - ctx := s.newContext() + ctx := newContext(s.Context()) _, err = s.FrontendClient().CreateSchedule(ctx, req) s.NoError(err) - s.cleanup(sid) s.Eventually(func() bool { return atomic.LoadInt32(&runs) == 1 }, 8*time.Second, 200*time.Millisecond) } -func (s *scheduleFunctionalSuiteBase) TestLastCompletionAndError() { +func testLastCompletionAndError(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + sid := "sched-test-last" wid := "sched-test-last-wf" wt := "sched-test-last-wt" @@ -636,7 +680,7 @@ func (s *scheduleFunctionalSuiteBase) TestLastCompletionAndError() { StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ WorkflowId: wid, WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, }, }, }, @@ -668,7 +712,7 @@ func (s *scheduleFunctionalSuiteBase) TestLastCompletionAndError() { switch num { case 1: - s.Equal("", lcr) + s.Empty(lcr) s.NoError(lastErr) return "this one succeeds", nil case 2: @@ -684,19 +728,23 @@ func (s *scheduleFunctionalSuiteBase) TestLastCompletionAndError() { panic("shouldn't be running anymore") } } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - ctx := s.newContext() + ctx := newContext(s.Context()) _, err := s.FrontendClient().CreateSchedule(ctx, req) s.NoError(err) - s.cleanup(sid) s.Eventually(func() bool { return atomic.LoadInt32(&testComplete) == 1 }, 20*time.Second, 200*time.Millisecond) } -// Tests that a schedule created in the V1 stack will also be visible in the V2 stack. -func (s *ScheduleV1FunctionalSuite) TestCHASMCanListV1Schedules() { - sid := "schedule-created-on-v1" +func testListSchedulesReturnsWorkflowStatus(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-list-running" + wid := "sched-test-list-running-wf" + wt := "sched-test-list-running-wt" + + // Set up a schedule that immediately starts a single running workflow schedule := &schedulepb.Schedule{ Spec: &schedulepb.ScheduleSpec{ Interval: []*schedulepb.IntervalSpec{ @@ -706,194 +754,166 @@ func (s *ScheduleV1FunctionalSuite) TestCHASMCanListV1Schedules() { Action: &schedulepb.ScheduleAction{ Action: &schedulepb.ScheduleAction_StartWorkflow{ StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: "wf-", - WorkflowType: &commonpb.WorkflowType{Name: "action"}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, }, }, }, } - req := &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), + patch := &schedulepb.SchedulePatch{ + TriggerImmediately: &schedulepb.TriggerImmediatelyRequest{}, } - // Create on V1 stack. - _, err := s.FrontendClient().CreateSchedule(s.newContext(), req) - s.NoError(err) + // The workflow sits open until we've asserted it can be listed as running + resumeSignal := "resume" + workflowFn := func(ctx workflow.Context) error { + selector := workflow.NewSelector(ctx) + selector.AddReceive(workflow.GetSignalChannel(ctx, resumeSignal), func(c workflow.ReceiveChannel, more bool) { + // nothing to do + }) + selector.Select(ctx) + return nil + } + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - // Pause so that `FutureActionTimes` doesn't change between calls. - _, err = s.FrontendClient().PatchSchedule(s.newContext(), &workflowservice.PatchScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Patch: &schedulepb.SchedulePatch{ - Pause: "halt", - }, - Identity: "test", - RequestId: uuid.NewString(), - }) + req := &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + InitialPatch: patch, + RequestId: uuid.NewString(), + } + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, req) s.NoError(err) - // Sanity test, list with V1 handler. - v1Entry := s.getScheduleEntryFomVisibility(sid, func(sle *schedulepb.ScheduleListEntry) bool { - return sle.GetInfo().Paused + // validate RecentActions made it to visibility + listResp := getScheduleEntryFromVisibility(s, sid, newContext, func(listResp *schedulepb.ScheduleListEntry) bool { + return len(listResp.Info.RecentActions) >= 1 }) - s.NotNil(v1Entry.GetInfo()) + s.Len(listResp.Info.RecentActions, 1) - // Count with V1 handler. - v1CountResp, err := s.FrontendClient().CountSchedules(s.newContext(), &workflowservice.CountSchedulesRequest{ + a1 := listResp.Info.RecentActions[0] + s.True(strings.HasPrefix(a1.StartWorkflowResult.WorkflowId, wid)) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, a1.StartWorkflowStatus) + + // let the started workflow complete + _, err = s.FrontendClient().SignalWorkflowExecution(newContext(s.Context()), &workflowservice.SignalWorkflowExecutionRequest{ Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: a1.StartWorkflowResult.WorkflowId, + RunId: a1.StartWorkflowResult.RunId, + }, + SignalName: resumeSignal, }) s.NoError(err) - s.GreaterOrEqual(v1CountResp.Count, int64(1), "Expected at least 1 schedule with V1 handler") - // Flip on CHASM experiment and make sure we can still list. - s.newContext = func() context.Context { - return metadata.NewOutgoingContext(testcore.NewContext(), metadata.Pairs( - headers.ExperimentHeaderName, "chasm-scheduler", - )) - } - chasmEntry := s.getScheduleEntryFomVisibility(sid, nil) - s.NotNil(chasmEntry.GetInfo()) - s.ProtoEqual(chasmEntry.GetInfo(), v1Entry.GetInfo()) + // now wait for second recent action to land in visbility + listResp = getScheduleEntryFromVisibility(s, sid, newContext, func(listResp *schedulepb.ScheduleListEntry) bool { + return len(listResp.Info.RecentActions) >= 2 + }) - // Count with CHASM handler and verify it matches V1 count. - chasmCountResp, err := s.FrontendClient().CountSchedules(s.newContext(), &workflowservice.CountSchedulesRequest{ - Namespace: s.Namespace().String(), + a1 = listResp.Info.RecentActions[0] + a2 := listResp.Info.RecentActions[1] + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, a1.StartWorkflowStatus) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, a2.StartWorkflowStatus) + + // Also verify that DescribeSchedule's output matches + descResp, err := s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, }) s.NoError(err) - s.Equal(v1CountResp.Count, chasmCountResp.Count, "CHASM and V1 counts should match") + assertSameRecentActions(s.T(), descResp, listResp) + + // Verify no duplicate RunIds in recent actions (regression for migration dedup bug). + assertRecentActionsNoDuplicateRunIDs(s.T(), descResp.Info.RecentActions) + assertRecentActionsNoDuplicateRunIDs(s.T(), listResp.Info.RecentActions) } -// TestRefresh applies to V1 scheduler only; V2 does not support/need manual refresh. -func (s *ScheduleV1FunctionalSuite) TestRefresh() { - sid := "sched-test-refresh" - wid := "sched-test-refresh-wf" - wt := "sched-test-refresh-wt" +func testUpdateIntervalTakesEffect(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-update-interval" + wid := "sched-test-update-interval-wf" + wt := "sched-test-update-interval-wt" + + var runs int32 + workflowFn := func(ctx workflow.Context) error { + workflow.SideEffect(ctx, func(ctx workflow.Context) any { + atomic.AddInt32(&runs, 1) + return 0 + }) + return nil + } + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + // Create schedule with a long interval (300s) - won't fire for 5 minutes. schedule := &schedulepb.Schedule{ Spec: &schedulepb.ScheduleSpec{ Interval: []*schedulepb.IntervalSpec{ - { - Interval: durationpb.New(30 * time.Second), - // start within three seconds - Phase: durationpb.New(time.Duration((time.Now().Unix()+3)%30) * time.Second), - }, + {Interval: durationpb.New(300 * time.Second)}, }, }, Action: &schedulepb.ScheduleAction{ Action: &schedulepb.ScheduleAction_StartWorkflow{ StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: wid, - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - WorkflowExecutionTimeout: durationpb.New(3 * time.Second), + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, }, }, }, } - req := &workflowservice.CreateScheduleRequest{ + + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, Schedule: schedule, Identity: "test", RequestId: uuid.NewString(), - } - - var runs int32 - workflowFn := func(ctx workflow.Context) error { - workflow.SideEffect(ctx, func(ctx workflow.Context) any { - atomic.AddInt32(&runs, 1) - return 0 - }) - s.NoError(workflow.Sleep(ctx, 10*time.Second)) // longer than execution timeout - return nil - } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - - _, err := s.FrontendClient().CreateSchedule(s.newContext(), req) - s.NoError(err) - s.cleanup(sid) - - s.Eventually(func() bool { return atomic.LoadInt32(&runs) == 1 }, 6*time.Second, 200*time.Millisecond) - - // workflow has started but is now sleeping. it will timeout in 2 seconds. - - describeResp, err := s.FrontendClient().DescribeSchedule(s.newContext(), &workflowservice.DescribeScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, }) s.NoError(err) - s.EqualValues(1, len(describeResp.Info.RunningWorkflows)) - - events1 := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) - expectedHistory := ` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 MarkerRecorded - 6 MarkerRecorded - 7 UpsertWorkflowSearchAttributes - 8 TimerStarted - 9 TimerFired - 10 WorkflowTaskScheduled - 11 WorkflowTaskStarted - 12 WorkflowTaskCompleted - 13 MarkerRecorded - 14 MarkerRecorded - 15 WorkflowPropertiesModified - 16 TimerStarted` - - s.EqualHistoryEvents(expectedHistory, events1) - - time.Sleep(4 * time.Second) //nolint:forbidigo - // now it has timed out, but the scheduler hasn't noticed yet. we can prove it by checking - // its history. - events2 := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) - s.EqualHistoryEvents(expectedHistory, events2) - - // when we describe we'll force a refresh and see it timed out - describeResp, err = s.FrontendClient().DescribeSchedule(s.newContext(), &workflowservice.DescribeScheduleRequest{ + // Update the interval to be very short (1s). + schedule.Spec.Interval[0].Interval = durationpb.New(1 * time.Second) + _, err = s.FrontendClient().UpdateSchedule(ctx, &workflowservice.UpdateScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), }) s.NoError(err) - s.EqualValues(0, len(describeResp.Info.RunningWorkflows)) - // check scheduler has gotten the refresh and done some stuff. signal is sent without waiting so we need to wait. - s.Eventually(func() bool { - events3 := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) - return len(events3) > len(events2) - }, 5*time.Second, 100*time.Millisecond) + // After updating to 1s interval, we should see runs start within a few seconds. + s.Eventually( + func() bool { return atomic.LoadInt32(&runs) >= 2 }, + 10*time.Second, + 500*time.Millisecond, + "expected at least 2 runs within 10s after updating interval to 1s", + ) } -// TestListBeforeRun only applies to V1, as V2 scheduler does not involve the -// per-NS worker or workflow. -func (s *ScheduleV1FunctionalSuite) TestListBeforeRun() { - sid := "sched-test-list-before-run" - wid := "sched-test-list-before-run-wf" - wt := "sched-test-list-before-run-wt" +func testListScheduleMatchingTimes(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) - // disable per-ns worker so that the schedule workflow never runs - s.OverrideDynamicConfig(dynamicconfig.WorkerPerNamespaceWorkerCount, 0) + sid := "sched-test-list-matching-times" schedule := &schedulepb.Schedule{ Spec: &schedulepb.ScheduleSpec{ Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(3 * time.Second)}, + {Interval: durationpb.New(1 * time.Hour)}, }, }, Action: &schedulepb.ScheduleAction{ Action: &schedulepb.ScheduleAction_StartWorkflow{ StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: wid, - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + WorkflowId: "wf-list-matching-times", + WorkflowType: &commonpb.WorkflowType{Name: "action"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, }, }, }, @@ -906,262 +926,9 @@ func (s *ScheduleV1FunctionalSuite) TestListBeforeRun() { RequestId: uuid.NewString(), } - startTime := time.Now() - - _, err := s.FrontendClient().CreateSchedule(s.newContext(), req) + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, req) s.NoError(err) - s.cleanup(sid) - - entry := s.getScheduleEntryFomVisibility(sid, nil) - s.NotNil(entry.Info) - s.ProtoEqual(schedule.Spec, entry.Info.Spec) - s.Equal(wt, entry.Info.WorkflowType.Name) - s.False(entry.Info.Paused) - s.Greater(len(entry.Info.FutureActionTimes), 1) - s.True(entry.Info.FutureActionTimes[0].AsTime().After(startTime)) -} - -// TestRateLimit applies only to V1, as V2 scheduler does not impose its own -// rate limiting. -func (s *ScheduleV1FunctionalSuite) TestRateLimit() { - sid := "sched-test-rate-limit-%d" - wid := "sched-test-rate-limit-wf-%d" - wt := "sched-test-rate-limit-wt" - - // Set 1/sec rate limit per namespace. - s.OverrideDynamicConfig(dynamicconfig.SchedulerNamespaceStartWorkflowRPS, 1.0) - - var runs int32 - workflowFn := func(ctx workflow.Context) error { - workflow.SideEffect(ctx, func(ctx workflow.Context) any { - atomic.AddInt32(&runs, 1) - return 0 - }) - return nil - } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - - // create 10 copies of the schedule - for i := 0; i < 10; i++ { - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(1 * time.Second)}, - }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: fmt.Sprintf(wid, i), - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, - }, - }, - } - _, err := s.FrontendClient().CreateSchedule(s.newContext(), &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: fmt.Sprintf(sid, i), - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), - }) - s.NoError(err) - s.cleanup(fmt.Sprintf(sid, i)) - } - - time.Sleep(5 * time.Second) //nolint:forbidigo - - // With no rate limit, we'd see 10/second == 50 workflows run. With a limit of 1/sec, we - // expect to see around 5. - s.Less(atomic.LoadInt32(&runs), int32(10)) -} - -func (s *scheduleFunctionalSuiteBase) TestListSchedulesReturnsWorkflowStatus() { - sid := "sched-test-list-running" - wid := "sched-test-list-running-wf" - wt := "sched-test-list-running-wt" - - // Set up a schedule that immediately starts a single running workflow - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(3 * time.Second)}, - }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: wid, - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, - }, - }, - } - patch := &schedulepb.SchedulePatch{ - TriggerImmediately: &schedulepb.TriggerImmediatelyRequest{}, - } - - // The workflow sits open until we've asserted it can be listed as running - resumeSignal := "resume" - workflowFn := func(ctx workflow.Context) error { - selector := workflow.NewSelector(ctx) - selector.AddReceive(workflow.GetSignalChannel(ctx, resumeSignal), func(c workflow.ReceiveChannel, more bool) { - // nothing to do - }) - selector.Select(ctx) - return nil - } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - - req := &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - InitialPatch: patch, - RequestId: uuid.NewString(), - } - ctx := s.newContext() - _, err := s.FrontendClient().CreateSchedule(ctx, req) - s.NoError(err) - s.cleanup(sid) - - // validate RecentActions made it to visibility - listResp := s.getScheduleEntryFomVisibility(sid, func(listResp *schedulepb.ScheduleListEntry) bool { - return len(listResp.Info.RecentActions) >= 1 - }) - s.Equal(1, len(listResp.Info.RecentActions)) - - a1 := listResp.Info.RecentActions[0] - s.True(strings.HasPrefix(a1.StartWorkflowResult.WorkflowId, wid)) - s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, a1.StartWorkflowStatus) - - // let the started workflow complete - _, err = s.FrontendClient().SignalWorkflowExecution(s.newContext(), &workflowservice.SignalWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: &commonpb.WorkflowExecution{ - WorkflowId: a1.StartWorkflowResult.WorkflowId, - RunId: a1.StartWorkflowResult.RunId, - }, - SignalName: resumeSignal, - }) - s.NoError(err) - - // now wait for second recent action to land in visbility - listResp = s.getScheduleEntryFomVisibility(sid, func(listResp *schedulepb.ScheduleListEntry) bool { - return len(listResp.Info.RecentActions) >= 2 - }) - - a1 = listResp.Info.RecentActions[0] - a2 := listResp.Info.RecentActions[1] - s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, a1.StartWorkflowStatus) - s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, a2.StartWorkflowStatus) - - // Also verify that DescribeSchedule's output matches - descResp, err := s.FrontendClient().DescribeSchedule(s.newContext(), &workflowservice.DescribeScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - }) - s.NoError(err) - s.assertSameRecentActions(descResp, listResp) -} - -func (s *scheduleFunctionalSuiteBase) TestUpdateIntervalTakesEffect() { - sid := "sched-test-update-interval" - wid := "sched-test-update-interval-wf" - wt := "sched-test-update-interval-wt" - - var runs int32 - workflowFn := func(ctx workflow.Context) error { - workflow.SideEffect(ctx, func(ctx workflow.Context) any { - atomic.AddInt32(&runs, 1) - return 0 - }) - return nil - } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - - // Create schedule with a long interval (300s) - won't fire for 5 minutes. - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(300 * time.Second)}, - }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: wid, - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, - }, - }, - } - - ctx := s.newContext() - _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), - }) - s.NoError(err) - s.cleanup(sid) - - // Update the interval to be very short (1s). - schedule.Spec.Interval[0].Interval = durationpb.New(1 * time.Second) - _, err = s.FrontendClient().UpdateSchedule(ctx, &workflowservice.UpdateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), - }) - s.NoError(err) - - // After updating to 1s interval, we should see runs start within a few seconds. - s.Eventually( - func() bool { return atomic.LoadInt32(&runs) >= 2 }, - 10*time.Second, - 500*time.Millisecond, - "expected at least 2 runs within 10s after updating interval to 1s", - ) -} - -func (s *scheduleFunctionalSuiteBase) TestListScheduleMatchingTimes() { - sid := "sched-test-list-matching-times" - - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(1 * time.Hour)}, - }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: "wf-list-matching-times", - WorkflowType: &commonpb.WorkflowType{Name: "action"}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, - }, - }, - } - req := &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), - } - - ctx := s.newContext() - _, err := s.FrontendClient().CreateSchedule(ctx, req) - s.NoError(err) - s.cleanup(sid) // Query for matching times over a 5-hour window. now := time.Now().UTC().Truncate(time.Hour).Add(time.Hour) // Start of next hour @@ -1179,8 +946,9 @@ func (s *scheduleFunctionalSuiteBase) TestListScheduleMatchingTimes() { s.Len(resp.GetStartTime(), 5) } -// A schedule's memo should have an upper bound on the number of spec items stored. -func (s *scheduleFunctionalSuiteBase) TestLimitMemoSpecSize() { +func testLimitMemoSpecSize(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + expectedLimit := scheduler.CurrentTweakablePolicies.SpecFieldLengthLimit sid := "sched-test-limit-memo-size" @@ -1194,7 +962,7 @@ func (s *scheduleFunctionalSuiteBase) TestLimitMemoSpecSize() { StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ WorkflowId: wid, WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, }, }, }, @@ -1232,307 +1000,49 @@ func (s *scheduleFunctionalSuiteBase) TestLimitMemoSpecSize() { Identity: "test", RequestId: uuid.NewString(), } - s.worker.RegisterWorkflowWithOptions( + s.SdkWorker().RegisterWorkflowWithOptions( func(ctx workflow.Context) error { return nil }, workflow.RegisterOptions{Name: wt}, ) - ctx := s.newContext() + ctx := newContext(s.Context()) _, err := s.FrontendClient().CreateSchedule(ctx, req) s.NoError(err) - s.cleanup(sid) // Verify the memo field length limit was enforced. - entry := s.getScheduleEntryFomVisibility(sid, nil) - s.Require().NotNil(entry) + entry := getScheduleEntryFromVisibility(s, sid, newContext, nil) + require.NotNil(t, entry) spec := entry.GetInfo().GetSpec() - s.Require().Equal(expectedLimit, len(spec.GetInterval())) - s.Require().Equal(expectedLimit, len(spec.GetStructuredCalendar())) - s.Require().Equal(expectedLimit, len(spec.GetExcludeStructuredCalendar())) + require.Len(t, spec.GetInterval(), expectedLimit) + require.Len(t, spec.GetStructuredCalendar(), expectedLimit) + require.Len(t, spec.GetExcludeStructuredCalendar(), expectedLimit) } -// TestNextTimeCache only applies to V1. -func (s *ScheduleV1FunctionalSuite) TestNextTimeCache() { - sid := "sched-test-next-time-cache" - wid := "sched-test-next-time-cache-wf" - wt := "sched-test-next-time-cache-wt" +func testCountSchedules(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(1 * time.Second)}, + // Create multiple schedules with different paused states + sidPrefix := "sched-test-count-" + wid := "sched-test-count-wf" + wt := "sched-test-count-wt" + + // Create 3 schedules: 2 active, 1 paused + for i := range 3 { + sid := fmt.Sprintf("%s%d", sidPrefix, i) + paused := i == 2 // Third schedule is paused + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: wid, - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, - }, - }, - } - req := &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), - } - - var runs atomic.Int32 - workflowFn := func(ctx workflow.Context) error { - workflow.SideEffect(ctx, func(ctx workflow.Context) any { - runs.Add(1) - return 0 - }) - return nil - } - s.worker.RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) - - _, err := s.FrontendClient().CreateSchedule(s.newContext(), req) - s.NoError(err) - s.cleanup(sid) - - // wait for at least 13 runs - const count = 13 - s.Eventually(func() bool { return runs.Load() >= count }, (count+10)*time.Second, 500*time.Millisecond) - - // there should be only four side effects for 13 runs, and only two mentioning "Next" - // (cache refills) - events := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) - var sideEffects, nextTimeSideEffects int - for _, e := range events { - if marker := e.GetMarkerRecordedEventAttributes(); marker.GetMarkerName() == "SideEffect" { - sideEffects++ - if p, ok := marker.Details["data"]; ok && len(p.Payloads) == 1 { - if string(p.Payloads[0].Metadata["messageType"]) == "temporal.server.api.schedule.v1.NextTimeCache" || - strings.Contains(payloads.ToString(p), `"Next"`) { - nextTimeSideEffects++ - } - } - } - } - - const ( - // These match the ones in the scheduler workflow, but they're not exported. - // Change these if those change. - FutureActionCountForList = 5 - NextTimeCacheV2Size = 14 - - // Calculate expected results - expectedCacheSize = NextTimeCacheV2Size - FutureActionCountForList + 1 - expectedRefills = (count + expectedCacheSize - 1) / expectedCacheSize - uuidCacheRefills = (count + 9) / 10 - ) - s.Equal(expectedRefills+uuidCacheRefills, sideEffects) - s.Equal(expectedRefills, nextTimeSideEffects) -} - -// getScheduleEntryFomVisibility polls visibility using ListSchedules until it finds a schedule -// with the given id and for which the optional predicate function returns true. -func (s *scheduleFunctionalSuiteBase) getScheduleEntryFomVisibility(sid string, predicate func(*schedulepb.ScheduleListEntry) bool) *schedulepb.ScheduleListEntry { - var slEntry *schedulepb.ScheduleListEntry - s.Require().Eventually(func() bool { // wait for visibility - listResp, err := s.FrontendClient().ListSchedules(s.newContext(), &workflowservice.ListSchedulesRequest{ - Namespace: s.Namespace().String(), - MaximumPageSize: 5, - }) - if err != nil { - return false - } - for _, ent := range listResp.Schedules { - if ent.ScheduleId == sid { - if predicate != nil && !predicate(ent) { - return false - } - slEntry = ent - return true - } - } - return false - }, 15*time.Second, 1*time.Second) - return slEntry -} - -func (s *scheduleFunctionalSuiteBase) assertSameRecentActions( - expected *workflowservice.DescribeScheduleResponse, actual *schedulepb.ScheduleListEntry, -) { - s.T().Helper() - if len(expected.Info.RecentActions) != len(actual.Info.RecentActions) { - s.T().Fatalf( - "RecentActions have different length expected %d, got %d", - len(expected.Info.RecentActions), - len(actual.Info.RecentActions)) - } - for i := range expected.Info.RecentActions { - if !proto.Equal(expected.Info.RecentActions[i], actual.Info.RecentActions[i]) { - s.T().Errorf( - "RecentActions are differ at index %d. Expected %v, got %v", - i, - expected.Info.RecentActions[i], - actual.Info.RecentActions[i], - ) - } - } -} - -func (s *scheduleFunctionalSuiteBase) cleanup(sid string) { - s.T().Cleanup(func() { - _, _ = s.FrontendClient().DeleteSchedule(s.newContext(), &workflowservice.DeleteScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Identity: "test", - }) - }) -} - -// TestCreateScheduleAlreadyExists verifies that creating a schedule with the same ID -// returns an AlreadyExists serviceerror. -func (s *ScheduleCHASMFunctionalSuite) TestCreateScheduleAlreadyExists() { - sid := "sched-test-already-exists" - - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(1 * time.Hour)}, - }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: "wf-already-exists", - WorkflowType: &commonpb.WorkflowType{Name: "action"}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, - }, - }, - } - req := &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), - } - - ctx := s.newContext() - _, err := s.FrontendClient().CreateSchedule(ctx, req) - s.NoError(err) - s.cleanup(sid) - - // Try to create again with a different request ID - should fail with AlreadyExists - req.RequestId = uuid.NewString() - _, err = s.FrontendClient().CreateSchedule(ctx, req) - s.Error(err) - - var alreadyExists *serviceerror.AlreadyExists - s.ErrorAs(err, &alreadyExists) - s.Contains(err.Error(), sid) -} - -func (s *ScheduleCHASMFunctionalSuite) TestPatchRejectsExcessBackfillers() { - sid := "sched-test-too-many-backfillers" - wt := "sched-test-too-many-backfillers-wt" - - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(1 * time.Hour)}, - }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: "wf-too-many-backfillers", - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, - }, - }, - State: &schedulepb.ScheduleState{Paused: true}, - } - - ctx := s.newContext() - _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Schedule: schedule, - Identity: "test", - RequestId: uuid.NewString(), - }) - s.NoError(err) - s.cleanup(sid) - - // Patch with 50 backfill requests at a time until we reach the limit of 100. - now := time.Now() - for i := 0; i < 100; i += 50 { - backfills := make([]*schedulepb.BackfillRequest, 50) - for j := range backfills { - backfills[j] = &schedulepb.BackfillRequest{ - StartTime: timestamppb.New(now), - EndTime: timestamppb.New(now.Add(time.Minute)), - OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, - } - } - _, err = s.FrontendClient().PatchSchedule(ctx, &workflowservice.PatchScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Patch: &schedulepb.SchedulePatch{ - BackfillRequest: backfills, - }, - Identity: "test", - RequestId: uuid.NewString(), - }) - s.NoError(err) - } - - // The next patch should be rejected. - _, err = s.FrontendClient().PatchSchedule(ctx, &workflowservice.PatchScheduleRequest{ - Namespace: s.Namespace().String(), - ScheduleId: sid, - Patch: &schedulepb.SchedulePatch{ - BackfillRequest: []*schedulepb.BackfillRequest{ - { - StartTime: timestamppb.New(now), - EndTime: timestamppb.New(now.Add(time.Minute)), - OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, - }, - }, - }, - Identity: "test", - RequestId: uuid.NewString(), - }) - s.Error(err) - var failedPrecondition *serviceerror.FailedPrecondition - s.ErrorAs(err, &failedPrecondition) - s.Contains(err.Error(), "too many concurrent backfillers") -} - -func (s *scheduleFunctionalSuiteBase) TestCountSchedules() { - // Create multiple schedules with different paused states - sidPrefix := "sched-test-count-" - wid := "sched-test-count-wf" - wt := "sched-test-count-wt" - - // Create 3 schedules: 2 active, 1 paused - for i := range 3 { - sid := fmt.Sprintf("%s%d", sidPrefix, i) - paused := i == 2 // Third schedule is paused - - schedule := &schedulepb.Schedule{ - Spec: &schedulepb.ScheduleSpec{ - Interval: []*schedulepb.IntervalSpec{ - {Interval: durationpb.New(1 * time.Hour)}, - }, - }, - Action: &schedulepb.ScheduleAction{ - Action: &schedulepb.ScheduleAction_StartWorkflow{ - StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ - WorkflowId: fmt.Sprintf("%s-%d", wid, i), - WorkflowType: &commonpb.WorkflowType{Name: wt}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: fmt.Sprintf("%s-%d", wid, i), + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, }, }, State: &schedulepb.ScheduleState{ @@ -1540,7 +1050,7 @@ func (s *scheduleFunctionalSuiteBase) TestCountSchedules() { }, } - _, err := s.FrontendClient().CreateSchedule(s.newContext(), &workflowservice.CreateScheduleRequest{ + _, err := s.FrontendClient().CreateSchedule(newContext(s.Context()), &workflowservice.CreateScheduleRequest{ Namespace: s.Namespace().String(), ScheduleId: sid, Schedule: schedule, @@ -1548,23 +1058,11 @@ func (s *scheduleFunctionalSuiteBase) TestCountSchedules() { RequestId: uuid.NewString(), }) s.NoError(err) - s.cleanup(sid) } - // Wait for schedules to appear in visibility - s.Eventually(func() bool { - countResp, err := s.FrontendClient().CountSchedules(s.newContext(), &workflowservice.CountSchedulesRequest{ - Namespace: s.Namespace().String(), - }) - if err != nil { - return false - } - return countResp.Count >= 3 - }, 15*time.Second, 1*time.Second) - // Test basic count (all schedules) s.Eventually(func() bool { - countResp, err := s.FrontendClient().CountSchedules(s.newContext(), &workflowservice.CountSchedulesRequest{ + countResp, err := s.FrontendClient().CountSchedules(newContext(s.Context()), &workflowservice.CountSchedulesRequest{ Namespace: s.Namespace().String(), }) return err == nil && countResp.Count >= 3 @@ -1572,7 +1070,7 @@ func (s *scheduleFunctionalSuiteBase) TestCountSchedules() { // Test count with query filter for paused schedules s.Eventually(func() bool { - countResp, err := s.FrontendClient().CountSchedules(s.newContext(), &workflowservice.CountSchedulesRequest{ + countResp, err := s.FrontendClient().CountSchedules(newContext(s.Context()), &workflowservice.CountSchedulesRequest{ Namespace: s.Namespace().String(), Query: fmt.Sprintf("%s = true", sadefs.TemporalSchedulePaused), }) @@ -1581,7 +1079,7 @@ func (s *scheduleFunctionalSuiteBase) TestCountSchedules() { // Test count with query filter for non-paused schedules s.Eventually(func() bool { - countResp, err := s.FrontendClient().CountSchedules(s.newContext(), &workflowservice.CountSchedulesRequest{ + countResp, err := s.FrontendClient().CountSchedules(newContext(s.Context()), &workflowservice.CountSchedulesRequest{ Namespace: s.Namespace().String(), Query: fmt.Sprintf("%s = false", sadefs.TemporalSchedulePaused), }) @@ -1589,11 +1087,12 @@ func (s *scheduleFunctionalSuiteBase) TestCountSchedules() { }, 15*time.Second, 1*time.Second, "Expected at least 2 non-paused schedules") } -func (s *scheduleFunctionalSuiteBase) TestSchedule_InternalTaskQueue() { - errorMessageKeyword := "internal per namespace task queue" +func testScheduleInternalTaskQueue(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + errorMessageKeyword := "internal per-namespace task queue" // Test CreateSchedule with internal task queue - s.Run("CreateSchedule_PerNSWorkerTaskQueue", func() { + t.Run("CreateSchedule_PerNSWorkerTaskQueue", func(t *testing.T) { sid := "sched-test-internal-tq-create" schedule := &schedulepb.Schedule{ Spec: &schedulepb.ScheduleSpec{ @@ -1619,16 +1118,16 @@ func (s *scheduleFunctionalSuiteBase) TestSchedule_InternalTaskQueue() { RequestId: uuid.NewString(), } - ctx := s.newContext() + ctx := newContext(s.Context()) _, err := s.FrontendClient().CreateSchedule(ctx, req) - s.Error(err) + require.Error(t, err) var invalidArgument *serviceerror.InvalidArgument - s.ErrorAs(err, &invalidArgument) - s.Contains(err.Error(), errorMessageKeyword) + require.ErrorAs(t, err, &invalidArgument) + require.Contains(t, err.Error(), errorMessageKeyword) }) // Test UpdateSchedule with internal task queue - s.Run("UpdateSchedule_PerNSWorkerTaskQueue", func() { + t.Run("UpdateSchedule_PerNSWorkerTaskQueue", func(t *testing.T) { // First create a schedule with a valid task queue sid := "sched-test-internal-tq-update" schedule := &schedulepb.Schedule{ @@ -1642,7 +1141,7 @@ func (s *scheduleFunctionalSuiteBase) TestSchedule_InternalTaskQueue() { StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ WorkflowId: "wf-update-internal-tq", WorkflowType: &commonpb.WorkflowType{Name: "action"}, - TaskQueue: &taskqueuepb.TaskQueue{Name: s.taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, }, }, }, @@ -1655,10 +1154,9 @@ func (s *scheduleFunctionalSuiteBase) TestSchedule_InternalTaskQueue() { RequestId: uuid.NewString(), } - ctx := s.newContext() + ctx := newContext(s.Context()) _, err := s.FrontendClient().CreateSchedule(ctx, req) - s.NoError(err) - s.cleanup(sid) + require.NoError(t, err) // Now try to update with internal task queue schedule.Action.GetStartWorkflow().TaskQueue = &taskqueuepb.TaskQueue{ @@ -1674,9 +1172,1474 @@ func (s *scheduleFunctionalSuiteBase) TestSchedule_InternalTaskQueue() { } _, err = s.FrontendClient().UpdateSchedule(ctx, updateReq) - s.Error(err) + require.Error(t, err) var invalidArgument *serviceerror.InvalidArgument - s.ErrorAs(err, &invalidArgument) - s.Contains(err.Error(), errorMessageKeyword) + require.ErrorAs(t, err, &invalidArgument) + require.Contains(t, err.Error(), errorMessageKeyword) + }) +} + +func testScheduledWorkflowDoubleReset(t *testing.T, newContext contextFactory, enableCHASMCallbacks bool) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + s.OverrideDynamicConfig(dynamicconfig.EnableCHASMCallbacks, enableCHASMCallbacks) + + sid := "sched-test-double-reset" + wid := "sched-test-double-reset-wf" + wt := "sched-test-double-reset-wt" + + s.SdkWorker().RegisterWorkflowWithOptions(func(ctx workflow.Context) error { + ch := workflow.GetSignalChannel(ctx, "complete") + var signal any + ch.Receive(ctx, &signal) + return nil + }, workflow.RegisterOptions{Name: wt}) + + ctx := newContext(s.Context()) + + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(24 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + }, + InitialPatch: &schedulepb.SchedulePatch{ + TriggerImmediately: &schedulepb.TriggerImmediatelyRequest{}, + }, + RequestId: uuid.NewString(), + }) + s.NoError(err) + + // Wait for scheduler to start the workflow and show it as RUNNING. + listEntry := getScheduleEntryFromVisibility(s, sid, newContext, func(ent *schedulepb.ScheduleListEntry) bool { + return len(ent.Info.RecentActions) >= 1 && + ent.Info.RecentActions[0].GetStartWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING + }) + a1 := listEntry.Info.RecentActions[0] + wfExec := &commonpb.WorkflowExecution{ + WorkflowId: a1.StartWorkflowResult.WorkflowId, + RunId: a1.StartWorkflowResult.RunId, + } + + s.WaitForHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted`, + s.GetHistoryFunc(s.Namespace().String(), wfExec), + 5*time.Second, + 10*time.Millisecond, + ) + + origDesc, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: wfExec, + }) + s.NoError(err) + var originalStartReqID string + for reqID, info := range origDesc.GetWorkflowExtendedInfo().GetRequestIdInfos() { + if info.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + originalStartReqID = reqID + break + } + } + s.NotEmpty(originalStartReqID, "original run must have a request ID for WorkflowExecutionStarted") + + resp1, err := s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: wfExec, + Reason: "double-reset-test-first", + WorkflowTaskFinishEventId: 3, + RequestId: uuid.NewString(), + }) + s.NoError(err) + resetRun1 := &commonpb.WorkflowExecution{ + WorkflowId: wfExec.WorkflowId, + RunId: resp1.RunId, + } + + s.EventuallyWithT(func(col *assert.CollectT) { + resetDesc, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: resetRun1, + }) + require.NoError(col, err) + var resetStartReqID string + for reqID, info := range resetDesc.GetWorkflowExtendedInfo().GetRequestIdInfos() { + if info.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + resetStartReqID = reqID + break + } + } + require.Equal(col, originalStartReqID, resetStartReqID, + "start request ID must be preserved across first reset") + }, 10*time.Second, 100*time.Millisecond) + + resp2, err := s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: resetRun1, + Reason: "double-reset-test-second", + WorkflowTaskFinishEventId: 3, + RequestId: uuid.NewString(), + }) + s.NoError(err) + resetRun2 := &commonpb.WorkflowExecution{ + WorkflowId: wfExec.WorkflowId, + RunId: resp2.RunId, + } + + s.EventuallyWithT(func(col *assert.CollectT) { + resetDesc, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: resetRun2, + }) + require.NoError(col, err) + var resetStartReqID string + for reqID, info := range resetDesc.GetWorkflowExtendedInfo().GetRequestIdInfos() { + if info.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + resetStartReqID = reqID + break + } + } + require.Equal(col, originalStartReqID, resetStartReqID, + "start request ID must be preserved across double reset") + }, 10*time.Second, 100*time.Millisecond) + + _, err = s.FrontendClient().SignalWorkflowExecution(ctx, &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: wfExec.WorkflowId, + }, + SignalName: "complete", + }) + s.NoError(err) + + getScheduleEntryFromVisibility(s, sid, newContext, func(ent *schedulepb.ScheduleListEntry) bool { + for _, action := range ent.Info.RecentActions { + if action.GetStartWorkflowResult().GetRunId() == wfExec.RunId { + return action.GetStartWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + } + } + return false + }) +} + +func testResetWithAdditionalCallback(t *testing.T, newContext contextFactory, enableCHASMCallbacks bool) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + s.OverrideDynamicConfig(dynamicconfig.EnableCHASMCallbacks, enableCHASMCallbacks) + s.OverrideDynamicConfig( + callbacks.AllowedAddresses, + []any{map[string]any{"Pattern": "*", "AllowInsecure": true}}, + ) + + sid := "sched-test-reset-extra-cb" + wid := "sched-test-reset-extra-cb-wf" + wt := "sched-test-reset-extra-cb-wt" + + ch := &completionHandler{ + requestCh: make(chan *nexusrpc.CompletionRequest, 1), + requestCompleteCh: make(chan error, 1), + } + defer func() { + close(ch.requestCh) + close(ch.requestCompleteCh) + }() + secondCallbackURL := func() string { + hh := nexusrpc.NewCompletionHTTPHandler(nexusrpc.CompletionHandlerOptions{Handler: ch}) + srv := httptest.NewServer(hh) + t.Cleanup(func() { srv.Close() }) + return srv.URL + "/callback" + }() + + s.SdkWorker().RegisterWorkflowWithOptions(func(ctx workflow.Context) error { + sigCh := workflow.GetSignalChannel(ctx, "complete") + var signal any + sigCh.Receive(ctx, &signal) + return nil + }, workflow.RegisterOptions{Name: wt}) + + ctx := newContext(s.Context()) + + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(24 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + }, + InitialPatch: &schedulepb.SchedulePatch{ + TriggerImmediately: &schedulepb.TriggerImmediatelyRequest{}, + }, + RequestId: uuid.NewString(), + }) + s.NoError(err) + + listEntry := getScheduleEntryFromVisibility(s, sid, newContext, func(ent *schedulepb.ScheduleListEntry) bool { + return len(ent.Info.RecentActions) >= 1 && + ent.Info.RecentActions[0].GetStartWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING + }) + a1 := listEntry.Info.RecentActions[0] + wfExec := &commonpb.WorkflowExecution{ + WorkflowId: a1.StartWorkflowResult.WorkflowId, + RunId: a1.StartWorkflowResult.RunId, + } + + s.WaitForHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted`, + s.GetHistoryFunc(s.Namespace().String(), wfExec), + 5*time.Second, + 10*time.Millisecond, + ) + + attachRequestID := uuid.NewString() + attachResp, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + RequestId: attachRequestID, + Namespace: s.Namespace().String(), + WorkflowId: wfExec.WorkflowId, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + WorkflowIdConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, + OnConflictOptions: &workflowpb.OnConflictOptions{ + AttachRequestId: true, + AttachCompletionCallbacks: true, + }, + CompletionCallbacks: []*commonpb.Callback{ + { + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: secondCallbackURL, + }, + }, + }, + }, + }) + s.NoError(err) + s.False(attachResp.Started, "expected to attach to existing run, not start a new one") + + s.WaitForHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionOptionsUpdated`, + s.GetHistoryFunc(s.Namespace().String(), wfExec), + 5*time.Second, + 10*time.Millisecond, + ) + + resetResp, err := s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: wfExec, + Reason: "reset-with-additional-callback-test", + WorkflowTaskFinishEventId: 3, + RequestId: uuid.NewString(), + }) + s.NoError(err) + resetRun := &commonpb.WorkflowExecution{ + WorkflowId: wfExec.WorkflowId, + RunId: resetResp.RunId, + } + + var startRequestID string + s.EventuallyWithT(func(col *assert.CollectT) { + descResp, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: resetRun, + }) + require.NoError(col, err) + require.Len(col, descResp.Callbacks, 2) + reqIDs := descResp.GetWorkflowExtendedInfo().GetRequestIdInfos() + attachInfo, ok := reqIDs[attachRequestID] + require.True(col, ok, "attachRequestId not found in RequestIdInfos") + require.Equal(col, enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED, attachInfo.GetEventType()) + for reqID, info := range reqIDs { + if info.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + startRequestID = reqID + break + } + } + require.NotEmpty(col, startRequestID, "no request ID found for WorkflowExecutionStarted") + require.NotEqual(col, startRequestID, attachRequestID, + "schedule callback and manually-attached callback must have different request IDs") + }, 10*time.Second, 100*time.Millisecond) + + _, err = s.FrontendClient().SignalWorkflowExecution(ctx, &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: wfExec.WorkflowId, + }, + SignalName: "complete", + }) + s.NoError(err) + + getScheduleEntryFromVisibility(s, sid, newContext, func(ent *schedulepb.ScheduleListEntry) bool { + for _, action := range ent.Info.RecentActions { + if action.GetStartWorkflowResult().GetRunId() == wfExec.RunId { + return action.GetStartWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + } + } + return false + }) + + select { + case completion := <-ch.requestCh: + s.Equal(nexus.OperationStateSucceeded, completion.State) + ch.requestCompleteCh <- nil + case <-time.After(10 * time.Second): + s.Fail("timeout waiting for second callback to be delivered") + } +} + +// testCreatesWorkflowSentinel tests that creating a CHASM schedule also starts a +// dummy workflow to reserve the schedule ID in the V1 workflow ID-space. +func testCreatesWorkflowSentinel(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := testcore.RandomizeStr("sid") + wid := testcore.RandomizeStr("wid") + wt := testcore.RandomizeStr("wt") + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: testcore.RandomizeStr("identity"), + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + // Verify the dummy workflow was created to reserve the V1 workflow ID. + sentinelWfID := scheduler.WorkflowIDPrefix + sid + var descResp *workflowservice.DescribeWorkflowExecutionResponse + s.Eventually(func() bool { + descResp, err = s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{WorkflowId: sentinelWfID}, + }) + return err == nil + }, 15*time.Second, 500*time.Millisecond, "dummy sentinel workflow should exist") + s.Equal(dummy.DummyWFTypeName, descResp.WorkflowExecutionInfo.Type.Name) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, descResp.WorkflowExecutionInfo.Status) + + // Verify visibility shows exactly one schedule (not the dummy workflow). + getScheduleEntryFromVisibility(s, sid, newContext, nil) + listResp, err := s.FrontendClient().ListSchedules(ctx, &workflowservice.ListSchedulesRequest{ + Namespace: s.Namespace().String(), + MaximumPageSize: 5, + }) + s.NoError(err) + s.Len(listResp.Schedules, 1) + + countResp, err := s.FrontendClient().CountSchedules(ctx, &workflowservice.CountSchedulesRequest{ + Namespace: s.Namespace().String(), + }) + s.NoError(err) + s.Equal(int64(1), countResp.Count) +} + +// testCreatesCHASMSentinel tests that creating a V1 schedule also creates a +// CHASM sentinel to reserve the schedule ID in the CHASM execution space. +func testCreatesCHASMSentinel(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := testcore.RandomizeStr("sid") + wid := testcore.RandomizeStr("wid") + wt := testcore.RandomizeStr("wt") + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: testcore.RandomizeStr("identity"), + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + // Verify a CHASM sentinel was created to reserve the schedule ID. + // DescribeSchedule should return NotFound, as well as CreateSentinel + nsID := s.NamespaceID().String() + s.Eventually(func() bool { + _, descErr := s.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: s.Namespace().String(), ScheduleId: sid}, + }, + ) + var notFoundErr *serviceerror.NotFound + if !errors.As(descErr, ¬FoundErr) { + return false + } + + // A CHASM CreateSchedule should also fail with NotFound because + // the sentinel blocks it. + _, createErr := s.GetTestCluster().SchedulerClient().CreateSchedule( + ctx, + &schedulerpb.CreateScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + RequestId: testcore.RandomizeStr("test-sentinel-check"), + Schedule: schedule, + }, + }, + ) + return errors.As(createErr, ¬FoundErr) + }, 15*time.Second, 500*time.Millisecond, "CHASM sentinel should exist for V1 schedule") + + // Verify visibility shows exactly one schedule (not the sentinel). + getScheduleEntryFromVisibility(s, sid, newContext, nil) + listResp, err := s.FrontendClient().ListSchedules(ctx, &workflowservice.ListSchedulesRequest{ + Namespace: s.Namespace().String(), + MaximumPageSize: 5, + }) + s.NoError(err) + s.Len(listResp.Schedules, 1) + + countResp, err := s.FrontendClient().CountSchedules(ctx, &workflowservice.CountSchedulesRequest{ + Namespace: s.Namespace().String(), + }) + s.NoError(err) + s.Equal(int64(1), countResp.Count) +} + +func testCreateScheduleAlreadyExists(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-already-exists" + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: "wf-already-exists", + WorkflowType: &commonpb.WorkflowType{Name: "action"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + req := &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + } + + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, req) + s.NoError(err) + + // Try to create again with a different request ID - should fail with AlreadyExists + req.RequestId = uuid.NewString() + _, err = s.FrontendClient().CreateSchedule(ctx, req) + s.Error(err) + + var alreadyStarted *serviceerror.WorkflowExecutionAlreadyStarted + s.ErrorAs(err, &alreadyStarted) + s.Contains(err.Error(), sid) +} + +// CreateSchedule is special-cased in the SDKs to translate +// serviceerror.WorkflowExecutionAlreadyStarted into +// temporal.ErrScheduleAlreadyRunning. This tests the SDK's behavior E2E against +// the handler. A similar test exists in the features repository. +func testCreateScheduleDuplicateSdkError(t *testing.T, useCHASM bool) { + opts := scheduleCommonOpts() + if useCHASM { + opts = append(opts, testcore.WithDynamicConfig(dynamicconfig.EnableCHASMSchedulerCreation, true)) + } + s := testcore.NewEnv(t, opts...) + + sid := "sched-test-duplicate-sdk-" + uuid.NewString()[:8] + schedOpts := sdkclient.ScheduleOptions{ + ID: sid, + Spec: sdkclient.ScheduleSpec{}, + Action: &sdkclient.ScheduleWorkflowAction{ + ID: "wf-" + sid, + Workflow: "noop", + TaskQueue: s.WorkerTaskQueue(), + }, + Paused: true, + } + + ctx := s.Context() + handle, err := s.SdkClient().ScheduleClient().Create(ctx, schedOpts) + s.NoError(err) + defer func() { _ = handle.Delete(context.Background()) }() + + _, err = s.SdkClient().ScheduleClient().Create(ctx, schedOpts) + s.ErrorIs(err, temporal.ErrScheduleAlreadyRunning) +} + +func testPatchRejectsExcessBackfillers(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + sid := "sched-test-too-many-backfillers" + wt := "sched-test-too-many-backfillers-wt" + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: "wf-too-many-backfillers", + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + State: &schedulepb.ScheduleState{Paused: true}, + } + + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + }) + s.NoError(err) + + // Patch with 50 backfill requests at a time until we reach the limit of 100. + now := time.Now() + for i := 0; i < 100; i += 50 { + backfills := make([]*schedulepb.BackfillRequest, 50) + for j := range backfills { + backfills[j] = &schedulepb.BackfillRequest{ + StartTime: timestamppb.New(now), + EndTime: timestamppb.New(now.Add(time.Minute)), + OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, + } + } + _, err = s.FrontendClient().PatchSchedule(ctx, &workflowservice.PatchScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Patch: &schedulepb.SchedulePatch{ + BackfillRequest: backfills, + }, + Identity: "test", + RequestId: uuid.NewString(), + }) + s.NoError(err) + } + + // The next patch should be rejected. + _, err = s.FrontendClient().PatchSchedule(ctx, &workflowservice.PatchScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Patch: &schedulepb.SchedulePatch{ + BackfillRequest: []*schedulepb.BackfillRequest{ + { + StartTime: timestamppb.New(now), + EndTime: timestamppb.New(now.Add(time.Minute)), + OverlapPolicy: enumspb.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, + }, + }, + }, + Identity: "test", + RequestId: uuid.NewString(), + }) + s.Error(err) + var failedPrecondition *serviceerror.FailedPrecondition + s.ErrorAs(err, &failedPrecondition) + s.Contains(err.Error(), "too many concurrent backfillers") +} + +func testMigrationCallbackAttach(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := testcore.RandomizeStr("sid") + wid := testcore.RandomizeStr("wid") + wt := testcore.RandomizeStr("wt") + + resumeSignal := "resume" + s.SdkWorker().RegisterWorkflowWithOptions( + func(ctx workflow.Context) error { + workflow.GetSignalChannel(ctx, resumeSignal).Receive(ctx, nil) + return nil + }, + workflow.RegisterOptions{Name: wt}, + ) + + ctx := newContext(s.Context()) + startResp, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + Identity: testcore.RandomizeStr("identity"), + RequestId: testcore.RandomizeStr("request-id"), + }) + s.NoError(err) + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(24 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + now := time.Now().UTC() + nsID := s.NamespaceID().String() + + migrationState := &schedulerpb.SchedulerMigrationState{ + SchedulerState: &schedulerpb.SchedulerState{ + Namespace: s.Namespace().String(), + NamespaceId: nsID, + ScheduleId: sid, + Schedule: schedule, + Info: &schedulepb.ScheduleInfo{}, + ConflictToken: 1, + }, + GeneratorState: &schedulerpb.GeneratorState{}, + InvokerState: &schedulerpb.InvokerState{ + BufferedStarts: []*schedulespb.BufferedStart{ + { + NominalTime: timestamppb.New(now), + ActualTime: timestamppb.New(now), + StartTime: timestamppb.New(now), + WorkflowId: wid, + RunId: startResp.RunId, + RequestId: uuid.NewString(), + Attempt: 1, + HasCallback: false, + }, + }, + }, + } + _, err = s.GetTestCluster().SchedulerClient().CreateFromMigrationState( + ctx, + &schedulerpb.CreateFromMigrationStateRequest{ + NamespaceId: nsID, + State: migrationState, + }, + ) + s.NoError(err) + + s.Eventually(func() bool { + descResp, err := s.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: s.Namespace().String(), ScheduleId: sid}, + }, + ) + if err != nil { + return false + } + running := descResp.GetFrontendResponse().GetInfo().GetRunningWorkflows() + return len(running) > 0 && running[0].WorkflowId == wid + }, 15*time.Second, 500*time.Millisecond, "CHASM scheduler should show running workflow") + + _, err = s.FrontendClient().SignalWorkflowExecution(ctx, &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: wid, + RunId: startResp.RunId, + }, + SignalName: resumeSignal, + }) + s.NoError(err) + + s.Eventually(func() bool { + descResp, err := s.GetTestCluster().SchedulerClient().DescribeSchedule( + ctx, + &schedulerpb.DescribeScheduleRequest{ + NamespaceId: nsID, + FrontendRequest: &workflowservice.DescribeScheduleRequest{Namespace: s.Namespace().String(), ScheduleId: sid}, + }, + ) + if err != nil { + return false + } + recent := descResp.GetFrontendResponse().GetInfo().GetRecentActions() + for _, action := range recent { + if action.GetStartWorkflowResult().GetWorkflowId() == wid && + action.GetStartWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED { + return true + } + } + return false + }, 15*time.Second, 500*time.Millisecond, "CHASM scheduler should reflect workflow completion") +} + +// testCHASMCanListV1Schedules tests that a schedule created in the V1 stack +// will also be visible in the V2 stack. +func testCHASMCanListV1Schedules(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "schedule-created-on-v1" + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(3 * time.Second)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: "wf-", + WorkflowType: &commonpb.WorkflowType{Name: "action"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + req := &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + } + + // Create on V1 stack. + _, err := s.FrontendClient().CreateSchedule(newContext(s.Context()), req) + s.NoError(err) + + // Pause so that `FutureActionTimes` doesn't change between calls. + _, err = s.FrontendClient().PatchSchedule(newContext(s.Context()), &workflowservice.PatchScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Patch: &schedulepb.SchedulePatch{ + Pause: "halt", + }, + Identity: "test", + RequestId: uuid.NewString(), + }) + s.NoError(err) + + // Sanity test, list with V1 handler. + v1Entry := getScheduleEntryFromVisibility(s, sid, newContext, func(sle *schedulepb.ScheduleListEntry) bool { + return sle.GetInfo().Paused + }) + s.NotNil(v1Entry.GetInfo()) + + // Count with V1 handler. + v1CountResp, err := s.FrontendClient().CountSchedules(newContext(s.Context()), &workflowservice.CountSchedulesRequest{ + Namespace: s.Namespace().String(), + }) + s.NoError(err) + s.GreaterOrEqual(v1CountResp.Count, int64(1), "Expected at least 1 schedule with V1 handler") + + // Flip on CHASM experiment and make sure we can still list. + chasmEntry := getScheduleEntryFromVisibility(s, sid, chasmContextFactory, nil) + s.NotNil(chasmEntry.GetInfo()) + s.ProtoEqual(chasmEntry.GetInfo(), v1Entry.GetInfo()) + + // Count with CHASM handler and verify it matches V1 count. + chasmCountResp, err := s.FrontendClient().CountSchedules(chasmContextFactory(s.Context()), &workflowservice.CountSchedulesRequest{ + Namespace: s.Namespace().String(), + }) + s.NoError(err) + s.Equal(v1CountResp.Count, chasmCountResp.Count, "CHASM and V1 counts should match") +} + +// testRefresh applies to V1 scheduler only; V2 does not support/need manual refresh. +func testRefresh(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-refresh" + wid := "sched-test-refresh-wf" + wt := "sched-test-refresh-wt" + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + { + Interval: durationpb.New(30 * time.Second), + // start within three seconds + Phase: durationpb.New(time.Duration((time.Now().Unix()+3)%30) * time.Second), + }, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + WorkflowExecutionTimeout: durationpb.New(3 * time.Second), + }, + }, + }, + } + req := &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + } + + var runs int32 + workflowFn := func(ctx workflow.Context) error { + workflow.SideEffect(ctx, func(ctx workflow.Context) any { + atomic.AddInt32(&runs, 1) + return 0 + }) + s.NoError(workflow.Sleep(ctx, 10*time.Second)) // longer than execution timeout + return nil + } + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + + _, err := s.FrontendClient().CreateSchedule(newContext(s.Context()), req) + s.NoError(err) + + s.Eventually(func() bool { return atomic.LoadInt32(&runs) == 1 }, 6*time.Second, 200*time.Millisecond) + + // workflow has started but is now sleeping. it will timeout in 2 seconds. + + describeResp, err := s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + }) + s.NoError(err) + s.Len(describeResp.Info.RunningWorkflows, 1) + + events1 := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) + expectedHistory := ` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 MarkerRecorded + 6 MarkerRecorded + 7 UpsertWorkflowSearchAttributes + 8 TimerStarted + 9 TimerFired + 10 WorkflowTaskScheduled + 11 WorkflowTaskStarted + 12 WorkflowTaskCompleted + 13 MarkerRecorded + 14 MarkerRecorded + 15 WorkflowPropertiesModified + 16 TimerStarted` + + s.EqualHistoryEvents(expectedHistory, events1) + + time.Sleep(4 * time.Second) //nolint:forbidigo + // now it has timed out, but the scheduler hasn't noticed yet. we can prove it by checking + // its history. + + events2 := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) + s.EqualHistoryEvents(expectedHistory, events2) + + // when we describe we'll force a refresh and see it timed out + describeResp, err = s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + }) + s.NoError(err) + s.Empty(describeResp.Info.RunningWorkflows) + + // check scheduler has gotten the refresh and done some stuff. signal is sent without waiting so we need to wait. + s.Eventually(func() bool { + events3 := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) + return len(events3) > len(events2) + }, 5*time.Second, 100*time.Millisecond) +} + +// testListBeforeRun only applies to V1, as V2 scheduler does not involve the +// per-NS worker or workflow. +func testListBeforeRun(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, append(scheduleCommonOpts(), + testcore.WithDynamicConfig(dynamicconfig.WorkerPerNamespaceWorkerCount, 0), + )...) + + sid := "sched-test-list-before-run" + wid := "sched-test-list-before-run-wf" + wt := "sched-test-list-before-run-wt" + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(3 * time.Second)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + req := &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + } + + startTime := time.Now() + + _, err := s.FrontendClient().CreateSchedule(newContext(s.Context()), req) + s.NoError(err) + + entry := getScheduleEntryFromVisibility(s, sid, newContext, nil) + s.NotNil(entry.Info) + s.ProtoEqual(schedule.Spec, entry.Info.Spec) + s.Equal(wt, entry.Info.WorkflowType.Name) + s.False(entry.Info.Paused) + s.Greater(len(entry.Info.FutureActionTimes), 1) + s.True(entry.Info.FutureActionTimes[0].AsTime().After(startTime)) +} + +// testRateLimit applies only to V1, as V2 scheduler does not impose its own rate limiting. +func testRateLimit(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, append(scheduleCommonOpts(), + testcore.WithDynamicConfig(dynamicconfig.SchedulerNamespaceStartWorkflowRPS, 1.0), + )...) + + sid := "sched-test-rate-limit-%d" + wid := "sched-test-rate-limit-wf-%d" + wt := "sched-test-rate-limit-wt" + + var runs int32 + workflowFn := func(ctx workflow.Context) error { + workflow.SideEffect(ctx, func(ctx workflow.Context) any { + atomic.AddInt32(&runs, 1) + return 0 + }) + return nil + } + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + + // create 10 copies of the schedule + for i := range 10 { + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Second)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: fmt.Sprintf(wid, i), + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + _, err := s.FrontendClient().CreateSchedule(newContext(s.Context()), &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: fmt.Sprintf(sid, i), + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + }) + s.NoError(err) + } + + time.Sleep(5 * time.Second) //nolint:forbidigo + + // With no rate limit, we'd see 10/second == 50 workflows run. With a limit of 1/sec, we + // expect to see around 5. + s.Less(atomic.LoadInt32(&runs), int32(10)) +} + +// testNextTimeCache only applies to V1. +func testNextTimeCache(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-next-time-cache" + wid := "sched-test-next-time-cache-wf" + wt := "sched-test-next-time-cache-wt" + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Second)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + req := &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + } + + var runs atomic.Int32 + workflowFn := func(ctx workflow.Context) error { + workflow.SideEffect(ctx, func(ctx workflow.Context) any { + runs.Add(1) + return 0 + }) + return nil + } + s.SdkWorker().RegisterWorkflowWithOptions(workflowFn, workflow.RegisterOptions{Name: wt}) + + _, err := s.FrontendClient().CreateSchedule(newContext(s.Context()), req) + s.NoError(err) + + // wait for at least 13 runs + const count = 13 + s.Eventually(func() bool { return runs.Load() >= count }, (count+10)*time.Second, 500*time.Millisecond) + + // there should be only four side effects for 13 runs, and only two mentioning "Next" + // (cache refills) + events := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: scheduler.WorkflowIDPrefix + sid}) + var sideEffects, nextTimeSideEffects int + for _, e := range events { + if marker := e.GetMarkerRecordedEventAttributes(); marker.GetMarkerName() == "SideEffect" { + sideEffects++ + if p, ok := marker.Details["data"]; ok && len(p.Payloads) == 1 { + if string(p.Payloads[0].Metadata["messageType"]) == "temporal.server.api.schedule.v1.NextTimeCache" || + strings.Contains(payloads.ToString(p), `"Next"`) { + nextTimeSideEffects++ + } + } + } + } + + const ( + // These match the ones in the scheduler workflow, but they're not exported. + // Change these if those change. + FutureActionCountForList = 5 + NextTimeCacheV2Size = 14 + + // Calculate expected results + expectedCacheSize = NextTimeCacheV2Size - FutureActionCountForList + 1 + expectedRefills = (count + expectedCacheSize - 1) / expectedCacheSize + uuidCacheRefills = (count + 9) / 10 + ) + s.Equal(expectedRefills+uuidCacheRefills, sideEffects) + s.Equal(expectedRefills, nextTimeSideEffects) +} + +// getScheduleEntryFromVisibility polls visibility using ListSchedules until it finds a schedule +// with the given id and for which the optional predicate function returns true. +func getScheduleEntryFromVisibility(env testcore.Env, sid string, newContext contextFactory, predicate func(*schedulepb.ScheduleListEntry) bool) *schedulepb.ScheduleListEntry { + env.T().Helper() + var slEntry *schedulepb.ScheduleListEntry + require.Eventually(env.T(), func() bool { // wait for visibility + listResp, err := env.FrontendClient().ListSchedules(newContext(env.Context()), &workflowservice.ListSchedulesRequest{ + Namespace: env.Namespace().String(), + MaximumPageSize: 5, + }) + if err != nil { + return false + } + for _, ent := range listResp.Schedules { + if ent.ScheduleId == sid { + if predicate != nil && !predicate(ent) { + return false + } + slEntry = ent + return true + } + } + return false + }, 15*time.Second, 1*time.Second) + return slEntry +} + +func durationNear(t *testing.T, value, target time.Duration) { + t.Helper() + const tolerance = 5 * time.Second + require.Greater(t, value, target-tolerance) + require.Less(t, value, target+tolerance) +} + +func assertSameRecentActions( + t *testing.T, + expected *workflowservice.DescribeScheduleResponse, actual *schedulepb.ScheduleListEntry, +) { + t.Helper() + if len(expected.Info.RecentActions) != len(actual.Info.RecentActions) { + t.Fatalf( + "RecentActions have different length expected %d, got %d", + len(expected.Info.RecentActions), + len(actual.Info.RecentActions)) + } + for i := range expected.Info.RecentActions { + if !proto.Equal(expected.Info.RecentActions[i], actual.Info.RecentActions[i]) { + t.Errorf( + "RecentActions are differ at index %d. Expected %v, got %v", + i, + expected.Info.RecentActions[i], + actual.Info.RecentActions[i], + ) + } + } +} + +// assertRecentActionsNoDuplicateRunIDs verifies that no two entries in +// RecentActions refer to the same workflow run. Duplicates can occur if the +// migration between V1 and V2 schedulers doesn't properly deduplicate entries +// that appear in both RunningWorkflows and RecentActions. +func assertRecentActionsNoDuplicateRunIDs(t *testing.T, actions []*schedulepb.ScheduleActionResult) { + t.Helper() + seen := make(map[string]int) // runId -> index of first occurrence + for i, action := range actions { + runID := action.GetStartWorkflowResult().GetRunId() + if runID == "" { + continue + } + if firstIdx, ok := seen[runID]; ok { + t.Errorf( + "duplicate RunId %q in RecentActions at indices %d and %d (workflowId=%q)", + runID, firstIdx, i, action.GetStartWorkflowResult().GetWorkflowId(), + ) + } + seen[runID] = i + } +} +func testUpdateScheduleMemo(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-update-memo" + wid := "sched-test-update-memo-wf" + wt := "sched-test-update-memo-wt" + + s.SdkWorker().RegisterWorkflowWithOptions( + func(ctx workflow.Context) error { return nil }, + workflow.RegisterOptions{Name: wt}, + ) + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + memo1 := payload.EncodeString("val1") + memo2 := payload.EncodeString("val2") + + // Create schedule with initial memo. + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "key1": memo1, + "key2": memo2, + }, + }, + }) + require.NoError(t, err) + + // Verify initial memo. + describeResp, err := s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + }) + require.NoError(t, err) + require.Equal(t, memo1.Data, describeResp.Memo.Fields["key1"].Data) + require.Equal(t, memo2.Data, describeResp.Memo.Fields["key2"].Data) + + // Update: replace memo with only key3 (key1 and key2 should be gone). + memo3 := payload.EncodeString("new") + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "key3": memo3, + }, + }, + }) + require.NoError(t, err) + + // Verify replaced memo. + describeResp, err = s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + }) + require.NoError(t, err) + require.Nil(t, describeResp.Memo.Fields["key1"], "key1 should be gone after replace") + require.Nil(t, describeResp.Memo.Fields["key2"], "key2 should be gone after replace") + require.Equal(t, memo3.Data, describeResp.Memo.Fields["key3"].Data, "key3 should be set") + + // Update with nil memo (no change). + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + }) + require.NoError(t, err) + + // Verify memo unchanged. + describeResp, err = s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + }) + require.NoError(t, err) + require.Equal(t, memo3.Data, describeResp.Memo.Fields["key3"].Data, "key3 should be unchanged") + + // Update with empty memo (clear all). + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{}, + }, + }) + require.NoError(t, err) + + // Verify memo cleared. + describeResp, err = s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + }) + require.NoError(t, err) + require.Empty(t, describeResp.Memo.GetFields(), "memo should be empty after replace with empty map") +} + +func testUpdateScheduleMemoRejected(t *testing.T, newContext contextFactory) { + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-update-memo-rejected" + wid := "sched-test-update-memo-rejected-wf" + wt := "sched-test-update-memo-rejected-wt" + + s.SdkWorker().RegisterWorkflowWithOptions( + func(ctx workflow.Context) error { return nil }, + workflow.RegisterOptions{Name: wt}, + ) + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create V1 schedule. + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + }) + require.NoError(t, err) + + // Update with memo should be rejected. + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{ + "key": payload.EncodeString("value"), + }, + }, + }) + require.Error(t, err) + var failedPrecondition *serviceerror.FailedPrecondition + require.ErrorAs(t, err, &failedPrecondition) + require.Contains(t, err.Error(), "memo updates are not supported on workflow-backed schedules") +} + +func testUpdateScheduleMemoOnly(t *testing.T, newContext contextFactory) { + // UpdateScheduleRequest uses replace semantics for the schedule field, so omitting it + // causes the schedule to be unset. Memo-only updates require the server to skip replacing + // the schedule when the field is nil, similar to how memo and search_attributes are handled. + t.Skip("memo-only updates not yet supported: omitting the schedule field unsets the schedule") + + s := testcore.NewEnv(t, scheduleCommonOpts()...) + + sid := "sched-test-update-memo-only" + wid := "sched-test-update-memo-only-wf" + wt := "sched-test-update-memo-only-wt" + + s.SdkWorker().RegisterWorkflowWithOptions( + func(ctx workflow.Context) error { return nil }, + workflow.RegisterOptions{Name: wt}, + ) + + schedule := &schedulepb.Schedule{ + Spec: &schedulepb.ScheduleSpec{ + Interval: []*schedulepb.IntervalSpec{ + {Interval: durationpb.New(1 * time.Hour)}, + }, + }, + Action: &schedulepb.ScheduleAction{ + Action: &schedulepb.ScheduleAction_StartWorkflow{ + StartWorkflow: &workflowpb.NewWorkflowExecutionInfo{ + WorkflowId: wid, + WorkflowType: &commonpb.WorkflowType{Name: wt}, + TaskQueue: &taskqueuepb.TaskQueue{Name: s.WorkerTaskQueue(), Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }, + }, + }, + } + + // Create schedule with initial memo. + memo1 := payload.EncodeString("val1") + ctx := newContext(s.Context()) + _, err := s.FrontendClient().CreateSchedule(ctx, &workflowservice.CreateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Schedule: schedule, + Identity: "test", + RequestId: uuid.NewString(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{"key1": memo1}, + }, + }) + require.NoError(t, err) + + // Update only memo, without setting the schedule field. + memo2 := payload.EncodeString("val2") + _, err = s.FrontendClient().UpdateSchedule(newContext(s.Context()), &workflowservice.UpdateScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, + Identity: "test", + RequestId: uuid.NewString(), + Memo: &commonpb.Memo{ + Fields: map[string]*commonpb.Payload{"key1": memo2}, + }, + }) + require.NoError(t, err) + + // Verify memo was updated and schedule is still intact. + describeResp, err := s.FrontendClient().DescribeSchedule(newContext(s.Context()), &workflowservice.DescribeScheduleRequest{ + Namespace: s.Namespace().String(), + ScheduleId: sid, }) + require.NoError(t, err) + require.Equal(t, memo2.Data, describeResp.Memo.Fields["key1"].Data, "memo should be updated") + require.NotNil(t, describeResp.Schedule.Spec, "schedule spec should not be nil") + require.NotEmpty(t, describeResp.Schedule.Spec.Interval, "schedule spec intervals should be preserved") + require.NotNil(t, describeResp.Schedule.Action, "schedule action should be preserved") } diff --git a/tests/signal_workflow_test.go b/tests/signal_workflow_test.go index 2b8926a3280..475dbce799a 100644 --- a/tests/signal_workflow_test.go +++ b/tests/signal_workflow_test.go @@ -5,7 +5,6 @@ import ( "encoding/binary" "fmt" "strconv" - "strings" "testing" "time" @@ -66,7 +65,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow() { Identity: identity, Header: header, }) - s.NotNil(err0) + s.Error(err0) s.IsType(&serviceerror.NotFound{}, err0) // Start workflow execution @@ -97,7 +96,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -174,7 +173,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow() { s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(signalName, signalEvent.GetWorkflowExecutionSignaledEventAttributes().SignalName) s.ProtoEqual(signalInput, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Input) s.Equal(identity, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Identity) @@ -200,7 +199,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow() { s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(signalName, signalEvent.GetWorkflowExecutionSignaledEventAttributes().SignalName) s.ProtoEqual(signalInput, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Input) s.Equal(identity, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Identity) @@ -228,7 +227,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow() { Input: nil, Identity: identity, }) - s.NotNil(err) + s.Error(err) s.IsType(&serviceerror.NotFound{}, err) } @@ -271,7 +270,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow_DuplicateRequest() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -352,7 +351,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow_DuplicateRequest() { s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(signalName, signalEvent.GetWorkflowExecutionSignaledEventAttributes().SignalName) s.ProtoEqual(signalInput, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Input) s.Equal(identity, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Identity) @@ -368,7 +367,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow_DuplicateRequest() { s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(0, numOfSignaledEvent) } @@ -426,7 +425,7 @@ func (s *SignalWorkflowTestSuite) TestSignalExternalWorkflowCommand() { if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -481,7 +480,7 @@ func (s *SignalWorkflowTestSuite) TestSignalExternalWorkflowCommand() { if externalActivityCounter < externalActivityCount { externalActivityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, externalActivityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, externalActivityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -662,7 +661,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow_Cron_NoWorkflowTaskCreated( _, err = poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) s.NoError(err) - s.True(workflowTaskDelay > time.Second*2) + s.Greater(workflowTaskDelay, time.Second*2) } func (s *SignalWorkflowTestSuite) TestSignalWorkflow_WorkflowCloseAttempted() { @@ -714,8 +713,10 @@ func (s *SignalWorkflowTestSuite) TestSignalWorkflow_WorkflowCloseAttempted() { Identity: identity, RequestId: uuid.NewString(), }) - s.Error(err) - s.Error(consts.ErrWorkflowClosing, err) + var resourceExhausted *serviceerror.ResourceExhausted + s.ErrorAs(err, &resourceExhausted) + s.Equal(consts.ErrWorkflowClosing.Cause, resourceExhausted.Cause) + s.Equal(consts.ErrWorkflowClosing.Message, resourceExhausted.Message) } attemptCount++ @@ -797,7 +798,7 @@ func (s *SignalWorkflowTestSuite) TestSignalExternalWorkflowCommand_WithoutRunID if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -851,7 +852,7 @@ func (s *SignalWorkflowTestSuite) TestSignalExternalWorkflowCommand_WithoutRunID if externalActivityCounter < externalActivityCount { externalActivityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, externalActivityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, externalActivityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -953,7 +954,7 @@ CheckHistoryLoopForSignalSent: s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(signalName, signalEvent.GetWorkflowExecutionSignaledEventAttributes().SignalName) s.ProtoEqual(signalInput, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Input) s.Equal("history-service", signalEvent.GetWorkflowExecutionSignaledEventAttributes().Identity) @@ -994,7 +995,7 @@ func (s *SignalWorkflowTestSuite) TestSignalExternalWorkflowCommand_UnKnownTarge if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -1116,7 +1117,7 @@ func (s *SignalWorkflowTestSuite) TestSignalExternalWorkflowCommand_SignalSelf() if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -1248,7 +1249,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -1348,7 +1349,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow() { s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(signalName, signalEvent.GetWorkflowExecutionSignaledEventAttributes().SignalName) s.ProtoEqual(signalInput, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Input) s.Equal(identity, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Identity) @@ -1385,11 +1386,11 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow() { s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(signalName, signalEvent.GetWorkflowExecutionSignaledEventAttributes().SignalName) s.ProtoEqual(signalInput, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Input) s.Equal(identity, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Identity) - s.True(startedEvent != nil) + s.NotNil(startedEvent) s.ProtoEqual(header, startedEvent.GetWorkflowExecutionStartedEventAttributes().Header) // Send signal to not existed workflow @@ -1411,7 +1412,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow() { s.NoError(err) s.False(workflowComplete) - s.True(signalEvent != nil) + s.NotNil(signalEvent) s.Equal(signalName, signalEvent.GetWorkflowExecutionSignaledEventAttributes().SignalName) s.ProtoEqual(signalInput, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Input) s.Equal(identity, signalEvent.GetWorkflowExecutionSignaledEventAttributes().Identity) @@ -1476,7 +1477,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow() { } listClosedResp, err := s.FrontendClient().ListClosedWorkflowExecutions(testcore.NewContext(), listClosedRequest) s.NoError(err) - s.Equal(1, len(listClosedResp.Executions)) + s.Len(listClosedResp.Executions, 1) } func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow_ResolveIDDeduplication() { @@ -1519,7 +1520,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow_ResolveIDDeduplica if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -1590,7 +1591,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow_ResolveIDDeduplica resp, err := s.FrontendClient().SignalWithStartWorkflowExecution(ctx, sRequest) s.Nil(resp) s.Error(err) - s.True(strings.Contains(err.Error(), "reject duplicate workflow Id")) + s.Contains(err.Error(), "reject duplicate workflow Id") s.IsType(&serviceerror.WorkflowExecutionAlreadyStarted{}, err) // test WorkflowIdReusePolicy: AllowDuplicateFailedOnly @@ -1599,7 +1600,7 @@ func (s *SignalWorkflowTestSuite) TestSignalWithStartWorkflow_ResolveIDDeduplica resp, err = s.FrontendClient().SignalWithStartWorkflowExecution(ctx, sRequest) s.Nil(resp) s.Error(err) - s.True(strings.Contains(err.Error(), "allow duplicate workflow Id if last run failed")) + s.Contains(err.Error(), "allow duplicate workflow Id if last run failed") s.IsType(&serviceerror.WorkflowExecutionAlreadyStarted{}, err) // test WorkflowIdReusePolicy: AllowDuplicate diff --git a/tests/sizelimit_test.go b/tests/sizelimit_test.go index 358a3b30890..3b880547479 100644 --- a/tests/sizelimit_test.go +++ b/tests/sizelimit_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -21,40 +20,41 @@ import ( "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/testing/parallelsuite" + "go.temporal.io/server/common/testing/taskpoller" + "go.temporal.io/server/common/testing/testvars" "go.temporal.io/server/service/history/consts" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) -type SizeLimitFunctionalSuite struct { - testcore.FunctionalTestBase +type SizeLimitSuite struct { + parallelsuite.Suite[*SizeLimitSuite] } func TestSizeLimitFunctionalSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(SizeLimitFunctionalSuite)) + parallelsuite.Run(t, &SizeLimitSuite{}) } -func (s *SizeLimitFunctionalSuite) SetupSuite() { - dynamicConfigOverrides := map[dynamicconfig.Key]any{ - dynamicconfig.HistoryCountLimitWarn.Key(): 10, - dynamicconfig.HistoryCountLimitError.Key(): 20, - dynamicconfig.HistorySizeLimitWarn.Key(): 5000, - dynamicconfig.HistorySizeLimitError.Key(): 9000, - dynamicconfig.BlobSizeLimitWarn.Key(): 1, - dynamicconfig.BlobSizeLimitError.Key(): 1000, - dynamicconfig.MutableStateSizeLimitWarn.Key(): 200, - dynamicconfig.MutableStateSizeLimitError.Key(): 1100, - } - s.FunctionalTestBase.SetupSuiteWithCluster(testcore.WithDynamicConfigOverrides(dynamicConfigOverrides)) -} +func (s *SizeLimitSuite) TestTerminateWorkflowCausedByHistoryCountLimit() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.HistoryCountLimitWarn, 10), + testcore.WithDynamicConfig(dynamicconfig.HistoryCountLimitError, 20), + // Poller identity is persisted in mutable state when an activity starts. + // If identity changes to a longer value (for example tv.WorkerIdentity()), + // low mutable-state/history-size limits can be hit before history-count limits. + testcore.WithDynamicConfig(dynamicconfig.HistorySizeLimitWarn, 10*1024*1024), + testcore.WithDynamicConfig(dynamicconfig.HistorySizeLimitError, 50*1024*1024), + testcore.WithDynamicConfig(dynamicconfig.MutableStateSizeLimitWarn, 1*1024*1024), + testcore.WithDynamicConfig(dynamicconfig.MutableStateSizeLimitError, 8*1024*1024), + ) -func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByHistoryCountLimit() { id := "functional-terminate-workflow-by-history-count-limit-test" wt := "functional-terminate-workflow-by-history-count-limit-test-type" tq := "functional-terminate-workflow-by-history-count-limit-test-taskqueue" - identity := "worker1" + tv := testvars.New(s.T()).WithTaskQueue(tq) activityName := "activity_type1" workflowType := &commonpb.WorkflowType{Name: wt} @@ -63,20 +63,19 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByHistoryCountLimi request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: workflowType, TaskQueue: taskQueue, Input: nil, WorkflowRunTimeout: durationpb.New(100 * time.Second), WorkflowTaskTimeout: durationpb.New(10 * time.Second), - Identity: identity, } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + we, err0 := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) s.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) activityCount := int32(4) activityCounter := int32(0) @@ -84,7 +83,7 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByHistoryCountLimi if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -109,25 +108,11 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByHistoryCountLimi }}, nil } - atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - - return payloads.EncodeString("Activity Result"), false, nil - } - - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: taskQueue, - Identity: identity, - WorkflowTaskHandler: wtHandler, - ActivityTaskHandler: atHandler, - Logger: s.Logger, - T: s.T(), - } + poller := env.TaskPoller() for i := int32(0); i < activityCount-1; i++ { - dwResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + dwResp, err := env.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: id, RunId: we.RunId, @@ -137,12 +122,19 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByHistoryCountLimi // Poll workflow task only if it is running if dwResp.WorkflowExecutionInfo.Status == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING { - _, err := poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + _, err := poller.PollAndHandleWorkflowTask(tv, func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + cmds, err := wtHandler(task) + return &workflowservice.RespondWorkflowTaskCompletedRequest{Commands: cmds}, err + }) + env.Logger.Info("PollAndHandleWorkflowTask", tag.Error(err)) s.NoError(err) - err = poller.PollAndProcessActivityTask(false) - s.Logger.Info("PollAndProcessActivityTask", tag.Error(err)) + _, err = poller.PollAndHandleActivityTask(tv, func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) { + return &workflowservice.RespondActivityTaskCompletedRequest{ + Result: payloads.EncodeString("Activity Result"), + }, nil + }, taskpoller.WithTimeout(90*time.Second)) + env.Logger.Info("PollAndHandleActivityTask", tag.Error(err)) s.NoError(err) } } @@ -150,18 +142,17 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByHistoryCountLimi var signalErr error // Send signals until workflow is force terminated SignalLoop: - for i := 0; i < 10; i++ { + for range 10 { // Send another signal without RunID signalName := "another signal" signalInput := payloads.EncodeString("another signal input") - _, signalErr = s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + _, signalErr = env.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{ WorkflowId: id, }, SignalName: signalName, Input: signalInput, - Identity: identity, }) if signalErr != nil { @@ -171,9 +162,9 @@ SignalLoop: // Signalling workflow should result in force terminating the workflow execution and returns with ResourceExhausted // error. InvalidArgument is returned by the client. s.EqualError(signalErr, common.FailureReasonHistoryCountExceedsLimit) - s.IsType(&serviceerror.InvalidArgument{}, signalErr) + s.ErrorAs(signalErr, new(*serviceerror.InvalidArgument)) - historyEvents := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ + historyEvents := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: id, RunId: we.GetRunId(), }) @@ -204,10 +195,10 @@ SignalLoop: // verify visibility is correctly processed from open to close s.Eventually( func() bool { - resp, err1 := s.FrontendClient().ListClosedWorkflowExecutions( + resp, err1 := env.FrontendClient().ListClosedWorkflowExecutions( testcore.NewContext(), &workflowservice.ListClosedWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), MaximumPageSize: 100, StartTimeFilter: &filterpb.StartTimeFilter{ EarliestTime: nil, @@ -224,7 +215,7 @@ SignalLoop: if len(resp.Executions) == 1 { return true } - s.Logger.Info("Closed WorkflowExecution is not yet visible") + env.Logger.Info("Closed WorkflowExecution is not yet visible") return false }, testcore.WaitForESToSettle, @@ -232,78 +223,79 @@ SignalLoop: ) } -func (s *SizeLimitFunctionalSuite) TestWorkflowFailed_PayloadSizeTooLarge() { - +func (s *SizeLimitSuite) TestWorkflowFailed_PayloadSizeTooLarge() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.BlobSizeLimitWarn, 1), + testcore.WithDynamicConfig(dynamicconfig.BlobSizeLimitError, 1000), + ) id := "functional-workflow-failed-large-payload" wt := "functional-workflow-failed-large-payload-type" tl := "functional-workflow-failed-large-payload-taskqueue" - identity := "worker1" + tv := testvars.New(s.T()).WithTaskQueue(tl) largePayload := make([]byte, 1001) pl, err := payloads.Encode(largePayload) s.NoError(err) sigReadyToSendChan := make(chan struct{}, 1) sigSendDoneChan := make(chan struct{}) - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - select { - case sigReadyToSendChan <- struct{}{}: - default: - } - select { - case <-sigSendDoneChan: - } - return []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_RECORD_MARKER, - Attributes: &commandpb.Command_RecordMarkerCommandAttributes{ - RecordMarkerCommandAttributes: &commandpb.RecordMarkerCommandAttributes{ - MarkerName: "large-payload", - Details: map[string]*commonpb.Payloads{"test": pl}, - }, - }, - }, - }, nil - } - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - Identity: identity, - WorkflowTaskHandler: wtHandler, - ActivityTaskHandler: nil, - Logger: s.Logger, - T: s.T(), - } + poller := env.TaskPoller() request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Input: nil, WorkflowTaskTimeout: durationpb.New(60 * time.Second), - Identity: identity, + Identity: "worker", } - we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + we, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) s.NoError(err) go func() { - _, err = poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + _, err = poller.PollAndHandleWorkflowTask(tv, func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + select { + case sigReadyToSendChan <- struct{}{}: + default: + } + + select { + case <-sigSendDoneChan: + case <-env.Context().Done(): + return nil, env.Context().Err() + } + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_RECORD_MARKER, + Attributes: &commandpb.Command_RecordMarkerCommandAttributes{ + RecordMarkerCommandAttributes: &commandpb.RecordMarkerCommandAttributes{ + MarkerName: "large-payload", + Details: map[string]*commonpb.Payloads{"test": pl}, + }, + }, + }, + }, + }, nil + }) + env.Logger.Info("PollAndHandleWorkflowTask", tag.Error(err)) }() select { case <-sigReadyToSendChan: + case <-env.Context().Done(): + s.FailNow("timed out waiting for workflow task handler to be ready") } - _, err = s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: id, RunId: we.GetRunId()}, SignalName: "signal-name", - Identity: identity, + Identity: "worker", RequestId: uuid.NewString(), }) s.NoError(err) @@ -311,8 +303,8 @@ func (s *SizeLimitFunctionalSuite) TestWorkflowFailed_PayloadSizeTooLarge() { // Wait for workflow to fail. var historyEvents []*historypb.HistoryEvent - for i := 0; i < 10; i++ { - historyEvents = s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: id, RunId: we.GetRunId()}) + for range 10 { + historyEvents = env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{WorkflowId: id, RunId: we.GetRunId()}) lastEvent := historyEvents[len(historyEvents)-1] if lastEvent.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_FAILED { break @@ -328,11 +320,16 @@ func (s *SizeLimitFunctionalSuite) TestWorkflowFailed_PayloadSizeTooLarge() { 6 WorkflowExecutionTerminated`, historyEvents) } -func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByMsSizeLimit() { +func (s *SizeLimitSuite) TestTerminateWorkflowCausedByMsSizeLimit() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.MutableStateSizeLimitWarn, 200), + testcore.WithDynamicConfig(dynamicconfig.MutableStateSizeLimitError, 1100), + ) id := "functional-terminate-workflow-by-ms-size-limit-test" wt := "functional-terminate-workflow-by-ms-size-limit-test-type" tq := "functional-terminate-workflow-by-ms-size-limit-test-taskqueue" - identity := "worker1" + tv := testvars.New(s.T()).WithTaskQueue(tq) activityName := "activity_type1" workflowType := &commonpb.WorkflowType{Name: wt} @@ -341,71 +338,47 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByMsSizeLimit() { request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: workflowType, TaskQueue: taskQueue, Input: nil, WorkflowRunTimeout: durationpb.New(100 * time.Second), WorkflowTaskTimeout: durationpb.New(1 * time.Second), - Identity: identity, + Identity: "worker", } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + we, err0 := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) s.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) activityCount := int32(4) - activitiesScheduled := false activityLargePayload := payloads.EncodeBytes(make([]byte, 900)) wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - if !activitiesScheduled { - cmds := make([]*commandpb.Command, activityCount) - for i := range cmds { - cmds[i] = &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: convert.Int32ToString(int32(i)), - ActivityType: &commonpb.ActivityType{Name: activityName}, - TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, - Input: activityLargePayload, - ScheduleToCloseTimeout: durationpb.New(100 * time.Second), - ScheduleToStartTimeout: durationpb.New(10 * time.Second), - StartToCloseTimeout: durationpb.New(50 * time.Second), - HeartbeatTimeout: durationpb.New(5 * time.Second), - }}, - } + cmds := make([]*commandpb.Command, activityCount) + for i := range cmds { + cmds[i] = &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: convert.Int32ToString(int32(i)), + ActivityType: &commonpb.ActivityType{Name: activityName}, + TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + Input: activityLargePayload, + ScheduleToCloseTimeout: durationpb.New(100 * time.Second), + ScheduleToStartTimeout: durationpb.New(10 * time.Second), + StartToCloseTimeout: durationpb.New(50 * time.Second), + HeartbeatTimeout: durationpb.New(5 * time.Second), + }}, } - return cmds, nil } - - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ - Result: payloads.EncodeString("Done"), - }}, - }}, nil - } - - atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - - return payloads.EncodeString("Activity Result"), false, nil + return cmds, nil } - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: taskQueue, - Identity: identity, - WorkflowTaskHandler: wtHandler, - ActivityTaskHandler: atHandler, - Logger: s.Logger, - T: s.T(), - } + poller := env.TaskPoller() - dwResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + dwResp, err := env.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), Execution: &commonpb.WorkflowExecution{ WorkflowId: id, RunId: we.RunId, @@ -415,28 +388,31 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByMsSizeLimit() { // Poll workflow task only if it is running if dwResp.WorkflowExecutionInfo.Status == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING { - _, err := poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + _, err := poller.PollAndHandleWorkflowTask(tv, func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + cmds, err := wtHandler(task) + return &workflowservice.RespondWorkflowTaskCompletedRequest{Commands: cmds}, err + }) + env.Logger.Info("PollAndHandleWorkflowTask", tag.Error(err)) // Workflow should be force terminated at this point s.EqualError(err, common.FailureReasonMutableStateSizeExceedsLimit) } // Send another signal without RunID - _, signalErr := s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + _, signalErr := env.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{ WorkflowId: id, }, SignalName: "another signal", Input: payloads.EncodeString("another signal input"), - Identity: identity, + Identity: "worker", }) s.EqualError(signalErr, consts.ErrWorkflowCompleted.Error()) - s.IsType(&serviceerror.NotFound{}, signalErr) + s.ErrorAs(signalErr, new(*serviceerror.NotFound)) - historyEvents := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ + historyEvents := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: id, RunId: we.GetRunId(), }) @@ -450,10 +426,10 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByMsSizeLimit() { // verify visibility is correctly processed from open to close s.Eventually( func() bool { - resp, err1 := s.FrontendClient().ListClosedWorkflowExecutions( + resp, err1 := env.FrontendClient().ListClosedWorkflowExecutions( testcore.NewContext(), &workflowservice.ListClosedWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), MaximumPageSize: 100, StartTimeFilter: &filterpb.StartTimeFilter{ EarliestTime: nil, @@ -470,7 +446,7 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByMsSizeLimit() { if len(resp.Executions) == 1 { return true } - s.Logger.Info("Closed WorkflowExecution is not yet visible") + env.Logger.Info("Closed WorkflowExecution is not yet visible") return false }, testcore.WaitForESToSettle, @@ -478,47 +454,51 @@ func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByMsSizeLimit() { ) } -func (s *SizeLimitFunctionalSuite) TestTerminateWorkflowCausedByHistorySizeLimit() { +func (s *SizeLimitSuite) TestTerminateWorkflowCausedByHistorySizeLimit() { + env := testcore.NewEnv( + s.T(), + testcore.WithDynamicConfig(dynamicconfig.HistorySizeLimitWarn, 5000), + testcore.WithDynamicConfig(dynamicconfig.HistorySizeLimitError, 9000), + ) id := "functional-terminate-workflow-by-history-size-limit-test" wt := "functional-terminate-workflow-by-history-size-limit-test-type" tq := "functional-terminate-workflow-by-history-size-limit-test-taskqueue" - identity := "worker1" workflowType := &commonpb.WorkflowType{Name: wt} taskQueue := &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL} request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: workflowType, TaskQueue: taskQueue, Input: nil, WorkflowRunTimeout: durationpb.New(100 * time.Second), WorkflowTaskTimeout: durationpb.New(10 * time.Second), - Identity: identity, + Identity: "worker", } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + we, err0 := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) s.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) var signalErr error // Send signals until workflow is force terminated largePayload := make([]byte, 900) SignalLoop: - for i := 0; i < 10; i++ { + for range 10 { // Send another signal without RunID signalName := "another signal" signalInput, err := payloads.Encode(largePayload) s.NoError(err) - _, signalErr = s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + _, signalErr = env.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{ WorkflowId: id, }, SignalName: signalName, Input: signalInput, - Identity: identity, + Identity: "worker", }) if signalErr != nil { @@ -528,9 +508,9 @@ SignalLoop: // Signalling workflow should result in force terminating the workflow execution and returns with ResourceExhausted // error. InvalidArgument is returned by the client. s.EqualError(signalErr, common.FailureReasonHistorySizeExceedsLimit) - s.IsType(&serviceerror.InvalidArgument{}, signalErr) + s.ErrorAs(signalErr, new(*serviceerror.InvalidArgument)) - historyEvents := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ + historyEvents := env.GetHistory(env.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: id, RunId: we.GetRunId(), }) @@ -551,10 +531,10 @@ SignalLoop: // verify visibility is correctly processed from open to close s.Eventually( func() bool { - resp, err1 := s.FrontendClient().ListClosedWorkflowExecutions( + resp, err1 := env.FrontendClient().ListClosedWorkflowExecutions( testcore.NewContext(), &workflowservice.ListClosedWorkflowExecutionsRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), MaximumPageSize: 100, StartTimeFilter: &filterpb.StartTimeFilter{ EarliestTime: nil, @@ -571,7 +551,7 @@ SignalLoop: if len(resp.Executions) == 1 { return true } - s.Logger.Info("Closed WorkflowExecution is not yet visible") + env.Logger.Info("Closed WorkflowExecution is not yet visible") return false }, testcore.WaitForESToSettle, diff --git a/tests/standalone_activity_test.go b/tests/standalone_activity_test.go index f4db71cb6a5..234e9d8a512 100644 --- a/tests/standalone_activity_test.go +++ b/tests/standalone_activity_test.go @@ -2,6 +2,7 @@ package tests import ( "context" + "errors" "fmt" "testing" "time" @@ -274,58 +275,151 @@ func (s *standaloneActivityTestSuite) TestPollActivityTaskQueue() { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() - activityID := testcore.RandomizeStr(t.Name()) - taskQueue := testcore.RandomizeStr(t.Name()) - namespace := s.Namespace().String() + t.Run("FirstAttempt", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + namespace := s.Namespace().String() - startToCloseTimeout := durationpb.New(1 * time.Minute) - scheduleToCloseTimeout := durationpb.New(2 * time.Minute) - heartbeatTimeout := durationpb.New(20 * time.Second) - priority := &commonpb.Priority{ - FairnessKey: "test-key", - } + startToCloseTimeout := durationpb.New(1 * time.Minute) + scheduleToCloseTimeout := durationpb.New(2 * time.Minute) + heartbeatTimeout := durationpb.New(20 * time.Second) + priority := &commonpb.Priority{ + FairnessKey: "test-key", + } - startResp, err := s.FrontendClient().StartActivityExecution(ctx, &workflowservice.StartActivityExecutionRequest{ - Namespace: namespace, - ActivityId: activityID, - ActivityType: s.tv.ActivityType(), - Identity: s.tv.WorkerIdentity(), - Input: defaultInput, - TaskQueue: &taskqueuepb.TaskQueue{ - Name: taskQueue, - }, - StartToCloseTimeout: startToCloseTimeout, - ScheduleToCloseTimeout: scheduleToCloseTimeout, - HeartbeatTimeout: heartbeatTimeout, - RequestId: s.tv.RequestID(), - Priority: priority, - Header: defaultHeader, + startResp, err := s.FrontendClient().StartActivityExecution(ctx, &workflowservice.StartActivityExecutionRequest{ + Namespace: namespace, + ActivityId: activityID, + ActivityType: s.tv.ActivityType(), + Identity: s.tv.WorkerIdentity(), + Input: defaultInput, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + }, + StartToCloseTimeout: startToCloseTimeout, + ScheduleToCloseTimeout: scheduleToCloseTimeout, + HeartbeatTimeout: heartbeatTimeout, + RequestId: s.tv.RequestID(), + Priority: priority, + Header: defaultHeader, + }) + require.NoError(t, err) + + pollTaskResp, err := s.FrontendClient().PollActivityTaskQueue(ctx, &workflowservice.PollActivityTaskQueueRequest{ + Namespace: namespace, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + Identity: s.tv.WorkerIdentity(), + }) + require.NoError(t, err) + require.Equal(t, activityID, pollTaskResp.GetActivityId()) + require.Equal(t, namespace, pollTaskResp.GetWorkflowNamespace()) + protorequire.ProtoEqual(t, s.tv.ActivityType(), pollTaskResp.GetActivityType()) + require.Equal(t, startResp.GetRunId(), pollTaskResp.GetActivityRunId()) + protorequire.ProtoEqual(t, defaultInput, pollTaskResp.GetInput()) + require.False(t, pollTaskResp.GetStartedTime().AsTime().IsZero()) + require.False(t, pollTaskResp.GetScheduledTime().AsTime().IsZero()) + require.EqualValues(t, 1, pollTaskResp.Attempt) + protorequire.ProtoEqual(t, startToCloseTimeout, pollTaskResp.GetStartToCloseTimeout()) + protorequire.ProtoEqual(t, scheduleToCloseTimeout, pollTaskResp.GetScheduleToCloseTimeout()) + protorequire.ProtoEqual(t, heartbeatTimeout, pollTaskResp.GetHeartbeatTimeout()) + protorequire.ProtoEqual(t, priority, pollTaskResp.GetPriority()) + protorequire.ProtoEqual(t, defaultHeader, pollTaskResp.GetHeader()) + require.NotNil(t, pollTaskResp.TaskToken) + protorequire.ProtoEqual(t, pollTaskResp.GetScheduledTime(), pollTaskResp.GetCurrentAttemptScheduledTime()) // Equal on first attempt }) - require.NoError(t, err) - pollTaskResp, err := s.FrontendClient().PollActivityTaskQueue(ctx, &workflowservice.PollActivityTaskQueueRequest{ - Namespace: namespace, - TaskQueue: &taskqueuepb.TaskQueue{ - Name: taskQueue, - Kind: enumspb.TASK_QUEUE_KIND_NORMAL, - }, - Identity: s.tv.WorkerIdentity(), + t.Run("RetriedAttempt", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + namespace := s.Namespace().String() + + startToCloseTimeout := durationpb.New(1 * time.Minute) + scheduleToCloseTimeout := durationpb.New(2 * time.Minute) + heartbeatTimeout := durationpb.New(20 * time.Second) + priority := &commonpb.Priority{ + FairnessKey: "test-key", + } + + startResp, err := s.FrontendClient().StartActivityExecution(ctx, &workflowservice.StartActivityExecutionRequest{ + Namespace: namespace, + ActivityId: activityID, + ActivityType: s.tv.ActivityType(), + Identity: s.tv.WorkerIdentity(), + Input: defaultInput, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + }, + StartToCloseTimeout: startToCloseTimeout, + ScheduleToCloseTimeout: scheduleToCloseTimeout, + HeartbeatTimeout: heartbeatTimeout, + RequestId: s.tv.RequestID(), + Priority: priority, + Header: defaultHeader, + }) + require.NoError(t, err) + + pollTaskResp, err := s.FrontendClient().PollActivityTaskQueue(ctx, &workflowservice.PollActivityTaskQueueRequest{ + Namespace: namespace, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + Identity: s.tv.WorkerIdentity(), + }) + require.NoError(t, err) + + nextRetryDelay := durationpb.New(1 * time.Second) + _, err = s.FrontendClient().RespondActivityTaskFailed(ctx, &workflowservice.RespondActivityTaskFailedRequest{ + Namespace: s.Namespace().String(), + TaskToken: pollTaskResp.TaskToken, + Failure: &failurepb.Failure{ + Message: "retryable failure", + FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + NonRetryable: false, + NextRetryDelay: nextRetryDelay, + }}, + }, + }) + require.NoError(t, err) + + describeResp, err := s.FrontendClient().DescribeActivityExecution(ctx, &workflowservice.DescribeActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: startResp.GetRunId(), + }) + require.NoError(t, err) + + pollTaskResp, err = s.FrontendClient().PollActivityTaskQueue(ctx, &workflowservice.PollActivityTaskQueueRequest{ + Namespace: namespace, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + Identity: s.tv.WorkerIdentity(), + }) + require.NoError(t, err) + require.Equal(t, activityID, pollTaskResp.GetActivityId()) + require.Equal(t, namespace, pollTaskResp.GetWorkflowNamespace()) + protorequire.ProtoEqual(t, s.tv.ActivityType(), pollTaskResp.GetActivityType()) + require.Equal(t, startResp.GetRunId(), pollTaskResp.GetActivityRunId()) + protorequire.ProtoEqual(t, defaultInput, pollTaskResp.GetInput()) + require.False(t, pollTaskResp.GetStartedTime().AsTime().IsZero()) + require.False(t, pollTaskResp.GetScheduledTime().AsTime().IsZero()) + require.EqualValues(t, 2, pollTaskResp.Attempt) + protorequire.ProtoEqual(t, startToCloseTimeout, pollTaskResp.GetStartToCloseTimeout()) + protorequire.ProtoEqual(t, scheduleToCloseTimeout, pollTaskResp.GetScheduleToCloseTimeout()) + protorequire.ProtoEqual(t, heartbeatTimeout, pollTaskResp.GetHeartbeatTimeout()) + protorequire.ProtoEqual(t, priority, pollTaskResp.GetPriority()) + protorequire.ProtoEqual(t, defaultHeader, pollTaskResp.GetHeader()) + require.NotNil(t, pollTaskResp.TaskToken) + + expectedAttemptScheduledTime := timestamppb.New( + describeResp.GetInfo().GetLastAttemptCompleteTime().AsTime().Add(nextRetryDelay.AsDuration())) + protorequire.ProtoEqual(t, expectedAttemptScheduledTime, pollTaskResp.GetCurrentAttemptScheduledTime()) }) - require.NoError(t, err) - require.Equal(t, activityID, pollTaskResp.GetActivityId()) - require.Equal(t, namespace, pollTaskResp.GetWorkflowNamespace()) - protorequire.ProtoEqual(t, s.tv.ActivityType(), pollTaskResp.GetActivityType()) - require.Equal(t, startResp.GetRunId(), pollTaskResp.GetActivityRunId()) - protorequire.ProtoEqual(t, defaultInput, pollTaskResp.GetInput()) - require.False(t, pollTaskResp.GetStartedTime().AsTime().IsZero()) - require.False(t, pollTaskResp.GetScheduledTime().AsTime().IsZero()) - require.EqualValues(t, 1, pollTaskResp.Attempt) - protorequire.ProtoEqual(t, startToCloseTimeout, pollTaskResp.GetStartToCloseTimeout()) - protorequire.ProtoEqual(t, scheduleToCloseTimeout, pollTaskResp.GetScheduleToCloseTimeout()) - protorequire.ProtoEqual(t, heartbeatTimeout, pollTaskResp.GetHeartbeatTimeout()) - protorequire.ProtoEqual(t, priority, pollTaskResp.GetPriority()) - protorequire.ProtoEqual(t, defaultHeader, pollTaskResp.GetHeader()) - require.NotNil(t, pollTaskResp.TaskToken) } func (s *standaloneActivityTestSuite) TestStart() { @@ -524,7 +618,7 @@ func (s *standaloneActivityTestSuite) TestComplete() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) }) t.Run("StaleAttemptToken", func(t *testing.T) { @@ -584,7 +678,7 @@ func (s *standaloneActivityTestSuite) TestComplete() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) // Complete with the attempt 2 token and should succeed _, err = s.FrontendClient().RespondActivityTaskCompleted(ctx, &workflowservice.RespondActivityTaskCompletedRequest{ @@ -816,7 +910,7 @@ func (s *standaloneActivityTestSuite) TestFail() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) }) t.Run("StaleAttemptToken", func(t *testing.T) { @@ -876,7 +970,7 @@ func (s *standaloneActivityTestSuite) TestFail() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) // Fail with the attempt 2 token and should be no error _, err = s.FrontendClient().RespondActivityTaskFailed(ctx, &workflowservice.RespondActivityTaskFailedRequest{ @@ -1007,6 +1101,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { t.Run("ByToken", func(t *testing.T) { activityID := testcore.RandomizeStr(t.Name()) taskQueue := testcore.RandomizeStr(t.Name()) + identity := "client-that-requested-cancellation" startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) runID := startResp.RunId @@ -1017,7 +1112,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, - Identity: "cancelling-worker", + Identity: identity, RequestId: s.tv.RequestID(), Reason: "Test Cancellation", }) @@ -1060,6 +1155,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { require.Greater(t, info.GetExecutionDuration().AsDuration(), time.Duration(0)) require.NotNil(t, info.GetCloseTime()) protorequire.ProtoEqual(t, details, activityResp.GetOutcome().GetFailure().GetCanceledFailureInfo().GetDetails()) + require.Equal(t, identity, activityResp.GetOutcome().GetFailure().GetCanceledFailureInfo().GetIdentity()) }) testByIDCases := []struct { @@ -1083,6 +1179,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { activityID := testcore.RandomizeStr(tc.name) taskQueue := testcore.RandomizeStr(tc.name) + identity := "client-that-requested-cancellation" startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) runID := startResp.RunId @@ -1093,7 +1190,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, - Identity: "cancelling-worker", + Identity: identity, RequestId: s.tv.RequestID(), Reason: "Test Cancellation", }) @@ -1141,6 +1238,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { "expected Canceled but is %s", info.GetStatus()) require.Equal(t, "Test Cancellation", info.GetCanceledReason()) protorequire.ProtoEqual(t, details, activityResp.GetOutcome().GetFailure().GetCanceledFailureInfo().GetDetails()) + require.Equal(t, identity, activityResp.GetOutcome().GetFailure().GetCanceledFailureInfo().GetIdentity()) }) } @@ -1178,12 +1276,12 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) - for i := 0; i < 2; i++ { + for range 2 { _, err := s.FrontendClient().RequestCancelActivityExecution(ctx, &workflowservice.RequestCancelActivityExecutionRequest{ Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", RequestId: "cancel-request-id", Reason: "Test Cancellation", }) @@ -1218,6 +1316,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { t.Run("DifferentRequestIDFails", func(t *testing.T) { activityID := testcore.RandomizeStr(t.Name()) taskQueue := testcore.RandomizeStr(t.Name()) + identity := "client-that-requested-cancellation" startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) runID := startResp.RunId @@ -1228,7 +1327,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, - Identity: "cancelling-worker", + Identity: identity, RequestId: "cancel-request-id", Reason: "Test Cancellation", }) @@ -1238,7 +1337,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, - Identity: "cancelling-worker", + Identity: identity, RequestId: "different-cancel-request-id", Reason: "Test Cancellation", }) @@ -1311,7 +1410,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", RequestId: s.tv.RequestID(), Reason: "Test Cancellation", }) @@ -1412,7 +1511,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { _, err := s.FrontendClient().RequestCancelActivityExecution(ctx, &workflowservice.RequestCancelActivityExecutionRequest{ Namespace: s.Namespace().String(), Reason: "Test Cancellation", - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", }) var invalidArgErr *serviceerror.InvalidArgument @@ -1425,7 +1524,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { ActivityId: string(make([]byte, defaultMaxIDLengthLimit+1)), // dynamic config default is 1000 Namespace: s.Namespace().String(), Reason: "Test Cancellation", - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", }) var invalidArgErr *serviceerror.InvalidArgument @@ -1440,7 +1539,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { RequestId: string(make([]byte, defaultMaxIDLengthLimit+1)), // dynamic config default is 1000 Namespace: s.Namespace().String(), Reason: "Test Cancellation", - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", }) var invalidArgErr *serviceerror.InvalidArgument @@ -1469,7 +1568,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { RunId: "invalid-run-id", Namespace: s.Namespace().String(), Reason: "Test Cancellation", - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", }) var invalidArgErr *serviceerror.InvalidArgument @@ -1489,7 +1588,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { ActivityId: testcore.RandomizeStr(t.Name()), Namespace: s.Namespace().String(), Reason: string(make([]byte, blobSizeLimitError+1)), - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", }) var invalidArgErr *serviceerror.InvalidArgument @@ -1509,7 +1608,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, - Identity: "cancelling-worker", + Identity: "client-that-requested-cancellation", RequestId: s.tv.RequestID(), Reason: "Test Cancellation", }) @@ -1529,6 +1628,8 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { }) t.Run("StaleToken", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + t.Cleanup(cancel) activityID := testcore.RandomizeStr(t.Name()) taskQueue := testcore.RandomizeStr(t.Name()) _, err := s.startActivity(ctx, activityID, taskQueue) @@ -1555,10 +1656,12 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) }) t.Run("StaleAttemptToken", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + t.Cleanup(cancel) // Start an activity with retries, fail first attempt, then try to complete with old token. // Use NextRetryDelay=1s to ensure the retry dispatch happens within test timeout. activityID := testcore.RandomizeStr(t.Name()) @@ -1623,7 +1726,7 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) // Heartbeat then cancel with the attempt 2 token and should be no error heartbeatResp, err := s.FrontendClient().RecordActivityTaskHeartbeat(ctx, &workflowservice.RecordActivityTaskHeartbeatRequest{ @@ -1642,6 +1745,8 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { }) t.Run("MismatchedTokenNamespace", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + t.Cleanup(cancel) activityID := testcore.RandomizeStr(t.Name()) taskQueue := testcore.RandomizeStr(t.Name()) existingNamespace := s.Namespace().String() @@ -1683,6 +1788,8 @@ func (s *standaloneActivityTestSuite) TestRequestCancel() { // The validation ensures that the namespace in the request matches the namespace in the token's // ComponentRef, preventing cross-namespace token reuse attacks. t.Run("MismatchedTokenComponentRef", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + t.Cleanup(cancel) activityID := testcore.RandomizeStr(t.Name()) taskQueue := testcore.RandomizeStr(t.Name()) existingNamespace := s.Namespace().String() @@ -1774,12 +1881,13 @@ func (s *standaloneActivityTestSuite) TestTerminate() { s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) + identity := "terminator" _, err := s.FrontendClient().TerminateActivityExecution(ctx, &workflowservice.TerminateActivityExecutionRequest{ Namespace: s.Namespace().String(), ActivityId: activityID, RunId: runID, Reason: "Test Termination", - Identity: "terminator", + Identity: identity, }) require.NoError(t, err) @@ -1807,8 +1915,12 @@ func (s *standaloneActivityTestSuite) TestTerminate() { require.Nil(t, info.GetLastFailure()) expectedFailure := &failurepb.Failure{ - Message: "Test Termination", - FailureInfo: &failurepb.Failure_TerminatedFailureInfo{}, + Message: "Test Termination", + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{ + TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{ + Identity: identity, + }, + }, } protorequire.ProtoEqual(t, expectedFailure, activityResp.GetOutcome().GetFailure()) }) @@ -2009,6 +2121,338 @@ func (s *standaloneActivityTestSuite) TestTerminate() { }) } +func (s *standaloneActivityTestSuite) eventuallyTerminated(ctx context.Context, t *testing.T, activityID, runID string) { + t.Helper() + require.Eventually(t, func() bool { + resp, err := s.FrontendClient().DescribeActivityExecution(ctx, &workflowservice.DescribeActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + return err == nil && resp.GetInfo().GetStatus() == enumspb.ACTIVITY_EXECUTION_STATUS_TERMINATED + }, 5*time.Second, 100*time.Millisecond) +} + +func (s *standaloneActivityTestSuite) eventuallyTimedOut(ctx context.Context, t *testing.T, activityID, runID string) { + t.Helper() + require.Eventually(t, func() bool { + resp, err := s.FrontendClient().DescribeActivityExecution(ctx, &workflowservice.DescribeActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + return err == nil && resp.GetInfo().GetStatus() == enumspb.ACTIVITY_EXECUTION_STATUS_TIMED_OUT + }, 10*time.Second, 100*time.Millisecond) +} + +func (s *standaloneActivityTestSuite) eventuallyDeleted(ctx context.Context, t *testing.T, activityID, runID string) { + t.Helper() + require.Eventually(t, func() bool { + _, err := s.FrontendClient().DescribeActivityExecution(ctx, &workflowservice.DescribeActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + var notFoundErr *serviceerror.NotFound + return errors.As(err, ¬FoundErr) + }, 5*time.Second, 100*time.Millisecond) +} + +func (s *standaloneActivityTestSuite) TestDelete() { + t := s.T() + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + + t.Run("DeleteScheduledActivity", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + _, err := s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + }) + + t.Run("DeleteRunningActivity", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) + + _, err := s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + }) + + t.Run("DeleteCompletedActivity", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + pollTaskResp := s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) + _, err := s.FrontendClient().RespondActivityTaskCompleted(ctx, &workflowservice.RespondActivityTaskCompletedRequest{ + Namespace: s.Namespace().String(), + TaskToken: pollTaskResp.TaskToken, + Result: defaultResult, + Identity: defaultIdentity, + }) + require.NoError(t, err) + + _, err = s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + }) + + t.Run("DeleteTerminatedActivity", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + _ = s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) + _, err := s.FrontendClient().TerminateActivityExecution(ctx, &workflowservice.TerminateActivityExecutionRequest{ + Namespace: s.Namespace().String(), + Identity: defaultIdentity, + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + s.eventuallyTerminated(ctx, t, activityID, runID) + + _, err = s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + }) + + t.Run("DeleteCancelRequestedActivity", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + _ = s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) + + _, err := s.FrontendClient().RequestCancelActivityExecution(ctx, &workflowservice.RequestCancelActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + Identity: defaultIdentity, + RequestId: s.tv.RequestID(), + }) + require.NoError(t, err) + + _, err = s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + }) + + t.Run("DeleteFailedActivity", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + pollTaskResp := s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) + _, err := s.FrontendClient().RespondActivityTaskFailed(ctx, &workflowservice.RespondActivityTaskFailedRequest{ + Namespace: s.Namespace().String(), + TaskToken: pollTaskResp.TaskToken, + Failure: defaultFailure, + Identity: defaultIdentity, + }) + require.NoError(t, err) + + _, err = s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + }) + + t.Run("DeleteTimedOutActivity", func(t *testing.T) { + timedOutCtx, timedOutCancel := context.WithTimeout(t.Context(), 30*time.Second) + defer timedOutCancel() + + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp, err := s.FrontendClient().StartActivityExecution(timedOutCtx, &workflowservice.StartActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + ActivityType: s.tv.ActivityType(), + Input: defaultInput, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + }, + StartToCloseTimeout: durationpb.New(1 * time.Second), + RetryPolicy: &commonpb.RetryPolicy{ + MaximumAttempts: 1, + }, + RequestId: s.tv.RequestID(), + }) + require.NoError(t, err) + runID := startResp.RunId + + _, err = s.FrontendClient().PollActivityTaskQueue(timedOutCtx, &workflowservice.PollActivityTaskQueueRequest{ + Namespace: s.Namespace().String(), + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + Identity: s.tv.WorkerIdentity(), + }) + require.NoError(t, err) + + s.eventuallyTimedOut(timedOutCtx, t, activityID, runID) + + _, err = s.FrontendClient().DeleteActivityExecution(timedOutCtx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(timedOutCtx, t, activityID, runID) + }) + + t.Run("DeleteDeletedActivity", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + _, err := s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + + _, err = s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: runID, + }) + var notFoundErr *serviceerror.NotFound + require.ErrorAs(t, err, ¬FoundErr) + require.ErrorContains(t, err, fmt.Sprintf("activity not found for ID: %s", activityID)) + }) + + t.Run("DeleteActivityNoRunID", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp := s.startAndValidateActivity(ctx, t, activityID, taskQueue) + runID := startResp.RunId + + pollTaskResp := s.pollActivityTaskAndValidate(ctx, t, activityID, taskQueue, runID) + _, err := s.FrontendClient().RespondActivityTaskCompleted(ctx, &workflowservice.RespondActivityTaskCompletedRequest{ + Namespace: s.Namespace().String(), + TaskToken: pollTaskResp.TaskToken, + Result: defaultResult, + Identity: defaultIdentity, + }) + require.NoError(t, err) + + _, err = s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + }) + require.NoError(t, err) + + s.eventuallyDeleted(ctx, t, activityID, runID) + }) + + t.Run("DeleteNonExistent", func(t *testing.T) { + activityID := testcore.RandomizeStr(t.Name()) + + _, err := s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + }) + + var notFoundErr *serviceerror.NotFound + require.ErrorAs(t, err, ¬FoundErr) + require.ErrorContains(t, err, fmt.Sprintf("activity not found for ID: %s", activityID)) + }) + + t.Run("RequestValidations", func(t *testing.T) { + t.Run("EmptyActivityID", func(t *testing.T) { + _, err := s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + }) + + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Equal(t, "activity ID is required", invalidArgErr.Message) + }) + + t.Run("ActivityIDTooLong", func(t *testing.T) { + _, err := s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: string(make([]byte, defaultMaxIDLengthLimit+1)), + }) + + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Equal(t, fmt.Sprintf("activity ID exceeds length limit. Length=%d Limit=%d", + defaultMaxIDLengthLimit+1, defaultMaxIDLengthLimit), invalidArgErr.Message) + }) + + t.Run("InvalidRunID", func(t *testing.T) { + _, err := s.FrontendClient().DeleteActivityExecution(ctx, &workflowservice.DeleteActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: testcore.RandomizeStr(t.Name()), + RunId: "invalid-run-id", + }) + + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Equal(t, "invalid run id: must be a valid UUID", invalidArgErr.Message) + }) + }) +} + func (s *standaloneActivityTestSuite) TestRetryWithoutScheduleToCloseTimeout() { t := s.T() ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) @@ -3056,7 +3500,7 @@ func (s *standaloneActivityTestSuite) TestListActivityExecutions() { testActivityType := testcore.RandomizeStr(t.Name()) // Start multiple activities of the same type - for i := 0; i < 2; i++ { + for range 2 { _, err := s.FrontendClient().StartActivityExecution(ctx, &workflowservice.StartActivityExecutionRequest{ Namespace: s.Namespace().String(), ActivityId: testcore.RandomizeStr(t.Name()), @@ -3683,7 +4127,7 @@ func (s *standaloneActivityTestSuite) TestHeartbeat() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) }) t.Run("StaleAttemptToken", func(t *testing.T) { @@ -3751,7 +4195,7 @@ func (s *standaloneActivityTestSuite) TestHeartbeat() { require.Error(t, err) statusErr := serviceerror.ToStatus(err) require.Equal(t, codes.NotFound, statusErr.Code()) - require.Contains(t, statusErr.Message(), "activity task not found") + require.Contains(t, statusErr.Message(), fmt.Sprintf("activity not found for ID: %s", activityID)) }) t.Run("MismatchedNamespaceToken", func(t *testing.T) { @@ -4123,6 +4567,73 @@ func (s *standaloneActivityTestSuite) TestHeartbeat() { protorequire.ProtoEqual(t, defaultResult, pollResp.GetOutcome().GetResult()) }) + t.Run("HeartbeatWithNoTimeoutDoesNotKillActivity", func(t *testing.T) { + // Start activity with no heartbeat timeout, worker accepts, worker + // heartbeats, wait for any spurious timeout task to fire, then + // verify the activity is still running and can be completed. + activityID := testcore.RandomizeStr(t.Name()) + taskQueue := testcore.RandomizeStr(t.Name()) + + startResp, err := s.FrontendClient().StartActivityExecution(ctx, &workflowservice.StartActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + ActivityType: s.tv.ActivityType(), + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue}, + StartToCloseTimeout: durationpb.New(1 * time.Minute), + // No HeartbeatTimeout set. + RetryPolicy: &commonpb.RetryPolicy{ + MaximumAttempts: 1, + }, + }) + require.NoError(t, err) + + pollTaskResp, err := s.FrontendClient().PollActivityTaskQueue(ctx, &workflowservice.PollActivityTaskQueueRequest{ + Namespace: s.Namespace().String(), + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + }) + require.NoError(t, err) + require.NotEmpty(t, pollTaskResp.TaskToken) + + _, err = s.FrontendClient().RecordActivityTaskHeartbeat(ctx, &workflowservice.RecordActivityTaskHeartbeatRequest{ + Namespace: s.Namespace().String(), + TaskToken: pollTaskResp.TaskToken, + Details: heartbeatDetails, + }) + require.NoError(t, err) + + // Wait long enough for a spurious zero-duration timeout task to fire. + time.Sleep(2 * time.Second) //nolint:forbidigo + + // Activity should still be running. + descResp, err := s.FrontendClient().DescribeActivityExecution(ctx, &workflowservice.DescribeActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: startResp.RunId, + }) + require.NoError(t, err) + require.Equal(t, enumspb.ACTIVITY_EXECUTION_STATUS_RUNNING, descResp.GetInfo().GetStatus(), + "activity should still be running but is %s", descResp.GetInfo().GetStatus()) + + // Complete the activity to confirm it's still operable. + _, err = s.FrontendClient().RespondActivityTaskCompleted(ctx, &workflowservice.RespondActivityTaskCompletedRequest{ + Namespace: s.Namespace().String(), + TaskToken: pollTaskResp.TaskToken, + Result: defaultResult, + }) + require.NoError(t, err) + + descResp, err = s.FrontendClient().DescribeActivityExecution(ctx, &workflowservice.DescribeActivityExecutionRequest{ + Namespace: s.Namespace().String(), + ActivityId: activityID, + RunId: startResp.RunId, + IncludeOutcome: true, + }) + require.NoError(t, err) + require.Equal(t, enumspb.ACTIVITY_EXECUTION_STATUS_COMPLETED, descResp.GetInfo().GetStatus(), + "expected status=Completed but is %s", descResp.GetInfo().GetStatus()) + protorequire.ProtoEqual(t, defaultResult, descResp.GetOutcome().GetResult()) + }) + t.Run("RecordHeartbeatByIDStaysAlive", func(t *testing.T) { // Start activity, worker accepts, worker heartbeats within timeout, // more time passes, worker heartbeats again, worker completes. diff --git a/tests/stickytq_test.go b/tests/stickytq_test.go index be4913a4c6d..1842a6d480f 100644 --- a/tests/stickytq_test.go +++ b/tests/stickytq_test.go @@ -133,7 +133,7 @@ func (s *StickyTqTestSuite) TestStickyTimeout_NonTransientWorkflowTask() { // Wait for workflow task timeout stickyTimeout := false WaitForStickyTimeoutLoop: - for i := 0; i < 10; i++ { + for range 10 { events := s.GetHistory(s.Namespace().String(), workflowExecution) for _, event := range events { if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_TIMED_OUT { @@ -193,7 +193,8 @@ WaitForStickyTimeoutLoop: 12 WorkflowExecutionSignaled 13 WorkflowTaskScheduled 14 WorkflowTaskStarted - 15 WorkflowTaskFailed`, events) + 15 WorkflowTaskFailed + 16 WorkflowTaskScheduled`, events) // Complete workflow execution _, err = poller.PollAndProcessWorkflowTask(testcore.WithDumpHistory, testcore.WithRespondSticky, testcore.WithExpectedAttemptCount(3)) @@ -321,7 +322,7 @@ func (s *StickyTqTestSuite) TestStickyTaskqueueResetThenTimeout() { // Wait for workflow task timeout stickyTimeout := false WaitForStickyTimeoutLoop: - for i := 0; i < 10; i++ { + for range 10 { events := s.GetHistory(s.Namespace().String(), workflowExecution) for _, event := range events { if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_TIMED_OUT { @@ -381,7 +382,8 @@ WaitForStickyTimeoutLoop: 12 WorkflowExecutionSignaled 13 WorkflowTaskScheduled 14 WorkflowTaskStarted - 15 WorkflowTaskFailed`, events) + 15 WorkflowTaskFailed + 16 WorkflowTaskScheduled`, events) // Complete workflow execution _, err = poller.PollAndProcessWorkflowTask(testcore.WithDumpHistory, testcore.WithRespondSticky, testcore.WithExpectedAttemptCount(3)) diff --git a/tests/task_queue_stats_test.go b/tests/task_queue_stats_test.go index 36e6aaab792..a2646aeddd3 100644 --- a/tests/task_queue_stats_test.go +++ b/tests/task_queue_stats_test.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" deploymentpb "go.temporal.io/api/deployment/v1" @@ -21,163 +20,151 @@ import ( deploymentspb "go.temporal.io/server/api/deployment/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/worker_versioning" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" ) -type ( - // TaskQueueStatsSuite tests are querying task queue stats. - // - // There are currently three ways to do that: - // 1. DescribeTaskQueue with ReportStats=true - // 2. DescribeTaskQueue with ApiMode=ENHANCED and ReportStats=true [deprecated] - // 3. DescribeWorkerDeploymentVersion with ReportTaskQueueStats=true - // - // Unless a test calls out a specific methods, all three methods are tested in each test case. - TaskQueueStatsSuite struct { - testcore.FunctionalTestBase - usePriMatcher bool - useNewDeploymentData bool - - minPriority int - maxPriority int - defaultPriority int - partitionCount int - } +type taskQueueExpectations struct { + BacklogCount int + MaxExtraTasks int + CachedEnabled bool +} - TaskQueueExpectations struct { - BacklogCount int - MaxExtraTasks int - CachedEnabled bool - } +// taskQueueExpectationsByType maps task queue types to their expectations +type taskQueueExpectationsByType map[enumspb.TaskQueueType]taskQueueExpectations - // TaskQueueExpectationsByType maps task queue types to their expectations - TaskQueueExpectationsByType map[enumspb.TaskQueueType]TaskQueueExpectations -) +type workflowTasksAndActivitiesPollerParams struct { + tqName string + deploymentName string + buildID string + identity string + logPrefix string + activityIDPrefix string + maxToSchedule int + maxConsecEmptyPoll int + versioningBehavior enumspb.VersioningBehavior +} + +// TaskQueueStatsSuite groups task queue stats tests that are run with different matcher configurations. +type TaskQueueStatsSuite struct { + parallelsuite.Suite[*TaskQueueStatsSuite] +} // TODO(pri): remove once the classic matcher is removed func TestTaskQueueStats_Classic_Suite(t *testing.T) { - t.Parallel() - suite.Run(t, &TaskQueueStatsSuite{usePriMatcher: false}) + parallelsuite.Run(t, &TaskQueueStatsSuite{}, false) // usePriMatcher = false } func TestTaskQueueStats_Pri_Suite(t *testing.T) { - t.Parallel() - suite.Run(t, &TaskQueueStatsSuite{usePriMatcher: true}) + parallelsuite.Run(t, &TaskQueueStatsSuite{}, true) // usePriMatcher = true } -func (s *TaskQueueStatsSuite) SetupTest() { - s.minPriority = 1 - s.maxPriority = 5 - s.defaultPriority = 3 - s.partitionCount = 2 // kept low to reduce test time on CI - - s.FunctionalTestBase.SetupTest() - s.OverrideDynamicConfig(dynamicconfig.EnableDeploymentVersions, true) - s.OverrideDynamicConfig(dynamicconfig.FrontendEnableWorkerVersioningWorkflowAPIs, true) - s.OverrideDynamicConfig(dynamicconfig.MatchingUseNewMatcher, s.usePriMatcher) - s.OverrideDynamicConfig(dynamicconfig.MatchingPriorityLevels, s.maxPriority) -} - -func (s *TaskQueueStatsSuite) TestDescribeTaskQueue_NonRoot() { - resp, err := s.FrontendClient().DescribeTaskQueue(context.Background(), &workflowservice.DescribeTaskQueueRequest{ - Namespace: s.Namespace().String(), +func (s *TaskQueueStatsSuite) TestDescribeTaskQueue_NonRoot(usePriMatcher bool) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, testcore.MatchingBehavior{}) + resp, err := env.FrontendClient().DescribeTaskQueue(context.Background(), &workflowservice.DescribeTaskQueueRequest{ + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: "/_sys/foo/1", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, }) s.NoError(err) s.NotNil(resp) - _, err = s.FrontendClient().DescribeTaskQueue(context.Background(), + _, err = env.FrontendClient().DescribeTaskQueue(context.Background(), &workflowservice.DescribeTaskQueueRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: "/_sys/foo/1", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, ReportStats: true, }) s.ErrorContains(err, "DescribeTaskQueue stats are only supported for the root partition") } -func (s *TaskQueueStatsSuite) TestNoTasks_ValidateStats() { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL - - s.publishConsumeWorkflowTasksValidateStats(0, false) -} - -func (s *TaskQueueStatsSuite) TestMultipleTasks_WithMatchingBehavior_ValidateStats() { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL - - s.RunTestWithMatchingBehavior(func() { - s.publishConsumeWorkflowTasksValidateStats(4, false) - }) +func (s *TaskQueueStatsSuite) TestNoTasks_ValidateStats(usePriMatcher bool) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, testcore.MatchingBehavior{}, + testcore.WithDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 2), + testcore.WithDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 2), + testcore.WithDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second), + testcore.WithDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond), + ) + env.publishConsumeWorkflowTasksValidateStats(0, false) } -// NOTE: Cache _eviction_ is already covered by the other tests. -func (s *TaskQueueStatsSuite) TestAddMultipleTasks_ValidateStats_Cached() { - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Hour) // using a long TTL to verify caching - +func (s *TaskQueueStatsSuite) TestAddMultipleTasks_ValidateStats_Cached(usePriMatcher bool) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, testcore.MatchingBehavior{}, + testcore.WithDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second), + testcore.WithDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Hour), + ) tqName := "tq-" + common.GenerateRandomString(5) - s.createDeploymentInTaskQueue(tqName) + env.createDeploymentInTaskQueue(tqName) // Enqueue all workflows. - total := s.enqueueWorkflows(2, tqName) + total := env.enqueueWorkflows(2, tqName) // Verify workflow add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) // Expect at least *one* of the workflow/activity tasks to be in the stats. - expectations := TaskQueueExpectations{ + expectations := taskQueueExpectations{ BacklogCount: 1, // ie at least one task in the backlog MaxExtraTasks: total, // ie at most all tasks can be in the backlog CachedEnabled: true, } // Enqueue 1 activity set, to make sure the workflow backlog has some tasks. - s.enqueueActivitiesForEachWorkflow(1, tqName) + env.enqueueActivitiesForEachWorkflow(1, tqName) // Expect the workflow backlog to be non-empty now. // This query will cache the stats for the remainder of the test. - s.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, expectations, false) + env.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, expectations, false) // Enqueue remaining activities. - s.enqueueActivitiesForEachWorkflow(1, tqName) + env.enqueueActivitiesForEachWorkflow(1, tqName) // Poll 2 activities, ie 1 per version, to make sure the activity backlog has some tasks. - s.pollActivities(2, tqName) + env.pollActivities(2, tqName) // Verify activity dispatch rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, false, true) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, false, true) // Expect the activity backlog to be non-empty now. // This query will cache the stats for the remainder of the test. - s.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, expectations, false) + env.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, expectations, false) // Poll remaining activities. - s.pollActivities(total-2, tqName) + env.pollActivities(total-2, tqName) // Despite having polled all the workflows/activies; the stats won't have changed at all since they were cached. - s.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, expectations, false) - s.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, expectations, false) + env.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, expectations, false) + env.validateTaskQueueStatsByType(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, expectations, false) +} + +// TestVersioningSuite runs version-specific tests across all matching behavior combinations. +// Note: matching behaviors configure partition count based on forwarding behavior. +// Do NOT override MatchingNumTaskqueueReadPartitions/WritePartitions inside the subtest. +func (s *TaskQueueStatsSuite) TestVersioningSuite(usePriMatcher bool) { + for _, behavior := range testcore.AllMatchingBehaviors() { + s.T().Run(behavior.Name()+"Suite", func(t *testing.T) { //nolint:testifylint // nested parallelsuite.Run needs raw *testing.T + parallelsuite.Run(t, &TaskQueueStatsVersionSuite{}, usePriMatcher, behavior) + }) + } } -func (s *TaskQueueStatsSuite) TestCurrentVersionAbsorbsUnversionedBacklog_NoRamping() { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, s.partitionCount) +// TaskQueueStatsVersionSuite groups task queue stats tests that run across matching behavior combinations. +type TaskQueueStatsVersionSuite struct { + parallelsuite.Suite[*TaskQueueStatsVersionSuite] +} - s.RunTestWithMatchingBehavior(func() { - s.currentVersionAbsorbsUnversionedBacklogNoRamping(s.partitionCount) - }) +func (s *TaskQueueStatsVersionSuite) TestMultipleTasks_ValidateStats(usePriMatcher bool, behavior testcore.MatchingBehavior) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, behavior) + env.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) + env.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) + env.publishConsumeWorkflowTasksValidateStats(4, false) } -func (s *TaskQueueStatsSuite) currentVersionAbsorbsUnversionedBacklogNoRamping(numPartitions int) { - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL +func (s *TaskQueueStatsVersionSuite) TestCurrentVersionAbsorbsUnversionedBacklog_NoRamping(usePriMatcher bool, behavior testcore.MatchingBehavior) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, behavior) + env.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) + env.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() @@ -188,21 +175,21 @@ func (s *TaskQueueStatsSuite) currentVersionAbsorbsUnversionedBacklogNoRamping(n // Register this version in the task queue pollerCtx, cancelPoller := context.WithCancel(testcore.NewContext()) - s.createVersionsInTaskQueue(pollerCtx, tqName, deploymentName, currentBuildID) + env.createVersionsInTaskQueue(pollerCtx, tqName, deploymentName, currentBuildID) // Set current version only (no ramping) - s.setCurrentVersion(deploymentName, currentBuildID) + env.setCurrentVersion(deploymentName, currentBuildID) // Stopping the pollers so that we verify the backlog expectations cancelPoller() // Enqueue unversioned backlog - unversionedWorkflowCount := 10 * numPartitions - s.startUnversionedWorkflows(unversionedWorkflowCount, tqName) + unversionedWorkflowCount := 10 * env.partitionCount + env.startUnversionedWorkflows(unversionedWorkflowCount, tqName) // Verify workflow add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) - currentStatsExpectation := TaskQueueExpectations{ + currentStatsExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflowCount, MaxExtraTasks: 0, } @@ -211,7 +198,7 @@ func (s *TaskQueueStatsSuite) currentVersionAbsorbsUnversionedBacklogNoRamping(n a := require.New(c) // DescribeWorkerDeploymentVersion: current version should also show the full backlog for this task queue. - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[workflow]", @@ -220,29 +207,27 @@ func (s *TaskQueueStatsSuite) currentVersionAbsorbsUnversionedBacklogNoRamping(n deploymentName, currentBuildID, currentStatsExpectation, - numPartitions, ) // DescribeTaskQueue Legacy Mode: Since the task queue is part of the current version, the legacy mode should report the total backlog count. - s.requireLegacyTaskQueueStatsRelaxed( + env.requireLegacyTaskQueueStatsRelaxed( ctx, a, "DescribeTaskQueue[legacy]", tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, currentStatsExpectation, - numPartitions, ) }, 10*time.Second, 200*time.Millisecond) // The backlog count for the activity task queue should be equal to the number of activities scheduled since the activity task queue is part of the current version. - activitesToSchedule := 10 * numPartitions - s.completeWorkflowTasksAndScheduleActivities(tqName, deploymentName, currentBuildID, activitesToSchedule) + activitesToSchedule := 10 * env.partitionCount + env.completeWorkflowTasksAndScheduleActivities(tqName, deploymentName, currentBuildID, activitesToSchedule) // Verify activity add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, true, false) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, true, false) - activityStatsExpectation := TaskQueueExpectations{ + activityStatsExpectation := taskQueueExpectations{ BacklogCount: activitesToSchedule, MaxExtraTasks: 0, } @@ -252,7 +237,7 @@ func (s *TaskQueueStatsSuite) currentVersionAbsorbsUnversionedBacklogNoRamping(n // Since the activity task queue is part of the current version, // the DescribeWorkerDeploymentVersion should report the backlog count for the activity task queue. - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[activity][after-scheduling-activities]", @@ -261,187 +246,24 @@ func (s *TaskQueueStatsSuite) currentVersionAbsorbsUnversionedBacklogNoRamping(n deploymentName, currentBuildID, activityStatsExpectation, - numPartitions, ) // DescribeTaskQueue Legacy Mode: Since the activity task queue is part of the current version, the legacy mode should report the total backlog count. - s.requireLegacyTaskQueueStatsRelaxed( + env.requireLegacyTaskQueueStatsRelaxed( ctx, a, "DescribeTaskQueue[legacy][activity]", tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, activityStatsExpectation, - numPartitions, ) }, 10*time.Second, 200*time.Millisecond) } -func (s *TaskQueueStatsSuite) TestRampingAndCurrentAbsorbUnversionedBacklog() { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, s.partitionCount) - - s.RunTestWithMatchingBehavior(func() { - s.rampingAndCurrentAbsorbsUnversionedBacklog(s.partitionCount) - }) -} - -func (s *TaskQueueStatsSuite) TestCurrentAbsorbsUnversionedBacklog_WhenRampingToUnversioned() { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, s.partitionCount) - - s.RunTestWithMatchingBehavior(func() { - s.currentAbsorbsUnversionedBacklogWhenRampingToUnversioned(s.partitionCount) - }) -} - -func (s *TaskQueueStatsSuite) currentAbsorbsUnversionedBacklogWhenRampingToUnversioned(numPartitions int) { - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL - - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - deploymentName := testcore.RandomizeStr("deployment") - tqName := "tq-" + common.GenerateRandomString(5) - currentBuildID := "v1" - - pollCtx, cancelPoll := context.WithCancel(ctx) - s.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, currentBuildID) - cancelPoll() // cancel the pollers so that we can verify the backlog expectations - - // Set current version. - s.setCurrentVersion(deploymentName, currentBuildID) - - rampPercentage := 20 - s.setRampingVersion(deploymentName, "", rampPercentage) - - // Enqueue unversioned backlog. - unversionedWorkflowCount := 10 * numPartitions - s.startUnversionedWorkflows(unversionedWorkflowCount, tqName) - - // Verify workflow add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) - - currentExpectation := TaskQueueExpectations{ - BacklogCount: unversionedWorkflowCount * (100 - rampPercentage) / 100, - MaxExtraTasks: 0, - } - legacyExpectation := TaskQueueExpectations{ - BacklogCount: unversionedWorkflowCount, - MaxExtraTasks: 0, - } - - s.EventuallyWithT(func(c *assert.CollectT) { - a := require.New(c) - - // There is no way right now for a user to query stats of the "unversioned" version. All we can do in this case - // is to query the current version's stats and see that it is attributed 80% of the unversioned backlog. - s.requireWDVTaskQueueStatsRelaxed( - ctx, - a, - "DescribeWorkerDeploymentVersion[current][workflow][ramping-to-unversioned]", - tqName, - enumspb.TASK_QUEUE_TYPE_WORKFLOW, - deploymentName, - currentBuildID, - currentExpectation, - numPartitions, - ) - - // Since the task queue is part of both the current and ramping versions, the legacy mode should report the total backlog count. - s.requireLegacyTaskQueueStatsRelaxed( - ctx, - a, - "DescribeTaskQueue[legacy][workflow][ramping-to-unversioned]", - tqName, - enumspb.TASK_QUEUE_TYPE_WORKFLOW, - legacyExpectation, - numPartitions, - ) - }, 10*time.Second, 200*time.Millisecond) -} - -func (s *TaskQueueStatsSuite) TestRampingAbsorbsUnversionedBacklog_WhenCurrentIsUnversioned() { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, s.partitionCount) - - s.RunTestWithMatchingBehavior(func() { - s.rampingAbsorbsUnversionedBacklogWhenCurrentIsUnversioned(s.partitionCount) - }) -} - -func (s *TaskQueueStatsSuite) rampingAbsorbsUnversionedBacklogWhenCurrentIsUnversioned(numPartitions int) { - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL - - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - deploymentName := testcore.RandomizeStr("deployment") - tqName := "tq-" + common.GenerateRandomString(5) - rampingBuildID := "v2" - - pollCtx, cancelPoll := context.WithCancel(ctx) - s.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, rampingBuildID) - cancelPoll() // cancel the pollers so that we can verify the backlog expectations - - // Set current to unversioned (nil current version). - s.setCurrentVersion(deploymentName, "") - - // Set ramping to a versioned deployment. - rampPercentage := 30 - s.setRampingVersion(deploymentName, rampingBuildID, rampPercentage) - - // Enqueue unversioned backlog. - unversionedWorkflowCount := 10 * numPartitions - s.startUnversionedWorkflows(unversionedWorkflowCount, tqName) - - // Verify workflow add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) - - rampingExpectation := TaskQueueExpectations{ - BacklogCount: unversionedWorkflowCount * rampPercentage / 100, - MaxExtraTasks: 0, - } - legacyExpectation := TaskQueueExpectations{ - BacklogCount: unversionedWorkflowCount, - MaxExtraTasks: 0, - } - - s.EventuallyWithT(func(c *assert.CollectT) { - a := require.New(c) - - // We can't query "unversioned" as a WorkerDeploymentVersion, but we can validate that the ramping version - // is attributed its ramp share of the unversioned backlog. - s.requireWDVTaskQueueStatsRelaxed( - ctx, - a, - "DescribeWorkerDeploymentVersion[ramping][workflow][current-unversioned]", - tqName, - enumspb.TASK_QUEUE_TYPE_WORKFLOW, - deploymentName, - rampingBuildID, - rampingExpectation, - numPartitions, - ) - - // Legacy mode should continue to report the total backlog for the task queue. - s.requireLegacyTaskQueueStatsRelaxed( - ctx, - a, - "DescribeTaskQueue[legacy][workflow][current-unversioned]", - tqName, - enumspb.TASK_QUEUE_TYPE_WORKFLOW, - legacyExpectation, - numPartitions, - ) - }, 10*time.Second, 200*time.Millisecond) -} - -func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPartitions int) { - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL +func (s *TaskQueueStatsVersionSuite) TestRampingAndCurrentAbsorbUnversionedBacklog(usePriMatcher bool, behavior testcore.MatchingBehavior) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, behavior) + env.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) + env.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() @@ -452,33 +274,33 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart rampingBuildID := "v2" pollCtx, cancelPoll := context.WithCancel(ctx) - s.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, currentBuildID) - s.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, rampingBuildID) + env.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, currentBuildID) + env.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, rampingBuildID) cancelPoll() // cancel the pollers so that we can verify the backlog expectations // Set ramping version to 30% rampPercentage := 30 - s.setRampingVersion(deploymentName, rampingBuildID, rampPercentage) + env.setRampingVersion(deploymentName, rampingBuildID, rampPercentage) // Set current version - s.setCurrentVersion(deploymentName, currentBuildID) + env.setCurrentVersion(deploymentName, currentBuildID) // Enqueue unversioned backlog. - unversionedWorkflowCount := 10 * numPartitions - s.startUnversionedWorkflows(unversionedWorkflowCount, tqName) + unversionedWorkflowCount := 10 * env.partitionCount + env.startUnversionedWorkflows(unversionedWorkflowCount, tqName) // Verify workflow add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) - currentExpectation := TaskQueueExpectations{ + currentExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflowCount * (100 - rampPercentage) / 100, MaxExtraTasks: 0, } - rampingExpectation := TaskQueueExpectations{ + rampingExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflowCount * rampPercentage / 100, MaxExtraTasks: 0, } - legacyExpectation := TaskQueueExpectations{ + legacyExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflowCount, MaxExtraTasks: 0, } @@ -491,7 +313,7 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart // DescribeWorkerDeploymentVersion: current version should also show only 70% of the unversioned backlog for this task queue // as a ramping version, with ramp set to 30%, exists and absorbs 30% of the unversioned backlog. - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[current][workflow]", @@ -500,11 +322,10 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart deploymentName, currentBuildID, currentExpectation, - numPartitions, ) // DescribeWorkerDeploymentVersion: ramping version should show the remaining 30% of the unversioned backlog for this task queue - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[ramping][workflow]", @@ -513,23 +334,21 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart deploymentName, rampingBuildID, rampingExpectation, - numPartitions, ) // Since the task queue is part of both the current and ramping versions, the legacy mode should report the total backlog count. - s.requireLegacyTaskQueueStatsRelaxed( + env.requireLegacyTaskQueueStatsRelaxed( ctx, a, "DescribeTaskQueue[legacy][workflow]", tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, legacyExpectation, - numPartitions, ) }, 10*time.Second, 200*time.Millisecond) // Here, since the activity task queue is present both in the current and in the ramping version, the backlog count would differ depending on the version described. // Poll with BOTH buildIDs in parallel to drain all workflow tasks (hash distribution splits them between current and ramping) - s.pollWorkflowTasksAndScheduleActivitiesParallel( + env.pollWorkflowTasksAndScheduleActivitiesParallel( workflowTasksAndActivitiesPollerParams{ tqName: tqName, deploymentName: deploymentName, @@ -555,18 +374,18 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart ) // Verify activity add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, true, false) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, true, false) // It is important to note that the expected values here are theoretical values based on the ramp percentage. In other words, 70% of the unversioned backlog // may not be scheduled on the current version by matching since it makes it's decision based on the workflowID of the workflow. However, when the number of workflows // to schedule is high, the expected value of workflows scheduled on the current version will be close to the theoretical value. Here, we shall just be verifying if // the theoretical statistics that are being reported are correct. - activitiesOnCurrentVersionExpectation := TaskQueueExpectations{ + activitiesOnCurrentVersionExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflowCount * (100 - rampPercentage) / 100, MaxExtraTasks: 0, } - activitiesOnRampingVersionExpectation := TaskQueueExpectations{ + activitiesOnRampingVersionExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflowCount * rampPercentage / 100, MaxExtraTasks: 0, } @@ -575,7 +394,7 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart a := require.New(c) // Validate current version activity stats - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[activity][after-scheduling-activities][current-version]", @@ -584,11 +403,10 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart deploymentName, currentBuildID, activitiesOnCurrentVersionExpectation, - numPartitions, ) // Validate ramping version activity stats - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[activity][after-scheduling-activities][ramping-version]", @@ -597,27 +415,151 @@ func (s *TaskQueueStatsSuite) rampingAndCurrentAbsorbsUnversionedBacklog(numPart deploymentName, rampingBuildID, activitiesOnRampingVersionExpectation, - numPartitions, ) }, 10*time.Second, 200*time.Millisecond) } -func (s *TaskQueueStatsSuite) TestInactiveVersionDoesNotAbsorbUnversionedBacklog() { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, s.partitionCount) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, s.partitionCount) +func (s *TaskQueueStatsVersionSuite) TestCurrentAbsorbsUnversionedBacklog_WhenRampingToUnversioned(usePriMatcher bool, behavior testcore.MatchingBehavior) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, behavior) + env.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) + env.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL - s.RunTestWithMatchingBehavior(func() { - s.inactiveVersionDoesNotAbsorbUnversionedBacklog(s.partitionCount) - }) + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + deploymentName := testcore.RandomizeStr("deployment") + tqName := "tq-" + common.GenerateRandomString(5) + currentBuildID := "v1" + + pollCtx, cancelPoll := context.WithCancel(ctx) + env.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, currentBuildID) + cancelPoll() // cancel the pollers so that we can verify the backlog expectations + + // Set current version. + env.setCurrentVersion(deploymentName, currentBuildID) + + rampPercentage := 20 + env.setRampingVersion(deploymentName, "", rampPercentage) + + // Enqueue unversioned backlog. + unversionedWorkflowCount := 10 * env.partitionCount + env.startUnversionedWorkflows(unversionedWorkflowCount, tqName) + + // Verify workflow add rate + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) + + currentExpectation := taskQueueExpectations{ + BacklogCount: unversionedWorkflowCount * (100 - rampPercentage) / 100, + MaxExtraTasks: 0, + } + legacyExpectation := taskQueueExpectations{ + BacklogCount: unversionedWorkflowCount, + MaxExtraTasks: 0, + } + + s.EventuallyWithT(func(c *assert.CollectT) { + a := require.New(c) + + // There is no way right now for a user to query stats of the "unversioned" version. All we can do in this case + // is to query the current version's stats and see that it is attributed 80% of the unversioned backlog. + env.requireWDVTaskQueueStatsRelaxed( + ctx, + a, + "DescribeWorkerDeploymentVersion[current][workflow][ramping-to-unversioned]", + tqName, + enumspb.TASK_QUEUE_TYPE_WORKFLOW, + deploymentName, + currentBuildID, + currentExpectation, + ) + + // Since the task queue is part of both the current and ramping versions, the legacy mode should report the total backlog count. + env.requireLegacyTaskQueueStatsRelaxed( + ctx, + a, + "DescribeTaskQueue[legacy][workflow][ramping-to-unversioned]", + tqName, + enumspb.TASK_QUEUE_TYPE_WORKFLOW, + legacyExpectation, + ) + }, 10*time.Second, 200*time.Millisecond) +} + +func (s *TaskQueueStatsVersionSuite) TestRampingAbsorbsUnversionedBacklog_WhenCurrentIsUnversioned(usePriMatcher bool, behavior testcore.MatchingBehavior) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, behavior) + env.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) + env.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + deploymentName := testcore.RandomizeStr("deployment") + tqName := "tq-" + common.GenerateRandomString(5) + rampingBuildID := "v2" + + pollCtx, cancelPoll := context.WithCancel(ctx) + env.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, rampingBuildID) + cancelPoll() // cancel the pollers so that we can verify the backlog expectations + + // Set current to unversioned (nil current version). + env.setCurrentVersion(deploymentName, "") + + // Set ramping to a versioned deployment. + rampPercentage := 30 + env.setRampingVersion(deploymentName, rampingBuildID, rampPercentage) + + // Enqueue unversioned backlog. + unversionedWorkflowCount := 10 * env.partitionCount + env.startUnversionedWorkflows(unversionedWorkflowCount, tqName) + + // Verify workflow add rate + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) + + rampingExpectation := taskQueueExpectations{ + BacklogCount: unversionedWorkflowCount * rampPercentage / 100, + MaxExtraTasks: 0, + } + legacyExpectation := taskQueueExpectations{ + BacklogCount: unversionedWorkflowCount, + MaxExtraTasks: 0, + } + + s.EventuallyWithT(func(c *assert.CollectT) { + a := require.New(c) + + // We can't query "unversioned" as a WorkerDeploymentVersion, but we can validate that the ramping version + // is attributed its ramp share of the unversioned backlog. + env.requireWDVTaskQueueStatsRelaxed( + ctx, + a, + "DescribeWorkerDeploymentVersion[ramping][workflow][current-unversioned]", + tqName, + enumspb.TASK_QUEUE_TYPE_WORKFLOW, + deploymentName, + rampingBuildID, + rampingExpectation, + ) + + // Legacy mode should continue to report the total backlog for the task queue. + env.requireLegacyTaskQueueStatsRelaxed( + ctx, + a, + "DescribeTaskQueue[legacy][workflow][current-unversioned]", + tqName, + enumspb.TASK_QUEUE_TYPE_WORKFLOW, + legacyExpectation, + ) + }, 10*time.Second, 200*time.Millisecond) } -func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(numPartitions int) { +func (s *TaskQueueStatsVersionSuite) TestInactiveVersionDoesNotAbsorbUnversionedBacklog(usePriMatcher bool, behavior testcore.MatchingBehavior) { + env := newTaskQueueStatsContext(s.T(), usePriMatcher, behavior) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - s.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL + env.OverrideDynamicConfig(dynamicconfig.MatchingLongPollExpirationInterval, 10*time.Second) + env.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 1*time.Millisecond) // zero means no TTL tqName := "tq-" + common.GenerateRandomString(5) deploymentName := testcore.RandomizeStr("deployment") @@ -626,31 +568,31 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num pollCtx, cancelPoll := context.WithCancel(testcore.NewContext()) - s.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, currentBuildID) - s.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, inactiveBuildID) + env.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, currentBuildID) + env.createVersionsInTaskQueue(pollCtx, tqName, deploymentName, inactiveBuildID) // Set current version - s.setCurrentVersion(deploymentName, currentBuildID) + env.setCurrentVersion(deploymentName, currentBuildID) // Stopping the pollers so that we verify the backlog expectations cancelPoll() // Enqueue unversioned backlog. - unversionedWorkflows := 10 * numPartitions - s.startUnversionedWorkflows(unversionedWorkflows, tqName) + unversionedWorkflows := 10 * env.partitionCount + env.startUnversionedWorkflows(unversionedWorkflows, tqName) // Enqueue pinned workflows. - pinnedWorkflows := 10 * numPartitions - s.startPinnedWorkflows(pinnedWorkflows, tqName, deploymentName, inactiveBuildID) + pinnedWorkflows := 10 * env.partitionCount + env.startPinnedWorkflows(pinnedWorkflows, tqName, deploymentName, inactiveBuildID) // Verify workflow add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, false) - currentExpectation := TaskQueueExpectations{ + currentExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflows, MaxExtraTasks: 0, } - inactiveExpectation := TaskQueueExpectations{ + inactiveExpectation := taskQueueExpectations{ BacklogCount: pinnedWorkflows, MaxExtraTasks: 0, } @@ -662,7 +604,7 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num a := require.New(c) // DescribeWorkerDeploymentVersion: current version should should show 100% of the unversioned backlog for this task queue - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[current][workflow]", @@ -671,11 +613,10 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num deploymentName, currentBuildID, currentExpectation, - numPartitions, ) // DescribeWorkerDeploymentVersion: inactive version should only show the pinned workflows that are scheduled on it. - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[inactive][workflow]", @@ -684,12 +625,11 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num deploymentName, inactiveBuildID, inactiveExpectation, - numPartitions, ) }, 10*time.Second, 200*time.Millisecond) // Polling the workflow tasks and scheduling activities - s.pollWorkflowTasksAndScheduleActivitiesParallel( + env.pollWorkflowTasksAndScheduleActivitiesParallel( workflowTasksAndActivitiesPollerParams{ tqName: tqName, deploymentName: deploymentName, @@ -715,19 +655,19 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num ) // Verify workflow dispatch rate and activity add rate - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, true) - s.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, true, false) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW, true, true) + env.validateRates(tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY, true, false) // Validate activity backlogs - currentActivityExpectation := TaskQueueExpectations{ + currentActivityExpectation := taskQueueExpectations{ BacklogCount: unversionedWorkflows, MaxExtraTasks: 0, } - inactiveActivityExpectation := TaskQueueExpectations{ + inactiveActivityExpectation := taskQueueExpectations{ BacklogCount: pinnedWorkflows, MaxExtraTasks: 0, } - workflowTaskQueueEmptyExpectation := TaskQueueExpectations{ + workflowTaskQueueEmptyExpectation := taskQueueExpectations{ BacklogCount: 0, MaxExtraTasks: 0, } @@ -736,7 +676,7 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num a := require.New(c) // The activity task queue of the current version should have the backlog count for the activities that were scheduled - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[current][activity]", @@ -745,11 +685,10 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num deploymentName, currentBuildID, currentActivityExpectation, - numPartitions, ) // The workflow task queue of the current version should be empty since activities were scheduled - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[current][workflow]", @@ -758,11 +697,10 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num deploymentName, currentBuildID, workflowTaskQueueEmptyExpectation, - numPartitions, ) // The workflow task queue of the inactive version should be empty since activities were scheduled - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[inactive][workflow]", @@ -771,11 +709,10 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num deploymentName, inactiveBuildID, workflowTaskQueueEmptyExpectation, - numPartitions, ) // The activity task queue of the inactive version should have the backlog count for the activities that were scheduled - s.requireWDVTaskQueueStatsRelaxed( + env.requireWDVTaskQueueStatsRelaxed( ctx, a, "DescribeWorkerDeploymentVersion[inactive][activity]", @@ -784,15 +721,50 @@ func (s *TaskQueueStatsSuite) inactiveVersionDoesNotAbsorbUnversionedBacklog(num deploymentName, inactiveBuildID, inactiveActivityExpectation, - numPartitions, ) }, 10*time.Second, 200*time.Millisecond) } +// taskQueueStatsContext holds the per-test environment and configuration for task queue stats tests. +type taskQueueStatsContext struct { + testcore.Env + usePriMatcher bool + minPriority int + maxPriority int + defaultPriority int + partitionCount int +} + +func newTaskQueueStatsContext( + t *testing.T, + usePriMatcher bool, + behavior testcore.MatchingBehavior, + extraOpts ...testcore.TestOption, +) *taskQueueStatsContext { + opts := []testcore.TestOption{ + testcore.WithDynamicConfig(dynamicconfig.EnableDeploymentVersions, true), + testcore.WithDynamicConfig(dynamicconfig.FrontendEnableWorkerVersioningWorkflowAPIs, true), + testcore.WithDynamicConfig(dynamicconfig.MatchingUseNewMatcher, usePriMatcher), + testcore.WithDynamicConfig(dynamicconfig.MatchingPriorityLevels, 5), // maxPriority + } + opts = append(opts, behavior.Options()...) + opts = append(opts, extraOpts...) + env := testcore.NewEnv(t, opts...) + behavior.InjectHooks(env) + return &taskQueueStatsContext{ + Env: env, + usePriMatcher: usePriMatcher, + minPriority: 1, + maxPriority: 5, + defaultPriority: 3, + partitionCount: 2, // kept low to reduce test time on CI + } +} + // requireWDVTaskQueueStatsRelaxed asserts task queue statistics by allowing for over-counting in multi-partition ramping scenarios. // The production code intentionally uses math.Ceil for both ramping and current percentage // calculations across partitions, which can result in slight over-counting. -func (s *TaskQueueStatsSuite) requireWDVTaskQueueStatsRelaxed( +func (s *taskQueueStatsContext) requireWDVTaskQueueStatsRelaxed( ctx context.Context, a *require.Assertions, label string, @@ -800,8 +772,7 @@ func (s *TaskQueueStatsSuite) requireWDVTaskQueueStatsRelaxed( tqType enumspb.TaskQueueType, deploymentName string, buildID string, - expectation TaskQueueExpectations, - numPartitions int, + expectation taskQueueExpectations, ) { stats, found, err := s.describeWDVTaskQueueStats(ctx, tqName, tqType, deploymentName, buildID) a.NoError(err) @@ -810,21 +781,20 @@ func (s *TaskQueueStatsSuite) requireWDVTaskQueueStatsRelaxed( // Use the existing validateTaskQueueStats with MaxExtraTasks set to numPartitions // to account for ceiling operations across partitions - expectation.MaxExtraTasks = numPartitions + expectation.MaxExtraTasks = s.partitionCount validateTaskQueueStats(label, a, stats, expectation) } // requireLegacyTaskQueueStatsRelaxed asserts task queue statistics by allowing for over-counting in multi-partition scenarios. // The production code intentionally uses math.Ceil for both ramping and current percentage calculations across partitions, // which can result in slight over-counting. -func (s *TaskQueueStatsSuite) requireLegacyTaskQueueStatsRelaxed( +func (s *taskQueueStatsContext) requireLegacyTaskQueueStatsRelaxed( ctx context.Context, a *require.Assertions, label string, tqName string, tqType enumspb.TaskQueueType, - expectation TaskQueueExpectations, - numPartitions int, + expectation taskQueueExpectations, ) { stats, found, err := s.describeLegacyTaskQueueStats(ctx, tqName, tqType) a.NoError(err) @@ -833,17 +803,17 @@ func (s *TaskQueueStatsSuite) requireLegacyTaskQueueStatsRelaxed( // Use the existing validateTaskQueueStats with MaxExtraTasks set to numPartitions // to account for ceiling operations across partitions - expectation.MaxExtraTasks = numPartitions + expectation.MaxExtraTasks = s.partitionCount validateTaskQueueStats(label, a, stats, expectation) } // Publishes versioned and unversioned entities; with one entity per priority (plus default priority). Multiplied by `sets`. -func (s *TaskQueueStatsSuite) publishConsumeWorkflowTasksValidateStats(sets int, singlePartition bool) { +func (s *taskQueueStatsContext) publishConsumeWorkflowTasksValidateStats(sets int, singlePartition bool) { tqName := "tq-" + common.GenerateRandomString(5) s.createDeploymentInTaskQueue(tqName) // verify both workflow and activity backlogs are empty - expectations := TaskQueueExpectationsByType{ + expectations := taskQueueExpectationsByType{ enumspb.TASK_QUEUE_TYPE_WORKFLOW: { BacklogCount: 0, MaxExtraTasks: 0, @@ -872,7 +842,7 @@ func (s *TaskQueueStatsSuite) publishConsumeWorkflowTasksValidateStats(sets int, } // verify workflow backlog is not empty, activity backlog is empty - expectations[enumspb.TASK_QUEUE_TYPE_WORKFLOW] = TaskQueueExpectations{ + expectations[enumspb.TASK_QUEUE_TYPE_WORKFLOW] = taskQueueExpectations{ BacklogCount: total, MaxExtraTasks: maxExtraTasksAllowed, } @@ -881,7 +851,7 @@ func (s *TaskQueueStatsSuite) publishConsumeWorkflowTasksValidateStats(sets int, // poll all workflow tasks and enqueue one activity task for each workflow totalAct := s.enqueueActivitiesForEachWorkflow(sets, tqName) - s.EqualValues(total, totalAct, "should have enqueued the same number of activities as workflows") + require.Equal(s.T(), total, totalAct, "should have enqueued the same number of activities as workflows") // verify workflow dispatch rate and activity add rate if sets > 0 { @@ -890,11 +860,11 @@ func (s *TaskQueueStatsSuite) publishConsumeWorkflowTasksValidateStats(sets int, } // verify workflow backlog is empty, activity backlog is not - expectations[enumspb.TASK_QUEUE_TYPE_WORKFLOW] = TaskQueueExpectations{ + expectations[enumspb.TASK_QUEUE_TYPE_WORKFLOW] = taskQueueExpectations{ BacklogCount: 0, MaxExtraTasks: maxExtraTasksAllowed, } - expectations[enumspb.TASK_QUEUE_TYPE_ACTIVITY] = TaskQueueExpectations{ + expectations[enumspb.TASK_QUEUE_TYPE_ACTIVITY] = taskQueueExpectations{ BacklogCount: total, MaxExtraTasks: maxExtraTasksAllowed, } @@ -910,11 +880,11 @@ func (s *TaskQueueStatsSuite) publishConsumeWorkflowTasksValidateStats(sets int, } // verify both workflow and activity backlogs are empty - expectations[enumspb.TASK_QUEUE_TYPE_WORKFLOW] = TaskQueueExpectations{ + expectations[enumspb.TASK_QUEUE_TYPE_WORKFLOW] = taskQueueExpectations{ BacklogCount: 0, MaxExtraTasks: maxExtraTasksAllowed, } - expectations[enumspb.TASK_QUEUE_TYPE_ACTIVITY] = TaskQueueExpectations{ + expectations[enumspb.TASK_QUEUE_TYPE_ACTIVITY] = taskQueueExpectations{ BacklogCount: 0, MaxExtraTasks: maxExtraTasksAllowed, } @@ -922,7 +892,7 @@ func (s *TaskQueueStatsSuite) publishConsumeWorkflowTasksValidateStats(sets int, s.validateAllTaskQueueStats(tqName, expectations, singlePartition) } -func (s *TaskQueueStatsSuite) startUnversionedWorkflows(count int, tqName string) { +func (s *taskQueueStatsContext) startUnversionedWorkflows(count int, tqName string) { wt := "functional-workflow-current-absorbs-unversioned" workflowType := &commonpb.WorkflowType{Name: wt} request := &workflowservice.StartWorkflowExecutionRequest{ @@ -938,11 +908,11 @@ func (s *TaskQueueStatsSuite) startUnversionedWorkflows(count int, tqName string for range count { request.WorkflowId = uuid.NewString() // starting "count" different Unversioned workflows. _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err) + require.NoError(s.T(), err) } } -func (s *TaskQueueStatsSuite) startPinnedWorkflows(count int, tqName string, deploymentName string, buildID string) { +func (s *taskQueueStatsContext) startPinnedWorkflows(count int, tqName string, deploymentName string, buildID string) { wt := "functional-workflow-pinned" workflowType := &commonpb.WorkflowType{Name: wt} @@ -969,29 +939,15 @@ func (s *TaskQueueStatsSuite) startPinnedWorkflows(count int, tqName string, dep for range count { request.WorkflowId = uuid.NewString() // starting "n" different Pinned workflows. _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err) + require.NoError(s.T(), err) } } -type workflowTasksAndActivitiesPollerParams struct { - tqName string - deploymentName string - buildID string - identity string - logPrefix string - activityIDPrefix string - maxToSchedule int - maxConsecEmptyPoll int - versioningBehavior enumspb.VersioningBehavior -} - -// pollWorkflowTasksAndScheduleActivitiesParallel polls workflow tasks and schedules activities in parallel for workers of two different buildID's. -func (s *TaskQueueStatsSuite) pollWorkflowTasksAndScheduleActivitiesParallel(params ...workflowTasksAndActivitiesPollerParams) { +func (s *taskQueueStatsContext) pollWorkflowTasksAndScheduleActivitiesParallel(params ...workflowTasksAndActivitiesPollerParams) { var wg sync.WaitGroup errCh := make(chan error, len(params)) for _, p := range params { - p := p wg.Go(func() { _, err := s.pollWorkflowTasksAndScheduleActivities(p) errCh <- err @@ -1001,12 +957,12 @@ func (s *TaskQueueStatsSuite) pollWorkflowTasksAndScheduleActivitiesParallel(par wg.Wait() close(errCh) for err := range errCh { - s.NoError(err) + require.NoError(s.T(), err) } } -func (s *TaskQueueStatsSuite) pollWorkflowTasksAndScheduleActivities(params workflowTasksAndActivitiesPollerParams) (int, error) { - deploymentOpts := s.createDeploymentOptions(params.deploymentName, params.buildID) +func (s *taskQueueStatsContext) pollWorkflowTasksAndScheduleActivities(params workflowTasksAndActivitiesPollerParams) (int, error) { + deploymentOpts := createDeploymentOptions(params.deploymentName, params.buildID) scheduled := 0 emptyPollCount := 0 @@ -1064,13 +1020,13 @@ func (s *TaskQueueStatsSuite) pollWorkflowTasksAndScheduleActivities(params work return scheduled, nil } -func (s *TaskQueueStatsSuite) completeWorkflowTasksAndScheduleActivities( +func (s *taskQueueStatsContext) completeWorkflowTasksAndScheduleActivities( tqName string, deploymentName string, buildID string, activityCount int, ) { - deploymentOpts := s.createDeploymentOptions(deploymentName, buildID) + deploymentOpts := createDeploymentOptions(deploymentName, buildID) for i := 0; i < activityCount; { resp, err := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ @@ -1079,7 +1035,7 @@ func (s *TaskQueueStatsSuite) completeWorkflowTasksAndScheduleActivities( Identity: "current-version-worker", DeploymentOptions: deploymentOpts, }) - s.NoError(err) + require.NoError(s.T(), err) if resp == nil || resp.GetAttempt() < 1 { fmt.Println("Empty poll! Continuing...") continue @@ -1109,13 +1065,13 @@ func (s *TaskQueueStatsSuite) completeWorkflowTasksAndScheduleActivities( } _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(), respondReq) - s.NoError(err) + require.NoError(s.T(), err) i++ } } // TODO (Shivam): We may have to wait for the propagation status to show completed if we are using async workflows here. -func (s *TaskQueueStatsSuite) setCurrentVersion(deploymentName, buildID string) { +func (s *taskQueueStatsContext) setCurrentVersion(deploymentName, buildID string) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -1124,11 +1080,11 @@ func (s *TaskQueueStatsSuite) setCurrentVersion(deploymentName, buildID string) DeploymentName: deploymentName, BuildId: buildID, }) - s.NoError(err) + require.NoError(s.T(), err) } // TODO (Shivam): We may have to wait for the propagation status to show completed if we are using async workflows here. -func (s *TaskQueueStatsSuite) setRampingVersion(deploymentName, buildID string, rampPercentage int) { +func (s *taskQueueStatsContext) setRampingVersion(deploymentName, buildID string, rampPercentage int) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -1138,10 +1094,10 @@ func (s *TaskQueueStatsSuite) setRampingVersion(deploymentName, buildID string, BuildId: buildID, Percentage: float32(rampPercentage), }) - s.NoError(err) + require.NoError(s.T(), err) } -func (s *TaskQueueStatsSuite) describeWDVTaskQueueStats( +func (s *taskQueueStatsContext) describeWDVTaskQueueStats( ctx context.Context, tqName string, tqType enumspb.TaskQueueType, @@ -1169,7 +1125,7 @@ func (s *TaskQueueStatsSuite) describeWDVTaskQueueStats( // DescribeTaskQueue Legacy Mode shall report the stats for this task queue from all the different versions // that the task queue is part of. -func (s *TaskQueueStatsSuite) describeLegacyTaskQueueStats( +func (s *taskQueueStatsContext) describeLegacyTaskQueueStats( ctx context.Context, tqName string, tqType enumspb.TaskQueueType, @@ -1185,7 +1141,8 @@ func (s *TaskQueueStatsSuite) describeLegacyTaskQueueStats( } return resp.GetStats(), true, nil } -func (s *TaskQueueStatsSuite) enqueueWorkflows(sets int, tqName string) int { + +func (s *taskQueueStatsContext) enqueueWorkflows(sets int, tqName string) int { deploymentOpts := s.deploymentOptions(tqName) var total int @@ -1223,7 +1180,7 @@ func (s *TaskQueueStatsSuite) enqueueWorkflows(sets int, tqName string) int { } _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err) + require.NoError(s.T(), err) total++ } @@ -1234,13 +1191,13 @@ func (s *TaskQueueStatsSuite) enqueueWorkflows(sets int, tqName string) int { return total } -func (s *TaskQueueStatsSuite) createVersionsInTaskQueue(ctx context.Context, tqName string, deploymentName string, buildID string) { +func (s *taskQueueStatsContext) createVersionsInTaskQueue(ctx context.Context, tqName string, deploymentName string, buildID string) { go func() { _, _ = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: "random", - DeploymentOptions: s.createDeploymentOptions(deploymentName, buildID), + DeploymentOptions: createDeploymentOptions(deploymentName, buildID), }) }() @@ -1249,12 +1206,12 @@ func (s *TaskQueueStatsSuite) createVersionsInTaskQueue(ctx context.Context, tqN Namespace: s.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: "random", - DeploymentOptions: s.createDeploymentOptions(deploymentName, buildID), + DeploymentOptions: createDeploymentOptions(deploymentName, buildID), }) }() // Wait for the version to be created. - s.EventuallyWithT(func(c *assert.CollectT) { + require.EventuallyWithT(s.T(), func(c *assert.CollectT) { a := require.New(c) resp, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, &workflowservice.DescribeWorkerDeploymentVersionRequest{ Namespace: s.Namespace().String(), @@ -1269,39 +1226,36 @@ func (s *TaskQueueStatsSuite) createVersionsInTaskQueue(ctx context.Context, tqN } // TODO (Shivam): Remove this guy. -func (s *TaskQueueStatsSuite) createDeploymentInTaskQueue(tqName string) { +func (s *taskQueueStatsContext) createDeploymentInTaskQueue(tqName string) { // Using old DeploymentData format var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() + wg.Go(func() { _, _ = s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: "random", DeploymentOptions: s.deploymentOptions(tqName), }) - }() + }) - go func() { - defer wg.Done() + wg.Go(func() { _, _ = s.FrontendClient().PollActivityTaskQueue(testcore.NewContext(), &workflowservice.PollActivityTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: "random", DeploymentOptions: s.deploymentOptions(tqName), }) - }() + }) wg.Wait() } -func (s *TaskQueueStatsSuite) enqueueActivitiesForEachWorkflow(sets int, tqName string) int { +func (s *taskQueueStatsContext) enqueueActivitiesForEachWorkflow(sets int, tqName string) int { deploymentOpts := s.deploymentOptions(tqName) var total int - for version := 0; version < 2; version++ { // 0=unversioned, 1=versioned + for version := range 2 { // 0=unversioned, 1=versioned for priority := 0; priority <= s.maxPriority; priority++ { for i := 0; i < sets; { // not counting up here to allow for retries pollReq := &workflowservice.PollWorkflowTaskQueueRequest{ @@ -1313,7 +1267,7 @@ func (s *TaskQueueStatsSuite) enqueueActivitiesForEachWorkflow(sets int, tqName } resp, err := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), pollReq) - s.NoError(err) + require.NoError(s.T(), err) if resp == nil || resp.GetAttempt() < 1 { continue } @@ -1344,7 +1298,7 @@ func (s *TaskQueueStatsSuite) enqueueActivitiesForEachWorkflow(sets int, tqName } _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(), respondReq) - s.NoError(err) + require.NoError(s.T(), err) i++ total++ @@ -1355,7 +1309,7 @@ func (s *TaskQueueStatsSuite) enqueueActivitiesForEachWorkflow(sets int, tqName return total } -func (s *TaskQueueStatsSuite) pollActivities(count int, tqName string) { +func (s *taskQueueStatsContext) pollActivities(count int, tqName string) { for i := 0; i < count; { pollReq := &workflowservice.PollActivityTaskQueueRequest{ Namespace: s.Namespace().String(), @@ -1371,7 +1325,7 @@ func (s *TaskQueueStatsSuite) pollActivities(count int, tqName string) { resp, err := s.FrontendClient().PollActivityTaskQueue( testcore.NewContext(), pollReq, ) - s.NoError(err) + require.NoError(s.T(), err) if resp == nil || resp.GetAttempt() < 1 { continue // poll again on empty responses } @@ -1380,9 +1334,9 @@ func (s *TaskQueueStatsSuite) pollActivities(count int, tqName string) { s.T().Logf("Polled %d activities", count) } -func (s *TaskQueueStatsSuite) validateAllTaskQueueStats( +func (s *taskQueueStatsContext) validateAllTaskQueueStats( tqName string, - expectations TaskQueueExpectationsByType, + expectations taskQueueExpectationsByType, singlePartition bool, ) { for tqType, expectation := range expectations { @@ -1393,7 +1347,7 @@ func (s *TaskQueueStatsSuite) validateAllTaskQueueStats( // validateRates verifies TasksAddRate and/or TasksDispatchRate in a dedicated EventuallyWithT block. // This should be called immediately after the relevant operation (enqueue for add rate, poll for dispatch rate) // to ensure the rate is checked while still fresh (before the 30-second sliding window decays). -func (s *TaskQueueStatsSuite) validateRates( +func (s *taskQueueStatsContext) validateRates( tqName string, tqType enumspb.TaskQueueType, expectAddRate bool, @@ -1409,7 +1363,7 @@ func (s *TaskQueueStatsSuite) validateRates( ReportStats: true, } - s.EventuallyWithT(func(c *assert.CollectT) { + require.EventuallyWithT(s.T(), func(c *assert.CollectT) { a := require.New(c) label := "validateRates[" + tqType.String() + "]" @@ -1429,10 +1383,10 @@ func (s *TaskQueueStatsSuite) validateRates( }, 5*time.Second, 100*time.Millisecond) } -func (s *TaskQueueStatsSuite) validateTaskQueueStatsByType( +func (s *taskQueueStatsContext) validateTaskQueueStatsByType( tqName string, tqType enumspb.TaskQueueType, - expectation TaskQueueExpectations, + expectation taskQueueExpectations, singlePartition bool, ) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -1450,11 +1404,11 @@ func (s *TaskQueueStatsSuite) validateTaskQueueStatsByType( s.validateDescribeWorkerDeploymentVersion(ctx, tqName, tqType, halfExpectation) } -func (s *TaskQueueStatsSuite) validateDescribeTaskQueueWithDefaultMode( +func (s *taskQueueStatsContext) validateDescribeTaskQueueWithDefaultMode( ctx context.Context, tqName string, tqType enumspb.TaskQueueType, - expectation TaskQueueExpectations, + expectation taskQueueExpectations, singlePartition bool, ) { req := &workflowservice.DescribeTaskQueueRequest{ @@ -1465,13 +1419,13 @@ func (s *TaskQueueStatsSuite) validateDescribeTaskQueueWithDefaultMode( // test stats are not reported by default (and therefore also not cached) resp, err := s.FrontendClient().DescribeTaskQueue(ctx, req) - s.NoError(err) - s.NotNil(resp) - s.Nil(resp.Stats, "stats should not be reported by default") + require.NoError(s.T(), err) + require.NotNil(s.T(), resp) + require.Nil(s.T(), resp.Stats, "stats should not be reported by default") //nolint:staticcheck // SA1019 deprecated - s.Nil(resp.TaskQueueStatus, "status should not be reported by default") + require.Nil(s.T(), resp.TaskQueueStatus, "status should not be reported by default") - s.EventuallyWithT(func(c *assert.CollectT) { + require.EventuallyWithT(s.T(), func(c *assert.CollectT) { a := require.New(c) label := "DescribeTaskQueue_DefaultMode[" + tqType.String() + "]" @@ -1497,11 +1451,11 @@ func (s *TaskQueueStatsSuite) validateDescribeTaskQueueWithDefaultMode( }, 5*time.Second, 100*time.Millisecond) } -func (s *TaskQueueStatsSuite) validateDescribeTaskQueueWithEnhancedMode( +func (s *taskQueueStatsContext) validateDescribeTaskQueueWithEnhancedMode( ctx context.Context, tqName string, tqType enumspb.TaskQueueType, - expectation TaskQueueExpectations, + expectation taskQueueExpectations, ) { deploymentOpts := s.deploymentOptions(tqName) req := &workflowservice.DescribeTaskQueueRequest{ @@ -1522,14 +1476,14 @@ func (s *TaskQueueStatsSuite) validateDescribeTaskQueueWithEnhancedMode( if !expectation.CachedEnabled { // skip if testing caching; as this would pin the result to the cache resp, err := s.FrontendClient().DescribeTaskQueue(ctx, req) - s.NoError(err) - s.NotNil(resp) - s.Nil(resp.Stats, "stats should not be reported by default") + require.NoError(s.T(), err) + require.NotNil(s.T(), resp) + require.Nil(s.T(), resp.Stats, "stats should not be reported by default") //nolint:staticcheck // SA1019 deprecated - s.Nil(resp.TaskQueueStatus, "status should not be reported") + require.Nil(s.T(), resp.TaskQueueStatus, "status should not be reported") } - s.EventuallyWithT(func(c *assert.CollectT) { + require.EventuallyWithT(s.T(), func(c *assert.CollectT) { a := require.New(c) req.ReportStats = true @@ -1538,24 +1492,31 @@ func (s *TaskQueueStatsSuite) validateDescribeTaskQueueWithEnhancedMode( a.NotNil(resp) //nolint:staticcheck // SA1019 deprecated - a.Equal(2, len(resp.GetVersionsInfo()), "should be 2: 1 default/unversioned + 1 versioned") + a.Len(resp.GetVersionsInfo(), 2, "should be 2: 1 default/unversioned + 1 versioned") //nolint:staticcheck // SA1019 deprecated for _, v := range resp.GetVersionsInfo() { a.Equal(enumspb.BUILD_ID_TASK_REACHABILITY_UNSPECIFIED, v.GetTaskReachability()) info := v.GetTypesInfo()[int32(tqType)] a.NotNil(info, "should have info for task queue type %s", tqType) + if info == nil { + return + } + a.NotNil(info.Stats, "should have stats for task queue type %s", tqType) + if info.Stats == nil { + return + } validateTaskQueueStats("DescribeTaskQueue_EnhancedMode["+tqType.String()+"]", a, info.Stats, expectation) } }, 5*time.Second, 100*time.Millisecond) } -func (s *TaskQueueStatsSuite) validateDescribeWorkerDeploymentVersion( +func (s *taskQueueStatsContext) validateDescribeWorkerDeploymentVersion( ctx context.Context, tqName string, tqType enumspb.TaskQueueType, - expectation TaskQueueExpectations, + expectation taskQueueExpectations, ) { deploymentOpts := s.deploymentOptions(tqName) req := &workflowservice.DescribeWorkerDeploymentVersionRequest{ @@ -1568,18 +1529,18 @@ func (s *TaskQueueStatsSuite) validateDescribeWorkerDeploymentVersion( // test stats are not reported by default (and therefore also not cached) resp, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, req) - s.NoError(err) - s.NotNil(resp) + require.NoError(s.T(), err) + require.NotNil(s.T(), resp) for _, info := range resp.VersionTaskQueues { - s.Nil(info.Stats, "stats should not be reported by default") + require.Nil(s.T(), info.Stats, "stats should not be reported by default") } - s.EventuallyWithT(func(c *assert.CollectT) { + require.EventuallyWithT(s.T(), func(c *assert.CollectT) { a := require.New(c) req.ReportTaskQueueStats = true resp, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, req) - s.NoError(err) + a.NoError(err) a.Len(resp.VersionTaskQueues, 2, "should be 1 task queue for Workflows and 1 for Activities") for _, info := range resp.VersionTaskQueues { @@ -1597,11 +1558,11 @@ func (s *TaskQueueStatsSuite) validateDescribeWorkerDeploymentVersion( }, 5*time.Second, 100*time.Millisecond) } -func (s *TaskQueueStatsSuite) validateTaskQueueStatsByPriority( +func (s *taskQueueStatsContext) validateTaskQueueStatsByPriority( label string, a *require.Assertions, stats map[int32]*taskqueuepb.TaskQueueStats, - taskQueueExpectation TaskQueueExpectations, + taskQueueExpectation taskQueueExpectations, ) { a.Len(stats, s.maxPriority, "%s: stats should contain %d priorities", label, s.maxPriority) @@ -1632,11 +1593,20 @@ func (s *TaskQueueStatsSuite) validateTaskQueueStatsByPriority( label, taskQueueExpectation.BacklogCount, accBacklogCount) } +// TODO: Remove this once older stats tests are refactored to use the createDeploymentOptions function. +func (s *taskQueueStatsContext) deploymentOptions(tqName string) *deploymentpb.WorkerDeploymentOptions { + return &deploymentpb.WorkerDeploymentOptions{ + DeploymentName: tqName + "-deployment", + BuildId: "build-id", + WorkerVersioningMode: enumspb.WORKER_VERSIONING_MODE_VERSIONED, + } +} + func validateTaskQueueStatsStrict( label string, a *require.Assertions, stats *taskqueuepb.TaskQueueStats, - expectation TaskQueueExpectations, + expectation taskQueueExpectations, ) { a.Equal(int64(expectation.BacklogCount), stats.ApproximateBacklogCount, "%s: ApproximateBacklogCount should be %d, got %d", @@ -1651,7 +1621,7 @@ func validateTaskQueueStats( label string, a *require.Assertions, stats *taskqueuepb.TaskQueueStats, - expectation TaskQueueExpectations, + expectation taskQueueExpectations, ) { // Actual counter can be greater than the expected due to history retries. We make sure the counter is in // range [expected, expected+maxBacklogExtraTasks] @@ -1669,16 +1639,7 @@ func validateTaskQueueStats( label, stats.ApproximateBacklogAge.AsDuration()) } -// TODO: Remove this once older stats tests are refactored to use the createDeploymentOptions function. -func (s *TaskQueueStatsSuite) deploymentOptions(tqName string) *deploymentpb.WorkerDeploymentOptions { - return &deploymentpb.WorkerDeploymentOptions{ - DeploymentName: tqName + "-deployment", - BuildId: "build-id", - WorkerVersioningMode: enumspb.WORKER_VERSIONING_MODE_VERSIONED, - } -} - -func (s *TaskQueueStatsSuite) createDeploymentOptions(deploymentName string, buildID string) *deploymentpb.WorkerDeploymentOptions { +func createDeploymentOptions(deploymentName string, buildID string) *deploymentpb.WorkerDeploymentOptions { return &deploymentpb.WorkerDeploymentOptions{ DeploymentName: deploymentName, BuildId: buildID, diff --git a/tests/task_queue_test.go b/tests/task_queue_test.go index 391885c6a88..628d2f4f83b 100644 --- a/tests/task_queue_test.go +++ b/tests/task_queue_test.go @@ -8,11 +8,13 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" + nexuspb "go.temporal.io/api/nexus/v1" + "go.temporal.io/api/operatorservice/v1" + querypb "go.temporal.io/api/query/v1" taskqueuepb "go.temporal.io/api/taskqueue/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" @@ -20,11 +22,16 @@ import ( sdkclient "go.temporal.io/sdk/client" "go.temporal.io/sdk/worker" "go.temporal.io/sdk/workflow" + "go.temporal.io/server/api/matchingservice/v1" + taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/metrics/metricstest" "go.temporal.io/server/common/payloads" "go.temporal.io/server/common/testing/taskpoller" + "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/common/tqid" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" ) @@ -62,9 +69,7 @@ func (s *TaskQueueSuite) RunTaskQueueRateLimitTest(nPartitions, nWorkers int, ti } func (s *TaskQueueSuite) taskQueueRateLimitTest(nPartitions, nWorkers int, timeToDrain time.Duration, useNewMatching bool) { - if useNewMatching { - s.OverrideDynamicConfig(dynamicconfig.MatchingUseNewMatcher, true) - } + s.OverrideDynamicConfig(dynamicconfig.MatchingUseNewMatcher, useNewMatching) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, nPartitions) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, nPartitions) @@ -88,7 +93,7 @@ func (s *TaskQueueSuite) taskQueueRateLimitTest(nPartitions, nWorkers int, timeT defer cancel() // start workflows to create a backlog - for wfidx := 0; wfidx < maxBacklog; wfidx++ { + for wfidx := range maxBacklog { _, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ TaskQueue: tv.TaskQueue().GetName(), ID: fmt.Sprintf("wf%d", wfidx), @@ -135,7 +140,7 @@ func (s *TaskQueueSuite) taskQueueRateLimitTest(nPartitions, nWorkers int, timeT // start some workers workers := make([]worker.Worker, nWorkers) - for i := 0; i < nWorkers; i++ { + for i := range nWorkers { workers[i] = worker.New(s.SdkClient(), tv.TaskQueue().GetName(), worker.Options{}) workers[i].RegisterWorkflow(helloRateLimitTest) err := workers[i].Start() @@ -361,7 +366,7 @@ func (s *TaskQueueSuite) TestTaskQueueAPIRateLimitZero() { // Wait for the duration and ensure no tasks executed s.False(common.AwaitWaitGroup(&wg, drainTimeout), "Some activities unexpectedly completed despite API RPS = 0") - s.Len(runTimes, 0, "No activities should run when API rate limit is 0") + s.Empty(runTimes, "No activities should run when API rate limit is 0") } func (s *TaskQueueSuite) TestTaskQueueRateLimit_UpdateFromWorkerConfigAndAPI() { @@ -452,7 +457,7 @@ func (s *TaskQueueSuite) TestTaskQueueRateLimit_UpdateFromWorkerConfigAndAPI() { }) s.NoError(err) - require.Eventually(s.T(), func() bool { + s.Require().Eventually(func() bool { describeResp, err := s.FrontendClient().DescribeTaskQueue(context.Background(), &workflowservice.DescribeTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: activityTaskQueue}, @@ -944,6 +949,469 @@ func (s *TaskQueueSuite) runActivitiesWithPriorities( return perKeyTimes, allTimes } +func (s *TaskQueueSuite) TestTaskDispatchLatencyMetric_WorkflowAndActivity() { + s.testTaskDispatchLatencyMetric(testTaskDispatchLatencyEmitted) +} + +func (s *TaskQueueSuite) TestTaskDispatchLatencyMetric_Query() { + s.testTaskDispatchLatencyMetric(testQueryTaskDispatchLatencyEmitted) +} + +func (s *TaskQueueSuite) TestTaskDispatchLatencyMetric_Nexus() { + s.testTaskDispatchLatencyMetric(testNexusTaskDispatchLatencyEmitted) +} + +func (s *TaskQueueSuite) testTaskDispatchLatencyMetric(scenario func(s *testcore.TestEnv, expectedForwarded, expectedSource, expectedPartitionID string, forwardDelay time.Duration)) { + baseOpts := []testcore.TestOption{ + testcore.WithDynamicConfig(dynamicconfig.MatchingForwarderMaxChildrenPerNode, 3), + testcore.WithDynamicConfig(dynamicconfig.MatchingEmitTaskDispatchLatencyAtPoll, true), + } + + runWithMatchingBehaviors(s.T(), baseOpts, func(s *testcore.TestEnv, b testcore.MatchingBehavior) { + + // When task forwarding is forced, inject a delay so we can verify + // the latency metric captures forwarding time. + var forwardDelay time.Duration + if b.ForceTaskForward { + forwardDelay = 100 * time.Millisecond + s.InjectHook(testhooks.NewHook(testhooks.MatchingForwardTaskDelay, forwardDelay)) + forwardDelay *= 2 // two forward hops + } + + // Determine expected tag values based on matching behavior. + expectedForwarded := "false" + expectedPartitionID := "0" + if b.ForceTaskForward { + expectedForwarded = "true" + expectedPartitionID = "11" + } + + // When async is forced, tasks always go through DB backlog. + // When sync is allowed, the source depends on timing and is non-deterministic. + expectedSource := "History" + if b.ForceAsync { + expectedSource = "DbBacklog" + } + + scenario(s, expectedForwarded, expectedSource, expectedPartitionID, forwardDelay) + }) +} + +func testTaskDispatchLatencyEmitted(s *testcore.TestEnv, expectedForwarded, expectedSource, expectedPartitionID string, minLatency time.Duration) { + tv := testvars.New(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + capture := s.StartNamespaceMetricCapture() + + activityStarted := make(chan struct{}) + + // Poll and handle the workflow task: schedule an activity. + go func() { + _, err := s.TaskPoller().PollAndHandleWorkflowTask(tv, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ + ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: "act-1", + ActivityType: tv.ActivityType(), + TaskQueue: tv.TaskQueue(), + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + }, nil + }, + ) + s.NoError(err) + }() + + // Poll and handle the activity task. + go func() { + _, err := s.TaskPoller().PollAndHandleActivityTask(tv, + func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) { + activityStarted <- struct{}{} + return &workflowservice.RespondActivityTaskCompletedRequest{ + Result: tv.Any().Payloads(), + }, nil + }, + ) + s.NoError(err) + }() + + // Wait for pollers to arrive at root partition 0 for both task queue types + // before starting the workflow to ensure deterministic sync match. + for _, tqType := range []enumspb.TaskQueueType{ + enumspb.TASK_QUEUE_TYPE_WORKFLOW, + enumspb.TASK_QUEUE_TYPE_ACTIVITY, + } { + tqType := tqType + s.Eventually(func() bool { + resp, err := s.GetTestCluster().MatchingClient().DescribeTaskQueuePartition( + ctx, &matchingservice.DescribeTaskQueuePartitionRequest{ + NamespaceId: s.NamespaceID().String(), + TaskQueuePartition: &taskqueuespb.TaskQueuePartition{ + TaskQueue: tv.TaskQueue().GetName(), + TaskQueueType: tqType, + }, + Versions: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true}, + ReportPollers: true, + }, + ) + if err != nil { + return false + } + for _, vi := range resp.GetVersionsInfoInternal() { + if len(vi.GetPhysicalTaskQueueInfo().GetPollers()) > 0 { + return true + } + } + return false + }, 10*time.Second, 50*time.Millisecond, "pollers did not arrive at root partition") + } + + // Start a workflow to generate a workflow task. + _, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowId: tv.WorkflowID(), + WorkflowType: tv.WorkflowType(), + TaskQueue: tv.TaskQueue(), + Identity: tv.ClientIdentity(), + Priority: &commonpb.Priority{ + PriorityKey: 2, + }, + }) + s.NoError(err) + + <-activityStarted + // Filter recordings for our specific task queue to avoid interference from other activity. + tqName := tv.TaskQueue().GetName() + recordings := capture.CollectMetric("task_dispatch_latency", func(rec *metricstest.CapturedRecording) bool { + return rec.Tags["taskqueue"] == tqName + }) + s.NotEmpty(recordings, "expected task_dispatch_latency metric to be recorded") + + // Verify no redundant emissions: expect exactly one recording per task type + // (1 workflow task + 1 activity task). The metric is emitted only at the partition + // serving the poll (forwarded-poll intermediaries skip already-started tasks, + // and matchers skip emission when EmitTaskDispatchLatencyAtPoll is enabled). + var workflowCount, activityCount int + for _, rec := range recordings { + latency, ok := rec.Value.(time.Duration) + s.True(ok, "expected metric value to be time.Duration") + s.Greater(latency, time.Duration(0), "expected positive dispatch latency") + if minLatency > 0 { + s.GreaterOrEqual(latency, minLatency, "expected dispatch latency to include forwarding delay") + } + + // Validate source tag. + source := rec.Tags["source"] + s.True( + source == "History" || source == "DbBacklog", + "unexpected source tag value: %s", source, + ) + if expectedSource != "" { + s.Equal(expectedSource, source, "unexpected source tag") + } + + // Validate forwarded tag. + s.Equal(expectedForwarded, rec.Tags["forwarded"], "unexpected forwarded tag") + + // Validate taskqueue tag (breakdown enabled by default). + s.Equal(tqName, rec.Tags["taskqueue"], "unexpected taskqueue tag") + + // Validate partition tag: poll is always served at root partition 0. + s.Equal(expectedPartitionID, rec.Tags["partition"], "unexpected partition tag") + + // Validate worker_version tag matches the deployment options passed to the poll. + // With BreakdownMetricsByBuildID enabled (default), the tag is "deploymentName:buildId". + s.Equal("__unversioned__", rec.Tags["worker_version"], "unexpected worker_version tag") + + // Validate task_priority tag is present (empty string for default priority). + s.Equal("2", rec.Tags["task_priority"], "unexpected task_priority tag") + + switch rec.Tags["task_type"] { + case "Workflow": + workflowCount++ + case "Activity": + activityCount++ + default: + s.Failf("unexpected task_type", "got %s", rec.Tags["task_type"]) + } + } + + s.Equal(1, workflowCount, "expected exactly 1 task_dispatch_latency recording for workflow task") + s.Equal(1, activityCount, "expected exactly 1 task_dispatch_latency recording for activity task") +} + +func testNexusTaskDispatchLatencyEmitted(s *testcore.TestEnv, expectedForwarded, _, expectedPartitionID string, minLatency time.Duration) { + tv := testvars.New(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Create a nexus endpoint targeting our task queue. + _, err := s.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: "nexus-" + uuid.New().String(), + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: s.Namespace().String(), + TaskQueue: tv.TaskQueue().GetName(), + }, + }, + }, + }, + }) + s.NoError(err) + + capture := s.StartNamespaceMetricCapture() + + nexusDone := make(chan struct{}) + + // Start nexus task poller goroutine. + go func() { + _, err := s.TaskPoller().PollNexusTask(&workflowservice.PollNexusTaskQueueRequest{}).HandleTask(tv, + func(task *workflowservice.PollNexusTaskQueueResponse) (*workflowservice.RespondNexusTaskCompletedRequest, error) { + close(nexusDone) + return &workflowservice.RespondNexusTaskCompletedRequest{}, nil + }, + ) + s.NoError(err) + }() + + // Wait for nexus poller to arrive at root partition before dispatching. + s.Eventually(func() bool { + resp, err := s.GetTestCluster().MatchingClient().DescribeTaskQueuePartition( + ctx, &matchingservice.DescribeTaskQueuePartitionRequest{ + NamespaceId: s.NamespaceID().String(), + TaskQueuePartition: &taskqueuespb.TaskQueuePartition{ + TaskQueue: tv.TaskQueue().GetName(), + TaskQueueType: enumspb.TASK_QUEUE_TYPE_NEXUS, + }, + Versions: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true}, + ReportPollers: true, + }, + ) + if err != nil { + return false + } + for _, vi := range resp.GetVersionsInfoInternal() { + if len(vi.GetPhysicalTaskQueueInfo().GetPollers()) > 0 { + return true + } + } + return false + }, 10*time.Second, 50*time.Millisecond, "nexus poller did not arrive at root partition") + + // Build the DispatchNexusTask request. When forwarding is expected, target + // partition 11 directly via the RPC name so the child partition forwards to root. + tqName := tv.TaskQueue().GetName() + dispatchTQName := tqName + if expectedForwarded == "true" { + tqFamily, tqErr := tqid.NewTaskQueueFamily(s.NamespaceID().String(), tqName) + s.NoError(tqErr) + nexusTQ := tqFamily.TaskQueue(enumspb.TASK_QUEUE_TYPE_NEXUS) + dispatchTQName = nexusTQ.NormalPartition(11).RpcName() + } + + _, err = s.GetTestCluster().MatchingClient().DispatchNexusTask(ctx, &matchingservice.DispatchNexusTaskRequest{ + NamespaceId: s.NamespaceID().String(), + TaskQueue: &taskqueuepb.TaskQueue{ + Name: dispatchTQName, + }, + Request: &nexuspb.Request{ + Header: map[string]string{ + "key": "value", + }, + Variant: &nexuspb.Request_StartOperation{ + StartOperation: &nexuspb.StartOperationRequest{ + Service: tv.Any().String(), + Operation: tv.Any().String(), + }, + }, + }, + }) + s.NoError(err) + + <-nexusDone + // Filter recordings for our specific task queue. + recordings := capture.CollectMetric("task_dispatch_latency", func(rec *metricstest.CapturedRecording) bool { + return rec.Tags["taskqueue"] == tqName + }) + s.NotEmpty(recordings, "expected task_dispatch_latency metric for nexus task") + + var nexusCount int + for _, rec := range recordings { + latency, ok := rec.Value.(time.Duration) + s.True(ok, "expected metric value to be time.Duration") + s.Greater(latency, time.Duration(0), "expected positive dispatch latency") + if minLatency > 0 { + s.GreaterOrEqual(latency, minLatency, "expected dispatch latency to include forwarding delay") + } + + s.Equal("Nexus", rec.Tags["task_type"], "expected Nexus task_type") + + // Nexus tasks are always sync-matched (source is History). + s.Equal("History", rec.Tags["source"], "unexpected source tag for nexus") + + s.Equal(expectedForwarded, rec.Tags["forwarded"], "unexpected forwarded tag") + s.Equal(tqName, rec.Tags["taskqueue"], "unexpected taskqueue tag") + s.Equal(expectedPartitionID, rec.Tags["partition"], "unexpected partition tag") + s.Equal("__unversioned__", rec.Tags["worker_version"], "unexpected worker_version tag") + s.Empty(rec.Tags["task_priority"], "expected empty task_priority for nexus (no priority support)") + nexusCount++ + } + + s.Equal(1, nexusCount, "expected exactly 1 task_dispatch_latency recording for nexus task") +} + +func testQueryTaskDispatchLatencyEmitted(s *testcore.TestEnv, expectedForwarded, _, expectedPartitionID string, minLatency time.Duration) { + tv := testvars.New(s.T()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + wftDone := make(chan struct{}) + + // Poll and handle the initial workflow task to get the workflow running. + go func() { + _, err := s.TaskPoller().PollAndHandleWorkflowTask(tv, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{}, nil + }, + ) + s.NoError(err) + close(wftDone) + }() + + // Wait for poller to arrive at root partition before starting workflow. + s.Eventually(func() bool { + resp, err := s.GetTestCluster().MatchingClient().DescribeTaskQueuePartition( + ctx, &matchingservice.DescribeTaskQueuePartitionRequest{ + NamespaceId: s.NamespaceID().String(), + TaskQueuePartition: &taskqueuespb.TaskQueuePartition{ + TaskQueue: tv.TaskQueue().GetName(), + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + }, + Versions: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true}, + ReportPollers: true, + }, + ) + if err != nil { + return false + } + for _, vi := range resp.GetVersionsInfoInternal() { + if len(vi.GetPhysicalTaskQueueInfo().GetPollers()) > 0 { + return true + } + } + return false + }, 10*time.Second, 50*time.Millisecond, "pollers did not arrive at root partition") + + // Start workflow and wait for the initial WFT to be handled. + _, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowId: tv.WorkflowID(), + WorkflowType: tv.WorkflowType(), + TaskQueue: tv.TaskQueue(), + Identity: tv.ClientIdentity(), + Priority: &commonpb.Priority{ + PriorityKey: 2, + }, + }) + s.NoError(err) + <-wftDone + + // Now start metric capture (after WFT so only query metric is captured). + capture := s.StartNamespaceMetricCapture() + + queryDone := make(chan struct{}) + + // Start query poller goroutine. + go func() { + _, err := s.TaskPoller().PollWorkflowTask(&workflowservice.PollWorkflowTaskQueueRequest{}). + HandleLegacyQuery(tv, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondQueryTaskCompletedRequest, error) { + return &workflowservice.RespondQueryTaskCompletedRequest{ + CompletedType: enumspb.QUERY_RESULT_TYPE_ANSWERED, + QueryResult: payloads.EncodeString("query-result"), + }, nil + }, + ) + s.NoError(err) + close(queryDone) + }() + + // Wait for query poller to arrive before issuing query. + s.Eventually(func() bool { + resp, err := s.GetTestCluster().MatchingClient().DescribeTaskQueuePartition( + ctx, &matchingservice.DescribeTaskQueuePartitionRequest{ + NamespaceId: s.NamespaceID().String(), + TaskQueuePartition: &taskqueuespb.TaskQueuePartition{ + TaskQueue: tv.TaskQueue().GetName(), + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + }, + Versions: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true}, + ReportPollers: true, + }, + ) + if err != nil { + return false + } + for _, vi := range resp.GetVersionsInfoInternal() { + if len(vi.GetPhysicalTaskQueueInfo().GetPollers()) > 0 { + return true + } + } + return false + }, 10*time.Second, 50*time.Millisecond, "query poller did not arrive at root partition") + + // Issue the query. + _, err = s.FrontendClient().QueryWorkflow(ctx, &workflowservice.QueryWorkflowRequest{ + Namespace: s.Namespace().String(), + Execution: &commonpb.WorkflowExecution{WorkflowId: tv.WorkflowID()}, + Query: &querypb.WorkflowQuery{QueryType: "test-query"}, + }) + s.NoError(err) + + <-queryDone + // Filter recordings for our specific task queue. + tqName := tv.TaskQueue().GetName() + recordings := capture.CollectMetric("task_dispatch_latency", func(rec *metricstest.CapturedRecording) bool { + return rec.Tags["taskqueue"] == tqName + }) + s.NotEmpty(recordings, "expected task_dispatch_latency metric for query task") + + var queryCount int + for _, rec := range recordings { + latency, ok := rec.Value.(time.Duration) + s.True(ok, "expected metric value to be time.Duration") + s.Greater(latency, time.Duration(0), "expected positive dispatch latency") + if minLatency > 0 { + s.GreaterOrEqual(latency, minLatency, "expected dispatch latency to include forwarding delay") + } + + // Query tasks are dispatched through the workflow task queue. + s.Equal("Workflow", rec.Tags["task_type"], "expected Workflow task_type for query") + + // Queries are always sync-matched (source is History). + s.Equal("History", rec.Tags["source"], "unexpected source tag for query") + + s.Equal(expectedForwarded, rec.Tags["forwarded"], "unexpected forwarded tag") + s.Equal(tqName, rec.Tags["taskqueue"], "unexpected taskqueue tag") + s.Equal(expectedPartitionID, rec.Tags["partition"], "unexpected partition tag") + s.Equal("__unversioned__", rec.Tags["worker_version"], "unexpected worker_version tag") + s.Equal("2", rec.Tags["task_priority"], "expected empty task_priority for query (default priority)") + queryCount++ + } + + s.Equal(1, queryCount, "expected exactly 1 task_dispatch_latency recording for query task") +} + func (s *TaskQueueSuite) TestShutdownWorkerCancelsOutstandingPolls() { s.OverrideDynamicConfig(dynamicconfig.EnableCancelWorkerPollsOnShutdown, true) @@ -1013,4 +1481,43 @@ func (s *TaskQueueSuite) TestShutdownWorkerCancelsOutstandingPolls() { s.NotEqual(tv.WorkerIdentity(), poller.GetIdentity(), "poller should be removed from DescribeTaskQueue after shutdown") } + + // Verify that subsequent polls from the same worker are rejected immediately + // (the shutdown worker cache prevents zombie re-polls from stealing tasks). + // Use a long timeout so we can distinguish "rejected quickly" from "timed out". + rePollTimeout := 5 * time.Minute + + // Workflow poll should be rejected immediately. + wfStart := time.Now() + rePollCtx, rePollCancel := context.WithTimeout(ctx, rePollTimeout) + defer rePollCancel() + rePollResp, err := s.FrontendClient().PollWorkflowTaskQueue(rePollCtx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: s.Namespace().String(), + TaskQueue: tv.TaskQueue(), + Identity: tv.WorkerIdentity(), + WorkerInstanceKey: workerInstanceKey, + }) + s.NoError(err) + s.NotNil(rePollResp) + s.Empty(rePollResp.GetTaskToken(), "re-poll from shutdown worker should return empty response") + // TODO: Replace timing assertion with an explicit poll response field indicating + // shutdown rejection, so we don't rely on timing to distinguish cache rejection + // from natural poll timeout. Requires adding a field to PollWorkflowTaskQueueResponse + // and PollActivityTaskQueueResponse in the public API proto. + s.Less(time.Since(wfStart), 2*time.Minute, "workflow re-poll should be rejected quickly, not wait for timeout") + + // Activity poll should also be rejected immediately. + actStart := time.Now() + actCtx, actCancel := context.WithTimeout(ctx, rePollTimeout) + defer actCancel() + actResp, err := s.FrontendClient().PollActivityTaskQueue(actCtx, &workflowservice.PollActivityTaskQueueRequest{ + Namespace: s.Namespace().String(), + TaskQueue: tv.TaskQueue(), + Identity: tv.WorkerIdentity(), + WorkerInstanceKey: workerInstanceKey, + }) + s.NoError(err) + s.NotNil(actResp) + s.Empty(actResp.GetTaskToken(), "activity re-poll from shutdown worker should return empty response") + s.Less(time.Since(actStart), 2*time.Minute, "activity re-poll should be rejected quickly, not wait for timeout") } diff --git a/tests/testcore/context.go b/tests/testcore/context.go index 10988e4b910..604c36e39f2 100644 --- a/tests/testcore/context.go +++ b/tests/testcore/context.go @@ -2,14 +2,77 @@ package testcore import ( "context" + "os" + "testing" "time" "go.temporal.io/server/common/debug" + "go.temporal.io/server/common/headers" "go.temporal.io/server/common/rpc" ) -// NewContext create new context with default timeout 90 seconds. -func NewContext() context.Context { - ctx, _ := rpc.NewContextWithTimeoutAndVersionHeaders(90 * time.Second * debug.TimeoutMultiplier) +// NewContext creates a context with default 90-second timeout and RPC headers. +// +// NOTE: If you're using testcore.NewEnv, you can use env.Context() directly - it already +// includes RPC headers. This function is primarily for legacy tests or creating standalone +// contexts outside of the TestEnv framework. +// +// If a parent context is provided, the returned context will be canceled when +// either the timeout expires OR the parent is canceled. +func NewContext(parent ...context.Context) context.Context { + if len(parent) > 0 && parent[0] != nil { + // Create RPC context derived from parent + ctx, _ := rpc.NewContextFromParentWithTimeoutAndVersionHeaders( + parent[0], + defaultTestTimeout, + ) + return ctx + } + + // Create standalone RPC context + ctx, _ := rpc.NewContextWithTimeoutAndVersionHeaders(defaultTestTimeout) + return ctx +} + +// calculateTimeout determines the appropriate timeout duration based on custom timeout, +// environment variable, and default values. +// +// Priority order: +// 1. Custom timeout (via WithTimeout option) +// 2. TEMPORAL_TEST_TIMEOUT environment variable (in seconds) +// 3. Default 90 seconds +func calculateTimeout(customTimeout time.Duration) time.Duration { + if customTimeout > 0 { + return customTimeout * debug.TimeoutMultiplier + } + + if envTimeout := os.Getenv("TEMPORAL_TEST_TIMEOUT"); envTimeout != "" { + if dur, err := time.ParseDuration(envTimeout); err == nil && dur > 0 { + return dur * debug.TimeoutMultiplier + } + } + + return defaultTestTimeout +} + +// setupTestTimeoutWithContext creates a context that will be canceled on timeout, +// and reports the timeout error during cleanup. Returns a context that tests can +// use to be interrupted when timeout occurs. The context includes RPC version headers. +func setupTestTimeoutWithContext(t *testing.T, customTimeout time.Duration) context.Context { + t.Helper() + + timeout := calculateTimeout(customTimeout) + ctx, cancel := context.WithTimeout(t.Context(), timeout) + ctx = headers.SetVersions(ctx) + + // Register cleanup to cancel context and check timeout. + // t.Cleanup() functions run in LIFO order, so this runs after test code. + t.Cleanup(func() { + cancel() + if ctx.Err() == context.DeadlineExceeded { + t.Errorf("Test exceeded timeout of %v", timeout) + } + }) + return ctx } diff --git a/tests/testcore/functional_test_base.go b/tests/testcore/functional_test_base.go index d12ac272941..541e7353dd7 100644 --- a/tests/testcore/functional_test_base.go +++ b/tests/testcore/functional_test_base.go @@ -26,6 +26,7 @@ import ( "go.temporal.io/server/api/adminservice/v1" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common" + "go.temporal.io/server/common/archiver/provider" "go.temporal.io/server/common/config" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" @@ -37,6 +38,7 @@ import ( "go.temporal.io/server/common/primitives" "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/rpc" + "go.temporal.io/server/common/searchattribute" "go.temporal.io/server/common/telemetry" "go.temporal.io/server/common/testing/historyrequire" "go.temporal.io/server/common/testing/protorequire" @@ -79,7 +81,7 @@ type ( // Fields used by SDK based tests. sdkClient sdkclient.Client - worker sdkworker.Worker + sdkWorker sdkworker.Worker taskQueue string // TODO (alex): replace with v2 @@ -92,13 +94,15 @@ type ( } // TestClusterParams contains the variables which are used to configure test cluster via the TestClusterOption type. TestClusterParams struct { - ServiceOptions map[primitives.ServiceName][]fx.Option - DynamicConfigOverrides map[dynamicconfig.Key]any - ArchivalEnabled bool - EnableMTLS bool - FaultInjectionConfig *config.FaultInjection - NumHistoryShards int32 - SharedCluster bool + ServiceOptions map[primitives.ServiceName][]fx.Option + DynamicConfigOverrides map[dynamicconfig.Key]any + ArchivalEnabled bool + EnableMTLS bool + FaultInjectionConfig *config.FaultInjection + NumHistoryShards int32 + SharedCluster bool + CustomHistoryArchiverFactory provider.CustomHistoryArchiverFactory + CustomVisibilityArchiverFactory provider.CustomVisibilityArchiverFactory } TestClusterOption func(params *TestClusterParams) ) @@ -167,6 +171,18 @@ func WithSharedCluster() TestClusterOption { } } +func WithCustomHistoryArchiverFactory(factory provider.CustomHistoryArchiverFactory) TestClusterOption { + return func(params *TestClusterParams) { + params.CustomHistoryArchiverFactory = factory + } +} + +func WithCustomVisibilityArchiverFactory(factory provider.CustomVisibilityArchiverFactory) TestClusterOption { + return func(params *TestClusterParams) { + params.CustomVisibilityArchiverFactory = factory + } +} + func (s *FunctionalTestBase) GetTestCluster() *TestCluster { return s.testCluster } @@ -211,8 +227,8 @@ func (s *FunctionalTestBase) WorkerGRPCAddress() string { return s.GetTestCluster().WorkerGRPCAddress() } -func (s *FunctionalTestBase) Worker() sdkworker.Worker { - return s.worker +func (s *FunctionalTestBase) SdkWorker() sdkworker.Worker { + return s.sdkWorker } func (s *FunctionalTestBase) SdkClient() sdkclient.Client { @@ -268,11 +284,13 @@ func (s *FunctionalTestBase) setupCluster(options ...TestClusterOption) { HistoryConfig: HistoryConfig{ NumHistoryShards: cmp.Or(params.NumHistoryShards, 4), }, - DynamicConfigOverrides: params.DynamicConfigOverrides, - ServiceFxOptions: params.ServiceOptions, - EnableMetricsCapture: true, - EnableArchival: params.ArchivalEnabled, - EnableMTLS: params.EnableMTLS, + DynamicConfigOverrides: params.DynamicConfigOverrides, + ServiceFxOptions: params.ServiceOptions, + EnableMetricsCapture: true, + EnableArchival: params.ArchivalEnabled, + EnableMTLS: params.EnableMTLS, + CustomHistoryArchiverFactory: params.CustomHistoryArchiverFactory, + CustomVisibilityArchiverFactory: params.CustomVisibilityArchiverFactory, } // Apply configuration for shared clusters. @@ -329,6 +347,7 @@ func (s *FunctionalTestBase) SetupSubTest() { s.initAssertions() } +// TODO: remove once `parallelsuite` and testEnv is rolled out everywhere func (s *FunctionalTestBase) initAssertions() { // `s.Assertions` (as well as other test helpers which depends on `s.T()`) must be initialized on // both test and subtest levels (but not suite level, where `s.T()` is `nil`). @@ -387,8 +406,8 @@ func (s *FunctionalTestBase) setupSdk() { s.taskQueue = RandomizeStr("tq") workerOptions := sdkworker.Options{} - s.worker = sdkworker.New(s.sdkClient, s.taskQueue, workerOptions) - err = s.worker.Start() + s.sdkWorker = sdkworker.New(s.sdkClient, s.taskQueue, workerOptions) + err = s.sdkWorker.Start() s.NoError(err) } @@ -432,8 +451,8 @@ func (s *FunctionalTestBase) TearDownSubTest() { } func (s *FunctionalTestBase) tearDownSdk() { - if s.worker != nil { - s.worker.Stop() + if s.sdkWorker != nil { + s.sdkWorker.Stop() } if s.sdkClient != nil { s.sdkClient.Close() @@ -453,6 +472,7 @@ func (s *FunctionalTestBase) RegisterNamespace( ) (namespace.ID, error) { currentClusterName := s.testCluster.testBase.ClusterMetadata.GetCurrentClusterName() nsID := namespace.ID(uuid.NewString()) + expectedSearchAttributes := searchattribute.TestSearchAttributesToRegister() namespaceRequest := &persistence.CreateNamespaceRequest{ Namespace: &persistencespb.NamespaceDetail{ Info: &persistencespb.NamespaceInfo{ @@ -468,14 +488,6 @@ func (s *FunctionalTestBase) RegisterNamespace( VisibilityArchivalState: archivalState, VisibilityArchivalUri: visibilityArchivalURI, BadBinaries: &namespacepb.BadBinaries{Binaries: map[string]*namespacepb.BadBinaryInfo{}}, - CustomSearchAttributeAliases: map[string]string{ - "Bool01": "CustomBoolField", - "Datetime01": "CustomDatetimeField", - "Double01": "CustomDoubleField", - "Int01": "CustomIntField", - "Keyword01": "CustomKeywordField", - "Text01": "CustomTextField", - }, }, ReplicationConfig: &persistencespb.NamespaceReplicationConfig{ ActiveClusterName: currentClusterName, @@ -494,6 +506,54 @@ func (s *FunctionalTestBase) RegisterNamespace( return namespace.EmptyID, err } + namespaceCacheDeadline := time.Now().Add(5 * NamespaceCacheRefreshInterval) + ticker := time.NewTicker(NamespaceCacheRefreshInterval / 2) + defer ticker.Stop() + for { + _, describeErr := s.FrontendClient().DescribeNamespace(NewContext(), &workflowservice.DescribeNamespaceRequest{ + Namespace: nsName.String(), + }) + if describeErr == nil { + break + } + if time.Now().After(namespaceCacheDeadline) { + return namespace.EmptyID, fmt.Errorf("namespace cache did not refresh for %q before deadline", nsName.String()) + } + <-ticker.C + } + + _, err = s.OperatorClient().AddSearchAttributes(NewContext(), &operatorservice.AddSearchAttributesRequest{ + Namespace: nsName.String(), + SearchAttributes: expectedSearchAttributes, + }) + if err != nil { + return namespace.EmptyID, err + } + + namespaceCacheDeadline = time.Now().Add(5 * NamespaceCacheRefreshInterval) + for { + listResp, listErr := s.OperatorClient().ListSearchAttributes(NewContext(), &operatorservice.ListSearchAttributesRequest{ + Namespace: nsName.String(), + }) + if listErr == nil { + customAttrs := listResp.GetCustomAttributes() + allFound := true + for saName := range expectedSearchAttributes { + if _, ok := customAttrs[saName]; !ok { + allFound = false + break + } + } + if allFound { + break + } + } + if time.Now().After(namespaceCacheDeadline) { + return namespace.EmptyID, fmt.Errorf("search attributes were not ready for %q before deadline", nsName.String()) + } + <-ticker.C + } + s.Logger.Info("Register namespace succeeded", tag.WorkflowNamespace(nsName.String()), tag.WorkflowNamespaceID(nsID.String()), @@ -523,7 +583,7 @@ func (s *FunctionalTestBase) GetHistoryFunc(namespace string, execution *commonp Execution: execution, MaximumPageSize: 5, // Use small page size to force pagination code path }) - require.NoError(s.T(), err) + s.Require().NoError(err) events := historyResponse.History.Events for historyResponse.NextPageToken != nil { @@ -532,7 +592,7 @@ func (s *FunctionalTestBase) GetHistoryFunc(namespace string, execution *commonp Execution: execution, NextPageToken: historyResponse.NextPageToken, }) - require.NoError(s.T(), err) + s.Require().NoError(err) events = append(events, historyResponse.History.Events...) } @@ -576,11 +636,23 @@ func (s *FunctionalTestBase) OverrideDynamicConfig(setting dynamicconfig.Generic return s.testCluster.host.overrideDynamicConfig(s.T(), setting.Key(), value) } -func (s *FunctionalTestBase) InjectHook(key testhooks.Key, value any) (cleanup func()) { - if s.isShared { - s.T().Fatalf("InjectHook cannot be called on a shared cluster; use testcore.WithDedicatedCluster()") +// InjectHook sets a test hook inside the cluster. +func (s *FunctionalTestBase) InjectHook(hook testhooks.Hook) (cleanup func()) { + var scope any + switch hook.Scope() { + case testhooks.ScopeNamespace: + scope = s.NamespaceID() + case testhooks.ScopeGlobal: + scope = testhooks.GlobalScope + default: + s.T().Fatalf("InjectHook: unknown scope %v", hook.Scope()) } - return s.testCluster.host.injectHook(s.T(), key, value) + return s.testCluster.host.injectHook(s.T(), hook, scope) +} + +// Context returns a context with RPC headers for use in this test. +func (s *FunctionalTestBase) Context() context.Context { + return NewContext() } // CloseShard closes the shard that contains the given workflow. @@ -605,55 +677,18 @@ func (s *FunctionalTestBase) GetNamespaceID(namespace string) string { } func (s *FunctionalTestBase) RunTestWithMatchingBehavior(subtest func()) { - for _, forcePollForward := range []bool{false, true} { - for _, forceTaskForward := range []bool{false, true} { - for _, forceAsync := range []bool{false, true} { - name := "NoTaskForward" - if forceTaskForward { - // force two levels of forwarding - name = "ForceTaskForward" - } - if forcePollForward { - name += "ForcePollForward" - } else { - name += "NoPollForward" - } - if forceAsync { - name += "ForceAsync" - } else { - name += "AllowSync" - } - - s.Run( - name, func() { - if forceTaskForward || forcePollForward { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 13) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 13) - } else { - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) - } - if forceTaskForward { - s.InjectHook(testhooks.MatchingLBForceWritePartition, 11) - } else { - s.InjectHook(testhooks.MatchingLBForceWritePartition, 0) - } - if forcePollForward { - s.InjectHook(testhooks.MatchingLBForceReadPartition, 5) - } else { - s.InjectHook(testhooks.MatchingLBForceReadPartition, 0) - } - if forceAsync { - s.InjectHook(testhooks.MatchingDisableSyncMatch, true) - } else { - s.InjectHook(testhooks.MatchingDisableSyncMatch, false) - } - - subtest() - }, - ) + for _, behavior := range AllMatchingBehaviors() { + s.Run(behavior.Name(), func() { + if behavior.ForceTaskForward || behavior.ForcePollForward { + s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 13) + s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 13) + } else { + s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) + s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) } - } + behavior.InjectHooks(s) + subtest() + }) } } @@ -666,6 +701,15 @@ func (s *FunctionalTestBase) WaitForChannel(ctx context.Context, ch chan struct{ } } +func (s *FunctionalTestBase) SendToChannel(ctx context.Context, ch chan struct{}) { + s.T().Helper() + select { + case ch <- struct{}{}: + case <-ctx.Done(): + s.FailNow("context timeout while sending to channel") + } +} + // TODO (alex): change to nsName namespace.Name func (s *FunctionalTestBase) SendSignal(nsName string, execution *commonpb.WorkflowExecution, signalName string, input *commonpb.Payloads, identity string) error { diff --git a/tests/testcore/matching_behavior.go b/tests/testcore/matching_behavior.go new file mode 100644 index 00000000000..525c4cae015 --- /dev/null +++ b/tests/testcore/matching_behavior.go @@ -0,0 +1,89 @@ +package testcore + +import ( + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/testing/testhooks" +) + +// MatchingBehavior describes a test scenario for matching service behavior. +type MatchingBehavior struct { + ForceTaskForward bool + ForcePollForward bool + ForceAsync bool +} + +type hookInjector interface { + InjectHook(hook testhooks.Hook) (cleanup func()) +} + +// Name returns a descriptive name for this behavior combination. +func (b MatchingBehavior) Name() string { + name := "NoTaskForward" + if b.ForceTaskForward { + name = "ForceTaskForward" + } + if b.ForcePollForward { + name += "ForcePollForward" + } else { + name += "NoPollForward" + } + if b.ForceAsync { + name += "ForceAsync" + } else { + name += "AllowSync" + } + return name +} + +// Options returns the TestOptions to configure matching behavior. +func (b MatchingBehavior) Options() []TestOption { + var opts []TestOption + if b.ForceTaskForward || b.ForcePollForward { + opts = append(opts, + WithDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 13), + WithDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 13), + ) + } else { + opts = append(opts, + WithDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1), + WithDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1), + ) + } + return opts +} + +// InjectHooks injects the test hooks for this matching behavior. +func (b MatchingBehavior) InjectHooks(env hookInjector) { + if b.ForceTaskForward { + env.InjectHook(testhooks.NewHook(testhooks.MatchingLBForceWritePartition, 11)) + } else { + env.InjectHook(testhooks.NewHook(testhooks.MatchingLBForceWritePartition, 0)) + } + if b.ForcePollForward { + env.InjectHook(testhooks.NewHook(testhooks.MatchingLBForceReadPartition, 5)) + } else { + env.InjectHook(testhooks.NewHook(testhooks.MatchingLBForceReadPartition, 0)) + } + if b.ForceAsync { + env.InjectHook(testhooks.NewHook(testhooks.MatchingDisableSyncMatch, true)) + } else { + env.InjectHook(testhooks.NewHook(testhooks.MatchingDisableSyncMatch, false)) + } +} + +// AllMatchingBehaviors returns all 8 combinations of matching behaviors for testing. +func AllMatchingBehaviors() []MatchingBehavior { + var behaviors []MatchingBehavior + for _, forcePollForward := range []bool{false, true} { + for _, forceTaskForward := range []bool{false, true} { + for _, forceAsync := range []bool{false, true} { + behaviors = append(behaviors, MatchingBehavior{ + ForceTaskForward: forceTaskForward, + ForcePollForward: forcePollForward, + ForceAsync: forceAsync, + }) + } + } + } + return behaviors +} diff --git a/tests/testcore/metric_capture.go b/tests/testcore/metric_capture.go new file mode 100644 index 00000000000..65946dcdb33 --- /dev/null +++ b/tests/testcore/metric_capture.go @@ -0,0 +1,132 @@ +package testcore + +import ( + "fmt" + "sync" + + "go.temporal.io/server/common/metrics/metricstest" +) + +const collectMetricNilKeepPanic = "CollectMetric keep func must not be nil; use Metric(name) to collect all readings" + +type GlobalMetricCapture struct { + capture *metricstest.Capture + + mu sync.Mutex + queriedMetrics map[string]struct{} +} + +func newGlobalMetricCapture(capture *metricstest.Capture) *GlobalMetricCapture { + return &GlobalMetricCapture{ + capture: capture, + queriedMetrics: make(map[string]struct{}), + } +} + +func (c *GlobalMetricCapture) Metric(name string) []*metricstest.CapturedRecording { + return c.collectMetric(name, nil) +} + +// CollectMetric returns the recordings for the named metric that the caller chooses to keep. +func (c *GlobalMetricCapture) CollectMetric(name string, keep func(*metricstest.CapturedRecording) bool) []*metricstest.CapturedRecording { + if keep == nil { + panic(collectMetricNilKeepPanic) + } + return c.collectMetric(name, keep) +} + +func (c *GlobalMetricCapture) collectMetric(name string, keep func(*metricstest.CapturedRecording) bool) []*metricstest.CapturedRecording { + c.mu.Lock() + c.queriedMetrics[name] = struct{}{} + c.mu.Unlock() + + keepAll := keep == nil + var collected []*metricstest.CapturedRecording + for _, rec := range c.capture.Snapshot()[name] { + if keepAll || keep(rec) { + collected = append(collected, rec) + } + } + return collected +} + +// checkForNamespaceCaptureMisuse panics when all queried metrics that produced +// recordings were namespace-scoped. Metrics with no recordings are treated as +// inconclusive and do not trigger misuse detection. +func (c *GlobalMetricCapture) checkForNamespaceCaptureMisuse() { + queriedMetrics := c.queriedMetricNames() + + if len(queriedMetrics) == 0 { + return + } + + if allQueriedMetricsAreNamespaceScoped(c.capture.Snapshot(), queriedMetrics) { + panic("GlobalMetricCapture was used, but all queried metrics were namespace-scoped; use NamespaceMetricCapture instead") + } +} + +func (c *GlobalMetricCapture) queriedMetricNames() []string { + c.mu.Lock() + defer c.mu.Unlock() + + queriedMetrics := make([]string, 0, len(c.queriedMetrics)) + for name := range c.queriedMetrics { + queriedMetrics = append(queriedMetrics, name) + } + return queriedMetrics +} + +func allQueriedMetricsAreNamespaceScoped(snap metricstest.CaptureSnapshot, queriedMetrics []string) bool { + for _, name := range queriedMetrics { + recordings := snap[name] + if len(recordings) == 0 { + return false + } + for _, rec := range recordings { + if _, ok := rec.Tags["namespace"]; !ok { + return false + } + } + } + return true +} + +type NamespaceMetricCapture struct { + capture *metricstest.Capture + namespace string +} + +func newNamespaceMetricCapture(capture *metricstest.Capture, namespace string) *NamespaceMetricCapture { + return &NamespaceMetricCapture{ + capture: capture, + namespace: namespace, + } +} + +func (c *NamespaceMetricCapture) Metric(name string) []*metricstest.CapturedRecording { + return c.collectMetric(name, nil) +} + +// CollectMetric returns the recordings for the named metric that belong to the test namespace +// and that the caller chooses to keep. It panics if the requested metric is not namespace-scoped. +func (c *NamespaceMetricCapture) CollectMetric(name string, keep func(*metricstest.CapturedRecording) bool) []*metricstest.CapturedRecording { + if keep == nil { + panic(collectMetricNilKeepPanic) + } + return c.collectMetric(name, keep) +} + +func (c *NamespaceMetricCapture) collectMetric(name string, keep func(*metricstest.CapturedRecording) bool) []*metricstest.CapturedRecording { + keepAll := keep == nil + var collected []*metricstest.CapturedRecording + for _, rec := range c.capture.Snapshot()[name] { + namespace, ok := rec.Tags["namespace"] + if !ok { + panic(fmt.Sprintf("metric %q is not namespace-scoped; use GlobalMetricCapture instead", name)) + } + if namespace == c.namespace && (keepAll || keep(rec)) { + collected = append(collected, rec) + } + } + return collected +} diff --git a/tests/testcore/metric_capture_test.go b/tests/testcore/metric_capture_test.go new file mode 100644 index 00000000000..c865a2e1574 --- /dev/null +++ b/tests/testcore/metric_capture_test.go @@ -0,0 +1,93 @@ +package testcore + +import ( + "testing" + + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/metrics/metricstest" + "go.temporal.io/server/common/testing/parallelsuite" +) + +type MetricCaptureSuite struct { + parallelsuite.Suite[*MetricCaptureSuite] +} + +func TestMetricCaptureSuite(t *testing.T) { + parallelsuite.Run(t, &MetricCaptureSuite{}) +} + +func (s *MetricCaptureSuite) TestNamespaceMetricCapture() { + handler := metricstest.NewCaptureHandler() + capture := handler.StartCapture() + s.T().Cleanup(func() { + handler.StopCapture(capture) + }) + + s.Run("returns only recordings for the test namespace", func(s *MetricCaptureSuite) { + // Record the same metric for two namespaces. + const metricName = "namespaced_metric" + metricsHandler := handler.WithTags(metrics.NamespaceTag("test-ns")) + metricsHandler.Counter(metricName).Record(1) + handler.WithTags(metrics.NamespaceTag("other-ns")).Counter(metricName).Record(1) + + namespaceCapture := newNamespaceMetricCapture(capture, "test-ns") + + recordings := namespaceCapture.Metric(metricName) + s.Len(recordings, 1) + s.Equal("test-ns", recordings[0].Tags["namespace"]) + }) + + s.Run("panics when the metric is not namespace-scoped", func(s *MetricCaptureSuite) { + // Record the metric without a namespace tag. + handler.Counter("cluster_metric").Record(1) + + namespaceCapture := newNamespaceMetricCapture(capture, "test-ns") + + s.PanicsWithValue( + `metric "cluster_metric" is not namespace-scoped; use GlobalMetricCapture instead`, + func() { + namespaceCapture.Metric("cluster_metric") + }, + ) + }) +} + +func (s *MetricCaptureSuite) TestGlobalMetricCapture() { + s.Run("panics when only namespace-scoped metrics were queried", func(s *MetricCaptureSuite) { + handler := metricstest.NewCaptureHandler() + capture := handler.StartCapture() + s.T().Cleanup(func() { + handler.StopCapture(capture) + }) + + globalCapture := newGlobalMetricCapture(capture) + handler.WithTags(metrics.NamespaceTag("test-ns")).Counter("namespaced_metric").Record(1) + + recordings := globalCapture.Metric("namespaced_metric") + s.Len(recordings, 1) + + s.PanicsWithValue( + "GlobalMetricCapture was used, but all queried metrics were namespace-scoped; use NamespaceMetricCapture instead", + func() { + globalCapture.checkForNamespaceCaptureMisuse() + }, + ) + }) + + s.Run("does not panic when a non-namespace metric was queried", func(s *MetricCaptureSuite) { + handler := metricstest.NewCaptureHandler() + capture := handler.StartCapture() + s.T().Cleanup(func() { + handler.StopCapture(capture) + }) + + globalCapture := newGlobalMetricCapture(capture) + handler.Counter("cluster_metric").Record(1) + + recordings := globalCapture.Metric("cluster_metric") + s.Len(recordings, 1) + s.NotPanics(func() { + globalCapture.checkForNamespaceCaptureMisuse() + }) + }) +} diff --git a/tests/testcore/onebox.go b/tests/testcore/onebox.go index aecbd43e8f5..e6309a015aa 100644 --- a/tests/testcore/onebox.go +++ b/tests/testcore/onebox.go @@ -21,6 +21,7 @@ import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/api/matchingservice/v1" "go.temporal.io/server/chasm" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" chasmtests "go.temporal.io/server/chasm/lib/tests" "go.temporal.io/server/client" "go.temporal.io/server/common" @@ -76,11 +77,12 @@ type ( chasmVisibilityMgr chasm.VisibilityManager // These are routing/load balancing clients but do not do retries: - adminClient adminservice.AdminServiceClient - frontendClient workflowservice.WorkflowServiceClient - operatorClient operatorservice.OperatorServiceClient - historyClient historyservice.HistoryServiceClient - matchingClient matchingservice.MatchingServiceClient + adminClient adminservice.AdminServiceClient + frontendClient workflowservice.WorkflowServiceClient + operatorClient operatorservice.OperatorServiceClient + historyClient historyservice.HistoryServiceClient + matchingClient matchingservice.MatchingServiceClient + schedulerClient schedulerpb.SchedulerServiceClient dcClient *dynamicconfig.MemoryClient testHooks testhooks.TestHooks @@ -167,7 +169,7 @@ type ( ESClient esclient.Client MockAdminClient map[string]adminservice.AdminServiceClient NamespaceReplicationTaskExecutor nsreplication.TaskExecutor - DynamicConfigOverrides map[dynamicconfig.Key]interface{} + DynamicConfigOverrides map[dynamicconfig.Key]any TLSConfigProvider *encryption.FixedTLSConfigProvider CaptureMetricsHandler *metricstest.CaptureHandler // ServiceFxOptions is populated by WithFxOptionsForService. @@ -313,6 +315,10 @@ func (c *TemporalImpl) MatchingClient() matchingservice.MatchingServiceClient { return c.matchingClient } +func (c *TemporalImpl) SchedulerClient() schedulerpb.SchedulerServiceClient { + return c.schedulerClient +} + func (c *TemporalImpl) DcClient() *dynamicconfig.MemoryClient { return c.dcClient } @@ -351,6 +357,7 @@ func (c *TemporalImpl) startFrontend() { var rpcFactory common.RPCFactory var historyRawClient resource.HistoryRawClient var matchingRawClient resource.MatchingRawClient + var schedulerClient schedulerpb.SchedulerServiceClient var grpcResolver *membership.GRPCResolver for _, host := range c.hostsByProtocolByService[grpcProtocol][serviceName].All { @@ -413,7 +420,7 @@ func (c *TemporalImpl) startFrontend() { temporal.TraceExportModule, temporal.ServiceTracingModule, frontend.Module, - fx.Populate(&namespaceRegistry, &rpcFactory, &historyRawClient, &matchingRawClient, &grpcResolver), + fx.Populate(&namespaceRegistry, &rpcFactory, &historyRawClient, &matchingRawClient, &schedulerClient, &grpcResolver), temporal.FxLogAdapter, c.getFxOptionsForService(primitives.FrontendService), chasmFxOptions, @@ -437,9 +444,10 @@ func (c *TemporalImpl) startFrontend() { c.adminClient = adminservice.NewAdminServiceClient(connection) c.operatorClient = operatorservice.NewOperatorServiceClient(connection) - // We also set the history and matching clients here, stealing them from one of the frontends. + // We also set the history, matching, and scheduler clients here, stealing them from one of the frontends. c.historyClient = historyRawClient c.matchingClient = matchingRawClient + c.schedulerClient = schedulerClient // Address for SDKs c.frontendMembershipAddress = grpcResolver.MakeURL(serviceName) @@ -707,6 +715,8 @@ func (c *TemporalImpl) TlsConfigProvider() *encryption.FixedTLSConfigProvider { return c.tlsConfigProvider } +// Deprecated: metric capture is cluster-global. +// Use (*TestEnv).StartGlobalMetricCapture() or (*TestEnv).StartNamespaceMetricCapture() instead. func (c *TemporalImpl) CaptureMetricsHandler() *metricstest.CaptureHandler { return c.captureMetricsHandler } @@ -957,13 +967,13 @@ func sdkClientFactoryProvider( } func (c *TemporalImpl) overrideDynamicConfig(t *testing.T, name dynamicconfig.Key, value any) func() { - cleanup := c.dcClient.OverrideValue(name, value) + cleanup := c.dcClient.PartialOverrideValue(name, value) t.Cleanup(cleanup) return cleanup } -func (c *TemporalImpl) injectHook(t *testing.T, key testhooks.Key, value any) func() { - cleanup := testhooks.Set(c.testHooks, key, value) +func (c *TemporalImpl) injectHook(t *testing.T, hook testhooks.Hook, scope any) func() { + cleanup := hook.Apply(c.testHooks, scope) t.Cleanup(cleanup) return cleanup } diff --git a/tests/testcore/replication_stream_recorder.go b/tests/testcore/replication_stream_recorder.go index 493ea819a3c..7b53d43c612 100644 --- a/tests/testcore/replication_stream_recorder.go +++ b/tests/testcore/replication_stream_recorder.go @@ -160,7 +160,7 @@ func (r *ReplicationStreamRecorder) UnaryInterceptor(clusterName string) grpc.Un return func( ctx context.Context, method string, - req, reply interface{}, + req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, @@ -225,7 +225,7 @@ type recordingClientStream struct { targetAddress string } -func (s *recordingClientStream) SendMsg(m interface{}) error { +func (s *recordingClientStream) SendMsg(m any) error { if msg, ok := m.(proto.Message); ok { // SendMsg means this cluster is SENDING a message (could be request or ack) s.recorder.recordMessage(s.method, msg, DirectionSend, s.clusterName, s.targetAddress, true) @@ -233,7 +233,7 @@ func (s *recordingClientStream) SendMsg(m interface{}) error { return s.ClientStream.SendMsg(m) } -func (s *recordingClientStream) RecvMsg(m interface{}) error { +func (s *recordingClientStream) RecvMsg(m any) error { err := s.ClientStream.RecvMsg(m) if err == nil { if msg, ok := m.(proto.Message); ok { @@ -248,10 +248,10 @@ func (s *recordingClientStream) RecvMsg(m interface{}) error { func (r *ReplicationStreamRecorder) UnaryServerInterceptor(clusterName string) grpc.UnaryServerInterceptor { return func( ctx context.Context, - req interface{}, + req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, - ) (interface{}, error) { + ) (any, error) { // Capture incoming request if it's a replication-related call if isReplicationMethod(info.FullMethod) { if protoReq, ok := req.(proto.Message); ok { @@ -275,7 +275,7 @@ func (r *ReplicationStreamRecorder) UnaryServerInterceptor(clusterName string) g // StreamServerInterceptor returns a gRPC stream server interceptor that captures stream messages func (r *ReplicationStreamRecorder) StreamServerInterceptor(clusterName string) grpc.StreamServerInterceptor { return func( - srv interface{}, + srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, @@ -302,7 +302,7 @@ type recordingServerStream struct { clusterName string } -func (s *recordingServerStream) SendMsg(m interface{}) error { +func (s *recordingServerStream) SendMsg(m any) error { if msg, ok := m.(proto.Message); ok { // Server SendMsg means this server is SENDING a message to the client s.recorder.recordMessage(s.method, msg, DirectionServerSend, s.clusterName, "server", true) @@ -310,7 +310,7 @@ func (s *recordingServerStream) SendMsg(m interface{}) error { return s.ServerStream.SendMsg(m) } -func (s *recordingServerStream) RecvMsg(m interface{}) error { +func (s *recordingServerStream) RecvMsg(m any) error { err := s.ServerStream.RecvMsg(m) if err == nil { if msg, ok := m.(proto.Message); ok { diff --git a/tests/testcore/shard_salt.txt b/tests/testcore/shard_salt.txt index 8a01c2103b9..b0d2298901b 100644 --- a/tests/testcore/shard_salt.txt +++ b/tests/testcore/shard_salt.txt @@ -1 +1 @@ --salt-824 +-salt-5180 diff --git a/tests/testcore/test_cluster.go b/tests/testcore/test_cluster.go index 984dd3c018a..4888633c4ad 100644 --- a/tests/testcore/test_cluster.go +++ b/tests/testcore/test_cluster.go @@ -20,6 +20,7 @@ import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/api/matchingservice/v1" persistencespb "go.temporal.io/server/api/persistence/v1" + schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/common/archiver" "go.temporal.io/server/common/archiver/filestore" "go.temporal.io/server/common/archiver/provider" @@ -34,12 +35,14 @@ import ( "go.temporal.io/server/common/metrics/metricstest" "go.temporal.io/server/common/namespace/nsreplication" "go.temporal.io/server/common/persistence" + persistenceclient "go.temporal.io/server/common/persistence/client" persistencetests "go.temporal.io/server/common/persistence/persistence-tests" + "go.temporal.io/server/common/persistence/serialization" esclient "go.temporal.io/server/common/persistence/visibility/store/elasticsearch/client" "go.temporal.io/server/common/pprof" "go.temporal.io/server/common/primitives" + "go.temporal.io/server/common/resolver" "go.temporal.io/server/common/rpc/encryption" - "go.temporal.io/server/common/searchattribute" "go.temporal.io/server/common/telemetry" "go.temporal.io/server/common/testing/freeport" "go.temporal.io/server/temporal" @@ -71,21 +74,23 @@ type ( // TestClusterConfig are config for a test cluster TestClusterConfig struct { - EnableArchival bool - IsMasterCluster bool - ClusterMetadata cluster.Config - Persistence persistencetests.TestBaseOptions - FrontendConfig FrontendConfig - HistoryConfig HistoryConfig - MatchingConfig MatchingConfig - WorkerConfig WorkerConfig - ESConfig *esclient.Config - MockAdminClient map[string]adminservice.AdminServiceClient - FaultInjection *config.FaultInjection - DynamicConfigOverrides map[dynamicconfig.Key]any - EnableMTLS bool - EnableMetricsCapture bool - SpanExporters map[telemetry.SpanExporterType]sdktrace.SpanExporter + EnableArchival bool + IsMasterCluster bool + ClusterMetadata cluster.Config + Persistence persistencetests.TestBaseOptions + FrontendConfig FrontendConfig + HistoryConfig HistoryConfig + MatchingConfig MatchingConfig + WorkerConfig WorkerConfig + ESConfig *esclient.Config + MockAdminClient map[string]adminservice.AdminServiceClient + FaultInjection *config.FaultInjection + DynamicConfigOverrides map[dynamicconfig.Key]any + EnableMTLS bool + EnableMetricsCapture bool + SpanExporters map[telemetry.SpanExporterType]sdktrace.SpanExporter + CustomHistoryArchiverFactory provider.CustomHistoryArchiverFactory + CustomVisibilityArchiverFactory provider.CustomVisibilityArchiverFactory // ServiceFxOptions can be populated using WithFxOptionsForService. ServiceFxOptions map[primitives.ServiceName][]fx.Option } @@ -161,7 +166,12 @@ func (f *defaultPersistenceTestBaseFactory) NewTestBase(options *persistencetest return persistencetests.NewTestBase(options) } -func newClusterWithPersistenceTestBaseFactory(t *testing.T, clusterConfig *TestClusterConfig, logger log.Logger, tbFactory PersistenceTestBaseFactory) (*TestCluster, error) { +func newClusterWithPersistenceTestBaseFactory( + t *testing.T, + clusterConfig *TestClusterConfig, + logger log.Logger, + tbFactory PersistenceTestBaseFactory, +) (*TestCluster, error) { // determine number of hosts per service const minNodes = 1 clusterConfig.FrontendConfig.NumFrontendHosts = max(minNodes, clusterConfig.FrontendConfig.NumFrontendHosts) @@ -212,18 +222,22 @@ func newClusterWithPersistenceTestBaseFactory(t *testing.T, clusterConfig *TestC testBase := tbFactory.NewTestBase(&clusterConfig.Persistence) testBase.Setup(clusterMetadataConfig) - archiverBase := newArchiverBase(clusterConfig.EnableArchival, testBase.ExecutionManager, logger) + archiverBase := newArchiverBase( + clusterConfig.EnableArchival, + clusterConfig.CustomHistoryArchiverFactory, + clusterConfig.CustomVisibilityArchiverFactory, + testBase.ExecutionManager, + logger, + ) + var err error pConfig := testBase.DefaultTestCluster.Config() pConfig.NumHistoryShards = clusterConfig.HistoryConfig.NumHistoryShards var ( - indexName string - esClient esclient.Client - saTypeMap searchattribute.NameTypeMap + esClient esclient.Client ) if !UseSQLVisibility() { - saTypeMap = searchattribute.TestEsNameTypeMap() clusterConfig.ESConfig = &esclient.Config{ Indices: map[string]string{ esclient.VisibilityAppName: RandomizeStr("temporal_visibility_v1_test"), @@ -236,7 +250,7 @@ func newClusterWithPersistenceTestBaseFactory(t *testing.T, clusterConfig *TestC DisableGzip: true, // lowers memory and CPU usage significantly in tests } - err := setupIndex(clusterConfig.ESConfig, logger) + err = setupIndex(clusterConfig.ESConfig, logger) if err != nil { return nil, err } @@ -245,18 +259,12 @@ func newClusterWithPersistenceTestBaseFactory(t *testing.T, clusterConfig *TestC pConfig.DataStores[pConfig.VisibilityStore] = config.DataStore{ Elasticsearch: clusterConfig.ESConfig, } - indexName = clusterConfig.ESConfig.GetVisibilityIndex() esClient, err = esclient.NewClient(clusterConfig.ESConfig, nil, logger) if err != nil { return nil, err } } else { - saTypeMap = searchattribute.TestNameTypeMap() clusterConfig.ESConfig = nil - storeConfig := pConfig.DataStores[pConfig.VisibilityStore] - if storeConfig.SQL != nil { - indexName = storeConfig.SQL.DatabaseName - } } clusterInfoMap := make(map[string]cluster.ClusterInformation) @@ -276,19 +284,28 @@ func newClusterWithPersistenceTestBaseFactory(t *testing.T, clusterConfig *TestC ClusterAddress: clusterInfo.RPCAddress, HttpAddress: clusterInfo.HTTPAddress, InitialFailoverVersion: clusterInfo.InitialFailoverVersion, - }}) + }}, + ) if err != nil { return nil, err } } clusterMetadataConfig.ClusterInformation = clusterInfoMap - // This will save custom test search attributes to cluster metadata. - // Actual Elasticsearch fields are created in setupIndex. - err := testBase.SearchAttributesManager.SaveSearchAttributes( - context.Background(), - indexName, - saTypeMap.Custom(), + cfg := &config.Config{ + Persistence: pConfig, + ClusterMetadata: clusterMetadataConfig, + Visibility: config.Visibility{}, + } + clusterMetadataConfig, pConfig, err = temporal.ApplyClusterMetadataConfigProvider( + logger, + cfg, + resolver.NewNoopResolver(), + persistenceclient.FactoryProvider, + testBase.AbstractDataStoreFactory, + testBase.VisibilityStoreFactory, + metrics.NoopMetricsHandler, + serialization.NewSerializer(), ) if err != nil { return nil, err @@ -322,7 +339,7 @@ func newClusterWithPersistenceTestBaseFactory(t *testing.T, clusterConfig *TestC MatchingConfig: clusterConfig.MatchingConfig, WorkerConfig: clusterConfig.WorkerConfig, MockAdminClient: clusterConfig.MockAdminClient, - NamespaceReplicationTaskExecutor: nsreplication.NewTaskExecutor(clusterConfig.ClusterMetadata.CurrentClusterName, testBase.MetadataManager, logger), + NamespaceReplicationTaskExecutor: nsreplication.NewTaskExecutor(clusterConfig.ClusterMetadata.CurrentClusterName, testBase.MetadataManager, nsreplication.NewNoopDataMerger(), logger), DynamicConfigOverrides: clusterConfig.DynamicConfigOverrides, TLSConfigProvider: tlsConfigProvider, ServiceFxOptions: clusterConfig.ServiceFxOptions, @@ -417,10 +434,6 @@ func setupIndex(esConfig *esclient.Config, logger log.Logger) error { logger.Info("Index created.", tag.ESIndex(esConfig.GetVisibilityIndex())) logger.Info("Add custom search attributes for tests.") - _, err = esClient.PutMapping(ctx, esConfig.GetVisibilityIndex(), searchattribute.TestEsNameTypeMap().Custom()) - if err != nil { - return err - } if err := waitForYellowStatus(esClient, esConfig.GetVisibilityIndex()); err != nil { return err } @@ -464,12 +477,18 @@ func newPProfInitializerImpl(logger log.Logger, port int) *pprof.PProfInitialize } } -func newArchiverBase(enabled bool, executionManager persistence.ExecutionManager, logger log.Logger) *ArchiverBase { +func newArchiverBase( + enabled bool, + customHistoryArchiverFactory provider.CustomHistoryArchiverFactory, + customVisibilityArchiverFactory provider.CustomVisibilityArchiverFactory, + executionManager persistence.ExecutionManager, + logger log.Logger, +) *ArchiverBase { dcCollection := dynamicconfig.NewNoopCollection() if !enabled { return &ArchiverBase{ metadata: archiver.NewArchivalMetadata(dcCollection, "", false, "", false, &config.ArchivalNamespaceDefaults{}), - provider: provider.NewArchiverProvider(nil, nil, nil, logger, metrics.NoopMetricsHandler), + provider: provider.NewArchiverProvider(nil, nil, nil, nil, nil, logger, metrics.NoopMetricsHandler), } } @@ -492,6 +511,8 @@ func newArchiverBase(enabled bool, executionManager persistence.ExecutionManager &config.VisibilityArchiverProvider{ Filestore: cfg, }, + customHistoryArchiverFactory, + customVisibilityArchiverFactory, executionManager, logger, metrics.NoopMetricsHandler, @@ -564,6 +585,11 @@ func (tc *TestCluster) MatchingClient() matchingservice.MatchingServiceClient { return tc.host.MatchingClient() } +// SchedulerClient returns a scheduler client from the test cluster +func (tc *TestCluster) SchedulerClient() schedulerpb.SchedulerServiceClient { + return tc.host.SchedulerClient() +} + // ExecutionManager returns an execution manager factory from the test cluster func (tc *TestCluster) ExecutionManager() persistence.ExecutionManager { return tc.host.GetExecutionManager() diff --git a/tests/testcore/test_data_converter.go b/tests/testcore/test_data_converter.go index 22d60191a6e..30a0cf6b249 100644 --- a/tests/testcore/test_data_converter.go +++ b/tests/testcore/test_data_converter.go @@ -26,7 +26,7 @@ func NewTestDataConverter() converter.DataConverter { return &TestDataConverter{} } -func (tdc *TestDataConverter) ToPayloads(values ...interface{}) (*commonpb.Payloads, error) { +func (tdc *TestDataConverter) ToPayloads(values ...any) (*commonpb.Payloads, error) { tdc.NumOfCallToPayloads++ result := &commonpb.Payloads{} for i, value := range values { @@ -40,7 +40,7 @@ func (tdc *TestDataConverter) ToPayloads(values ...interface{}) (*commonpb.Paylo return result, nil } -func (tdc *TestDataConverter) FromPayloads(payloads *commonpb.Payloads, valuePtrs ...interface{}) error { +func (tdc *TestDataConverter) FromPayloads(payloads *commonpb.Payloads, valuePtrs ...any) error { tdc.NumOfCallFromPayloads++ for i, p := range payloads.GetPayloads() { err := tdc.FromPayload(p, valuePtrs[i]) @@ -51,7 +51,7 @@ func (tdc *TestDataConverter) FromPayloads(payloads *commonpb.Payloads, valuePtr return nil } -func (tdc *TestDataConverter) ToPayload(value interface{}) (*commonpb.Payload, error) { +func (tdc *TestDataConverter) ToPayload(value any) (*commonpb.Payload, error) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) if err := enc.Encode(value); err != nil { @@ -66,7 +66,7 @@ func (tdc *TestDataConverter) ToPayload(value interface{}) (*commonpb.Payload, e return p, nil } -func (tdc *TestDataConverter) FromPayload(payload *commonpb.Payload, valuePtr interface{}) error { +func (tdc *TestDataConverter) FromPayload(payload *commonpb.Payload, valuePtr any) error { encoding, ok := payload.GetMetadata()["encoding"] if !ok { return ErrEncodingIsNotSet @@ -100,7 +100,7 @@ func (tdc *TestDataConverter) ToString(payload *commonpb.Payload) string { return ErrEncodingIsNotSupported.Error() } - var value interface{} + var value any err := decodeGob(payload, &value) if err != nil { return err.Error() @@ -109,7 +109,7 @@ func (tdc *TestDataConverter) ToString(payload *commonpb.Payload) string { return fmt.Sprintf("%+v", value) } -func decodeGob(payload *commonpb.Payload, valuePtr interface{}) error { +func decodeGob(payload *commonpb.Payload, valuePtr any) error { dec := gob.NewDecoder(bytes.NewBuffer(payload.GetData())) return dec.Decode(valuePtr) } diff --git a/tests/testcore/test_env.go b/tests/testcore/test_env.go index f30d258d83b..043a7a06a12 100644 --- a/tests/testcore/test_env.go +++ b/tests/testcore/test_env.go @@ -1,6 +1,7 @@ package testcore import ( + "context" _ "embed" "fmt" "os" @@ -8,17 +9,24 @@ import ( "strings" "sync" "testing" + "time" "github.com/dgryski/go-farm" "github.com/stretchr/testify/require" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" + sdkclient "go.temporal.io/sdk/client" + sdkworker "go.temporal.io/sdk/worker" + "go.temporal.io/server/api/adminservice/v1" + "go.temporal.io/server/common/debug" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" - "go.temporal.io/server/common/testing/historyrequire" "go.temporal.io/server/common/testing/taskpoller" + "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/testing/testvars" + "go.temporal.io/server/components/nexusoperations" + "google.golang.org/grpc" ) // shardSalt is used to distribute functional tests across shards. @@ -28,24 +36,32 @@ import ( var shardSalt string var ( - _ Env = (*testEnv)(nil) - sequentialSuites sync.Map // map[string]*sequentialSuite + _ Env = (*TestEnv)(nil) + defaultTestTimeout = 90 * time.Second * debug.TimeoutMultiplier ) type Env interface { + // T returns the *testing.T. Deprecated: use the suite's T() method instead. T() *testing.T Namespace() namespace.Name NamespaceID() namespace.ID FrontendClient() workflowservice.WorkflowServiceClient + AdminClient() adminservice.AdminServiceClient GetTestCluster() *TestCluster CloseShard(namespaceID string, workflowID string) OverrideDynamicConfig(setting dynamicconfig.GenericSetting, value any) (cleanup func()) + Context() context.Context + InjectHook(hook testhooks.Hook) (cleanup func()) } -type testEnv struct { +type TestEnv struct { *FunctionalTestBase + + // Shadows FunctionalTestBase.Assertions with a per-test instance bound to + // this TestEnv's own *testing.T, avoiding data races when parallel tests + // share the same *FunctionalTestBase cluster. + // TODO: remove once all tests are migrated to TestEnv (and no longer use FunctionalTestBase directly). *require.Assertions - historyrequire.HistoryRequire Logger log.Logger @@ -55,6 +71,13 @@ type testEnv struct { taskPoller *taskpoller.TaskPoller t *testing.T tv *testvars.TestVars + ctx context.Context + + sdkClientOnce sync.Once + sdkClient sdkclient.Client + sdkWorkerOnce sync.Once + sdkWorker sdkworker.Worker + sdkWorkerTQ string } type TestOption func(*testOptions) @@ -62,6 +85,7 @@ type TestOption func(*testOptions) type testOptions struct { dedicatedCluster bool dynamicConfigSettings []dynamicConfigOverride + timeout time.Duration } type dynamicConfigOverride struct { @@ -77,6 +101,12 @@ func WithDedicatedCluster() TestOption { } } +// Deprecated: this option is no longer required and will be removed once all callers have been updated. +func WithSdkWorker() TestOption { + return func(o *testOptions) { + } +} + // WithDynamicConfig overrides a dynamic config setting for the test. // For settings that can be namespace-scoped, a namespace constraint is applied. // For all others that require a dedicated cluster, this implies `WithDedicatedCluster`. @@ -92,85 +122,43 @@ func WithDynamicConfig(setting dynamicconfig.GenericSetting, value any) TestOpti } } -// sequentialSuite holds state for a suite marked with MustRunSequential. -// It manages a single dedicated cluster shared by all tests in the suite. -type sequentialSuite struct { - cluster *FunctionalTestBase -} - -// MustRunSequential marks a test suite to run its tests sequentially instead -// of in parallel. Call this at the start of your test suite before any -// subtests are created. A single dedicated cluster will be created for this -// suite and torn down when the suite completes. -func MustRunSequential(t *testing.T, reason string) { - if strings.Contains(t.Name(), "/") { - panic("MustRunSequential must be called from a top-level test, not a subtest") - } - if reason == "" { - panic("MustRunSequential requires a reason") - } - - // Create a dedicated cluster for this suite. - suite := &sequentialSuite{ - cluster: testClusterPool.createCluster(t, nil, false), +// WithTimeout sets a custom timeout for the test. The test will fail if it runs longer +// than this duration. The timeout is multiplied by debug.TimeoutMultiplier when debugging. +// The TEMPORAL_TEST_TIMEOUT environment variable can also set the default timeout in seconds. +func WithTimeout(duration time.Duration) TestOption { + return func(o *testOptions) { + o.timeout = duration } - sequentialSuites.Store(t.Name(), suite) - - // Register cleanup to tear down the suite's cluster when the parent test completes. - t.Cleanup(func() { - sequentialSuites.Delete(t.Name()) - if err := suite.cluster.testCluster.TearDownCluster(); err != nil { - t.Logf("Failed to tear down sequential suite cluster: %v", err) - } - }) } // NewEnv creates a new test environment with access to a Temporal cluster. -// -// By default, tests are marked as parallel. Use MustRunSequential on the -// test's parent `testing.T` to run them sequentially instead. -func NewEnv(t *testing.T, opts ...TestOption) *testEnv { +func NewEnv(t *testing.T, opts ...TestOption) *TestEnv { + t.Helper() + // Check test sharding early, before any expensive operations. checkTestShard(t) - // Check if this is a sequential suite by looking up the parent test name. - suiteName := t.Name() - if idx := strings.Index(suiteName, "/"); idx != -1 { - suiteName = suiteName[:idx] - } - suiteVal, sequential := sequentialSuites.Load(suiteName) - if !sequential { - t.Parallel() - } - var options testOptions for _, opt := range opts { opt(&options) } - var base *FunctionalTestBase - if sequential { - // Sequential suites use a single dedicated cluster for all tests. - suite := suiteVal.(*sequentialSuite) - base = suite.cluster - base.SetT(t) - } else { - // For dedicated clusters, pass all dynamic config settings at cluster creation. - var startupConfig map[dynamicconfig.Key]any - if options.dedicatedCluster && len(options.dynamicConfigSettings) > 0 { - startupConfig = make(map[dynamicconfig.Key]any, len(options.dynamicConfigSettings)) - for _, override := range options.dynamicConfigSettings { - startupConfig[override.setting.Key()] = override.value - } + // For dedicated clusters, pass all dynamic config settings at cluster creation. + var startupConfig map[dynamicconfig.Key]any + if options.dedicatedCluster && len(options.dynamicConfigSettings) > 0 { + startupConfig = make(map[dynamicconfig.Key]any, len(options.dynamicConfigSettings)) + for _, override := range options.dynamicConfigSettings { + startupConfig[override.setting.Key()] = override.value } - - // Obtain the test cluster from the pool. - base = testClusterPool.get(t, options.dedicatedCluster, startupConfig) } + + // Obtain the test cluster from the pool. + base := testClusterPool.get(t, options.dedicatedCluster, startupConfig) cluster := base.GetTestCluster() // Create a dedicated namespace for the test to help with test isolation. - ns := namespace.Name(RandomizeStr(t.Name())) + baseName := strings.ReplaceAll(t.Name(), "/", "-") + ns := namespace.Name(RandomizeStr(baseName)) nsID, err := base.RegisterNamespace( ns, 1, // 1 day retention @@ -182,10 +170,9 @@ func NewEnv(t *testing.T, opts ...TestOption) *testEnv { t.Fatalf("Failed to register namespace: %v", err) } - env := &testEnv{ + env := &TestEnv{ FunctionalTestBase: base, Assertions: require.New(t), - HistoryRequire: historyrequire.New(t), cluster: cluster, nsName: ns, nsID: nsID, @@ -193,8 +180,16 @@ func NewEnv(t *testing.T, opts ...TestOption) *testEnv { taskPoller: taskpoller.New(t, cluster.FrontendClient(), ns.String()), t: t, tv: testvars.New(t), + ctx: setupTestTimeoutWithContext(t, options.timeout), + sdkWorkerTQ: RandomizeStr("tq-" + t.Name()), } + // Set Nexus callback URL now that we have the cluster's HTTP address. Note that we set + // a default for the global config here so callers that rely on this can still use a shared cluster. + env.FunctionalTestBase.OverrideDynamicConfig( + nexusoperations.CallbackURLTemplate, + "http://"+env.HttpAPIAddress()+"/namespaces/{{.NamespaceName}}/nexus/callback") + // For shared clusters, apply all dynamic config settings as overrides. if !options.dedicatedCluster && len(options.dynamicConfigSettings) > 0 { for _, override := range options.dynamicConfigSettings { @@ -206,30 +201,133 @@ func NewEnv(t *testing.T, opts ...TestOption) *testEnv { } // Use test env-specific namespace here for test isolation. -func (e *testEnv) Namespace() namespace.Name { +func (e *TestEnv) Namespace() namespace.Name { return e.nsName } -func (e *testEnv) NamespaceID() namespace.ID { +func (e *TestEnv) NamespaceID() namespace.ID { return e.nsID } -func (e *testEnv) TaskPoller() *taskpoller.TaskPoller { +// InjectHook sets a test hook inside the cluster. +// +// It auto-detects the scope from the hook: +// - For namespace-scoped hooks: scopes it to the test's namespace +// - For global hooks: requires a dedicated cluster (fails early if used on shared cluster) +func (e *TestEnv) InjectHook(hook testhooks.Hook) (cleanup func()) { + var scope any + switch hook.Scope() { + case testhooks.ScopeNamespace: + scope = e.nsID + case testhooks.ScopeGlobal: + if e.isShared { + e.t.Fatal("InjectHook: global hooks require a dedicated cluster; use testcore.WithDedicatedCluster()") + } + scope = testhooks.GlobalScope + default: + e.t.Fatalf("InjectHook: unknown scope %v", hook.Scope()) + } + return e.cluster.host.injectHook(e.t, hook, scope) +} + +func (e *TestEnv) TaskPoller() *taskpoller.TaskPoller { return e.taskPoller } -func (e *testEnv) T() *testing.T { +// NoError asserts that err is nil. +// Deprecated: use require.NoError with the parent test or suite instead. +// TODO: remove once all tests are migrated to TestEnv (and no longer use FunctionalTestBase directly). +func (e *TestEnv) NoError(err error, msgAndArgs ...any) { + e.Assertions.NoError(err, msgAndArgs...) +} + +// Error asserts that err is not nil. +// Deprecated: use require.Error with the parent test or suite instead. +// TODO: remove once all tests are migrated to TestEnv (and no longer use FunctionalTestBase directly). +func (e *TestEnv) Error(err error, msgAndArgs ...any) { + e.Assertions.Error(err, msgAndArgs...) +} + +// Run executes a subtest. +// Deprecated: use the suite's Run method instead. +// TODO: remove once all tests are migrated to TestEnv (and no longer use FunctionalTestBase directly). +func (e *TestEnv) Run(name string, subtest func()) bool { + return e.FunctionalTestBase.Run(name, subtest) +} + +// T returns the *testing.T. Deprecated: use the suite's T() method instead. +func (e *TestEnv) T() *testing.T { return e.t } -func (e *testEnv) Tv() *testvars.TestVars { +func (e *TestEnv) Tv() *testvars.TestVars { return e.tv } +// Context returns the test-level timeout context with RPC version headers already included. +// This context will be canceled when the test timeout occurs. Use this directly for all RPC +// operations - no need to wrap with NewContext or add headers manually. +// +// For custom timeouts, use: +// +// ctx, cancel := context.WithTimeout(env.Context(), 10*time.Second) +// defer cancel() +func (e *TestEnv) Context() context.Context { + return e.ctx +} + +// SdkClient returns the SDK client. It is lazily initialized on the first call. +func (e *TestEnv) SdkClient() sdkclient.Client { + e.sdkClientOnce.Do(func() { + clientOptions := sdkclient.Options{ + HostPort: e.FrontendGRPCAddress(), + Namespace: e.nsName.String(), + Logger: log.NewSdkLogger(e.Logger), + } + + if provider := e.cluster.host.tlsConfigProvider; provider != nil { + clientOptions.ConnectionOptions.TLS = provider.FrontendClientConfig + } + + if interceptor := e.cluster.host.grpcClientInterceptor; interceptor != nil { + clientOptions.ConnectionOptions.DialOptions = []grpc.DialOption{ + grpc.WithUnaryInterceptor(interceptor.Unary()), + grpc.WithStreamInterceptor(interceptor.Stream()), + } + } + + var err error + e.sdkClient, err = sdkclient.Dial(clientOptions) + if err != nil { + e.t.Fatalf("Failed to create SDK client: %v", err) + } + e.t.Cleanup(func() { e.sdkClient.Close() }) + }) + return e.sdkClient +} + +// SdkWorker returns the SDK worker. It is lazily initialized on the first call. +func (e *TestEnv) SdkWorker() sdkworker.Worker { + e.sdkWorkerOnce.Do(func() { + client := e.SdkClient() // Ensure client is initialized + e.sdkWorker = sdkworker.New(client, e.sdkWorkerTQ, sdkworker.Options{}) + if err := e.sdkWorker.Start(); err != nil { + e.t.Fatalf("Failed to start SDK worker: %v", err) + } + e.t.Cleanup(func() { e.sdkWorker.Stop() }) + }) + return e.sdkWorker +} + +// WorkerTaskQueue returns the task queue name used by the SDK Worker. +func (e *TestEnv) WorkerTaskQueue() string { + return e.sdkWorkerTQ +} + // OverrideDynamicConfig overrides a dynamic config setting for the duration of this test. // For settings that can be namespace-scoped, a namespace constraint is applied. // All others cannot be applied to a shared cluster and require `WithDedicatedCluster`. -func (e *testEnv) OverrideDynamicConfig(setting dynamicconfig.GenericSetting, value any) (cleanup func()) { +func (e *TestEnv) OverrideDynamicConfig(setting dynamicconfig.GenericSetting, value any) (cleanup func()) { if e.isShared { if !canBeNamespaceScoped(setting.Precedence()) { e.t.Fatalf("OverrideDynamicConfig for setting %s (precedence %v) cannot be called on a shared cluster; use testcore.WithDedicatedCluster()", setting.Key(), setting.Precedence()) @@ -254,6 +352,44 @@ func (e *testEnv) OverrideDynamicConfig(setting dynamicconfig.GenericSetting, va return e.cluster.host.overrideDynamicConfig(e.t, setting.Key(), value) } +// StartGlobalMetricCapture starts a cluster-global metrics capture for this test and automatically stops it during cleanup. +// Metric capture is cluster-global, so it is only safe on dedicated clusters. +// Misuse detection is best-effort and only applies to queried metrics that produced recordings. +func (e *TestEnv) StartGlobalMetricCapture() *GlobalMetricCapture { + if e.isShared { + e.t.Fatal("StartGlobalMetricCapture cannot be called on a shared cluster; use testcore.WithDedicatedCluster()") + } + + handler := e.cluster.host.CaptureMetricsHandler() + if handler == nil { + e.t.Fatal("StartGlobalMetricCapture is unavailable because metrics capture is not enabled on this cluster") + } + + capture := handler.StartCapture() + globalCapture := newGlobalMetricCapture(capture) + e.t.Cleanup(func() { + defer handler.StopCapture(capture) + globalCapture.checkForNamespaceCaptureMisuse() + }) + return globalCapture +} + +// StartNamespaceMetricCapture starts a metrics capture that only allows safe per-metric namespace filtering. +// Namespace captures are safe on shared clusters because reads are restricted to +// per-metric namespace-filtered iteration and reject non-namespaced metrics. +func (e *TestEnv) StartNamespaceMetricCapture() *NamespaceMetricCapture { + handler := e.cluster.host.CaptureMetricsHandler() + if handler == nil { + e.t.Fatal("StartNamespaceMetricCapture is unavailable because metrics capture is not enabled on this cluster") + } + + capture := handler.StartCapture() + e.t.Cleanup(func() { + handler.StopCapture(capture) + }) + return newNamespaceMetricCapture(capture, e.Namespace().String()) +} + func canBeNamespaceScoped(p dynamicconfig.Precedence) bool { switch p { case dynamicconfig.PrecedenceNamespace, diff --git a/tests/testcore/utils.go b/tests/testcore/utils.go index b739e2e4884..608e05221d9 100644 --- a/tests/testcore/utils.go +++ b/tests/testcore/utils.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" historypb "go.temporal.io/api/history/v1" + "go.temporal.io/sdk/converter" "go.temporal.io/server/api/adminservice/v1" historyspb "go.temporal.io/server/api/history/v1" "go.temporal.io/server/api/historyservice/v1" @@ -59,6 +60,16 @@ func EventBatchesToVersionHistory( return versionHistory, nil } +// MustToPayload converts a value to a Payload using the default data converter. +func MustToPayload(t require.TestingT, v any) *commonpb.Payload { + if th, ok := t.(interface{ Helper() }); ok { + th.Helper() + } + payload, err := converter.GetDefaultDataConverter().ToPayload(v) + require.NoError(t, err) + return payload +} + func RandomizedNexusEndpoint(name string) string { re := regexp.MustCompile("[/_]") safeName := re.ReplaceAllString(name, "-") diff --git a/tests/testutils/tls.go b/tests/testutils/tls.go index ce3161af13f..e39404a085a 100644 --- a/tests/testutils/tls.go +++ b/tests/testutils/tls.go @@ -74,7 +74,7 @@ func GenerateTestCerts(tempDir string, commonName string, num int) ([]*tls.Certi } chains := make([]*tls.Certificate, num) - for i := 0; i < num; i++ { + for i := range num { certPubFile := tempDir + fmt.Sprintf("/cert_pub_%d.pem", i) certPrivFile := tempDir + fmt.Sprintf("/cert_priv_%d.pem", i) cert, err := GenerateServerCert(caCert, commonName, int64(i+100), certPubFile, certPrivFile) diff --git a/tests/transient_task_test.go b/tests/transient_task_test.go index bac2a20d20b..9ab4b269a04 100644 --- a/tests/transient_task_test.go +++ b/tests/transient_task_test.go @@ -8,7 +8,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -18,20 +17,22 @@ import ( "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" ) type TransientTaskSuite struct { - testcore.FunctionalTestBase + parallelsuite.Suite[*TransientTaskSuite] } func TestTransientTaskSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(TransientTaskSuite)) + parallelsuite.Run(t, &TransientTaskSuite{}) } func (s *TransientTaskSuite) TestTransientWorkflowTaskTimeout() { + env := testcore.NewEnv(s.T()) + id := "functional-transient-workflow-task-timeout-test" wt := "functional-transient-workflow-task-timeout-test-type" tl := "functional-transient-workflow-task-timeout-test-taskqueue" @@ -40,7 +41,7 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskTimeout() { // Start workflow execution request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -50,9 +51,9 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskTimeout() { Identity: identity, } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + we, err0 := env.FrontendClient().StartWorkflowExecution(env.Context(), request) + env.NoError(err0) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) workflowExecution := &commonpb.WorkflowExecution{ WorkflowId: id, @@ -87,40 +88,42 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskTimeout() { } poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: identity, WorkflowTaskHandler: wtHandler, ActivityTaskHandler: nil, - Logger: s.Logger, - T: s.T(), + Logger: env.Logger, + T: env.T(), } // First workflow task immediately fails and schedules a transient workflow task _, err := poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) // Now send a signal when transient workflow task is scheduled - err = s.SendSignal(s.Namespace().String(), workflowExecution, "signalA", nil, identity) - s.NoError(err, "failed to send signal to execution") + err = env.SendSignal(env.Namespace().String(), workflowExecution, "signalA", nil, identity) + env.NoError(err, "failed to send signal to execution") // Drop workflow task to cause a workflow task timeout _, err = poller.PollAndProcessWorkflowTask(testcore.WithDumpHistory, testcore.WithDropTask) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) // Now process signal and complete workflow execution _, err = poller.PollAndProcessWorkflowTask(testcore.WithDumpHistory, testcore.WithExpectedAttemptCount(2)) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) - s.Equal(1, signalCount) - s.True(workflowComplete) + env.Equal(1, signalCount) + env.True(workflowComplete) } func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { + env := testcore.NewEnv(s.T()) + id := "functional-transient-workflow-task-history-size-test" wt := "functional-transient-workflow-task-history-size-test-type" tl := "functional-transient-workflow-task-history-size-test-taskqueue" @@ -129,7 +132,7 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { // Start workflow execution request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -139,9 +142,9 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { Identity: identity, } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + we, err0 := env.FrontendClient().StartWorkflowExecution(env.Context(), request) + env.NoError(err0) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) workflowExecution := &commonpb.WorkflowExecution{ WorkflowId: id, @@ -149,7 +152,7 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { } // start with 20kb limit - s.OverrideDynamicConfig(dynamicconfig.HistorySizeSuggestContinueAsNew, 20*1024) + env.OverrideDynamicConfig(dynamicconfig.HistorySizeSuggestContinueAsNew, 20*1024) // workflow logic stage := 0 @@ -166,15 +169,15 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { // find workflow task started event event := task.History.Events[len(task.History.Events)-1] - s.Equal(event.GetEventType(), enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED) + env.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED, event.GetEventType()) attrs := event.GetWorkflowTaskStartedEventAttributes() - s.Logger.Info("wtHandler", tag.Counter(stage)) + env.Logger.Info("wtHandler", tag.Counter(stage)) stage++ switch stage { case 1: - s.Less(attrs.HistorySizeBytes, int64(10*1024)) - s.False(attrs.SuggestContinueAsNew) + env.Less(attrs.HistorySizeBytes, int64(10*1024)) + env.False(attrs.SuggestContinueAsNew) // record a large marker sawFields = append(sawFields, fields{size: attrs.HistorySizeBytes, suggest: attrs.SuggestContinueAsNew}) return []*commandpb.Command{{ @@ -186,8 +189,8 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { }}, nil case 2: - s.Greater(attrs.HistorySizeBytes, int64(10*1024)) - s.False(attrs.SuggestContinueAsNew) + env.Greater(attrs.HistorySizeBytes, int64(10*1024)) + env.False(attrs.SuggestContinueAsNew) // record another large marker sawFields = append(sawFields, fields{size: attrs.HistorySizeBytes, suggest: attrs.SuggestContinueAsNew}) return []*commandpb.Command{{ @@ -199,26 +202,26 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { }}, nil case 3: - s.Greater(attrs.HistorySizeBytes, int64(20*1024)) - s.True(attrs.SuggestContinueAsNew) + env.Greater(attrs.HistorySizeBytes, int64(20*1024)) + env.True(attrs.SuggestContinueAsNew) failedTaskSawSize = attrs.HistorySizeBytes // fail workflow task and we'll get a transient one return nil, errors.New("oops") //nolint:err113 case 4: // we might not get the same value but it shouldn't be smaller, and not too much larger - s.GreaterOrEqual(attrs.HistorySizeBytes, failedTaskSawSize) - s.Less(attrs.HistorySizeBytes, failedTaskSawSize+10000) - s.False(attrs.SuggestContinueAsNew) + env.GreaterOrEqual(attrs.HistorySizeBytes, failedTaskSawSize) + env.Less(attrs.HistorySizeBytes, failedTaskSawSize+10000) + env.False(attrs.SuggestContinueAsNew) sawFields = append(sawFields, fields{size: attrs.HistorySizeBytes, suggest: attrs.SuggestContinueAsNew}) return nil, nil case 5: // we should get just a little larger prevSize := sawFields[len(sawFields)-1].size - s.Greater(attrs.HistorySizeBytes, prevSize) - s.Less(attrs.HistorySizeBytes, prevSize+10000) - s.False(attrs.SuggestContinueAsNew) // now false + env.Greater(attrs.HistorySizeBytes, prevSize) + env.Less(attrs.HistorySizeBytes, prevSize+10000) + env.False(attrs.SuggestContinueAsNew) // now false workflowComplete = true sawFields = append(sawFields, fields{size: attrs.HistorySizeBytes, suggest: attrs.SuggestContinueAsNew}) @@ -234,60 +237,60 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { } poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: identity, WorkflowTaskHandler: wtHandler, ActivityTaskHandler: nil, - Logger: s.Logger, - T: s.T(), + Logger: env.Logger, + T: env.T(), } // stage 1 _, err := poller.PollAndProcessWorkflowTask(testcore.WithNoDumpCommands) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) - err = s.SendSignal(s.Namespace().String(), workflowExecution, "signal", nil, identity) - s.NoError(err, "failed to send signal to execution") + err = env.SendSignal(env.Namespace().String(), workflowExecution, "signal", nil, identity) + env.NoError(err, "failed to send signal to execution") // stage 2 _, err = poller.PollAndProcessWorkflowTask(testcore.WithNoDumpCommands) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) - err = s.SendSignal(s.Namespace().String(), workflowExecution, "signal", nil, identity) - s.NoError(err, "failed to send signal to execution") + err = env.SendSignal(env.Namespace().String(), workflowExecution, "signal", nil, identity) + env.NoError(err, "failed to send signal to execution") // stage 3: this one fails with a panic _, err = poller.PollAndProcessWorkflowTask(testcore.WithNoDumpCommands) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) // change the dynamic config so that SuggestContinueAsNew should now be false. the current // workflow task should still see true, but the next one will see false. - s.OverrideDynamicConfig(dynamicconfig.HistorySizeSuggestContinueAsNew, 8*1024*1024) + env.OverrideDynamicConfig(dynamicconfig.HistorySizeSuggestContinueAsNew, 8*1024*1024) // stage 4: transient task may not be immediately available after stage 3 failure // Increase retries to handle asynchronous task creation timing _, err = poller.PollAndProcessWorkflowTask(testcore.WithNoDumpCommands, testcore.WithRetries(3)) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) - err = s.SendSignal(s.Namespace().String(), workflowExecution, "signal", nil, identity) - s.NoError(err, "failed to send signal to execution") + err = env.SendSignal(env.Namespace().String(), workflowExecution, "signal", nil, identity) + env.NoError(err, "failed to send signal to execution") // drop workflow task to cause a workflow task timeout // Use increased retries to ensure we successfully poll and drop the task _, err = poller.PollAndProcessWorkflowTask(testcore.WithDropTask, testcore.WithNoDumpCommands, testcore.WithRetries(3)) - s.Logger.Info("PollAndProcessWorkflowTask (drop)", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask (drop)", tag.Error(err)) + env.NoError(err) // Wait for the workflow task timeout to actually fire before polling for stage 5 // With 10s timeout, poll for up to 15s to ensure timeout has occurred - s.EventuallyWithT(func(c *assert.CollectT) { - events := s.GetHistory(s.Namespace().String(), workflowExecution) + env.EventuallyWithT(func(c *assert.CollectT) { + events := env.GetHistory(env.Namespace().String(), workflowExecution) // Look for WorkflowTaskTimedOut event (should be event 21) timedOutFound := false for _, event := range events { @@ -301,18 +304,18 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { // stage 5: process task after timeout and complete workflow _, err = poller.PollAndProcessWorkflowTask(testcore.WithNoDumpCommands, testcore.WithRetries(3)) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) - s.True(workflowComplete) + env.True(workflowComplete) var sawFieldsFlat []any for _, f := range sawFields { sawFieldsFlat = append(sawFieldsFlat, f.size, f.suggest) } - allEvents := s.GetHistory(s.Namespace().String(), workflowExecution) - s.EqualHistoryEvents(fmt.Sprintf(` + allEvents := env.GetHistory(env.Namespace().String(), workflowExecution) + env.EqualHistoryEvents(fmt.Sprintf(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted {"HistorySizeBytes":%d, "SuggestContinueAsNew":%t} @@ -341,6 +344,8 @@ func (s *TransientTaskSuite) TestTransientWorkflowTaskHistorySize() { } func (s *TransientTaskSuite) TestNoTransientWorkflowTaskAfterFlushBufferedEvents() { + env := testcore.NewEnv(s.T()) + id := "functional-no-transient-workflow-task-after-flush-buffered-events-test" wt := "functional-no-transient-workflow-task-after-flush-buffered-events-test-type" tl := "functional-no-transient-workflow-task-after-flush-buffered-events-test-taskqueue" @@ -349,7 +354,7 @@ func (s *TransientTaskSuite) TestNoTransientWorkflowTaskAfterFlushBufferedEvents // Start workflow execution request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -359,10 +364,10 @@ func (s *TransientTaskSuite) TestNoTransientWorkflowTaskAfterFlushBufferedEvents Identity: identity, } - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err0) + we, err0 := env.FrontendClient().StartWorkflowExecution(env.Context(), request) + env.NoError(err0) - s.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) + env.Logger.Info("StartWorkflowExecution", tag.WorkflowRunID(we.RunId)) // workflow logic workflowComplete := false @@ -371,9 +376,9 @@ func (s *TransientTaskSuite) TestNoTransientWorkflowTaskAfterFlushBufferedEvents if !continueAsNewAndSignal { continueAsNewAndSignal = true // this will create new event when there is in-flight workflow task, and the new event will be buffered - _, err := s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), + _, err := env.FrontendClient().SignalWorkflowExecution(env.Context(), &workflowservice.SignalWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowExecution: &commonpb.WorkflowExecution{ WorkflowId: id, }, @@ -381,7 +386,7 @@ func (s *TransientTaskSuite) TestNoTransientWorkflowTaskAfterFlushBufferedEvents Input: payloads.EncodeString("buffered-signal-input"), Identity: identity, }) - s.NoError(err) + env.NoError(err) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, @@ -405,28 +410,28 @@ func (s *TransientTaskSuite) TestNoTransientWorkflowTaskAfterFlushBufferedEvents } poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: identity, WorkflowTaskHandler: wtHandler, - Logger: s.Logger, - T: s.T(), + Logger: env.Logger, + T: env.T(), } // fist workflow task, this try to do a continue as new but there is a buffered event, // so it will fail and create a new workflow task _, err := poller.PollAndProcessWorkflowTask(testcore.WithDumpHistory) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.Error(err) - s.IsType(&serviceerror.InvalidArgument{}, err) - s.Equal("UnhandledCommand", err.Error()) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.Error(err) + env.ErrorAs(err, new(*serviceerror.InvalidArgument)) + env.Equal("UnhandledCommand", err.Error()) // second workflow task, which will complete the workflow // this expect the workflow task to have attempt == 1 _, err = poller.PollAndProcessWorkflowTask(testcore.WithDumpHistory, testcore.WithExpectedAttemptCount(1)) - s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NoError(err) + env.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) + env.NoError(err) - s.True(workflowComplete) + env.True(workflowComplete) } diff --git a/tests/update_workflow_sdk_test.go b/tests/update_workflow_sdk_test.go index ff66d2ef853..48ce8124797 100644 --- a/tests/update_workflow_sdk_test.go +++ b/tests/update_workflow_sdk_test.go @@ -3,7 +3,6 @@ package tests import ( "context" "errors" - "fmt" "testing" "time" @@ -54,7 +53,7 @@ func (s *UpdateWorkflowSdkSuite) TestTerminateWorkflowAfterUpdateAdmitted() { run := s.startWorkflow(ctx, tv, workflowFn) s.updateWorkflowWaitAdmitted(ctx, tv, "update-arg") - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) s.NoError(s.SdkClient().TerminateWorkflow(ctx, tv.WorkflowID(), run.GetRunID(), "reason")) @@ -90,7 +89,7 @@ func (s *UpdateWorkflowSdkSuite) TestTimeoutWorkflowAfterUpdateAccepted() { return unreachableErr } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) wfRun, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ ID: tv.WorkflowID(), @@ -154,7 +153,7 @@ func (s *UpdateWorkflowSdkSuite) TestTerminateWorkflowAfterUpdateAccepted() { return unreachableErr } - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) wfRun := s.startWorkflow(ctx, tv, workflowFn) // Wait for the first WFT to complete. @@ -235,9 +234,9 @@ func (s *UpdateWorkflowSdkSuite) TestContinueAsNewAfterUpdateAdmitted() { return workflow.NewContinueAsNewError(ctx, workflowFn2) } - s.Worker().RegisterWorkflow(workflowFn1) - s.Worker().RegisterWorkflow(workflowFn2) - s.Worker().RegisterActivity(sendUpdateActivityFn) + s.SdkWorker().RegisterWorkflow(workflowFn1) + s.SdkWorker().RegisterWorkflow(workflowFn2) + s.SdkWorker().RegisterActivity(sendUpdateActivityFn) var firstRun sdkclient.WorkflowRun firstRun = s.startWorkflow(rootCtx, tv, workflowFn1) @@ -323,7 +322,7 @@ func (s *UpdateWorkflowSdkSuite) TestTimeoutWithRetryAfterUpdateAdmitted() { s.ErrorAs(err, &canErr) // "start" worker for workflowFn. - s.Worker().RegisterWorkflow(workflowFn) + s.SdkWorker().RegisterWorkflow(workflowFn) var secondRunID string s.Eventually(func() bool { @@ -377,7 +376,7 @@ func (s *UpdateWorkflowSdkSuite) updateWorkflowWaitAdmitted(ctx context.Context, var notFoundErr *serviceerror.NotFound s.ErrorAs(err, ¬FoundErr) // poll beat send in race return false - }, 5*time.Second, 100*time.Millisecond, fmt.Sprintf("update %s did not reach Admitted stage", tv.UpdateID())) + }, 5*time.Second, 100*time.Millisecond, "update %s did not reach Admitted stage", tv.UpdateID()) } func (s *UpdateWorkflowSdkSuite) updateWorkflowWaitAccepted(ctx context.Context, tv *testvars.TestVars, arg string) (sdkclient.WorkflowUpdateHandle, error) { @@ -386,7 +385,7 @@ func (s *UpdateWorkflowSdkSuite) updateWorkflowWaitAccepted(ctx context.Context, WorkflowID: tv.WorkflowID(), RunID: tv.RunID(), UpdateName: tv.HandlerName(), - Args: []interface{}{arg}, + Args: []any{arg}, WaitForStage: sdkclient.WorkflowUpdateStageAccepted, }) } diff --git a/tests/update_workflow_test.go b/tests/update_workflow_test.go index ec430018744..1d643c689a2 100644 --- a/tests/update_workflow_test.go +++ b/tests/update_workflow_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -23,7 +22,7 @@ import ( "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/metrics" - "go.temporal.io/server/common/metrics/metricstest" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/testing/protoutils" "go.temporal.io/server/common/testing/taskpoller" "go.temporal.io/server/common/testing/testhooks" @@ -34,12 +33,12 @@ import ( ) func speculativeWorkflowTaskOutcomes( - snap map[string][]*metricstest.CapturedRecording, + capture *testcore.GlobalMetricCapture, ) (commits, rollbacks int) { - for range snap[metrics.SpeculativeWorkflowTaskCommits.Name()] { + for range capture.Metric(metrics.SpeculativeWorkflowTaskCommits.Name()) { commits += 1 } - for range snap[metrics.SpeculativeWorkflowTaskRollbacks.Name()] { + for range capture.Metric(metrics.SpeculativeWorkflowTaskRollbacks.Name()) { rollbacks += 1 } return @@ -58,7 +57,7 @@ func loseUpdateRegistryAndAbandonPendingUpdates(s testcore.Env, tv *testvars.Tes func closeShard(s testcore.Env, wid string) { s.T().Helper() - resp, err := s.FrontendClient().DescribeNamespace(testcore.NewContext(), &workflowservice.DescribeNamespaceRequest{ + resp, err := s.FrontendClient().DescribeNamespace(testcore.NewContext(s.Context()), &workflowservice.DescribeNamespaceRequest{ Namespace: s.Namespace().String(), }) if err != nil { @@ -68,523 +67,764 @@ func closeShard(s testcore.Env, wid string) { s.CloseShard(resp.NamespaceInfo.Id, wid) } +type WorkflowUpdateSuite struct { + parallelsuite.Suite[*WorkflowUpdateSuite] +} + func TestWorkflowUpdateSuite(t *testing.T) { - t.Run("EmptySpeculativeWorkflowTask_AcceptComplete", func(t *testing.T) { - testCases := []struct { - name string - useRunID bool - }{ - { - name: "with RunID", - useRunID: true, - }, - { - name: "without RunID", - useRunID: false, - }, - } + parallelsuite.Run(t, &WorkflowUpdateSuite{}) +} - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Uses CaptureMetricsHandler which requires a dedicated cluster to avoid metric interference. - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - runID := mustStartWorkflow(s, s.Tv()) +func (s *WorkflowUpdateSuite) TestEmptySpeculativeWorkflowTask_AcceptComplete() { + testCases := []struct { + name string + useRunID bool + }{ + { + name: "with RunID", + useRunID: true, + }, + { + name: "without RunID", + useRunID: false, + }, + } - tv := s.Tv() - if tc.useRunID { - tv = tv.WithRunID(runID) - } + for _, tc := range testCases { + s.Run(tc.name, func(s *WorkflowUpdateSuite) { + // Uses CaptureMetricsHandler which requires a dedicated cluster to avoid metric interference. + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + runID := mustStartWorkflow(env, env.Tv()) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + tv := env.Tv() + if tc.useRunID { + tv = tv.WithRunID(runID) + } - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT events are not written to the history yet. - 6 WorkflowTaskStarted -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } + capture := env.StartGlobalMetricCapture() + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT events are not written to the history yet. + 6 WorkflowTaskStarted + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(5, updRequestMsg.GetEventId()) + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(5, updRequestMsg.GetEventId()) - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - updateResultCh := sendUpdateNoError(s, tv) + updateResultCh := sendUpdateNoError(env, tv) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res.NewTask) + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) + + // Test non-blocking poll + for _, waitPolicy := range []*updatepb.WaitPolicy{{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED}, nil} { + pollUpdateResp, err := pollUpdate(env, env.Tv(), waitPolicy) s.NoError(err) - s.NotNil(res.NewTask) - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) - - // Test non-blocking poll - for _, waitPolicy := range []*updatepb.WaitPolicy{{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED}, nil} { - pollUpdateResp, err := pollUpdate(s, s.Tv(), waitPolicy) - s.NoError(err) - s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, pollUpdateResp.Stage) - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), pollUpdateResp.Outcome.GetSuccess())) - // Even if tv doesn't have RunID, it should be returned as part of UpdateRef. - s.Equal(runID, pollUpdateResp.UpdateRef.GetWorkflowExecution().RunId) - } + s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, pollUpdateResp.Stage) + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), pollUpdateResp.Outcome.GetSuccess())) + // Even if tv doesn't have RunID, it should be returned as part of UpdateRef. + s.Equal(runID, pollUpdateResp.UpdateRef.GetWorkflowExecution().RunId) + } - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - commits, rollbacks := speculativeWorkflowTaskOutcomes(capture.Snapshot()) - s.Equal(1, commits) - s.Equal(0, rollbacks) + commits, rollbacks := speculativeWorkflowTaskOutcomes(capture) + s.Equal(1, commits) + s.Equal(0, rollbacks) - events := s.GetHistory(s.Namespace().String(), tv.WorkflowExecution()) + events := env.GetHistory(env.Namespace().String(), tv.WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Was speculative WT... - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted // ...and events were written to the history when WT completes. - 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. - 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} -`, events) - }) - } - }) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Was speculative WT... + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted // ...and events were written to the history when WT completes. + 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. + 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} + `, events) + }) + } +} - t.Run("NotEmptySpeculativeWorkflowTask_AcceptComplete", func(t *testing.T) { - testCases := []struct { - name string - useRunID bool - }{ - { - name: "with RunID", - useRunID: true, - }, - { - name: "without RunID", - useRunID: false, - }, - } +func (s *WorkflowUpdateSuite) TestNotEmptySpeculativeWorkflowTask_AcceptComplete() { + testCases := []struct { + name string + useRunID bool + }{ + { + name: "with RunID", + useRunID: true, + }, + { + name: "without RunID", + useRunID: false, + }, + } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - s := testcore.NewEnv(t) - runID := mustStartWorkflow(s, s.Tv()) - tv := s.Tv() - if tc.useRunID { - tv = tv.WithRunID(runID) - } + for _, tc := range testCases { + s.Run(tc.name, func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + runID := mustStartWorkflow(env, env.Tv()) + tv := env.Tv() + if tc.useRunID { + tv = tv.WithRunID(runID) + } - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with update unrelated command. - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: s.Tv().ActivityID(), - ActivityType: s.Tv().ActivityType(), - TaskQueue: s.Tv().TaskQueue(), - ScheduleToCloseTimeout: s.Tv().Any().InfiniteTimeout(), - }}, - }}, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled // Speculative WFT with ActivityTaskScheduled(5) event after WorkflowTaskCompleted(4). - 7 WorkflowTaskStarted -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with update unrelated command. + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: env.Tv().ActivityID(), + ActivityType: env.Tv().ActivityType(), + TaskQueue: env.Tv().TaskQueue(), + ScheduleToCloseTimeout: env.Tv().Any().InfiniteTimeout(), + }}, + }}, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled // Speculative WFT with ActivityTaskScheduled(5) event after WorkflowTaskCompleted(4). + 7 WorkflowTaskStarted + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(6, updRequestMsg.GetEventId()) + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(6, updRequestMsg.GetEventId()) - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - updateResultCh := sendUpdateNoError(s, tv) + updateResultCh := sendUpdateNoError(env, tv) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - events := s.GetHistory(s.Namespace().String(), tv.WorkflowExecution()) + events := env.GetHistory(env.Namespace().String(), tv.WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled // Speculative WFT was persisted when completed (event 8) - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted - 9 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 6} // WTScheduled event which delivered update to the worker. - 10 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 9} -`, events) - }) - } - }) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled // Speculative WFT was persisted when completed (event 8) + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted + 9 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 6} // WTScheduled event which delivered update to the worker. + 10 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 9} + `, events) + }) + } +} - t.Run("FirstNormalScheduledWorkflowTask_AcceptComplete", func(t *testing.T) { - testCases := []struct { - name string - useRunID bool - }{ - { - name: "with RunID", - useRunID: true, - }, - { - name: "without RunID", - useRunID: false, - }, - } +func (s *WorkflowUpdateSuite) TestFirstNormalScheduledWorkflowTask_AcceptComplete() { + testCases := []struct { + name string + useRunID bool + }{ + { + name: "with RunID", + useRunID: true, + }, + { + name: "without RunID", + useRunID: false, + }, + } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - s := testcore.NewEnv(t) - runID := mustStartWorkflow(s, s.Tv()) - tv := s.Tv() - if tc.useRunID { - tv = tv.WithRunID(runID) - } + for _, tc := range testCases { + s.Run(tc.name, func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + runID := mustStartWorkflow(env, env.Tv()) + tv := env.Tv() + if tc.useRunID { + tv = tv.WithRunID(runID) + } - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted // First normal WT. No speculative WT was created. -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted // First normal WT. No speculative WT was created. + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(2, updRequestMsg.GetEventId()) + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(2, updRequestMsg.GetEventId()) - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - updateResultCh := sendUpdateNoError(s, tv) + updateResultCh := sendUpdateNoError(env, tv) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - s.Equal(1, wtHandlerCalls) - s.Equal(1, msgHandlerCalls) + s.Equal(1, wtHandlerCalls) + s.Equal(1, msgHandlerCalls) - events := s.GetHistory(s.Namespace().String(), tv.WorkflowExecution()) + events := env.GetHistory(env.Namespace().String(), tv.WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker. - 6 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5} -`, events) - }) - } - }) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker. + 6 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5} + `, events) + }) + } +} - t.Run("NormalScheduledWorkflowTask_AcceptComplete", func(t *testing.T) { - testCases := []struct { - name string - useRunID bool - }{ - { - name: "with RunID", - useRunID: true, - }, - { - name: "without RunID", - useRunID: false, - }, - } +func (s *WorkflowUpdateSuite) TestNormalScheduledWorkflowTask_AcceptComplete() { + testCases := []struct { + name string + useRunID bool + }{ + { + name: "with RunID", + useRunID: true, + }, + { + name: "without RunID", + useRunID: false, + }, + } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - s := testcore.NewEnv(t) - runID := mustStartWorkflow(s, s.Tv()) - tv := s.Tv() - if tc.useRunID { - tv = tv.WithRunID(runID) - } + for _, tc := range testCases { + s.Run(tc.name, func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + runID := mustStartWorkflow(env, env.Tv()) + tv := env.Tv() + if tc.useRunID { + tv = tv.WithRunID(runID) + } - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled // This WT was already created by signal and no speculative WT was created. - 7 WorkflowTaskStarted`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled // This WT was already created by signal and no speculative WT was created. + 7 WorkflowTaskStarted`, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - s.Require().NotEmpty(task.Messages, "Task has no messages", task) - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + s.NotEmpty(task.Messages, "Task has no messages", task) + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(6, updRequestMsg.GetEventId()) + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(6, updRequestMsg.GetEventId()) - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Send signal to schedule new WT. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + // Send signal to schedule new WT. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) - updateResultCh := sendUpdateNoError(s, tv) + updateResultCh := sendUpdateNoError(env, tv) - // Process update in workflow. It will be attached to existing WT. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) + // Process update in workflow. It will be attached to existing WT. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - events := s.GetHistory(s.Namespace().String(), tv.WorkflowExecution()) + events := env.GetHistory(env.Namespace().String(), tv.WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted - 9 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 6} // WTScheduled event which delivered update to the worker. - 10 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 9} -`, events) - }) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted + 9 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 6} // WTScheduled event which delivered update to the worker. + 10 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 9} + `, events) + }) + } +} + +func (s *WorkflowUpdateSuite) TestRunningWorkflowTask_NewEmptySpeculativeWorkflowTask_Rejected() { + // Uses CaptureMetricsHandler which requires a dedicated cluster to avoid metric interference. + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + mustStartWorkflow(env, env.Tv()) + + capture := env.StartGlobalMetricCapture() + + var updateResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Send update after 1st WT has started. + updateResultCh = sendUpdateNoError(env, env.Tv()) + // Completes WT with empty command list to create next WFT w/o events. + return nil, nil + case 2: + s.EqualHistory(` + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted`, task.History) + // Message handled rejects update. + return nil, nil + case 3: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted // Speculative WT2 disappeared and new normal WT was created. + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted`, task.History) + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } - }) + } + + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1, 3: + return nil, nil + case 2: + s.Len(task.Messages, 1) + updRequestMsg := task.Messages[0] + s.EqualValues(5, updRequestMsg.GetEventId()) + + return env.UpdateRejectMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil + } + } + + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } + + // Drain first WT which starts 1st update. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + wt1Resp := res.NewTask + + // Reject update in 2nd WT. + wt2Resp, err := poller.HandlePartialWorkflowTask(wt1Resp.GetWorkflowTask(), false) + s.NoError(err) + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(3, wt2Resp.ResetHistoryEventId) + + // Send signal to create WT. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) + + // Complete workflow. + completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(completeWorkflowResp) + + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) + + commits, rollbacks := speculativeWorkflowTaskOutcomes(capture) + s.Equal(0, commits) + s.Equal(1, rollbacks) + + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted + 9 WorkflowExecutionCompleted`, events) +} + +func (s *WorkflowUpdateSuite) TestRunningWorkflowTask_NewNotEmptySpeculativeWorkflowTask_Rejected() { + env := testcore.NewEnv(s.T()) + + mustStartWorkflow(env, env.Tv()) + + var updateResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Send update after 1st WT has started. + updateResultCh = sendUpdateNoError(env, env.Tv()) + // Completes WT with update unrelated commands to create events that will be in the next speculative WFT. + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: env.Tv().ActivityID(), + ActivityType: env.Tv().ActivityType(), + TaskQueue: env.Tv().TaskQueue(), + ScheduleToCloseTimeout: env.Tv().Any().InfiniteTimeout(), + }}, + }}, nil + case 2: + s.EqualHistory(` + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted`, task.History) + // Message handled rejects update. + return nil, nil + case 3: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted // Empty speculative WFT was written in to the history because it shipped events. + 9 ActivityTaskStarted + 10 ActivityTaskCompleted + 11 WorkflowTaskScheduled + 12 WorkflowTaskStarted + `, task.History) + + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil + } + } - t.Run("RunningWorkflowTask_NewEmptySpeculativeWorkflowTask_Rejected", func(t *testing.T) { - // Uses CaptureMetricsHandler which requires a dedicated cluster to avoid metric interference. - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - mustStartWorkflow(s, s.Tv()) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1, 3: + return nil, nil + case 2: + s.Len(task.Messages, 1) + updRequestMsg := task.Messages[0] + s.EqualValues(6, updRequestMsg.GetEventId()) + + return env.UpdateRejectMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil + } + } + + atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { + return env.Tv().Any().Payloads(), false, nil + } + + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + ActivityTaskHandler: atHandler, + Logger: env.Logger, + T: s.T(), + } + + // Drain first WT which starts 1st update. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + wt1Resp := res.NewTask + + // Reject update in 2nd WT. + wt2Resp, err := poller.HandlePartialWorkflowTask(wt1Resp.GetWorkflowTask(), false) + s.NoError(err) + s.NotNil(wt2Resp) + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(0, wt2Resp.ResetHistoryEventId) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + // Schedule new WFT. + err = poller.PollAndProcessActivityTask(false) + s.NoError(err) + + // Complete workflow. + completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(completeWorkflowResp) + s.EqualValues(0, completeWorkflowResp.NewTask.ResetHistoryEventId) + + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) + + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted + 9 ActivityTaskStarted + 10 ActivityTaskCompleted + 11 WorkflowTaskScheduled + 12 WorkflowTaskStarted + 13 WorkflowTaskCompleted + 14 WorkflowExecutionCompleted`, events) +} - var updateResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse +func (s *WorkflowUpdateSuite) TestCompletedWorkflow() { + s.Run("receive outcome from completed Update", func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) wtHandlerCalls := 0 wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { wtHandlerCalls++ switch wtHandlerCalls { case 1: - // Send update after 1st WT has started. - updateResultCh = sendUpdateNoError(s, s.Tv()) - // Completes WT with empty command list to create next WFT w/o events. + // Completes first WT with empty command list. return nil, nil case 2: - s.EqualHistory(` - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT2 which was created while completing WT1. - 6 WorkflowTaskStarted`, task.History) - // Message handler rejects update. - return nil, nil - case 3: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted // Speculative WT2 disappeared and new normal WT was created. - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted`, task.History) - return []*commandpb.Command{{ + res := env.UpdateAcceptCompleteCommands(env.Tv()) + res = append(res, &commandpb.Command{ CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil + }) + return res, nil default: s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) return nil, nil @@ -595,14 +835,10 @@ func TestWorkflowUpdateSuite(t *testing.T) { msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { msgHandlerCalls++ switch msgHandlerCalls { - case 1, 3: + case 1: return nil, nil case 2: - s.Len(task.Messages, 1) - updRequestMsg := task.Messages[0] - s.EqualValues(5, updRequestMsg.GetEventId()) - - return s.UpdateRejectMessages(s.Tv(), updRequestMsg), nil + return env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), nil default: s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) return nil, nil @@ -611,111 +847,55 @@ func TestWorkflowUpdateSuite(t *testing.T) { //nolint:staticcheck // SA1019 TaskPoller replacement needed poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), WorkflowTaskHandler: wtHandler, MessageHandler: msgHandler, - Logger: s.Logger, + Logger: env.Logger, T: s.T(), } - // Drain first WT which starts 1st update. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - wt1Resp := res.NewTask - - // Reject update in 2nd WT. - wt2Resp, err := poller.HandlePartialWorkflowTask(wt1Resp.GetWorkflowTask(), false) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) s.NoError(err) - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(3, wt2Resp.ResetHistoryEventId) - // Send signal to create WT. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + // Send Update request. + updateResultCh := sendUpdateNoError(env, env.Tv()) - // Complete workflow. - completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + // Complete Update and Workflow. + _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) s.NoError(err) - s.NotNil(completeWorkflowResp) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - commits, rollbacks := speculativeWorkflowTaskOutcomes(capture.Snapshot()) - s.Equal(0, commits) - s.Equal(1, rollbacks) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted - 9 WorkflowExecutionCompleted`, events) - }) - t.Run("RunningWorkflowTask_NewNotEmptySpeculativeWorkflowTask_Rejected", func(t *testing.T) { - s := testcore.NewEnv(t) + // Receive Update result. + updateResult1 := <-updateResultCh + s.NotNil(updateResult1.GetOutcome().GetSuccess()) - mustStartWorkflow(s, s.Tv()) + // Send same Update request again, receiving the same Update result. + updateResultCh = sendUpdateNoError(env, env.Tv()) + updateResult2 := <-updateResultCh + s.Equal(updateResult1, updateResult2) + }) - var updateResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse + s.Run("receive update failure from accepted Update", func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) wtHandlerCalls := 0 wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { wtHandlerCalls++ switch wtHandlerCalls { case 1: - // Send update after 1st WT has started. - updateResultCh = sendUpdateNoError(s, s.Tv()) - // Completes WT with update unrelated commands to create events that will be in the next speculative WFT. - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: s.Tv().ActivityID(), - ActivityType: s.Tv().ActivityType(), - TaskQueue: s.Tv().TaskQueue(), - ScheduleToCloseTimeout: s.Tv().Any().InfiniteTimeout(), - }}, - }}, nil - case 2: - s.EqualHistory(` - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled // Speculative WFT2 with event (5) which was created while completing WFT1. - 7 WorkflowTaskStarted`, task.History) - // Message handler rejects update. + // Completes first WT with empty command list. return nil, nil - case 3: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted // Empty speculative WFT was written in to the history because it shipped events. - 9 ActivityTaskStarted - 10 ActivityTaskCompleted - 11 WorkflowTaskScheduled - 12 WorkflowTaskStarted -`, task.History) - - return []*commandpb.Command{{ + case 2: + res := env.UpdateAcceptCommands(env.Tv()) + res = append(res, &commandpb.Command{ CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil + }) + return res, nil default: s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) return nil, nil @@ -726,915 +906,739 @@ func TestWorkflowUpdateSuite(t *testing.T) { msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { msgHandlerCalls++ switch msgHandlerCalls { - case 1, 3: + case 1: return nil, nil case 2: - s.Len(task.Messages, 1) - updRequestMsg := task.Messages[0] - s.EqualValues(6, updRequestMsg.GetEventId()) - - return s.UpdateRejectMessages(s.Tv(), updRequestMsg), nil + return env.UpdateAcceptMessages(env.Tv(), task.Messages[0]), nil default: s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) return nil, nil } } - atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - return s.Tv().Any().Payloads(), false, nil - } - //nolint:staticcheck // SA1019 TaskPoller replacement needed poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), WorkflowTaskHandler: wtHandler, MessageHandler: msgHandler, - ActivityTaskHandler: atHandler, - Logger: s.Logger, + Logger: env.Logger, T: s.T(), } - // Drain first WT which starts 1st update. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) s.NoError(err) - wt1Resp := res.NewTask - // Reject update in 2nd WT. - wt2Resp, err := poller.HandlePartialWorkflowTask(wt1Resp.GetWorkflowTask(), false) - s.NoError(err) - s.NotNil(wt2Resp) - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(0, wt2Resp.ResetHistoryEventId) + // Send Update request. + updateResultCh := sendUpdate(testcore.NewContext(env.Context()), env, env.Tv()) - // Schedule new WFT. - err = poller.PollAndProcessActivityTask(false) + // Accept Update and complete Workflow. + _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) s.NoError(err) - // Complete workflow. - completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(completeWorkflowResp) - s.EqualValues(0, completeWorkflowResp.NewTask.ResetHistoryEventId) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted - 9 ActivityTaskStarted - 10 ActivityTaskCompleted - 11 WorkflowTaskScheduled - 12 WorkflowTaskStarted - 13 WorkflowTaskCompleted - 14 WorkflowExecutionCompleted`, events) - }) + // Receive Update result. + updateResult1 := <-updateResultCh + s.NoError(updateResult1.err) + s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", updateResult1.response.GetOutcome().GetFailure().GetMessage()) - t.Run("CompletedWorkflow", func(t *testing.T) { - t.Run("receive outcome from completed Update", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + // Send same Update request again, receiving the same failure. + updateResultCh = sendUpdate(testcore.NewContext(env.Context()), env, env.Tv()) + updateResult2 := <-updateResultCh + s.NoError(updateResult2.err) + s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", updateResult2.response.GetOutcome().GetFailure().GetMessage()) + }) +} - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - res := s.UpdateAcceptCompleteCommands(s.Tv()) - res = append(res, &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }) - return res, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil +func (s *WorkflowUpdateSuite) TestValidateWorkerMessages() { + testCases := []struct { + Name string + RespondWorkflowTaskError string + MessageFn func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message + CommandFn func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command + }{ + { + Name: "message-update-id-not-found-and-accepted-request-not-set", + RespondWorkflowTaskError: "wasn't found", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + return []*protocolpb.Message{ + { + Id: tv.MessageID() + "_update-accepted", + ProtocolInstanceId: tv.UpdateID() + tv.Any().String(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: nil, // Important not to pass original request back. + }), + }, } - } - - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - return s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil + }, + CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { + return []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-accepted", + }}, + }, } - } - - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } - - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - - // Send Update request. - updateResultCh := sendUpdateNoError(s, s.Tv()) - - // Complete Update and Workflow. - _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - - // Receive Update result. - updateResult1 := <-updateResultCh - s.NotNil(updateResult1.GetOutcome().GetSuccess()) - - // Send same Update request again, receiving the same Update result. - updateResultCh = sendUpdateNoError(s, s.Tv()) - updateResult2 := <-updateResultCh - s.Equal(updateResult1, updateResult2) - }) - - t.Run("receive update failure from accepted Update", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) - - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - res := s.UpdateAcceptCommands(s.Tv()) - res = append(res, &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }) - return res, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil + }, + }, + { + Name: "message-update-id-not-found-and-accepted-request-is-set", + RespondWorkflowTaskError: "", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) + return []*protocolpb.Message{ + { + Id: tv.MessageID() + "_update-accepted", + ProtocolInstanceId: tv.UpdateID() + tv.Any().String(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: updRequest, // Update will be resurrected from original request. + }), + }, } - } - - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - return s.UpdateAcceptMessages(s.Tv(), task.Messages[0]), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil + }, + CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { + return []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-accepted", + }}, + }, } - } - - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } - - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - - // Send Update request. - updateResultCh := sendUpdate(testcore.NewContext(), s, s.Tv()) - - // Accept Update and complete Workflow. - _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - - // Receive Update result. - updateResult1 := <-updateResultCh - s.NoError(updateResult1.err) - s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", updateResult1.response.GetOutcome().GetFailure().GetMessage()) - - // Send same Update request again, receiving the same failure. - updateResultCh = sendUpdate(testcore.NewContext(), s, s.Tv()) - updateResult2 := <-updateResultCh - s.NoError(updateResult2.err) - s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", updateResult2.response.GetOutcome().GetFailure().GetMessage()) - }) - }) - - t.Run("ValidateWorkerMessages", func(t *testing.T) { - testCases := []struct { - Name string - RespondWorkflowTaskError string - MessageFn func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message - CommandFn func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command - }{ - { - Name: "message-update-id-not-found-and-accepted-request-not-set", - RespondWorkflowTaskError: "wasn't found", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - return []*protocolpb.Message{ - { - Id: tv.MessageID() + "_update-accepted", - ProtocolInstanceId: tv.UpdateID() + tv.Any().String(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: nil, // Important not to pass original request back. - }), - }, - } - }, - CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { - return []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-accepted", - }}, - }, - } - }, }, - { - Name: "message-update-id-not-found-and-accepted-request-is-set", - RespondWorkflowTaskError: "", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) - return []*protocolpb.Message{ - { - Id: tv.MessageID() + "_update-accepted", - ProtocolInstanceId: tv.UpdateID() + tv.Any().String(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: updRequest, // Update will be resurrected from original request. - }), - }, - } - }, - CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { - return []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-accepted", - }}, - }, - } - }, + }, + { + Name: "command-reference-missed-message", + RespondWorkflowTaskError: "referenced absent message ID", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) + return []*protocolpb.Message{ + { + Id: tv.Any().String(), + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: updRequest, + }), + }, + } }, - { - Name: "command-reference-missed-message", - RespondWorkflowTaskError: "referenced absent message ID", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) - return []*protocolpb.Message{ - { - Id: tv.Any().String(), - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: updRequest, - }), - }, - } - }, - CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { - return []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-accepted", - }}, - }, - } - }, + CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { + return []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-accepted", + }}, + }, + } }, - { - Name: "complete-without-accept", - RespondWorkflowTaskError: "invalid state transition attempted", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) - return []*protocolpb.Message{ - { - Id: tv.MessageID() + "_update-completed", - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Response{ - Meta: updRequest.GetMeta(), - Outcome: &updatepb.Outcome{ - Value: &updatepb.Outcome_Success{ - Success: tv.Any().Payloads(), - }, + }, + { + Name: "complete-without-accept", + RespondWorkflowTaskError: "invalid state transition attempted", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) + return []*protocolpb.Message{ + { + Id: tv.MessageID() + "_update-completed", + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Response{ + Meta: updRequest.GetMeta(), + Outcome: &updatepb.Outcome{ + Value: &updatepb.Outcome_Success{ + Success: tv.Any().Payloads(), }, - }), - }, - } - }, - CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { - return []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-completed", - }}, - }, - } - }, + }, + }), + }, + } }, - { - Name: "accept-twice", - RespondWorkflowTaskError: "invalid state transition attempted", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) - return []*protocolpb.Message{ - { - Id: tv.WithMessageIDNumber(1).MessageID(), - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: updRequest, - }), - }, - { - Id: tv.WithMessageIDNumber(2).MessageID(), - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: updRequest, - }), - }, - } - }, - CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { - return []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.WithMessageIDNumber(1).MessageID(), - }}, - }, - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.WithMessageIDNumber(2).MessageID(), - }}, - }, - } - }, + CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { + return []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-completed", + }}, + }, + } }, - { - Name: "success-case", - RespondWorkflowTaskError: "", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) - return []*protocolpb.Message{ - { - Id: tv.MessageID() + "_update-accepted", - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: updRequest, - }), - }, - { - Id: tv.MessageID() + "_update-completed", - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Response{ - Meta: updRequest.GetMeta(), - Outcome: &updatepb.Outcome{ - Value: &updatepb.Outcome_Success{ - Success: tv.Any().Payloads(), - }, + }, + { + Name: "accept-twice", + RespondWorkflowTaskError: "invalid state transition attempted", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) + return []*protocolpb.Message{ + { + Id: tv.WithMessageIDNumber(1).MessageID(), + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: updRequest, + }), + }, + { + Id: tv.WithMessageIDNumber(2).MessageID(), + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: updRequest, + }), + }, + } + }, + CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { + return []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.WithMessageIDNumber(1).MessageID(), + }}, + }, + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.WithMessageIDNumber(2).MessageID(), + }}, + }, + } + }, + }, + { + Name: "success-case", + RespondWorkflowTaskError: "", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) + return []*protocolpb.Message{ + { + Id: tv.MessageID() + "_update-accepted", + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: updRequest, + }), + }, + { + Id: tv.MessageID() + "_update-completed", + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Response{ + Meta: updRequest.GetMeta(), + Outcome: &updatepb.Outcome{ + Value: &updatepb.Outcome_Success{ + Success: tv.Any().Payloads(), }, - }), - }, - } - }, - CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { - return []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-accepted", - }}, - }, - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-completed", - }}, - }, - } - }, + }, + }), + }, + } }, - { - Name: "success-case-no-commands", // PROTOCOL_MESSAGE commands are optional. - RespondWorkflowTaskError: "", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) - return []*protocolpb.Message{ - { - Id: tv.Any().String(), - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: updRequest, - }), - }, - { - Id: tv.Any().String(), - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Response{ - Meta: updRequest.GetMeta(), - Outcome: &updatepb.Outcome{ - Value: &updatepb.Outcome_Success{ - Success: tv.Any().Payloads(), - }, + CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { + return []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-accepted", + }}, + }, + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-completed", + }}, + }, + } + }, + }, + { + Name: "success-case-no-commands", // PROTOCOL_MESSAGE commands are optional. + RespondWorkflowTaskError: "", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) + return []*protocolpb.Message{ + { + Id: tv.Any().String(), + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: updRequest, + }), + }, + { + Id: tv.Any().String(), + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Response{ + Meta: updRequest.GetMeta(), + Outcome: &updatepb.Outcome{ + Value: &updatepb.Outcome_Success{ + Success: tv.Any().Payloads(), }, - }), - }, - } - }, + }, + }), + }, + } }, - { - Name: "invalid-command-order", - RespondWorkflowTaskError: "invalid state transition attempted", - MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { - updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) - return []*protocolpb.Message{ - { - Id: tv.MessageID() + "_update-accepted", - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ - AcceptedRequestMessageId: reqMsg.GetId(), - AcceptedRequestSequencingEventId: reqMsg.GetEventId(), - AcceptedRequest: updRequest, - }), - }, - { - Id: tv.MessageID() + "_update-completed", - ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), - SequencingId: nil, - Body: protoutils.MarshalAny(t, &updatepb.Response{ - Meta: updRequest.GetMeta(), - Outcome: &updatepb.Outcome{ - Value: &updatepb.Outcome_Success{ - Success: tv.Any().Payloads(), - }, + }, + { + Name: "invalid-command-order", + RespondWorkflowTaskError: "invalid state transition attempted", + MessageFn: func(t *testing.T, tv *testvars.TestVars, reqMsg *protocolpb.Message) []*protocolpb.Message { + updRequest := protoutils.UnmarshalAny[*updatepb.Request](t, reqMsg.GetBody()) + return []*protocolpb.Message{ + { + Id: tv.MessageID() + "_update-accepted", + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Acceptance{ + AcceptedRequestMessageId: reqMsg.GetId(), + AcceptedRequestSequencingEventId: reqMsg.GetEventId(), + AcceptedRequest: updRequest, + }), + }, + { + Id: tv.MessageID() + "_update-completed", + ProtocolInstanceId: updRequest.GetMeta().GetUpdateId(), + SequencingId: nil, + Body: protoutils.MarshalAny(t, &updatepb.Response{ + Meta: updRequest.GetMeta(), + Outcome: &updatepb.Outcome{ + Value: &updatepb.Outcome_Success{ + Success: tv.Any().Payloads(), }, - }), - }, - } - }, - CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { - return []*commandpb.Command{ - // Complete command goes before Accept command. - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-completed", - }}, - }, - { - CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, - Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ - MessageId: tv.MessageID() + "_update-accepted", - }}, - }, - } - }, + }, + }), + }, + } }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) - - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - if tc.CommandFn == nil { - return nil, nil - } - return tc.CommandFn(t, s.Tv(), task.History), nil + CommandFn: func(t *testing.T, tv *testvars.TestVars, history *historypb.History) []*commandpb.Command { + return []*commandpb.Command{ + // Complete command goes before Accept command. + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-completed", + }}, + }, + { + CommandType: enumspb.COMMAND_TYPE_PROTOCOL_MESSAGE, + Attributes: &commandpb.Command_ProtocolMessageCommandAttributes{ProtocolMessageCommandAttributes: &commandpb.ProtocolMessageCommandAttributes{ + MessageId: tv.MessageID() + "_update-accepted", + }}, + }, } + }, + }, + } - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - if tc.MessageFn == nil { - return nil, nil - } - s.Require().NotEmpty(task.Messages, "expected update message in task") - updRequestMsg := task.Messages[0] - return tc.MessageFn(t, s.Tv(), updRequestMsg), nil - } + for _, tc := range testCases { + s.Run(tc.Name, func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: t, + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + if tc.CommandFn == nil { + return nil, nil } + return tc.CommandFn(s.T(), env.Tv(), task.History), nil + } - halfSecondTimeoutCtx, cancel := context.WithTimeout(testcore.NewContext(), 500*time.Millisecond) - defer cancel() - updateResultCh := sendUpdate(halfSecondTimeoutCtx, s, s.Tv()) - - // Process update in workflow. - _, err := poller.PollAndProcessWorkflowTask() - updateResult := <-updateResultCh - if tc.RespondWorkflowTaskError != "" { - s.Error(err, "RespondWorkflowTaskCompleted should return an error contains `%v`", tc.RespondWorkflowTaskError) - s.Contains(err.Error(), tc.RespondWorkflowTaskError) - - var wfNotReady *serviceerror.WorkflowNotReady - s.ErrorAs(updateResult.err, &wfNotReady, "API caller should get serviceerror.WorkflowNotReady, if server got a validation error while processing worker response.") - s.Contains(updateResult.err.Error(), "Unable to perform workflow execution update due to unexpected workflow task failure.") - s.Nil(updateResult.response) - } else { - s.NoError(err) - s.NoError(updateResult.err) + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + if tc.MessageFn == nil { + return nil, nil } - }) - } - }) - - t.Run("StickySpeculativeWorkflowTask_AcceptComplete", func(t *testing.T) { - testCases := []struct { - name string - useRunID bool - }{ - { - name: "with RunID", - useRunID: true, - }, - { - name: "without RunID", - useRunID: false, - }, - } + s.NotEmpty(task.Messages, "expected update message in task") + updRequestMsg := task.Messages[0] + return tc.MessageFn(s.T(), env.Tv(), updRequestMsg), nil + } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - s := testcore.NewEnv(t) - runID := mustStartWorkflow(s, s.Tv()) - tv := s.Tv() - if tc.useRunID { - tv = tv.WithRunID(runID) - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Drain existing first WT from regular task queue, but respond with sticky queue enabled response, next WT will go to sticky queue. - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - StickyAttributes: s.Tv().StickyExecutionAttributes(3 * time.Second), - }, nil - }) + halfSecondTimeoutCtx, cancel := context.WithTimeout(env.Context(), 500*time.Millisecond) + defer cancel() + updateResultCh := sendUpdate(halfSecondTimeoutCtx, env, env.Tv()) + + // Process update in workflow. + _, err := poller.PollAndProcessWorkflowTask() + updateResult := <-updateResultCh + if tc.RespondWorkflowTaskError != "" { + s.Error(err, "RespondWorkflowTaskCompleted should return an error contains `%v`", tc.RespondWorkflowTaskError) + s.Contains(err.Error(), tc.RespondWorkflowTaskError) + + var wfNotReady *serviceerror.WorkflowNotReady + s.ErrorAs(updateResult.err, &wfNotReady, "API caller should get serviceerror.WorkflowNotReady, if server got a validation error while processing worker response.") + s.Contains(updateResult.err.Error(), "Unable to perform workflow execution update due to unexpected workflow task failure.") + s.Nil(updateResult.response) + } else { s.NoError(err) + s.NoError(updateResult.err) + } + }) + } +} - go func() { - // Process update in workflow task (it is sticky). - res, err := s.TaskPoller(). - PollWorkflowTask(&workflowservice.PollWorkflowTaskQueueRequest{TaskQueue: s.Tv().StickyTaskQueue()}). - HandleTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - // This WT contains partial history because sticky was enabled. - s.EqualHistory(` - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted`, task.History) - - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) - //nolint:testifylint // callback runs synchronously within HandleTask - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) //nolint:testifylint // callback runs synchronously within HandleTask - s.EqualValues(5, updRequestMsg.GetEventId()) //nolint:testifylint // callback runs synchronously within HandleTask - - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Commands: s.UpdateAcceptCompleteCommands(s.Tv()), - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), - }, nil - }) - //nolint:testifylint // intentional async polling pattern - s.NoError(err) - s.NotNil(res) //nolint:testifylint // intentional async polling pattern - s.EqualValues(0, res.ResetHistoryEventId) //nolint:testifylint // intentional async polling pattern - }() - - // This is to make sure that sticky poller above reached server first. - // And when update comes, stick poller is already available. - time.Sleep(500 * time.Millisecond) //nolint:forbidigo - updateResult := <-sendUpdateNoError(s, tv) - - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) +func (s *WorkflowUpdateSuite) TestStickySpeculativeWorkflowTask_AcceptComplete() { + testCases := []struct { + name string + useRunID bool + }{ + { + name: "with RunID", + useRunID: true, + }, + { + name: "without RunID", + useRunID: false, + }, + } - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted - 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. - 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} - `, s.GetHistory(s.Namespace().String(), tv.WorkflowExecution())) - }) - } - }) + for _, tc := range testCases { + s.Run(tc.name, func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + runID := mustStartWorkflow(env, env.Tv()) + tv := env.Tv() + if tc.useRunID { + tv = tv.WithRunID(runID) + } - t.Run("StickySpeculativeWorkflowTask_AcceptComplete_StickyWorkerUnavailable", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + // Drain existing first WT from regular task queue, but respond with sticky queue enabled response, next WT will go to sticky queue. + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + StickyAttributes: env.Tv().StickyExecutionAttributes(3 * time.Second), + }, nil + }) + s.NoError(err) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - // Worker gets full history because update was issued after sticky worker is gone. - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + go func() { + // Process update in workflow task (it is sticky). + res, err := env.TaskPoller(). + PollWorkflowTask(&workflowservice.PollWorkflowTaskQueueRequest{TaskQueue: env.Tv().StickyTaskQueue()}). + HandleTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + // This WT contains partial history because sticky was enabled. + s.EqualHistory(` + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted`, task.History) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + //nolint:testifylint // callback runs synchronously within HandleTask + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) //nolint:testifylint // callback runs synchronously within HandleTask + s.EqualValues(5, updRequestMsg.GetEventId()) //nolint:testifylint // callback runs synchronously within HandleTask - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(5, updRequestMsg.GetEventId()) + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: env.UpdateAcceptCompleteCommands(env.Tv()), + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), + }, nil + }) + //nolint:testifylint // intentional async polling pattern + s.NoError(err) + s.NotNil(res) //nolint:testifylint // intentional async polling pattern + s.EqualValues(0, res.ResetHistoryEventId) //nolint:testifylint // intentional async polling pattern + }() - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + // This is to make sure that sticky poller above reached server first. + // And when update comes, stick poller is already available. + time.Sleep(500 * time.Millisecond) //nolint:forbidigo + updateResult := <-sendUpdateNoError(env, tv) + + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. + 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} + `, env.GetHistory(env.Namespace().String(), tv.WorkflowExecution())) + }) + } +} + +func (s *WorkflowUpdateSuite) TestStickySpeculativeWorkflowTask_AcceptComplete_StickyWorkerUnavailable() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + // Worker gets full history because update was issued after sticky worker is gone. + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - StickyTaskQueue: s.Tv().StickyTaskQueue(), - StickyScheduleToStartTimeout: 3 * time.Second, - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(5, updRequestMsg.GetEventId()) + + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain existing WT from regular task queue, but respond with sticky enabled response to enable stick task queue. - _, err := poller.PollAndProcessWorkflowTask(testcore.WithRespondSticky, testcore.WithoutRetries) - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + StickyTaskQueue: env.Tv().StickyTaskQueue(), + StickyScheduleToStartTimeout: 3 * time.Second, + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - s.Logger.Info("Sleep 10+ seconds to make sure stickyPollerUnavailableWindow time has passed.") - time.Sleep(10*time.Second + 100*time.Millisecond) //nolint:forbidigo - s.Logger.Info("Sleep 10+ seconds is done.") + // Drain existing WT from regular task queue, but respond with sticky enabled response to enable stick task queue. + _, err := poller.PollAndProcessWorkflowTask(testcore.WithRespondSticky, testcore.WithoutRetries) + s.NoError(err) - // Now send an update. It should try sticky task queue first, but got "StickyWorkerUnavailable" error - // and resend it to normal. - // This can be observed in wtHandler: if history is partial => sticky task queue is used. - updateResultCh := sendUpdateNoError(s, s.Tv()) + env.Logger.Info("Sleep 10+ seconds to make sure stickyPollerUnavailableWindow time has passed.") + time.Sleep(10*time.Second + 100*time.Millisecond) //nolint:forbidigo + env.Logger.Info("Sleep 10+ seconds is done.") - // Process update in workflow task from non-sticky task queue. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted - 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. - 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} -`, events) - }) + // Now send an update. It should try sticky task queue first, but got "StickyWorkerUnavailable" error + // and resend it to normal. + // This can be observed in wtHandler: if history is partial => sticky task queue is used. + updateResultCh := sendUpdateNoError(env, env.Tv()) - t.Run("FirstNormalScheduledWorkflowTask_Reject", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + // Process update in workflow task from non-sticky task queue. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted`, task.History) - return nil, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(2, updRequestMsg.GetEventId()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. + 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} + `, events) +} - return s.UpdateRejectMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } - } +func (s *WorkflowUpdateSuite) TestFirstNormalScheduledWorkflowTask_Reject() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted`, task.History) + return nil, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - updateResultCh := sendUpdateNoError(s, s.Tv()) - - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResp := res.NewTask - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(0, updateResp.ResetHistoryEventId) - - s.Equal(1, wtHandlerCalls) - s.Equal(1, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled // First normal WT was scheduled before update and therefore all 3 events have to be written even if update was rejected. - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted // Empty completed WT. No new events were created after it. -`, events) - }) - - t.Run("EmptySpeculativeWorkflowTask_Reject", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) - - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) - - updateResultCh := sendUpdateNoError(s, s.Tv()) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(2, updRequestMsg.GetEventId()) + + return env.UpdateRejectMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil + } + } - // Process update in workflow. - res, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted - `, task.History) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + updateResultCh := sendUpdateNoError(env, env.Tv()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(5, updRequestMsg.GetEventId()) + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResp := res.NewTask + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(0, updateResp.ResetHistoryEventId) - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateRejectMessages(s.Tv(), updRequestMsg), - }, nil - }) - s.NoError(err) - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(3, res.ResetHistoryEventId) + s.Equal(1, wtHandlerCalls) + s.Equal(1, msgHandlerCalls) - // Send signal to create WT. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - // Process signal and complete workflow. - res, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled // Speculative WT was dropped and history starts from 5 again. - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted`, task.History) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled // First normal WT was scheduled before update and therefore all 3 events have to be written even if update was rejected. + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted // Empty completed WT. No new events were created after it. + `, events) +} - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Commands: []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ - CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, - }, +func (s *WorkflowUpdateSuite) TestEmptySpeculativeWorkflowTask_Reject() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + updateResultCh := sendUpdateNoError(env, env.Tv()) + + // Process update in workflow. + res, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(5, updRequestMsg.GetEventId()) + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateRejectMessages(env.Tv(), updRequestMsg), + }, nil + }) + s.NoError(err) + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(3, res.ResetHistoryEventId) + + // Send signal to create WT. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) + + // Process signal and complete workflow. + res, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled // Speculative WT was dropped and history starts from 5 again. + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted`, task.History) + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, }, }, - }, nil - }) - s.NoError(err) - s.NotNil(res) + }, + }, nil + }) + s.NoError(err) + s.NotNil(res) - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -1645,1680 +1649,1680 @@ func TestWorkflowUpdateSuite(t *testing.T) { 8 WorkflowTaskCompleted 9 WorkflowExecutionCompleted `, events) - }) - - t.Run("NotEmptySpeculativeWorkflowTask_Reject", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) - - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: s.Tv().ActivityID(), - ActivityType: s.Tv().ActivityType(), - TaskQueue: s.Tv().TaskQueue(), - ScheduleToCloseTimeout: s.Tv().Any().InfiniteTimeout(), - }}, - }}, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled // Speculative WFT will be written to the history because there is ActivityTaskScheduled(5) event. - 7 WorkflowTaskStarted -`, task.History) - return nil, nil - case 3: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted // Empty speculative WFT was written to the history because it shipped events. - 9 ActivityTaskStarted - 10 ActivityTaskCompleted - 11 WorkflowTaskScheduled - 12 WorkflowTaskStarted -`, task.History) - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } - - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1, 3: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) - - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(6, updRequestMsg.GetEventId()) - - return s.UpdateRejectMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } - } - - atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - return s.Tv().Any().Payloads(), false, nil - } - - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - ActivityTaskHandler: atHandler, - Logger: s.Logger, - T: s.T(), - } - - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - - updateResultCh := sendUpdateNoError(s, s.Tv()) - - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with normal workflow task") - - err = poller.PollAndProcessActivityTask(false) - s.NoError(err) - - // Complete workflow. - res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 ActivityTaskScheduled - 6 WorkflowTaskScheduled // Speculative WFT (6-8) presents in the history even though update was rejected. - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted - 9 ActivityTaskStarted - 10 ActivityTaskCompleted - 11 WorkflowTaskScheduled - 12 WorkflowTaskStarted - 13 WorkflowTaskCompleted - 14 WorkflowExecutionCompleted`, events) - }) - - t.Run("1stAccept_2ndAccept_2ndComplete_1stComplete", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) - tv1 := s.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1).WithActivityIDNumber(1) - tv2 := s.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2).WithActivityIDNumber(2) - - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted`, task.History) - return append(s.UpdateAcceptCommands(tv1), &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: tv1.ActivityID(), - ActivityType: tv1.ActivityType(), - TaskQueue: tv1.TaskQueue(), - ScheduleToCloseTimeout: tv1.Any().InfiniteTimeout(), - }}, - }), nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted // 1st update is accepted. - 6 ActivityTaskScheduled - 7 WorkflowTaskScheduled // New normal WT is created because of the 2nd update. - 8 WorkflowTaskStarted`, task.History) - return append(s.UpdateAcceptCommands(tv2), &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: tv2.ActivityID(), - ActivityType: tv2.ActivityType(), - TaskQueue: tv2.TaskQueue(), - ScheduleToCloseTimeout: tv2.Any().InfiniteTimeout(), - }}, - }), nil - case 3: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 ActivityTaskScheduled - 7 WorkflowTaskScheduled - 8 WorkflowTaskStarted - 9 WorkflowTaskCompleted - 10 WorkflowExecutionUpdateAccepted // 2nd update is accepted. - 11 ActivityTaskScheduled - 12 ActivityTaskStarted - 13 ActivityTaskCompleted - 14 WorkflowTaskScheduled - 15 WorkflowTaskStarted -`, task.History) - return s.UpdateCompleteCommands(tv2), nil - case 4: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 ActivityTaskScheduled - 7 WorkflowTaskScheduled - 8 WorkflowTaskStarted - 9 WorkflowTaskCompleted - 10 WorkflowExecutionUpdateAccepted - 11 ActivityTaskScheduled - 12 ActivityTaskStarted - 13 ActivityTaskCompleted - 14 WorkflowTaskScheduled - 15 WorkflowTaskStarted - 16 WorkflowTaskCompleted - 17 WorkflowExecutionUpdateCompleted // 2nd update is completed. - 18 ActivityTaskStarted - 19 ActivityTaskCompleted - 20 WorkflowTaskScheduled - 21 WorkflowTaskStarted -`, task.History) - return s.UpdateCompleteCommands(tv1), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } - - var upd1RequestMsg, upd2RequestMsg *protocolpb.Message - - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - upd1RequestMsg = task.Messages[0] - upd1Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd1RequestMsg.GetBody()) - s.Equal("args-value-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), upd1Request.GetInput().GetArgs())) - s.EqualValues(2, upd1RequestMsg.GetEventId()) - return s.UpdateAcceptMessages(tv1, upd1RequestMsg), nil - case 2: - upd2RequestMsg = task.Messages[0] - upd2Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd2RequestMsg.GetBody()) - s.Equal("args-value-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), upd2Request.GetInput().GetArgs())) - s.EqualValues(7, upd2RequestMsg.GetEventId()) - return s.UpdateAcceptMessages(tv2, upd2RequestMsg), nil - case 3: - s.NotNil(upd2RequestMsg) - return s.UpdateCompleteMessages(tv2, upd2RequestMsg), nil - case 4: - s.NotNil(upd1RequestMsg) - return s.UpdateCompleteMessages(tv1, upd1RequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } - } +} - atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - return s.Tv().Any().Payloads(), false, nil +func (s *WorkflowUpdateSuite) TestNotEmptySpeculativeWorkflowTask_Reject() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: env.Tv().ActivityID(), + ActivityType: env.Tv().ActivityType(), + TaskQueue: env.Tv().TaskQueue(), + ScheduleToCloseTimeout: env.Tv().Any().InfiniteTimeout(), + }}, + }}, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled // Speculative WFT will be written to the history because there is ActivityTaskScheduled(5) event. + 7 WorkflowTaskStarted + `, task.History) + return nil, nil + case 3: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted // Empty speculative WFT was written to the history because it shipped events. + 9 ActivityTaskStarted + 10 ActivityTaskCompleted + 11 WorkflowTaskScheduled + 12 WorkflowTaskStarted + `, task.History) + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - ActivityTaskHandler: atHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1, 3: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(6, updRequestMsg.GetEventId()) + + return env.UpdateRejectMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - updateResultCh1 := sendUpdateNoError(s, tv1) - - // Accept update1 in normal WT1. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - - // Send 2nd update and create speculative WT2. - updateResultCh2 := sendUpdateNoError(s, tv2) - - // Poll for WT2 which 2nd update. Accept update2. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) + atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { + return env.Tv().Any().Payloads(), false, nil + } - err = poller.PollAndProcessActivityTask(false) - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + ActivityTaskHandler: atHandler, + Logger: env.Logger, + T: s.T(), + } - // Complete update2 in WT3. - res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult2 := <-updateResultCh2 - s.Equal("success-result-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - err = poller.PollAndProcessActivityTask(false) - s.NoError(err) + updateResultCh := sendUpdateNoError(env, env.Tv()) - // Complete update1 in WT4. - res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult1 := <-updateResultCh1 - s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, updateResult1.Stage) - s.Equal("success-result-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), updateResult1.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) - - s.Equal(4, wtHandlerCalls) - s.Equal(4, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker. - 6 ActivityTaskScheduled - 7 WorkflowTaskScheduled - 8 WorkflowTaskStarted - 9 WorkflowTaskCompleted - 10 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 7} // WTScheduled event which delivered update to the worker. - 11 ActivityTaskScheduled - 12 ActivityTaskStarted - 13 ActivityTaskCompleted - 14 WorkflowTaskScheduled - 15 WorkflowTaskStarted - 16 WorkflowTaskCompleted - 17 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 10} // 2nd update is completed. - 18 ActivityTaskStarted - 19 ActivityTaskCompleted - 20 WorkflowTaskScheduled - 21 WorkflowTaskStarted - 22 WorkflowTaskCompleted - 23 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5} // 1st update is completed. -`, events) - }) + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with normal workflow task") - t.Run("1stAccept_2ndReject_1stComplete", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + err = poller.PollAndProcessActivityTask(false) + s.NoError(err) - tv1 := s.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1).WithActivityIDNumber(1) - tv2 := s.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2).WithActivityIDNumber(2) + // Complete workflow. + res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted`, task.History) - return append(s.UpdateAcceptCommands(tv1), &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: tv1.ActivityID(), - ActivityType: tv1.ActivityType(), - TaskQueue: tv1.TaskQueue(), - ScheduleToCloseTimeout: tv1.Any().InfiniteTimeout(), - }}, - }), nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted // 1st update is accepted. - 6 ActivityTaskScheduled - 7 WorkflowTaskScheduled // Speculative WFT with WorkflowExecutionUpdateAccepted(5) event. - 8 WorkflowTaskStarted -`, task.History) - // Message handler rejects 2nd update. - return nil, nil - case 3: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 ActivityTaskScheduled - 7 WorkflowTaskScheduled - 8 WorkflowTaskStarted - 9 WorkflowTaskCompleted // Speculative WFT is written to the history because it shipped event. - 10 ActivityTaskStarted - 11 ActivityTaskCompleted - 12 WorkflowTaskScheduled - 13 WorkflowTaskStarted -`, task.History) - return s.UpdateCompleteCommands(tv1), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) - var upd1RequestMsg *protocolpb.Message - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - upd1RequestMsg = task.Messages[0] - upd1Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd1RequestMsg.GetBody()) - s.Equal("args-value-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), upd1Request.GetInput().GetArgs())) - s.EqualValues(2, upd1RequestMsg.GetEventId()) - return s.UpdateAcceptMessages(tv1, upd1RequestMsg), nil - case 2: - upd2RequestMsg := task.Messages[0] - upd2Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd2RequestMsg.GetBody()) - s.Equal("args-value-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), upd2Request.GetInput().GetArgs())) - s.EqualValues(7, upd2RequestMsg.GetEventId()) - return s.UpdateRejectMessages(tv2, upd2RequestMsg), nil - case 3: - s.NotNil(upd1RequestMsg) - return s.UpdateCompleteMessages(tv1, upd1RequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } - } + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 ActivityTaskScheduled + 6 WorkflowTaskScheduled // Speculative WFT (6-8) presents in the history even though update was rejected. + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted + 9 ActivityTaskStarted + 10 ActivityTaskCompleted + 11 WorkflowTaskScheduled + 12 WorkflowTaskStarted + 13 WorkflowTaskCompleted + 14 WorkflowExecutionCompleted`, events) +} - atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - return s.Tv().Any().Payloads(), false, nil +func (s *WorkflowUpdateSuite) Test1stAccept_2ndAccept_2ndComplete_1stComplete() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + tv1 := env.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1).WithActivityIDNumber(1) + tv2 := env.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2).WithActivityIDNumber(2) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted`, task.History) + return append(env.UpdateAcceptCommands(tv1), &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: tv1.ActivityID(), + ActivityType: tv1.ActivityType(), + TaskQueue: tv1.TaskQueue(), + ScheduleToCloseTimeout: tv1.Any().InfiniteTimeout(), + }}, + }), nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted // 1st update is accepted. + 6 ActivityTaskScheduled + 7 WorkflowTaskScheduled // New normal WT is created because of the 2nd update. + 8 WorkflowTaskStarted`, task.History) + return append(env.UpdateAcceptCommands(tv2), &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: tv2.ActivityID(), + ActivityType: tv2.ActivityType(), + TaskQueue: tv2.TaskQueue(), + ScheduleToCloseTimeout: tv2.Any().InfiniteTimeout(), + }}, + }), nil + case 3: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 ActivityTaskScheduled + 7 WorkflowTaskScheduled + 8 WorkflowTaskStarted + 9 WorkflowTaskCompleted + 10 WorkflowExecutionUpdateAccepted // 2nd update is accepted. + 11 ActivityTaskScheduled + 12 ActivityTaskStarted + 13 ActivityTaskCompleted + 14 WorkflowTaskScheduled + 15 WorkflowTaskStarted + `, task.History) + return env.UpdateCompleteCommands(tv2), nil + case 4: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 ActivityTaskScheduled + 7 WorkflowTaskScheduled + 8 WorkflowTaskStarted + 9 WorkflowTaskCompleted + 10 WorkflowExecutionUpdateAccepted + 11 ActivityTaskScheduled + 12 ActivityTaskStarted + 13 ActivityTaskCompleted + 14 WorkflowTaskScheduled + 15 WorkflowTaskStarted + 16 WorkflowTaskCompleted + 17 WorkflowExecutionUpdateCompleted // 2nd update is completed. + 18 ActivityTaskStarted + 19 ActivityTaskCompleted + 20 WorkflowTaskScheduled + 21 WorkflowTaskStarted + `, task.History) + return env.UpdateCompleteCommands(tv1), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - ActivityTaskHandler: atHandler, - Logger: s.Logger, - T: s.T(), + var upd1RequestMsg, upd2RequestMsg *protocolpb.Message + + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + upd1RequestMsg = task.Messages[0] + upd1Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd1RequestMsg.GetBody()) + s.Equal("args-value-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), upd1Request.GetInput().GetArgs())) + s.EqualValues(2, upd1RequestMsg.GetEventId()) + return env.UpdateAcceptMessages(tv1, upd1RequestMsg), nil + case 2: + upd2RequestMsg = task.Messages[0] + upd2Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd2RequestMsg.GetBody()) + s.Equal("args-value-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), upd2Request.GetInput().GetArgs())) + s.EqualValues(7, upd2RequestMsg.GetEventId()) + return env.UpdateAcceptMessages(tv2, upd2RequestMsg), nil + case 3: + s.NotNil(upd2RequestMsg) + return env.UpdateCompleteMessages(tv2, upd2RequestMsg), nil + case 4: + s.NotNil(upd1RequestMsg) + return env.UpdateCompleteMessages(tv1, upd1RequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - updateResultCh1 := sendUpdateNoError(s, tv1) + atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { + return env.Tv().Any().Payloads(), false, nil + } - // Accept update1 in WT1. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + ActivityTaskHandler: atHandler, + Logger: env.Logger, + T: s.T(), + } - // Send 2nd update and create speculative WT2. - updateResultCh2 := sendUpdateNoError(s, tv2) + updateResultCh1 := sendUpdateNoError(env, tv1) - // Poll for WT2 which 2nd update. Reject update2. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with workflow task which had events") + // Accept update1 in normal WT1. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - updateResult2 := <-updateResultCh2 - s.Equal("rejection-of-"+tv2.UpdateID(), updateResult2.GetOutcome().GetFailure().GetMessage()) + // Send 2nd update and create speculative WT2. + updateResultCh2 := sendUpdateNoError(env, tv2) - err = poller.PollAndProcessActivityTask(false) - s.NoError(err) + // Poll for WT2 which 2nd update. Accept update2. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - // Complete update1 in WT3. - res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult1 := <-updateResultCh1 - s.Equal("success-result-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), updateResult1.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker. - 6 ActivityTaskScheduled - 7 WorkflowTaskScheduled - 8 WorkflowTaskStarted - 9 WorkflowTaskCompleted // WT which had rejected update. - 10 ActivityTaskStarted - 11 ActivityTaskCompleted - 12 WorkflowTaskScheduled - 13 WorkflowTaskStarted - 14 WorkflowTaskCompleted - 15 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5} -`, events) - }) + err = poller.PollAndProcessActivityTask(false) + s.NoError(err) - t.Run("SpeculativeWorkflowTask_Fail", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + // Complete update2 in WT3. + res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult2 := <-updateResultCh2 + s.Equal("success-result-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) + err = poller.PollAndProcessActivityTask(false) + s.NoError(err) - timeoutCtx, cancel := context.WithTimeout(testcore.NewContext(), 2*time.Second) - defer cancel() - updateResultCh := sendUpdate(timeoutCtx, s, s.Tv()) + // Complete update1 in WT4. + res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult1 := <-updateResultCh1 + s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, updateResult1.Stage) + s.Equal("success-result-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), updateResult1.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - // Try to accept update in workflow: get malformed response. - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted - `, task.History) + s.Equal(4, wtHandlerCalls) + s.Equal(4, msgHandlerCalls) - s.Require().NotEmpty(task.Messages, "expected update message in task") - updRequestMsg := task.Messages[0] - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Commands: s.UpdateAcceptCommands(s.Tv()), - // Emulate bug in worker/SDK update handler code. Return malformed acceptance response. - Messages: []*protocolpb.Message{ - { - Id: s.Tv().MessageID() + "_update-accepted", - ProtocolInstanceId: s.Tv().Any().String(), - SequencingId: nil, - Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{ - AcceptedRequestMessageId: updRequestMsg.GetId(), - AcceptedRequestSequencingEventId: updRequestMsg.GetEventId(), - AcceptedRequest: nil, // must not be nil! - }), - }, - }, - }, nil - }) - s.Error(err) - s.Contains(err.Error(), "wasn't found") - - // Update is aborted, speculative WFT failure is recorded into the history. - updateResult := <-updateResultCh - var wfNotReady *serviceerror.WorkflowNotReady - s.ErrorAs(updateResult.err, &wfNotReady) - s.Contains(updateResult.err.Error(), "Unable to perform workflow execution update due to unexpected workflow task failure.") - - // New transient WFT is created, but it is not shown in the history. - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskFailed`, events) - - // Send Update again. It will be delivered on existing transient WFT. - updateResultCh = sendUpdate(timeoutCtx, s, s.Tv()) - - // Try to accept 2nd update in workflow: get error. Poller will fail WFT, but the registry won't be cleared and Update won't be aborted. - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskFailed - 8 WorkflowTaskScheduled // Transient WFT - 9 WorkflowTaskStarted`, task.History) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - s.Require().NotEmpty(task.Messages, "expected update message in task") - updRequestMsg := task.Messages[0] - s.EqualValues(8, updRequestMsg.GetEventId()) - // Returning error will cause the poller to fail WFT. - return nil, errors.New("malformed request") - }) - // The error is from RespondWorkflowTaskFailed, which should go w/o error. - s.NoError(err) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker. + 6 ActivityTaskScheduled + 7 WorkflowTaskScheduled + 8 WorkflowTaskStarted + 9 WorkflowTaskCompleted + 10 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 7} // WTScheduled event which delivered update to the worker. + 11 ActivityTaskScheduled + 12 ActivityTaskStarted + 13 ActivityTaskCompleted + 14 WorkflowTaskScheduled + 15 WorkflowTaskStarted + 16 WorkflowTaskCompleted + 17 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 10} // 2nd update is completed. + 18 ActivityTaskStarted + 19 ActivityTaskCompleted + 20 WorkflowTaskScheduled + 21 WorkflowTaskStarted + 22 WorkflowTaskCompleted + 23 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5} // 1st update is completed. + `, events) +} - // Update timed out, but stays in the registry and will be delivered again on the new transient WFT. - updateResult = <-updateResultCh - s.Error(updateResult.err) - s.True(common.IsContextDeadlineExceededErr(updateResult.err), "UpdateWorkflowExecution must timeout after 2 seconds") - s.Nil(updateResult.response) - - // This WFT failure wasn't recorded because WFT was transient. - events = s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskFailed`, events) - - // Try to accept 2nd update in workflow 2nd time: get error. Poller will fail WT. Update is not aborted. - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - // 1st attempt UpdateWorkflowExecution call has timed out but the - // update is still running - s.Require().NotEmpty(task.Messages, "expected update message in task") - updRequestMsg := task.Messages[0] - s.EqualValues(8, updRequestMsg.GetEventId()) - // Fail WT one more time. This is transient WT and shouldn't appear in the history. - // Returning error will cause the poller to fail WT. - return nil, errors.New("malformed request") - }) - // The error is from RespondWorkflowTaskFailed, which should go w/o error. - s.NoError(err) +func (s *WorkflowUpdateSuite) Test1stAccept_2ndReject_1stComplete() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) - events = s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskFailed`, events) - - // Complete Update and workflow. - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskFailed - 8 WorkflowTaskScheduled // Transient WFT - 9 WorkflowTaskStarted`, task.History) + tv1 := env.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1).WithActivityIDNumber(1) + tv2 := env.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2).WithActivityIDNumber(2) - s.Require().NotEmpty(task.Messages, "expected update message in task") - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - Commands: append(s.UpdateAcceptCompleteCommands(s.Tv()), &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }), - }, nil - }) - s.NoError(err) + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted`, task.History) + return append(env.UpdateAcceptCommands(tv1), &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: tv1.ActivityID(), + ActivityType: tv1.ActivityType(), + TaskQueue: tv1.TaskQueue(), + ScheduleToCloseTimeout: tv1.Any().InfiniteTimeout(), + }}, + }), nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted // 1st update is accepted. + 6 ActivityTaskScheduled + 7 WorkflowTaskScheduled // Speculative WFT with WorkflowExecutionUpdateAccepted(5) event. + 8 WorkflowTaskStarted + `, task.History) + // Message handler rejects 2nd update. + return nil, nil + case 3: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 ActivityTaskScheduled + 7 WorkflowTaskScheduled + 8 WorkflowTaskStarted + 9 WorkflowTaskCompleted // Speculative WFT is written to the history because it shipped event. + 10 ActivityTaskStarted + 11 ActivityTaskCompleted + 12 WorkflowTaskScheduled + 13 WorkflowTaskStarted + `, task.History) + return env.UpdateCompleteCommands(tv1), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil + } + } - events = s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskFailed - 8 WorkflowTaskScheduled - 9 WorkflowTaskStarted - 10 WorkflowTaskCompleted // Transient WFT was completed successfully and ended up in the history. - 11 WorkflowExecutionUpdateAccepted - 12 WorkflowExecutionUpdateCompleted - 13 WorkflowExecutionCompleted`, events) - }) + var upd1RequestMsg *protocolpb.Message + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + upd1RequestMsg = task.Messages[0] + upd1Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd1RequestMsg.GetBody()) + s.Equal("args-value-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), upd1Request.GetInput().GetArgs())) + s.EqualValues(2, upd1RequestMsg.GetEventId()) + return env.UpdateAcceptMessages(tv1, upd1RequestMsg), nil + case 2: + upd2RequestMsg := task.Messages[0] + upd2Request := protoutils.UnmarshalAny[*updatepb.Request](s.T(), upd2RequestMsg.GetBody()) + s.Equal("args-value-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), upd2Request.GetInput().GetArgs())) + s.EqualValues(7, upd2RequestMsg.GetEventId()) + return env.UpdateRejectMessages(tv2, upd2RequestMsg), nil + case 3: + s.NotNil(upd1RequestMsg) + return env.UpdateCompleteMessages(tv1, upd1RequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil + } + } - t.Run("StartedSpeculativeWorkflowTask_ConvertToNormalBecauseOfBufferedSignal", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { + return env.Tv().Any().Payloads(), false, nil + } - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. Events 5 and 6 are written into the history when signal is received. - 6 WorkflowTaskStarted -`, task.History) - // Send signal which will be buffered. This will persist MS and speculative WT must be converted to normal. - err := s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) - return nil, nil - case 3: - s.EqualHistory(` - 7 WorkflowTaskCompleted - 8 WorkflowExecutionSignaled // It was buffered and got to the history after WT is completed. - 9 WorkflowTaskScheduled - 10 WorkflowTaskStarted`, task.History) - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + ActivityTaskHandler: atHandler, + Logger: env.Logger, + T: s.T(), + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1, 3: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] + updateResultCh1 := sendUpdateNoError(env, tv1) - s.EqualValues(5, updRequestMsg.GetEventId()) + // Accept update1 in WT1. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Update is rejected but corresponding speculative WT will be in the history anyway, because it was converted to normal due to buffered signal. - return s.UpdateRejectMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } - } + // Send 2nd update and create speculative WT2. + updateResultCh2 := sendUpdateNoError(env, tv2) - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } + // Poll for WT2 which 2nd update. Reject update2. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with workflow task which had events") - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + updateResult2 := <-updateResultCh2 + s.Equal("rejection-of-"+tv2.UpdateID(), updateResult2.GetOutcome().GetFailure().GetMessage()) - updateResultCh := sendUpdateNoError(s, s.Tv()) + err = poller.PollAndProcessActivityTask(false) + s.NoError(err) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResp := res.NewTask - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(0, updateResp.ResetHistoryEventId) + // Complete update1 in WT3. + res, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult1 := <-updateResultCh1 + s.Equal("success-result-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), updateResult1.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - // Complete workflow. - completeWorkflowResp, err := poller.HandlePartialWorkflowTask(updateResp.GetWorkflowTask(), false) - s.NoError(err) - s.NotNil(completeWorkflowResp) - s.Nil(completeWorkflowResp.GetWorkflowTask()) - s.EqualValues(0, completeWorkflowResp.ResetHistoryEventId) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted // Update was rejected on speculative WT, but events 5-7 are in the history because of buffered signal. - 8 WorkflowExecutionSignaled - 9 WorkflowTaskScheduled - 10 WorkflowTaskStarted - 11 WorkflowTaskCompleted - 12 WorkflowExecutionCompleted`, events) - }) + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) - t.Run("ScheduledSpeculativeWorkflowTask_ConvertToNormalBecauseOfSignal", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // It was initially speculative WT but was already converted to normal when signal was received. - 6 WorkflowExecutionSignaled - 7 WorkflowTaskStarted`, task.History) - return nil, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 2} // WTScheduled event which delivered update to the worker. + 6 ActivityTaskScheduled + 7 WorkflowTaskScheduled + 8 WorkflowTaskStarted + 9 WorkflowTaskCompleted // WT which had rejected update. + 10 ActivityTaskStarted + 11 ActivityTaskCompleted + 12 WorkflowTaskScheduled + 13 WorkflowTaskStarted + 14 WorkflowTaskCompleted + 15 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 5} + `, events) +} - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - s.Require().NotEmpty(task.Messages, "expected update message in task") - updRequestMsg := task.Messages[0] +func (s *WorkflowUpdateSuite) TestSpeculativeWorkflowTask_Fail() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + // Use test context with shorter timeout for this specific operation + timeoutCtx, cancel := context.WithTimeout(env.Context(), 2*time.Second) + defer cancel() + updateResultCh := sendUpdate(timeoutCtx, env, env.Tv()) + + // Try to accept update in workflow: get malformed response. + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + + s.NotEmpty(task.Messages, "expected update message in task") + updRequestMsg := task.Messages[0] + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: env.UpdateAcceptCommands(env.Tv()), + // Emulate bug in worker/SDK update handler code. Return malformed acceptance response. + Messages: []*protocolpb.Message{ + { + Id: env.Tv().MessageID() + "_update-accepted", + ProtocolInstanceId: env.Tv().Any().String(), + SequencingId: nil, + Body: protoutils.MarshalAny(s.T(), &updatepb.Acceptance{ + AcceptedRequestMessageId: updRequestMsg.GetId(), + AcceptedRequestSequencingEventId: updRequestMsg.GetEventId(), + AcceptedRequest: nil, // must not be nil! + }), + }, + }, + }, nil + }) + s.Error(err) + s.Contains(err.Error(), "wasn't found") + + // Update is aborted, speculative WFT failure is recorded into the history. + updateResult := <-updateResultCh + var wfNotReady *serviceerror.WorkflowNotReady + s.ErrorAs(updateResult.err, &wfNotReady) + s.Contains(updateResult.err.Error(), "Unable to perform workflow execution update due to unexpected workflow task failure.") + + // New transient WFT is created and is now included in the history. + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskFailed + 8 WorkflowTaskScheduled`, events) + + // Send Update again. It will be delivered on existing transient WFT. + updateResultCh = sendUpdate(timeoutCtx, env, env.Tv()) + + // Try to accept 2nd update in workflow: get error. Poller will fail WFT, but the registry won't be cleared and Update won't be aborted. + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskFailed + 8 WorkflowTaskScheduled // Transient WFT + 9 WorkflowTaskStarted`, task.History) + + s.NotEmpty(task.Messages, "expected update message in task") + updRequestMsg := task.Messages[0] + s.EqualValues(8, updRequestMsg.GetEventId()) + // Returning error will cause the poller to fail WFT. + return nil, errors.New("malformed request") + }) + // The error is from RespondWorkflowTaskFailed, which should go w/o error. + s.NoError(err) + + // Update timed out, but stays in the registry and will be delivered again on the new transient WFT. + updateResult = <-updateResultCh + s.Error(updateResult.err) + s.True(common.IsContextDeadlineExceededErr(updateResult.err), "UpdateWorkflowExecution must timeout after 2 seconds") + s.Nil(updateResult.response) + + // This WFT failure wasn't recorded because WFT was transient, but the scheduled event is included. + events = env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskFailed + 8 WorkflowTaskScheduled`, events) + + // Try to accept 2nd update in workflow 2nd time: get error. Poller will fail WT. Update is not aborted. + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + // 1st attempt UpdateWorkflowExecution call has timed out but the + // update is still running + s.NotEmpty(task.Messages, "expected update message in task") + updRequestMsg := task.Messages[0] + s.EqualValues(8, updRequestMsg.GetEventId()) + // Fail WT one more time. This is transient WT and shouldn't appear in the history. + // Returning error will cause the poller to fail WT. + return nil, errors.New("malformed request") + }) + // The error is from RespondWorkflowTaskFailed, which should go w/o error. + s.NoError(err) - s.EqualValues(6, updRequestMsg.GetEventId()) + events = env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskFailed + 8 WorkflowTaskScheduled`, events) + + // Complete Update and workflow. + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskFailed + 8 WorkflowTaskScheduled // Transient WFT + 9 WorkflowTaskStarted`, task.History) + + s.NotEmpty(task.Messages, "expected update message in task") + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + Commands: append(env.UpdateAcceptCompleteCommands(env.Tv()), &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }), + }, nil + }) + s.NoError(err) - // Update is rejected but corresponding speculative WT was already converted to normal, - // and will be in the history anyway. - return s.UpdateRejectMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } - } + events = env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskFailed + 8 WorkflowTaskScheduled + 9 WorkflowTaskStarted + 10 WorkflowTaskCompleted // Transient WFT was completed successfully and ended up in the history. + 11 WorkflowExecutionUpdateAccepted + 12 WorkflowExecutionUpdateCompleted + 13 WorkflowExecutionCompleted`, events) +} - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), +func (s *WorkflowUpdateSuite) TestStartedSpeculativeWorkflowTask_ConvertToNormalBecauseOfBufferedSignal() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. Events 5 and 6 are written into the history when signal is received. + 6 WorkflowTaskStarted + `, task.History) + // Send signal which will be buffered. This will persist MS and speculative WT must be converted to normal. + err := env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) + return nil, nil + case 3: + s.EqualHistory(` + 7 WorkflowTaskCompleted + 8 WorkflowExecutionSignaled // It was buffered and got to the history after WT is completed. + 9 WorkflowTaskScheduled + 10 WorkflowTaskStarted`, task.History) + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1, 3: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + + s.EqualValues(5, updRequestMsg.GetEventId()) + + // Update is rejected but corresponding speculative WT will be in the history anyway, because it was converted to normal due to buffered signal. + return env.UpdateRejectMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil + } + } - updateResultCh := sendUpdateNoError(s, s.Tv()) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Send signal which will NOT be buffered because speculative WT is not started yet (only scheduled). - // This will persist MS and speculative WT must be converted to normal. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowExecutionSignaled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted // Update was rejected but WT events 5,7,8 are in the history because of signal. -`, events) - }) + updateResultCh := sendUpdateNoError(env, env.Tv()) - t.Run("SpeculativeWorkflowTask_StartToCloseTimeout", func(t *testing.T) { - // Uses CaptureMetricsHandler which requires a dedicated cluster to avoid metric interference. - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) - - request := &workflowservice.StartWorkflowExecutionRequest{ - RequestId: s.Tv().Any().String(), - Namespace: s.Namespace().String(), - WorkflowId: s.Tv().WorkflowID(), - WorkflowType: s.Tv().WorkflowType(), - TaskQueue: s.Tv().TaskQueue(), - WorkflowTaskTimeout: durationpb.New(1 * time.Second), // Important! - } + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResp := res.NewTask + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(0, updateResp.ResetHistoryEventId) - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err) + // Complete workflow. + completeWorkflowResp, err := poller.HandlePartialWorkflowTask(updateResp.GetWorkflowTask(), false) + s.NoError(err) + s.NotNil(completeWorkflowResp) + s.Nil(completeWorkflowResp.GetWorkflowTask()) + s.EqualValues(0, completeWorkflowResp.ResetHistoryEventId) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted -`, task.History) - // Emulate slow worker: sleep little more than WT timeout. - time.Sleep(request.WorkflowTaskTimeout.AsDuration() + 100*time.Millisecond) //nolint:forbidigo - // This doesn't matter because WT times out before update is applied. - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - case 3: - // Speculative WT timed out and retried as normal WT. - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskTimedOut - 8 WorkflowTaskScheduled {"Attempt":2 } // Transient WT. - 9 WorkflowTaskStarted`, task.History) - commands := append(s.UpdateAcceptCompleteCommands(s.Tv()), - &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }) - return commands, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - s.Len(task.Messages, 1) - updRequestMsg := task.Messages[0] + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - // This doesn't matter because WT times out before update is applied. - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - case 3: - // Update is still in registry and was sent again. - updRequestMsg := task.Messages[0] + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted // Update was rejected on speculative WT, but events 5-7 are in the history because of buffered signal. + 8 WorkflowExecutionSignaled + 9 WorkflowTaskScheduled + 10 WorkflowTaskStarted + 11 WorkflowTaskCompleted + 12 WorkflowExecutionCompleted`, events) +} - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } +func (s *WorkflowUpdateSuite) TestScheduledSpeculativeWorkflowTask_ConvertToNormalBecauseOfSignal() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // It was initially speculative WT but was already converted to normal when signal was received. + 6 WorkflowExecutionSignaled + 7 WorkflowTaskStarted`, task.History) + return nil, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + s.NotEmpty(task.Messages, "expected update message in task") + updRequestMsg := task.Messages[0] + + s.EqualValues(6, updRequestMsg.GetEventId()) + + // Update is rejected but corresponding speculative WT was already converted to normal, + // and will be in the history anyway. + return env.UpdateRejectMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err = poller.PollAndProcessWorkflowTask() - s.NoError(err) - - updateResultCh := sendUpdateNoError(s, s.Tv()) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Try to process update in workflow, but it takes more than WT timeout. So, WT times out. - _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.Error(err) - s.Equal("Workflow task not found.", err.Error()) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // ensure correct metrics were recorded - snap := capture.Snapshot() + updateResultCh := sendUpdateNoError(env, env.Tv()) - var speculativeWorkflowTaskTimeoutTasks int - for _, m := range snap[metrics.TaskRequests.Name()] { - if m.Tags[metrics.OperationTagName] == metrics.TaskTypeTimerActiveTaskSpeculativeWorkflowTaskTimeout { - speculativeWorkflowTaskTimeoutTasks += 1 - } - } - s.Equal(1, speculativeWorkflowTaskTimeoutTasks, "expected 1 speculative workflow task timeout task to be created") + // Send signal which will NOT be buffered because speculative WT is not started yet (only scheduled). + // This will persist MS and speculative WT must be converted to normal. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) - var speculativeStartToCloseTimeouts int - for _, m := range snap[metrics.StartToCloseTimeoutCounter.Name()] { - if m.Tags[metrics.OperationTagName] == metrics.TaskTypeTimerActiveTaskSpeculativeWorkflowTaskTimeout { - speculativeStartToCloseTimeouts += 1 - } - } - s.Equal(1, speculativeStartToCloseTimeouts, "expected 1 timeout of a speculative workflow task timeout task") + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - // New normal WT was created on server after speculative WT has timed out. - // It will accept and complete update first and workflow itself with the same WT. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResp := res.NewTask - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, updateResp.ResetHistoryEventId) - s.Nil(updateResp.GetWorkflowTask()) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskTimedOut // Timeout of speculative WT writes events 5-7 - 8 WorkflowTaskScheduled {"Attempt":2 } - 9 WorkflowTaskStarted - 10 WorkflowTaskCompleted - 11 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 8} // WTScheduled event which delivered update to the worker. - 12 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 11} - 13 WorkflowExecutionCompleted`, events) - }) + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - t.Run("SpeculativeWorkflowTask_ScheduleToStartTimeout", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - // Drain first WT and respond with sticky enabled response to enable sticky task queue. - stickyScheduleToStartTimeout := 1 * time.Second - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - StickyAttributes: s.Tv().StickyExecutionAttributes(stickyScheduleToStartTimeout), - }, nil - }) - s.NoError(err) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowExecutionSignaled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted // Update was rejected but WT events 5,7,8 are in the history because of signal. + `, events) +} - sendUpdateNoError(s, s.Tv()) +func (s *WorkflowUpdateSuite) TestSpeculativeWorkflowTask_StartToCloseTimeout() { + env := testcore.NewEnv(s.T()) + capture := env.StartNamespaceMetricCapture() + + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: env.Tv().Any().String(), + Namespace: env.Namespace().String(), + WorkflowId: env.Tv().WorkflowID(), + WorkflowType: env.Tv().WorkflowType(), + TaskQueue: env.Tv().TaskQueue(), + WorkflowTaskTimeout: durationpb.New(1 * time.Second), // Important! + } - s.Logger.Info("Wait for sticky timeout to fire. Sleep poller.StickyScheduleToStartTimeout+ seconds.", tag.Duration("StickyScheduleToStartTimeout", stickyScheduleToStartTimeout)) - time.Sleep(stickyScheduleToStartTimeout + 100*time.Millisecond) //nolint:forbidigo - s.Logger.Info("Sleep is done.") + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), request) + s.NoError(err) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + // Emulate slow worker: sleep little more than WT timeout. + time.Sleep(request.WorkflowTaskTimeout.AsDuration() + 100*time.Millisecond) //nolint:forbidigo + // This doesn't matter because WT times out before update is applied. + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + case 3: + // Speculative WT timed out and retried as normal WT. + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskTimedOut + 8 WorkflowTaskScheduled {"Attempt":2 } // Transient WT. + 9 WorkflowTaskStarted`, task.History) + commands := append(env.UpdateAcceptCompleteCommands(env.Tv()), + &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }) + return commands, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil + } + } - // Try to process update in workflow, poll from normal task queue. - res, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - // Speculative WFT timed out on sticky task queue. Server sent full history with sticky timeout event. - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskTimedOut - 7 WorkflowTaskScheduled {"Attempt":1} // Normal WT. - 8 WorkflowTaskStarted`, task.History) - - // Reject update, but WFT will still be in the history due to timeout on sticky queue. - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateRejectMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - s.NotNil(res) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT was written into the history because of timeout. - 6 WorkflowTaskTimedOut - 7 WorkflowTaskScheduled {"Attempt":1} // Second attempt WT is normal WT (clear stickiness reset attempts count). - 8 WorkflowTaskStarted - 9 WorkflowTaskCompleted // Normal WT is completed and events are in the history even update was rejected. - `, events) - }) + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + s.Len(task.Messages, 1) + updRequestMsg := task.Messages[0] + + // This doesn't matter because WT times out before update is applied. + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + case 3: + // Update is still in registry and was sent again. + updRequestMsg := task.Messages[0] + + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil + } + } - t.Run("SpeculativeWorkflowTask_ScheduleToStartTimeoutOnNormalTaskQueue", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled {"TaskQueue": {"Kind": 1}} // Speculative WT timed out on normal(1) task queue. - 6 WorkflowTaskTimedOut - 7 WorkflowTaskScheduled {"Attempt":1} // Normal WT is scheduled. - 8 WorkflowTaskStarted -`, task.History) - return nil, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + // Drain first WT. + _, err = poller.PollAndProcessWorkflowTask() + s.NoError(err) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + updateResultCh := sendUpdateNoError(env, env.Tv()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(7, updRequestMsg.GetEventId()) + // Try to process update in workflow, but it takes more than WT timeout. So, WT times out. + _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.Error(err) + s.Equal("Workflow task not found.", err.Error()) - return s.UpdateRejectMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + // ensure correct metrics were recorded + var speculativeWorkflowTaskTimeoutTasks int + for _, m := range capture.Metric(metrics.TaskRequests.Name()) { + if m.Tags[metrics.OperationTagName] == metrics.TaskTypeTimerActiveTaskSpeculativeWorkflowTaskTimeout { + speculativeWorkflowTaskTimeoutTasks += 1 } + } + s.Equal(1, speculativeWorkflowTaskTimeoutTasks, "expected 1 speculative workflow task timeout task to be created") - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + var speculativeStartToCloseTimeouts int + for _, m := range capture.Metric(metrics.StartToCloseTimeoutCounter.Name()) { + if m.Tags[metrics.OperationTagName] == metrics.TaskTypeTimerActiveTaskSpeculativeWorkflowTaskTimeout { + speculativeStartToCloseTimeouts += 1 } + } + s.Equal(1, speculativeStartToCloseTimeouts, "expected 1 timeout of a speculative workflow task timeout task") - // Drain existing WT from normal task queue. - _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) + // New normal WT was created on server after speculative WT has timed out. + // It will accept and complete update first and workflow itself with the same WT. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResp := res.NewTask + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, updateResp.ResetHistoryEventId) + s.Nil(updateResp.GetWorkflowTask()) - // Now send an update. It will create a speculative WT on normal task queue, - // which will time out in 5 seconds and create new normal WT. - updateResultCh := sendUpdateNoError(s, s.Tv()) + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) - // TODO: it would be nice to shutdown matching before sending an update to emulate case which is actually being tested here. - // But test infrastructure doesn't support it. 5 seconds sleep will cause same observable effect. - s.Logger.Info("Sleep 5+ seconds to make sure tasks.SpeculativeWorkflowTaskScheduleToStartTimeout time has passed.") - time.Sleep(5*time.Second + 100*time.Millisecond) //nolint:forbidigo - s.Logger.Info("Sleep 5+ seconds is done.") + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with normal workflow task") - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled {"TaskQueue": {"Kind": 1}} // Speculative WT timed out on normal(1) task queue. - 6 WorkflowTaskTimedOut - 7 WorkflowTaskScheduled {"Attempt":1} // Normal WT is scheduled. Even update was rejected, WT is in the history. - 8 WorkflowTaskStarted - 9 WorkflowTaskCompleted -`, events) - }) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskTimedOut // Timeout of speculative WT writes events 5-7 + 8 WorkflowTaskScheduled {"Attempt":2 } + 9 WorkflowTaskStarted + 10 WorkflowTaskCompleted + 11 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 8} // WTScheduled event which delivered update to the worker. + 12 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 11} + 13 WorkflowExecutionCompleted`, events) +} - t.Run("StartedSpeculativeWorkflowTask_TerminateWorkflow", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) +func (s *WorkflowUpdateSuite) TestSpeculativeWorkflowTask_ScheduleToStartTimeout() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + // Drain first WT and respond with sticky enabled response to enable sticky task queue. + stickyScheduleToStartTimeout := 1 * time.Second + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + StickyAttributes: env.Tv().StickyExecutionAttributes(stickyScheduleToStartTimeout), + }, nil + }) + s.NoError(err) + + sendUpdateNoError(env, env.Tv()) + + env.Logger.Info("Wait for sticky timeout to fire. Sleep poller.StickyScheduleToStartTimeout+ seconds.", tag.Duration("StickyScheduleToStartTimeout", stickyScheduleToStartTimeout)) + time.Sleep(stickyScheduleToStartTimeout + 100*time.Millisecond) //nolint:forbidigo + env.Logger.Info("Sleep is done.") + + // Try to process update in workflow, poll from normal task queue. + res, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + // Speculative WFT timed out on sticky task queue. Server sent full history with sticky timeout event. + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskTimedOut + 7 WorkflowTaskScheduled {"Attempt":1} // Normal WT. + 8 WorkflowTaskStarted`, task.History) + + // Reject update, but WFT will still be in the history due to timeout on sticky queue. + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateRejectMessages(env.Tv(), task.Messages[0]), + }, nil + }) + s.NoError(err) + s.NotNil(res) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - // Terminate workflow while speculative WT is running. - _, err := s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: s.Tv().WorkflowExecution(), - Reason: s.Tv().Any().String(), - }) - s.NoError(err) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT was written into the history because of timeout. + 6 WorkflowTaskTimedOut + 7 WorkflowTaskScheduled {"Attempt":1} // Second attempt WT is normal WT (clear stickiness reset attempts count). + 8 WorkflowTaskStarted + 9 WorkflowTaskCompleted // Normal WT is completed and events are in the history even update was rejected. + `, events) +} - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } +func (s *WorkflowUpdateSuite) TestSpeculativeWorkflowTask_ScheduleToStartTimeoutOnNormalTaskQueue() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled {"TaskQueue": {"Kind": 1}} // Speculative WT timed out on normal(1) task queue. + 6 WorkflowTaskTimedOut + 7 WorkflowTaskScheduled {"Attempt":1} // Normal WT is scheduled. + 8 WorkflowTaskStarted + `, task.History) + return nil, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(7, updRequestMsg.GetEventId()) + + return env.UpdateRejectMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + // Drain existing WT from normal task queue. + _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) - oneSecondTimeoutCtx, cancel := context.WithTimeout(testcore.NewContext(), 1*time.Second) - defer cancel() - updateResultCh := sendUpdate(oneSecondTimeoutCtx, s, s.Tv()) + // Now send an update. It will create a speculative WT on normal task queue, + // which will time out in 5 seconds and create new normal WT. + updateResultCh := sendUpdateNoError(env, env.Tv()) - // Process update in workflow. - _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.Error(err) - s.ErrorAs(err, new(*serviceerror.NotFound)) - s.ErrorContains(err, "Workflow task not found.") - - updateResult := <-updateResultCh - s.Error(updateResult.err) - var notFound *serviceerror.NotFound - s.ErrorAs(updateResult.err, ¬Found) - s.ErrorContains(updateResult.err, update.AbortedByWorkflowClosingErr.Error()) - s.Nil(updateResult.response) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT was converted to normal WT during termination. - 6 WorkflowTaskStarted - 7 WorkflowTaskFailed - 8 WorkflowExecutionTerminated`, events) - - msResp, err := s.AdminClient().DescribeMutableState(testcore.NewContext(), &adminservice.DescribeMutableStateRequest{ - Namespace: s.Namespace().String(), - Execution: s.Tv().WorkflowExecution(), - Archetype: chasm.WorkflowArchetype, - }) - s.NoError(err) - s.EqualValues(7, msResp.GetDatabaseMutableState().GetExecutionInfo().GetCompletionEventBatchId(), "completion_event_batch_id should point to WTFailed event") - }) + // TODO: it would be nice to shutdown matching before sending an update to emulate case which is actually being tested here. + // But test infrastructure doesn't support it. 5 seconds sleep will cause same observable effect. + env.Logger.Info("Sleep 5+ seconds to make sure tasks.SpeculativeWorkflowTaskScheduleToStartTimeout time has passed.") + time.Sleep(5*time.Second + 100*time.Millisecond) //nolint:forbidigo + env.Logger.Info("Sleep 5+ seconds is done.") - t.Run("ScheduledSpeculativeWorkflowTask_TerminateWorkflow", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(0, res.NewTask.ResetHistoryEventId, "no reset of event ID should happened after update rejection if it was delivered with normal workflow task") - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled {"TaskQueue": {"Kind": 1}} // Speculative WT timed out on normal(1) task queue. + 6 WorkflowTaskTimedOut + 7 WorkflowTaskScheduled {"Attempt":1} // Normal WT is scheduled. Even update was rejected, WT is in the history. + 8 WorkflowTaskStarted + 9 WorkflowTaskCompleted + `, events) +} + +func (s *WorkflowUpdateSuite) TestStartedSpeculativeWorkflowTask_TerminateWorkflow() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + // Terminate workflow while speculative WT is running. + _, err := env.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(env.Context()), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + WorkflowExecution: env.Tv().WorkflowExecution(), + Reason: env.Tv().Any().String(), + }) + s.NoError(err) + + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted`, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - oneSecondTimeoutCtx, cancel := context.WithTimeout(testcore.NewContext(), 1*time.Second) - defer cancel() - updateResultCh := sendUpdate(oneSecondTimeoutCtx, s, s.Tv()) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Terminate workflow after speculative WT is scheduled but not started. - _, err = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: s.Tv().WorkflowExecution(), - Reason: s.Tv().Any().String(), - }) - s.NoError(err) + oneSecondTimeoutCtx, cancel := context.WithTimeout(env.Context(), 1*time.Second) + defer cancel() + updateResultCh := sendUpdate(oneSecondTimeoutCtx, env, env.Tv()) - updateResult := <-updateResultCh - s.Error(updateResult.err) - var notFound *serviceerror.NotFound - s.ErrorAs(updateResult.err, ¬Found) - s.ErrorContains(updateResult.err, update.AbortedByWorkflowClosingErr.Error()) - s.Nil(updateResult.response) - - s.Equal(1, wtHandlerCalls) - s.Equal(1, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionTerminated // Speculative WTScheduled event is not written to history if WF is terminated. -`, events) - - msResp, err := s.AdminClient().DescribeMutableState(testcore.NewContext(), &adminservice.DescribeMutableStateRequest{ - Namespace: s.Namespace().String(), - Execution: s.Tv().WorkflowExecution(), - Archetype: chasm.WorkflowArchetype, - }) - s.NoError(err) - s.EqualValues(5, msResp.GetDatabaseMutableState().GetExecutionInfo().GetCompletionEventBatchId(), "completion_event_batch_id should point to WFTerminated event") + // Process update in workflow. + _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.Error(err) + s.ErrorAs(err, new(*serviceerror.NotFound)) + s.ErrorContains(err, "Workflow task not found.") + + updateResult := <-updateResultCh + s.Error(updateResult.err) + var notFound *serviceerror.NotFound + s.ErrorAs(updateResult.err, ¬Found) + s.ErrorContains(updateResult.err, update.AbortedByWorkflowClosingErr.Error()) + s.Nil(updateResult.response) + + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) + + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT was converted to normal WT during termination. + 6 WorkflowTaskStarted + 7 WorkflowTaskFailed + 8 WorkflowExecutionTerminated`, events) + + msResp, err := env.AdminClient().DescribeMutableState(testcore.NewContext(env.Context()), &adminservice.DescribeMutableStateRequest{ + Namespace: env.Namespace().String(), + Execution: env.Tv().WorkflowExecution(), + Archetype: chasm.WorkflowArchetype, }) + s.NoError(err) + s.EqualValues(7, msResp.GetDatabaseMutableState().GetExecutionInfo().GetCompletionEventBatchId(), "completion_event_batch_id should point to WTFailed event") +} - t.Run("CompleteWorkflow_AbortUpdates", func(t *testing.T) { - type testCase struct { - name string - description string - updateErr map[string]string // Update error by completionCommand.Name. - updateFailure string - commands func(s *testcore.FunctionalTestBase, tv *testvars.TestVars) []*commandpb.Command - messages func(s *testcore.FunctionalTestBase, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message +func (s *WorkflowUpdateSuite) TestScheduledSpeculativeWorkflowTask_TerminateWorkflow() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } - type completionCommand struct { - name string - finalStatus enumspb.WorkflowExecutionStatus - useRunID bool - command func(tv *testvars.TestVars) *commandpb.Command + } + + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } - testCases := []testCase{ - { - name: "update admitted", - description: "update in stateAdmitted must get an error", - updateErr: map[string]string{ - "workflow completed": update.AbortedByWorkflowClosingErr.Error(), - "workflow continued as new without runID": "workflow operation can not be applied because workflow is closing", - "workflow continued as new with runID": "workflow operation can not be applied because workflow is closing", - "workflow failed": update.AbortedByWorkflowClosingErr.Error(), - }, - updateFailure: "", - commands: func(s *testcore.FunctionalTestBase, _ *testvars.TestVars) []*commandpb.Command { return nil }, - messages: func(s *testcore.FunctionalTestBase, _ *testvars.TestVars, _ *protocolpb.Message) []*protocolpb.Message { - return nil - }, + } + + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } + + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) + + oneSecondTimeoutCtx, cancel := context.WithTimeout(env.Context(), 1*time.Second) + defer cancel() + updateResultCh := sendUpdate(oneSecondTimeoutCtx, env, env.Tv()) + + // Terminate workflow after speculative WT is scheduled but not started. + _, err = env.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(env.Context()), &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + WorkflowExecution: env.Tv().WorkflowExecution(), + Reason: env.Tv().Any().String(), + }) + s.NoError(err) + + updateResult := <-updateResultCh + s.Error(updateResult.err) + var notFound *serviceerror.NotFound + s.ErrorAs(updateResult.err, ¬Found) + s.ErrorContains(updateResult.err, update.AbortedByWorkflowClosingErr.Error()) + s.Nil(updateResult.response) + + s.Equal(1, wtHandlerCalls) + s.Equal(1, msgHandlerCalls) + + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionTerminated // Speculative WTScheduled event is not written to history if WF is terminated. + `, events) + + msResp, err := env.AdminClient().DescribeMutableState(testcore.NewContext(env.Context()), &adminservice.DescribeMutableStateRequest{ + Namespace: env.Namespace().String(), + Execution: env.Tv().WorkflowExecution(), + Archetype: chasm.WorkflowArchetype, + }) + s.NoError(err) + s.EqualValues(5, msResp.GetDatabaseMutableState().GetExecutionInfo().GetCompletionEventBatchId(), "completion_event_batch_id should point to WFTerminated event") +} + +func (s *WorkflowUpdateSuite) TestCompleteWorkflow_AbortUpdates() { + type testCase struct { + name string + description string + updateErr map[string]string // Update error by completionCommand.Name. + updateFailure string + commands func(env *testcore.TestEnv, tv *testvars.TestVars) []*commandpb.Command + messages func(env *testcore.TestEnv, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message + } + type completionCommand struct { + name string + finalStatus enumspb.WorkflowExecutionStatus + useRunID bool + command func(tv *testvars.TestVars) *commandpb.Command + } + testCases := []testCase{ + { + name: "update admitted", + description: "update in stateAdmitted must get an error", + updateErr: map[string]string{ + "workflow completed": update.AbortedByWorkflowClosingErr.Error(), + "workflow continued as new without runID": "workflow operation can not be applied because workflow is closing", + "workflow continued as new with runID": "workflow operation can not be applied because workflow is closing", + "workflow failed": update.AbortedByWorkflowClosingErr.Error(), }, - { - name: "update accepted", - description: "update in stateAccepted must get an update failure", - updateErr: map[string]string{"*": ""}, - updateFailure: "Workflow Update failed because the Workflow completed before the Update completed.", - commands: func(s *testcore.FunctionalTestBase, tv *testvars.TestVars) []*commandpb.Command { - return s.UpdateAcceptCommands(tv) - }, - messages: func(s *testcore.FunctionalTestBase, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message { - return s.UpdateAcceptMessages(tv, updRequestMsg) - }, + updateFailure: "", + commands: func(env *testcore.TestEnv, _ *testvars.TestVars) []*commandpb.Command { return nil }, + messages: func(env *testcore.TestEnv, _ *testvars.TestVars, _ *protocolpb.Message) []*protocolpb.Message { + return nil }, - { - name: "update completed", - description: "completed update must not be affected by workflow completion", - updateErr: map[string]string{"*": ""}, - updateFailure: "", - commands: func(s *testcore.FunctionalTestBase, tv *testvars.TestVars) []*commandpb.Command { - return s.UpdateAcceptCompleteCommands(tv) - }, - messages: func(s *testcore.FunctionalTestBase, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message { - return s.UpdateAcceptCompleteMessages(tv, updRequestMsg) - }, + }, + { + name: "update accepted", + description: "update in stateAccepted must get an update failure", + updateErr: map[string]string{"*": ""}, + updateFailure: "Workflow Update failed because the Workflow completed before the Update completed.", + commands: func(env *testcore.TestEnv, tv *testvars.TestVars) []*commandpb.Command { + return env.UpdateAcceptCommands(tv) }, - { - name: "update rejected", - description: "rejected update must be rejected with rejection from workflow", - updateErr: map[string]string{"*": ""}, - updateFailure: "rejection-of-", // Rejection from workflow. - commands: func(s *testcore.FunctionalTestBase, tv *testvars.TestVars) []*commandpb.Command { return nil }, - messages: func(s *testcore.FunctionalTestBase, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message { - return s.UpdateRejectMessages(tv, updRequestMsg) - }, + messages: func(env *testcore.TestEnv, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message { + return env.UpdateAcceptMessages(tv, updRequestMsg) }, - } + }, + { + name: "update completed", + description: "completed update must not be affected by workflow completion", + updateErr: map[string]string{"*": ""}, + updateFailure: "", + commands: func(env *testcore.TestEnv, tv *testvars.TestVars) []*commandpb.Command { + return env.UpdateAcceptCompleteCommands(tv) + }, + messages: func(env *testcore.TestEnv, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message { + return env.UpdateAcceptCompleteMessages(tv, updRequestMsg) + }, + }, + { + name: "update rejected", + description: "rejected update must be rejected with rejection from workflow", + updateErr: map[string]string{"*": ""}, + updateFailure: "rejection-of-", // Rejection from workflow. + commands: func(env *testcore.TestEnv, tv *testvars.TestVars) []*commandpb.Command { return nil }, + messages: func(env *testcore.TestEnv, tv *testvars.TestVars, updRequestMsg *protocolpb.Message) []*protocolpb.Message { + return env.UpdateRejectMessages(tv, updRequestMsg) + }, + }, + } - workflowCompletionCommands := []completionCommand{ - { - name: "workflow completed", - finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, - useRunID: false, - command: func(_ *testvars.TestVars) *commandpb.Command { - return &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - } - }, + workflowCompletionCommands := []completionCommand{ + { + name: "workflow completed", + finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, + useRunID: false, + command: func(_ *testvars.TestVars) *commandpb.Command { + return &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + } }, - { - name: "workflow continued as new with runID", - finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW, - useRunID: true, - command: func(tv *testvars.TestVars) *commandpb.Command { - return &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ - WorkflowType: tv.WorkflowType(), - TaskQueue: tv.TaskQueue(), - }}, - } - }, + }, + { + name: "workflow continued as new with runID", + finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW, + useRunID: true, + command: func(tv *testvars.TestVars) *commandpb.Command { + return &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv.WorkflowType(), + TaskQueue: tv.TaskQueue(), + }}, + } }, - { - name: "workflow continued as new without runID", - finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, // This is the status of new run because update doesn't go to particular runID. - useRunID: false, - command: func(tv *testvars.TestVars) *commandpb.Command { - return &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ - WorkflowType: tv.WorkflowType(), - TaskQueue: tv.TaskQueue(), - }}, - } - }, + }, + { + name: "workflow continued as new without runID", + finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, // This is the status of new run because update doesn't go to particular runID. + useRunID: false, + command: func(tv *testvars.TestVars) *commandpb.Command { + return &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv.WorkflowType(), + TaskQueue: tv.TaskQueue(), + }}, + } }, - { - name: "workflow failed", - finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_FAILED, - useRunID: true, - command: func(tv *testvars.TestVars) *commandpb.Command { - return &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_FailWorkflowExecutionCommandAttributes{FailWorkflowExecutionCommandAttributes: &commandpb.FailWorkflowExecutionCommandAttributes{ - Failure: tv.Any().ApplicationFailure(), - }}, - } - }, + }, + { + name: "workflow failed", + finalStatus: enumspb.WORKFLOW_EXECUTION_STATUS_FAILED, + useRunID: true, + command: func(tv *testvars.TestVars) *commandpb.Command { + return &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_FailWorkflowExecutionCommandAttributes{FailWorkflowExecutionCommandAttributes: &commandpb.FailWorkflowExecutionCommandAttributes{ + Failure: tv.Any().ApplicationFailure(), + }}, + } }, - } - - for _, tc := range testCases { - for _, wfCC := range workflowCompletionCommands { - t.Run(tc.name+" "+wfCC.name, func(t *testing.T) { - s := testcore.NewEnv(t) - runID := mustStartWorkflow(s, s.Tv()) - tv := s.Tv() - if wfCC.useRunID { - tv = tv.WithRunID(runID) - } + }, + } - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - return append(tc.commands(s.FunctionalTestBase, s.Tv()), wfCC.command(s.Tv())), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + for _, tc := range testCases { + for _, wfCC := range workflowCompletionCommands { + s.Run(tc.name+" "+wfCC.name, func(s *WorkflowUpdateSuite) { + env := testcore.NewEnv(s.T()) + runID := mustStartWorkflow(env, env.Tv()) + tv := env.Tv() + if wfCC.useRunID { + tv = tv.WithRunID(runID) + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - return tc.messages(s.FunctionalTestBase, s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + return append(tc.commands(env, env.Tv()), wfCC.command(env.Tv())), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + return tc.messages(env, env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - updateResultCh := sendUpdate(testcore.NewContext(), s, tv) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Complete workflow. - _, err = poller.PollAndProcessWorkflowTask() - s.NoError(err) + updateResultCh := sendUpdate(testcore.NewContext(env.Context()), env, tv) - updateResult := <-updateResultCh - expectedUpdateErr := tc.updateErr[wfCC.name] - if expectedUpdateErr == "" { - expectedUpdateErr = tc.updateErr["*"] - } - if expectedUpdateErr != "" { - s.Error(updateResult.err, tc.description) - s.Equal(expectedUpdateErr, updateResult.err.Error()) - } else { - s.NoError(updateResult.err, tc.description) - } + // Complete workflow. + _, err = poller.PollAndProcessWorkflowTask() + s.NoError(err) - if tc.updateFailure != "" { - s.NotNil(updateResult.response.GetOutcome().GetFailure(), tc.description) - s.Contains(updateResult.response.GetOutcome().GetFailure().GetMessage(), tc.updateFailure, tc.description) - } else { - s.Nil(updateResult.response.GetOutcome().GetFailure(), tc.description) - } + updateResult := <-updateResultCh + expectedUpdateErr := tc.updateErr[wfCC.name] + if expectedUpdateErr == "" { + expectedUpdateErr = tc.updateErr["*"] + } + if expectedUpdateErr != "" { + s.Error(updateResult.err, tc.description) + s.Equal(expectedUpdateErr, updateResult.err.Error()) + } else { + s.NoError(updateResult.err, tc.description) + } - if expectedUpdateErr == "" && tc.updateFailure == "" { - s.Equal(runID, updateResult.response.GetUpdateRef().GetWorkflowExecution().GetRunId(), "update wasn't applied to the same run as was started") - } + if tc.updateFailure != "" { + s.NotNil(updateResult.response.GetOutcome().GetFailure(), tc.description) + s.Contains(updateResult.response.GetOutcome().GetFailure().GetMessage(), tc.updateFailure, tc.description) + } else { + s.Nil(updateResult.response.GetOutcome().GetFailure(), tc.description) + } - // Check that update didn't block workflow completion. - descResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - Execution: tv.WorkflowExecution(), - }) - s.NoError(err) - s.Equal(wfCC.finalStatus, descResp.WorkflowExecutionInfo.Status) + if expectedUpdateErr == "" && tc.updateFailure == "" { + s.Equal(runID, updateResult.response.GetUpdateRef().GetWorkflowExecution().GetRunId(), "update wasn't applied to the same run as was started") + } - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) + // Check that update didn't block workflow completion. + descResp, err := env.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(env.Context()), &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + Execution: tv.WorkflowExecution(), }) - } - } - }) - - t.Run("SpeculativeWorkflowTask_Heartbeat", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) - - // Drain first WT. - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) - - updateResultCh := sendUpdateNoError(s, s.Tv()) - - // Heartbeat from speculative WT (no messages, no commands). - var updRequestMsg *protocolpb.Message - res, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Events (5 and 6) are for speculative WT, but they won't disappear after reject because speculative WT is converted to normal during heartbeat. - 6 WorkflowTaskStarted - `, task.History) - - s.Len(task.Messages, 1) - updRequestMsg = task.Messages[0] - s.EqualValues(5, updRequestMsg.GetEventId()) + s.NoError(err) + s.Equal(wfCC.finalStatus, descResp.WorkflowExecutionInfo.Status) - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - ReturnNewWorkflowTask: true, - ForceCreateNewWorkflowTask: true, - }, nil + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) }) - s.NoError(err) - - // Reject update from workflow. - updateResp, err := s.TaskPoller().HandleWorkflowTask(s.Tv(), - res.GetWorkflowTask(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistory(` - 7 WorkflowTaskCompleted - 8 WorkflowTaskScheduled // New WT (after heartbeat) is normal and won't disappear from the history after reject. - 9 WorkflowTaskStarted - `, task.History) - - s.Empty(task.Messages) + } + } +} - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateRejectMessages(s.Tv(), updRequestMsg), - }, nil - }) - s.NoError(err) - s.NotNil(updateResp) +func (s *WorkflowUpdateSuite) TestSpeculativeWorkflowTask_Heartbeat() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + // Drain first WT. + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + updateResultCh := sendUpdateNoError(env, env.Tv()) + + // Heartbeat from speculative WT (no messages, no commands). + var updRequestMsg *protocolpb.Message + res, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Events (5 and 6) are for speculative WT, but they won't disappear after reject because speculative WT is converted to normal during heartbeat. + 6 WorkflowTaskStarted + `, task.History) + + s.Len(task.Messages, 1) + updRequestMsg = task.Messages[0] + s.EqualValues(5, updRequestMsg.GetEventId()) + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + ReturnNewWorkflowTask: true, + ForceCreateNewWorkflowTask: true, + }, nil + }) + s.NoError(err) + + // Reject update from workflow. + updateResp, err := env.TaskPoller().HandleWorkflowTask(env.Tv(), + res.GetWorkflowTask(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistory(` + 7 WorkflowTaskCompleted + 8 WorkflowTaskScheduled // New WT (after heartbeat) is normal and won't disappear from the history after reject. + 9 WorkflowTaskStarted + `, task.History) + + s.Empty(task.Messages) + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateRejectMessages(env.Tv(), updRequestMsg), + }, nil + }) + s.NoError(err) + s.NotNil(updateResp) - updateResult := <-updateResultCh - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(0, updateResp.ResetHistoryEventId, "no reset of event ID should happened after update rejection because of heartbeat") + updateResult := <-updateResultCh + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(0, updateResp.ResetHistoryEventId, "no reset of event ID should happened after update rejection because of heartbeat") - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` + s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -3330,757 +3334,757 @@ func TestWorkflowUpdateSuite(t *testing.T) { 9 WorkflowTaskStarted 10 WorkflowTaskCompleted // After heartbeat new normal WT was created and events are written into the history even update is rejected. `, events) - }) - - t.Run("ScheduledSpeculativeWorkflowTask_LostUpdate", func(t *testing.T) { - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - mustStartWorkflow(s, s.Tv()) - - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted`, task.History) - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } +} - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - s.Empty(task.Messages, "update lost due to lost update registry") - return nil, nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } +func (s *WorkflowUpdateSuite) TestScheduledSpeculativeWorkflowTask_LostUpdate() { + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted`, task.History) + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + s.Empty(task.Messages, "update lost due to lost update registry") + return nil, nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - - halfSecondTimeoutCtx, cancel := context.WithTimeout(testcore.NewContext(), 500*time.Millisecond) - defer cancel() - updateResult := <-sendUpdate(halfSecondTimeoutCtx, s, s.Tv()) - s.Error(updateResult.err) - s.True(common.IsContextDeadlineExceededErr(updateResult.err), updateResult.err.Error()) - s.Nil(updateResult.response) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Lose update registry. Speculative WFT and update registry disappear. - loseUpdateRegistryAndAbandonPendingUpdates(s, s.Tv()) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) + + halfSecondTimeoutCtx, cancel := context.WithTimeout(env.Context(), 500*time.Millisecond) + defer cancel() + updateResult := <-sendUpdate(halfSecondTimeoutCtx, env, env.Tv()) + s.Error(updateResult.err) + s.True(common.IsContextDeadlineExceededErr(updateResult.err), updateResult.err.Error()) + s.Nil(updateResult.response) + + // Lose update registry. Speculative WFT and update registry disappear. + loseUpdateRegistryAndAbandonPendingUpdates(env, env.Tv()) + + // Ensure, there is no WFT. + pollCtx, cancel := context.WithTimeout(env.Context(), common.MinLongPollTimeout*2) + defer cancel() + pollResponse, err := env.FrontendClient().PollWorkflowTaskQueue(pollCtx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + }) + s.NoError(err) + s.Nil(pollResponse.Messages, "there should not be new WFT with messages") - // Ensure, there is no WFT. - pollCtx, cancel := context.WithTimeout(testcore.NewContext(), common.MinLongPollTimeout*2) - defer cancel() - pollResponse, err := s.FrontendClient().PollWorkflowTaskQueue(pollCtx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - }) - s.NoError(err) - s.Nil(pollResponse.Messages, "there should not be new WFT with messages") + // Send signal to schedule new WT. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) - // Send signal to schedule new WT. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + // Complete workflow and check that there is update messages. + completeWorkflowResp, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) + s.NotNil(completeWorkflowResp) - // Complete workflow and check that there is update messages. - completeWorkflowResp, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - s.NotNil(completeWorkflowResp) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted - 9 WorkflowExecutionCompleted`, events) - }) + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - t.Run("StartedSpeculativeWorkflowTask_LostUpdate", func(t *testing.T) { - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - mustStartWorkflow(s, s.Tv()) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. Events 5 and 6 will be lost. - 6 WorkflowTaskStarted -`, task.History) - - // Lose update registry. Update is lost and NotFound error will be returned to RespondWorkflowTaskCompleted. - loseUpdateRegistryAndAbandonPendingUpdates(s, s.Tv()) - - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - case 3: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted -`, task.History) - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted + 9 WorkflowExecutionCompleted`, events) +} - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - s.EqualValues(5, updRequestMsg.GetEventId()) +func (s *WorkflowUpdateSuite) TestStartedSpeculativeWorkflowTask_LostUpdate() { + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. Events 5 and 6 will be lost. + 6 WorkflowTaskStarted + `, task.History) - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - case 3: - s.Empty(task.Messages, "no messages since update registry was lost") - return nil, nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + // Lose update registry. Update is lost and NotFound error will be returned to RespondWorkflowTaskCompleted. + loseUpdateRegistryAndAbandonPendingUpdates(env, env.Tv()) + + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + case 3: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + `, task.History) + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + s.EqualValues(5, updRequestMsg.GetEventId()) + + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + case 3: + s.Empty(task.Messages, "no messages since update registry was lost") + return nil, nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - halfSecondTimeoutCtx, cancel := context.WithTimeout(testcore.NewContext(), 500*time.Millisecond) - defer cancel() - updateResultCh := sendUpdate(halfSecondTimeoutCtx, s, s.Tv()) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Process update in workflow. - _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.Error(err) - s.ErrorAs(err, new(*serviceerror.NotFound)) - s.ErrorContains(err, "Workflow task not found") + halfSecondTimeoutCtx, cancel := context.WithTimeout(env.Context(), 500*time.Millisecond) + defer cancel() + updateResultCh := sendUpdate(halfSecondTimeoutCtx, env, env.Tv()) - updateResult := <-updateResultCh - s.Error(updateResult.err) - s.True(common.IsContextDeadlineExceededErr(updateResult.err), updateResult.err.Error()) - s.Nil(updateResult.response) + // Process update in workflow. + _, err = poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.Error(err) + s.ErrorAs(err, new(*serviceerror.NotFound)) + s.ErrorContains(err, "Workflow task not found") - // Send signal to schedule new WFT. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + updateResult := <-updateResultCh + s.Error(updateResult.err) + s.True(common.IsContextDeadlineExceededErr(updateResult.err), updateResult.err.Error()) + s.Nil(updateResult.response) - // Complete workflow. - completeWorkflowResp, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - s.NotNil(completeWorkflowResp) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionSignaled - 6 WorkflowTaskScheduled - 7 WorkflowTaskStarted - 8 WorkflowTaskCompleted - 9 WorkflowExecutionCompleted`, events) - }) + // Send signal to schedule new WFT. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) - t.Run("FirstNormalWorkflowTask_UpdateResurrectedAfterRegistryCleared", func(t *testing.T) { - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - mustStartWorkflow(s, s.Tv()) + // Complete workflow. + completeWorkflowResp, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) + s.NotNil(completeWorkflowResp) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted -`, task.History) - // Clear update registry. Update will be resurrected in registry from acceptance message. - clearUpdateRegistryAndAbortPendingUpdates(s, s.Tv()) - - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 WorkflowExecutionUpdateCompleted - 7 WorkflowExecutionSignaled - 8 WorkflowTaskScheduled - 9 WorkflowTaskStarted -`, task.History) - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - updRequestMsg := task.Messages[0] - s.EqualValues(2, updRequestMsg.GetEventId()) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - case 2: - s.Empty(task.Messages, "update must be processed and not delivered again") - return nil, nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionSignaled + 6 WorkflowTaskScheduled + 7 WorkflowTaskStarted + 8 WorkflowTaskCompleted + 9 WorkflowExecutionCompleted`, events) +} + +func (s *WorkflowUpdateSuite) TestFirstNormalWorkflowTask_UpdateResurrectedAfterRegistryCleared() { + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + `, task.History) + // Clear update registry. Update will be resurrected in registry from acceptance message. + clearUpdateRegistryAndAbortPendingUpdates(env, env.Tv()) + + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 WorkflowExecutionUpdateCompleted + 7 WorkflowExecutionSignaled + 8 WorkflowTaskScheduled + 9 WorkflowTaskStarted + `, task.History) + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + updRequestMsg := task.Messages[0] + s.EqualValues(2, updRequestMsg.GetEventId()) + + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + case 2: + s.Empty(task.Messages, "update must be processed and not delivered again") + return nil, nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - updateResultCh := sendUpdateNoError(s, s.Tv()) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Process update in workflow. Update won't be found on server but will be resurrected from acceptance message and completed. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) + updateResultCh := sendUpdateNoError(env, env.Tv()) - // Client receives resurrected Update outcome. - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + // Process update in workflow. Update won't be found on server but will be resurrected from acceptance message and completed. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) - // Signal to create new WFT which shouldn't get any updates. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + // Client receives resurrected Update outcome. + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - // Complete workflow. - completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(completeWorkflowResp) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 WorkflowExecutionUpdateCompleted - 7 WorkflowExecutionSignaled - 8 WorkflowTaskScheduled - 9 WorkflowTaskStarted - 10 WorkflowTaskCompleted - 11 WorkflowExecutionCompleted`, events) - }) + // Signal to create new WFT which shouldn't get any updates. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) - t.Run("ScheduledSpeculativeWorkflowTask_DeduplicateID", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + // Complete workflow. + completeWorkflowResp, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(completeWorkflowResp) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - s.Len(task.Messages, 1, "2nd update must be deduplicated by ID") - updRequestMsg := task.Messages[0] + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 WorkflowExecutionUpdateCompleted + 7 WorkflowExecutionSignaled + 8 WorkflowTaskScheduled + 9 WorkflowTaskStarted + 10 WorkflowTaskCompleted + 11 WorkflowExecutionCompleted`, events) +} + +func (s *WorkflowUpdateSuite) TestScheduledSpeculativeWorkflowTask_DeduplicateID() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + s.Len(task.Messages, 1, "2nd update must be deduplicated by ID") + updRequestMsg := task.Messages[0] + + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - updateResultCh := sendUpdateNoError(s, s.Tv()) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Send second update with the same ID. - updateResultCh2 := sendUpdateNoError(s, s.Tv()) + updateResultCh := sendUpdateNoError(env, env.Tv()) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResp := res.NewTask - updateResult := <-updateResultCh - updateResult2 := <-updateResultCh2 - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess())) - s.EqualValues(0, updateResp.ResetHistoryEventId) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted - 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. - 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} -`, events) - }) + // Send second update with the same ID. + updateResultCh2 := sendUpdateNoError(env, env.Tv()) - t.Run("StartedSpeculativeWorkflowTask_DeduplicateID", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResp := res.NewTask + updateResult := <-updateResultCh + updateResult2 := <-updateResultCh2 + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess())) + s.EqualValues(0, updateResp.ResetHistoryEventId) - var updateResultCh2 <-chan *workflowservice.UpdateWorkflowExecutionResponse + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - // Send second update with the same ID when WT is started but not completed. - updateResultCh2 = sendUpdateNoError(s, s.Tv()) - - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - s.Len(task.Messages, 1, "2nd update should not has reached server yet") - updRequestMsg := task.Messages[0] - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. + 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} + `, events) +} + +func (s *WorkflowUpdateSuite) TestStartedSpeculativeWorkflowTask_DeduplicateID() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + var updateResultCh2 <-chan *workflowservice.UpdateWorkflowExecutionResponse + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + // Send second update with the same ID when WT is started but not completed. + updateResultCh2 = sendUpdateNoError(env, env.Tv()) + + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + s.Len(task.Messages, 1, "2nd update should not has reached server yet") + updRequestMsg := task.Messages[0] + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - updateResultCh := sendUpdateNoError(s, s.Tv()) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, res.NewTask.ResetHistoryEventId) - - updateResult2 := <-updateResultCh2 - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess())) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted - 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. - 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} -`, events) - }) + updateResultCh := sendUpdateNoError(env, env.Tv()) - t.Run("CompletedSpeculativeWorkflowTask_DeduplicateID", func(t *testing.T) { - testCases := []struct { - Name string - CloseShard bool - }{ - { - Name: "no shard reload", - CloseShard: false, - }, - { - Name: "with shard reload", - CloseShard: true, - }, - } + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, res.NewTask.ResetHistoryEventId) - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - // Uses closeShard conditionally which requires a dedicated cluster. - var opts []testcore.TestOption - if tc.CloseShard { - opts = append(opts, testcore.WithDedicatedCluster()) - } - s := testcore.NewEnv(t, opts...) - mustStartWorkflow(s, s.Tv()) + updateResult2 := <-updateResultCh2 + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess())) - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - case 3: - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }}, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - case 3: - s.Empty(task.Messages, "2nd update must be deduplicated by ID ") - return nil, nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. + 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} + `, events) +} + +func (s *WorkflowUpdateSuite) TestCompletedSpeculativeWorkflowTask_DeduplicateID() { + testCases := []struct { + Name string + CloseShard bool + }{ + { + Name: "no shard reload", + CloseShard: false, + }, + { + Name: "with shard reload", + CloseShard: true, + }, + } + + for _, tc := range testCases { + s.Run(tc.Name, func(s *WorkflowUpdateSuite) { + // Uses closeShard conditionally which requires a dedicated cluster. + var opts []testcore.TestOption + if tc.CloseShard { + opts = append(opts, testcore.WithDedicatedCluster()) + } + env := testcore.NewEnv(s.T(), opts...) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + case 3: + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + case 3: + s.Empty(task.Messages, "2nd update must be deduplicated by ID ") + return nil, nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - - updateResultCh := sendUpdateNoError(s, s.Tv()) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - // Process update in workflow. - _, err = poller.PollAndProcessWorkflowTask() - s.NoError(err) - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - if tc.CloseShard { - // Close shard to make sure that for completed updates deduplication works even after shard reload. - closeShard(s, s.Tv().WorkflowID()) - } + updateResultCh := sendUpdateNoError(env, env.Tv()) - // Send second update with the same ID. It must return immediately. - updateResult2 := <-sendUpdateNoError(s, s.Tv()) + // Process update in workflow. + _, err = poller.PollAndProcessWorkflowTask() + s.NoError(err) + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - // Ensure, there is no new WT. - pollCtx, cancel := context.WithTimeout(testcore.NewContext(), common.MinLongPollTimeout*2) - defer cancel() - pollResponse, err := s.FrontendClient().PollWorkflowTaskQueue(pollCtx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - }) - s.NoError(err) - s.Nil(pollResponse.Messages, "there must be no new WT") + if tc.CloseShard { + // Close shard to make sure that for completed updates deduplication works even after shard reload. + closeShard(env, env.Tv().WorkflowID()) + } - s.Equal( - "success-result-of-"+s.Tv().UpdateID(), - testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess()), - "results of the first update must be available") + // Send second update with the same ID. It must return immediately. + updateResult2 := <-sendUpdateNoError(env, env.Tv()) - // Send signal to schedule new WT. - err = s.SendSignal(s.Namespace().String(), s.Tv().WorkflowExecution(), s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.NoError(err) + // Ensure, there is no new WT. + pollCtx, cancel := context.WithTimeout(env.Context(), common.MinLongPollTimeout*2) + defer cancel() + pollResponse, err := env.FrontendClient().PollWorkflowTaskQueue(pollCtx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + }) + s.NoError(err) + s.Nil(pollResponse.Messages, "there must be no new WT") - // Complete workflow. - completeWorkflowResp, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - s.NotNil(completeWorkflowResp) + s.Equal( + "success-result-of-"+env.Tv().UpdateID(), + testcore.DecodeString(s.T(), updateResult2.GetOutcome().GetSuccess()), + "results of the first update must be available") - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) + // Send signal to schedule new WT. + err = env.SendSignal(env.Namespace().String(), env.Tv().WorkflowExecution(), env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.NoError(err) - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) + // Complete workflow. + completeWorkflowResp, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) + s.NotNil(completeWorkflowResp) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted - 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. - 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} - 10 WorkflowExecutionSignaled - 11 WorkflowTaskScheduled - 12 WorkflowTaskStarted - 13 WorkflowTaskCompleted - 14 WorkflowExecutionCompleted -`, events) - }) - } - }) + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) - t.Run("StaleSpeculativeWorkflowTask_Fail_BecauseOfDifferentStartedId", func(t *testing.T) { - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - /* - Test scenario: - An update created a speculative WT and WT is dispatched to the worker (started). - Update registry is cleared, speculative WT disappears from server. - Update is retired and second speculative WT is scheduled but not dispatched yet. - An activity completes, it converts the 2nd speculative WT into normal one. - The first speculative WT responds back, server fails request because WorkflowTaskStarted event Id is mismatched. - The second speculative WT responds back and server completes it. - */ + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - mustStartWorkflow(s, s.Tv()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} // WTScheduled event which delivered update to the worker. + 9 WorkflowExecutionUpdateCompleted {"AcceptedEventId": 8} + 10 WorkflowExecutionSignaled + 11 WorkflowTaskScheduled + 12 WorkflowTaskStarted + 13 WorkflowTaskCompleted + 14 WorkflowExecutionCompleted + `, events) + }) + } +} - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Schedule activity. - return []*commandpb.Command{{ - CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, - Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ - ActivityId: s.Tv().ActivityID(), - ActivityType: s.Tv().ActivityType(), - TaskQueue: s.Tv().TaskQueue(), - ScheduleToCloseTimeout: s.Tv().Any().InfiniteTimeout(), - }}, - }}, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } +func (s *WorkflowUpdateSuite) TestStaleSpeculativeWorkflowTask_Fail_BecauseOfDifferentStartedId() { + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + /* + Test scenario: + An update created a speculative WT and WT is dispatched to the worker (started). + Update registry is cleared, speculative WT disappears from server. + Update is retired and second speculative WT is scheduled but not dispatched yet. + An activity completes, it converts the 2nd speculative WT into normal one. + The first speculative WT responds back, server fails request because WorkflowTaskStarted event Id is mismatched. + The second speculative WT responds back and server completes it. + */ + + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Schedule activity. + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: env.Tv().ActivityID(), + ActivityType: env.Tv().ActivityType(), + TaskQueue: env.Tv().TaskQueue(), + ScheduleToCloseTimeout: env.Tv().Any().InfiniteTimeout(), + }}, + }}, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - return s.Tv().Any().Payloads(), false, nil - } + atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { + return env.Tv().Any().Payloads(), false, nil + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - WorkflowTaskHandler: wtHandler, - ActivityTaskHandler: atHandler, - Logger: s.Logger, - T: s.T(), - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + WorkflowTaskHandler: wtHandler, + ActivityTaskHandler: atHandler, + Logger: env.Logger, + T: s.T(), + } - // First WT will schedule activity. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) + // First WT will schedule activity. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) - // Send 1st update. It will create 2nd WT as speculative. - sendUpdateNoError(s, s.Tv()) + // Send 1st update. It will create 2nd WT as speculative. + sendUpdateNoError(env, env.Tv()) - // Poll 2nd speculative WT with 1st update. - wt2, err := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - }) - s.NoError(err) - s.NotNil(wt2) - s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token") - s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update") - s.EqualValues(7, wt2.StartedEventId) - s.EqualValues(6, wt2.Messages[0].GetEventId()) - s.EqualHistory(` + // Poll 2nd speculative WT with 1st update. + wt2, err := env.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(env.Context()), &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + }) + s.NoError(err) + s.NotNil(wt2) + s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token") + s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update") + s.EqualValues(7, wt2.StartedEventId) + s.EqualValues(6, wt2.Messages[0].GetEventId()) + s.EqualHistory(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4089,28 +4093,28 @@ func TestWorkflowUpdateSuite(t *testing.T) { 6 WorkflowTaskScheduled 7 WorkflowTaskStarted`, wt2.History) - // Clear update registry. Speculative WFT disappears from server. - clearUpdateRegistryAndAbortPendingUpdates(s, s.Tv()) + // Clear update registry. Speculative WFT disappears from server. + clearUpdateRegistryAndAbortPendingUpdates(env, env.Tv()) - // Wait for update request to be retry by frontend and recreated in registry. This will create a 3rd WFT as speculative. - waitUpdateAdmitted(s, s.Tv()) + // Wait for update request to be retry by frontend and recreated in registry. This will create a 3rd WFT as speculative. + waitUpdateAdmitted(env, env.Tv()) - // Before polling for the 3rd speculative WT, process activity. This will convert 3rd speculative WT to normal WT. - err = poller.PollAndProcessActivityTask(false) - s.NoError(err) + // Before polling for the 3rd speculative WT, process activity. This will convert 3rd speculative WT to normal WT. + err = poller.PollAndProcessActivityTask(false) + s.NoError(err) - // Poll the 3rd WFT (not speculative anymore) but must have 2nd update. - wt3, err := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - }) - s.NoError(err) - s.NotNil(wt3) - s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token") - s.Len(wt3.Messages, 1, "3rd workflow task must have a message with 2nd update") - s.EqualValues(9, wt3.StartedEventId) - s.EqualValues(8, wt3.Messages[0].GetEventId()) - s.EqualHistory(` + // Poll the 3rd WFT (not speculative anymore) but must have 2nd update. + wt3, err := env.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(env.Context()), &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + }) + s.NoError(err) + s.NotNil(wt3) + s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token") + s.Len(wt3.Messages, 1, "3rd workflow task must have a message with 2nd update") + s.EqualValues(9, wt3.StartedEventId) + s.EqualValues(8, wt3.Messages[0].GetEventId()) + s.EqualHistory(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4121,28 +4125,28 @@ func TestWorkflowUpdateSuite(t *testing.T) { 8 ActivityTaskCompleted 9 WorkflowTaskStarted`, wt3.History) - // Now try to complete 2nd WT (speculative). It should fail because WorkflowTaskStarted event Id is mismatched. - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), - TaskToken: wt2.TaskToken, - Commands: s.UpdateAcceptCompleteCommands(s.Tv()), - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), wt2.Messages[0]), - }) - s.Error(err, "Must fail because WorkflowTaskStarted event Id is different.") - s.ErrorAs(err, new(*serviceerror.NotFound)) - s.Contains(err.Error(), "Workflow task not found") - - // Complete 3rd WT. It should succeed. - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), - TaskToken: wt3.TaskToken, - Commands: s.UpdateAcceptCompleteCommands(s.Tv()), - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), wt3.Messages[0]), - }) - s.NoError(err) + // Now try to complete 2nd WT (speculative). It should fail because WorkflowTaskStarted event Id is mismatched. + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(env.Context()), &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), + TaskToken: wt2.TaskToken, + Commands: env.UpdateAcceptCompleteCommands(env.Tv()), + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), wt2.Messages[0]), + }) + s.Error(err, "Must fail because WorkflowTaskStarted event Id is different.") + s.ErrorAs(err, new(*serviceerror.NotFound)) + s.Contains(err.Error(), "Workflow task not found") + + // Complete 3rd WT. It should succeed. + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(env.Context()), &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), + TaskToken: wt3.TaskToken, + Commands: env.UpdateAcceptCompleteCommands(env.Tv()), + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), wt3.Messages[0]), + }) + s.NoError(err) - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4156,62 +4160,62 @@ func TestWorkflowUpdateSuite(t *testing.T) { 11 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId":8} 12 WorkflowExecutionUpdateCompleted `, events) - }) - - t.Run("StaleSpeculativeWorkflowTask_Fail_BecauseOfDifferentStartTime", func(t *testing.T) { - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - /* - Test scenario: - An update created a speculative WT and WT is dispatched to the worker (started). - WF context is cleared, speculative WT is disappeared from server. - Update is retried and second speculative WT is dispatched to worker with same WT scheduled/started Id and update Id. - The first speculative WT respond back, server reject it because startTime is different. - The second speculative WT respond back, server accept it. - */ - mustStartWorkflow(s, s.Tv()) +} - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - return nil, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } +func (s *WorkflowUpdateSuite) TestStaleSpeculativeWorkflowTask_Fail_BecauseOfDifferentStartTime() { + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + /* + Test scenario: + An update created a speculative WT and WT is dispatched to the worker (started). + WF context is cleared, speculative WT is disappeared from server. + Update is retried and second speculative WT is dispatched to worker with same WT scheduled/started Id and update Id. + The first speculative WT respond back, server reject it because startTime is different. + The second speculative WT respond back, server accept it. + */ + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + return nil, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - WorkflowTaskHandler: wtHandler, - Logger: s.Logger, - T: s.T(), - } + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + WorkflowTaskHandler: wtHandler, + Logger: env.Logger, + T: s.T(), + } - // First WT will schedule activity. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.NotNil(res) + // First WT will schedule activity. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.NotNil(res) - // Send update. It will create 2nd WT as speculative. - sendUpdateNoError(s, s.Tv()) + // Send update. It will create 2nd WT as speculative. + sendUpdateNoError(env, env.Tv()) - // Poll 2nd speculative WT with 1st update. - wt2, err := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - }) - s.NoError(err) - s.NotNil(wt2) - s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token") - s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update") - s.EqualValues(6, wt2.StartedEventId) - s.EqualValues(5, wt2.Messages[0].GetEventId()) - s.EqualHistory(` + // Poll 2nd speculative WT with 1st update. + wt2, err := env.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(env.Context()), &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + }) + s.NoError(err) + s.NotNil(wt2) + s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token") + s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update") + s.EqualValues(6, wt2.StartedEventId) + s.EqualValues(5, wt2.Messages[0].GetEventId()) + s.EqualHistory(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4219,24 +4223,24 @@ func TestWorkflowUpdateSuite(t *testing.T) { 5 WorkflowTaskScheduled 6 WorkflowTaskStarted`, wt2.History) - // Clear update registry. Speculative WFT disappears from server. - clearUpdateRegistryAndAbortPendingUpdates(s, s.Tv()) + // Clear update registry. Speculative WFT disappears from server. + clearUpdateRegistryAndAbortPendingUpdates(env, env.Tv()) - // Wait for update request to be retry by frontend and recreated in registry. This will create a 3rd WFT as speculative. - waitUpdateAdmitted(s, s.Tv()) + // Wait for update request to be retry by frontend and recreated in registry. This will create a 3rd WFT as speculative. + waitUpdateAdmitted(env, env.Tv()) - // Poll for the 3rd speculative WT. - wt3, err := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - }) - s.NoError(err) - s.NotNil(wt3) - s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token") - s.Len(wt3.Messages, 1, "3rd workflow task must have a message with 1st update") - s.EqualValues(6, wt3.StartedEventId) - s.EqualValues(5, wt3.Messages[0].GetEventId()) - s.EqualHistory(` + // Poll for the 3rd speculative WT. + wt3, err := env.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(env.Context()), &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + }) + s.NoError(err) + s.NotNil(wt3) + s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token") + s.Len(wt3.Messages, 1, "3rd workflow task must have a message with 1st update") + s.EqualValues(6, wt3.StartedEventId) + s.EqualValues(5, wt3.Messages[0].GetEventId()) + s.EqualHistory(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4244,28 +4248,28 @@ func TestWorkflowUpdateSuite(t *testing.T) { 5 WorkflowTaskScheduled 6 WorkflowTaskStarted`, wt3.History) - // Now try to complete 2nd (speculative) WT, it should fail. - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), - TaskToken: wt2.TaskToken, - Commands: s.UpdateAcceptCompleteCommands(s.Tv()), - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), wt2.Messages[0]), - }) - s.Error(err, "Must fail because workflow task start time is different.") - s.ErrorAs(err, new(*serviceerror.NotFound)) - s.Contains(err.Error(), "Workflow task not found") - - // Try to complete 3rd WT, it should succeed - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), - TaskToken: wt3.TaskToken, - Commands: s.UpdateAcceptCompleteCommands(s.Tv()), - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), wt3.Messages[0]), - }) - s.NoError(err, "2nd speculative WT should be completed because it has same WT scheduled/started Id and startTime matches the accepted message is valid (same update Id)") + // Now try to complete 2nd (speculative) WT, it should fail. + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(env.Context()), &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), + TaskToken: wt2.TaskToken, + Commands: env.UpdateAcceptCompleteCommands(env.Tv()), + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), wt2.Messages[0]), + }) + s.Error(err, "Must fail because workflow task start time is different.") + s.ErrorAs(err, new(*serviceerror.NotFound)) + s.Contains(err.Error(), "Workflow task not found") + + // Try to complete 3rd WT, it should succeed + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(env.Context()), &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), + TaskToken: wt3.TaskToken, + Commands: env.UpdateAcceptCompleteCommands(env.Tv()), + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), wt3.Messages[0]), + }) + s.NoError(err, "2nd speculative WT should be completed because it has same WT scheduled/started Id and startTime matches the accepted message is valid (same update Id)") - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4276,54 +4280,54 @@ func TestWorkflowUpdateSuite(t *testing.T) { 8 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId":5} 9 WorkflowExecutionUpdateCompleted `, events) - }) +} - t.Run("StaleSpeculativeWorkflowTask_Fail_NewWorkflowTaskWith2Updates", func(t *testing.T) { - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - /* - Test scenario: - An update created a speculative WT and WT is dispatched to the worker (started). - Mutable state cleared, speculative WT and update registry are disappeared from server. - First update is retried and another update come in. - Second speculative WT is dispatched to worker with same WT scheduled/started Id but 2 updates. - The first speculative WT responds back, server rejected it (different start time). - The second speculative WT responds back, server accepted it. - */ - - mustStartWorkflow(s, s.Tv()) - tv1 := s.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1) - tv2 := s.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2) - - testCtx := testcore.NewContext() - - // Drain first WFT. - wt1, err := s.FrontendClient().PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - }) - s.NoError(err) - s.NotNil(wt1) - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), - TaskToken: wt1.TaskToken, - }) - s.NoError(err) +func (s *WorkflowUpdateSuite) TestStaleSpeculativeWorkflowTask_Fail_NewWorkflowTaskWith2Updates() { + env := testcore.NewEnv(s.T(), testcore.WithDedicatedCluster()) + /* + Test scenario: + An update created a speculative WT and WT is dispatched to the worker (started). + Mutable state cleared, speculative WT and update registry are disappeared from server. + First update is retried and another update come in. + Second speculative WT is dispatched to worker with same WT scheduled/started Id but 2 updates. + The first speculative WT responds back, server rejected it (different start time). + The second speculative WT responds back, server accepted it. + */ + + mustStartWorkflow(env, env.Tv()) + tv1 := env.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1) + tv2 := env.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2) + + testCtx := testcore.NewContext(env.Context()) + + // Drain first WFT. + wt1, err := env.FrontendClient().PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + }) + s.NoError(err) + s.NotNil(wt1) + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), + TaskToken: wt1.TaskToken, + }) + s.NoError(err) - // Send 1st update. It will create 2nd speculative WFT. - sendUpdateNoError(s, tv1) + // Send 1st update. It will create 2nd speculative WFT. + sendUpdateNoError(env, tv1) - // Poll 2nd speculative WFT with 1st update. - wt2, err := s.FrontendClient().PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - }) - s.NoError(err) - s.NotNil(wt2) - s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token") - s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update") - s.EqualValues(6, wt2.StartedEventId) - s.EqualValues(5, wt2.Messages[0].GetEventId()) - s.EqualHistory(` + // Poll 2nd speculative WFT with 1st update. + wt2, err := env.FrontendClient().PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + }) + s.NoError(err) + s.NotNil(wt2) + s.NotEmpty(wt2.TaskToken, "2nd workflow task must have valid task token") + s.Len(wt2.Messages, 1, "2nd workflow task must have a message with 1st update") + s.EqualValues(6, wt2.StartedEventId) + s.EqualValues(5, wt2.Messages[0].GetEventId()) + s.EqualHistory(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4331,29 +4335,29 @@ func TestWorkflowUpdateSuite(t *testing.T) { 5 WorkflowTaskScheduled 6 WorkflowTaskStarted`, wt2.History) - // Clear update registry. Speculative WFT disappears from server. - clearUpdateRegistryAndAbortPendingUpdates(s, s.Tv()) + // Clear update registry. Speculative WFT disappears from server. + clearUpdateRegistryAndAbortPendingUpdates(env, env.Tv()) - // Make sure UpdateWorkflowExecution call for the update "1" is retried and new (3rd) WFT is created as speculative with updateID=1. - waitUpdateAdmitted(s, tv1) + // Make sure UpdateWorkflowExecution call for the update "1" is retried and new (3rd) WFT is created as speculative with updateID=1. + waitUpdateAdmitted(env, tv1) - // Send 2nd update (with DIFFERENT updateId). It reuses already created 3rd WFT. - sendUpdateNoError(s, tv2) - // updateID=1 is still blocked. There must be 2 blocked updates now. + // Send 2nd update (with DIFFERENT updateId). It reuses already created 3rd WFT. + sendUpdateNoError(env, tv2) + // updateID=1 is still blocked. There must be 2 blocked updates now. - // Poll the 3rd speculative WFT. - wt3, err := s.FrontendClient().PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{ - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - }) - s.NoError(err) - s.NotNil(wt3) - s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token") - s.Len(wt3.Messages, 2, "3rd workflow task must have a message with 1st and 2nd updates") - s.EqualValues(6, wt3.StartedEventId) - s.EqualValues(5, wt3.Messages[0].GetEventId()) - s.EqualValues(5, wt3.Messages[1].GetEventId()) - s.EqualHistory(` + // Poll the 3rd speculative WFT. + wt3, err := env.FrontendClient().PollWorkflowTaskQueue(testCtx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + }) + s.NoError(err) + s.NotNil(wt3) + s.NotEmpty(wt3.TaskToken, "3rd workflow task must have valid task token") + s.Len(wt3.Messages, 2, "3rd workflow task must have a message with 1st and 2nd updates") + s.EqualValues(6, wt3.StartedEventId) + s.EqualValues(5, wt3.Messages[0].GetEventId()) + s.EqualValues(5, wt3.Messages[1].GetEventId()) + s.EqualHistory(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4361,35 +4365,35 @@ func TestWorkflowUpdateSuite(t *testing.T) { 5 WorkflowTaskScheduled 6 WorkflowTaskStarted`, wt3.History) - // Now try to complete 2nd speculative WT, it should fail because start time does not match. - _, err = s.FrontendClient().RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), - TaskToken: wt2.TaskToken, - Commands: s.UpdateAcceptCompleteCommands(tv1), - Messages: s.UpdateAcceptCompleteMessages(tv1, wt2.Messages[0]), - ReturnNewWorkflowTask: true, - }) - s.Error(err, "Must fail because start time is different.") - s.Contains(err.Error(), "Workflow task not found") - s.ErrorAs(err, new(*serviceerror.NotFound)) - - // Complete of the 3rd WT should succeed. It must accept both updates. - wt4Resp, err := s.FrontendClient().RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{ - Namespace: s.Namespace().String(), - TaskToken: wt3.TaskToken, - Commands: append( - s.UpdateAcceptCompleteCommands(tv1), - s.UpdateAcceptCompleteCommands(tv2)...), - Messages: append( - s.UpdateAcceptCompleteMessages(tv1, wt3.Messages[0]), - s.UpdateAcceptCompleteMessages(tv2, wt3.Messages[1])...), - ReturnNewWorkflowTask: true, - }) - s.NoError(err) - s.NotNil(wt4Resp) + // Now try to complete 2nd speculative WT, it should fail because start time does not match. + _, err = env.FrontendClient().RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), + TaskToken: wt2.TaskToken, + Commands: env.UpdateAcceptCompleteCommands(tv1), + Messages: env.UpdateAcceptCompleteMessages(tv1, wt2.Messages[0]), + ReturnNewWorkflowTask: true, + }) + s.Error(err, "Must fail because start time is different.") + s.Contains(err.Error(), "Workflow task not found") + s.ErrorAs(err, new(*serviceerror.NotFound)) + + // Complete of the 3rd WT should succeed. It must accept both updates. + wt4Resp, err := env.FrontendClient().RespondWorkflowTaskCompleted(testCtx, &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: env.Namespace().String(), + TaskToken: wt3.TaskToken, + Commands: append( + env.UpdateAcceptCompleteCommands(tv1), + env.UpdateAcceptCompleteCommands(tv2)...), + Messages: append( + env.UpdateAcceptCompleteMessages(tv1, wt3.Messages[0]), + env.UpdateAcceptCompleteMessages(tv2, wt3.Messages[1])...), + ReturnNewWorkflowTask: true, + }) + s.NoError(err) + s.NotNil(wt4Resp) - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted @@ -4402,1414 +4406,1453 @@ func TestWorkflowUpdateSuite(t *testing.T) { 10 WorkflowExecutionUpdateAccepted {"AcceptedRequestSequencingEventId": 5} 11 WorkflowExecutionUpdateCompleted `, events) - }) - - t.Run("SpeculativeWorkflowTask_WorkerSkippedProcessing_RejectByServer", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) - tv1 := s.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1) - tv2 := s.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2) - - var update2ResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse - - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Speculative WT. - 6 WorkflowTaskStarted -`, task.History) - update2ResultCh = sendUpdateNoError(s, tv2) - return nil, nil - case 3: - s.EqualHistory(` - 4 WorkflowTaskCompleted // Speculative WT was dropped and history starts from 4 again. - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted`, task.History) - commands := append(s.UpdateAcceptCompleteCommands(tv2), - &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, - }) - return commands, nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil - } - } - - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) - - s.Equal("args-value-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.EqualValues(5, updRequestMsg.GetEventId()) - - // Don't process update in WT. - return nil, nil - case 3: - s.Len(task.Messages, 1) - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) +} - s.Equal("args-value-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.EqualValues(5, updRequestMsg.GetEventId()) - return s.UpdateAcceptCompleteMessages(tv2, updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } +func (s *WorkflowUpdateSuite) TestSpeculativeWorkflowTask_WorkerSkippedProcessing_RejectByServer() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + tv1 := env.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1) + tv2 := env.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2) + + var update2ResultCh <-chan *workflowservice.UpdateWorkflowExecutionResponse + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Speculative WT. + 6 WorkflowTaskStarted + `, task.History) + update2ResultCh = sendUpdateNoError(env, tv2) + return nil, nil + case 3: + s.EqualHistory(` + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted`, task.History) + commands := append(env.UpdateAcceptCompleteCommands(tv2), + &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}}, + }) + return commands, nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Identity: "old_worker", - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+tv1.UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.EqualValues(5, updRequestMsg.GetEventId()) + + // Don't process update in WT. + return nil, nil + case 3: + s.Len(task.Messages, 1) + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.EqualValues(5, updRequestMsg.GetEventId()) + return env.UpdateAcceptCompleteMessages(tv2, updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) - - updateResultCh := sendUpdateNoError(s, tv1) - - // Process 2nd WT which ignores update message. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResp := res.NewTask - updateResult := <-updateResultCh - s.Equal("Workflow Update is rejected because it wasn't processed by worker. Probably, Workflow Update is not supported by the worker.", updateResult.GetOutcome().GetFailure().GetMessage()) - s.EqualValues(3, updateResp.ResetHistoryEventId) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Identity: "old_worker", + Logger: env.Logger, + T: s.T(), + } - // Process 3rd WT which completes 2nd update and workflow. - update2Resp, err := poller.HandlePartialWorkflowTask(updateResp.GetWorkflowTask(), false) - s.NoError(err) - s.NotNil(update2Resp) - update2Result := <-update2ResultCh - s.Equal("success-result-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), update2Result.GetOutcome().GetSuccess())) - - s.Equal(3, wtHandlerCalls) - s.Equal(3, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // 1st speculative WT is not present in the history. This is 2nd speculative WT. - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted - 8 WorkflowExecutionUpdateAccepted - 9 WorkflowExecutionUpdateCompleted - 10 WorkflowExecutionCompleted`, events) - }) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - t.Run("LastWorkflowTask_HasUpdateMessage", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + updateResultCh := sendUpdateNoError(env, tv1) - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - completeWorkflowCommand := &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ - CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ - Result: s.Tv().Any().Payloads(), - }, - }, - } - return append(s.UpdateAcceptCommands(s.Tv()), completeWorkflowCommand), nil - }, - MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - return s.UpdateAcceptMessages(s.Tv(), task.Messages[0]), nil - }, - Logger: s.Logger, - T: s.T(), - } + // Process 2nd WT which ignores update message. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResp := res.NewTask + updateResult := <-updateResultCh + s.Equal("Workflow Update is rejected because it wasn't processed by worker. Probably, Workflow Update is not supported by the worker.", updateResult.GetOutcome().GetFailure().GetMessage()) + s.EqualValues(3, updateResp.ResetHistoryEventId) - updateResultCh := sendUpdateNoErrorWaitPolicyAccepted(s, s.Tv()) - _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResult := <-updateResultCh - s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, updateResult.GetStage()) - s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", updateResult.GetOutcome().GetFailure().GetMessage()) + // Process 3rd WT which completes 2nd update and workflow. + update2Resp, err := poller.HandlePartialWorkflowTask(updateResp.GetWorkflowTask(), false) + s.NoError(err) + s.NotNil(update2Resp) + update2Result := <-update2ResultCh + s.Equal("success-result-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), update2Result.GetOutcome().GetSuccess())) - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 WorkflowExecutionCompleted - `, s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution())) - }) + s.Equal(3, wtHandlerCalls) + s.Equal(3, msgHandlerCalls) - t.Run("SpeculativeWorkflowTask_QueryFailureClearsWFContext", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // 1st speculative WT is not present in the history. This is 2nd speculative WT. + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowExecutionUpdateAccepted + 9 WorkflowExecutionUpdateCompleted + 10 WorkflowExecutionCompleted`, events) +} - wtHandlerCalls := 0 - wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - wtHandlerCalls++ - switch wtHandlerCalls { - case 1: - // Completes first WT with empty command list. - return nil, nil - case 2: - s.EqualHistory(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted -`, task.History) - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - default: - s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) - return nil, nil +func (s *WorkflowUpdateSuite) TestLastWorkflowTask_HasUpdateMessage() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + completeWorkflowCommand := &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ + Result: env.Tv().Any().Payloads(), + }, + }, } - } + return append(env.UpdateAcceptCommands(env.Tv()), completeWorkflowCommand), nil + }, + MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + return env.UpdateAcceptMessages(env.Tv(), task.Messages[0]), nil + }, + Logger: env.Logger, + T: s.T(), + } - msgHandlerCalls := 0 - msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - msgHandlerCalls++ - switch msgHandlerCalls { - case 1: - return nil, nil - case 2: - updRequestMsg := task.Messages[0] - updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + updateResultCh := sendUpdateNoErrorWaitPolicyAccepted(env, env.Tv()) + _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResult := <-updateResultCh + s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, updateResult.GetStage()) + s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", updateResult.GetOutcome().GetFailure().GetMessage()) - s.Equal("args-value-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) - s.Equal(s.Tv().HandlerName(), updRequest.GetInput().GetName()) - s.EqualValues(5, updRequestMsg.GetEventId()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 WorkflowExecutionCompleted + `, env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution())) +} - return s.UpdateAcceptCompleteMessages(s.Tv(), updRequestMsg), nil - default: - s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) - return nil, nil - } +func (s *WorkflowUpdateSuite) TestSpeculativeWorkflowTask_QueryFailureClearsWFContext() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + wtHandlerCalls := 0 + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + wtHandlerCalls++ + switch wtHandlerCalls { + case 1: + // Completes first WT with empty command list. + return nil, nil + case 2: + s.EqualHistory(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + `, task.History) + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + default: + s.Failf("wtHandler called too many times", "wtHandler shouldn't be called %d times", wtHandlerCalls) + return nil, nil } + } - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - WorkflowTaskHandler: wtHandler, - MessageHandler: msgHandler, - Logger: s.Logger, - T: s.T(), + msgHandlerCalls := 0 + msgHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + msgHandlerCalls++ + switch msgHandlerCalls { + case 1: + return nil, nil + case 2: + updRequestMsg := task.Messages[0] + updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody()) + + s.Equal("args-value-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updRequest.GetInput().GetArgs())) + s.Equal(env.Tv().HandlerName(), updRequest.GetInput().GetName()) + s.EqualValues(5, updRequestMsg.GetEventId()) + + return env.UpdateAcceptCompleteMessages(env.Tv(), updRequestMsg), nil + default: + s.Failf("msgHandler called too many times", "msgHandler shouldn't be called %d times", msgHandlerCalls) + return nil, nil } + } - // Drain first WT. - _, err := poller.PollAndProcessWorkflowTask() - s.NoError(err) + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + WorkflowTaskHandler: wtHandler, + MessageHandler: msgHandler, + Logger: env.Logger, + T: s.T(), + } - updateResultCh := sendUpdateNoError(s, s.Tv()) + // Drain first WT. + _, err := poller.PollAndProcessWorkflowTask() + s.NoError(err) - type QueryResult struct { - Resp *workflowservice.QueryWorkflowResponse - Err error - } - queryFn := func(resCh chan<- QueryResult) { - // There is no query handler, and query timeout is ok for this test. - // But first query must not time out before 2nd query reached server, - // because 2 queries overflow the query buffer (default size 1), - // which leads to clearing of WF context. - shortCtx, cancel := context.WithTimeout(testcore.NewContext(), 100*time.Millisecond) - defer cancel() - queryResp, err := s.FrontendClient().QueryWorkflow(shortCtx, &workflowservice.QueryWorkflowRequest{ - Namespace: s.Namespace().String(), - Execution: s.Tv().WorkflowExecution(), - Query: &querypb.WorkflowQuery{ - QueryType: s.Tv().Any().String(), - }, - }) - resCh <- QueryResult{Resp: queryResp, Err: err} - } + updateResultCh := sendUpdateNoError(env, env.Tv()) - query1ResultCh := make(chan QueryResult) - query2ResultCh := make(chan QueryResult) - go queryFn(query1ResultCh) - go queryFn(query2ResultCh) - query1Res := <-query1ResultCh - query2Res := <-query2ResultCh - s.Error(query1Res.Err) - s.Error(query2Res.Err) - s.Nil(query1Res.Resp) - s.Nil(query2Res.Resp) - - var queryBufferFullErr *serviceerror.ResourceExhausted - if common.IsContextDeadlineExceededErr(query1Res.Err) { - s.True(common.IsContextDeadlineExceededErr(query1Res.Err), "one of query errors must be CDE") - s.ErrorAs(query2Res.Err, &queryBufferFullErr, "one of query errors must `query buffer is full`") - s.Contains(query2Res.Err.Error(), "query buffer is full", "one of query errors must `query buffer is full`") - } else { - s.ErrorAs(query1Res.Err, &queryBufferFullErr, "one of query errors must `query buffer is full`") - s.Contains(query1Res.Err.Error(), "query buffer is full", "one of query errors must `query buffer is full`") - s.True(common.IsContextDeadlineExceededErr(query2Res.Err), "one of query errors must be CDE") - } + type QueryResult struct { + Resp *workflowservice.QueryWorkflowResponse + Err error + } + queryFn := func(resCh chan<- QueryResult) { + // There is no query handler, and query timeout is ok for this test. + // But first query must not time out before 2nd query reached server, + // because 2 queries overflow the query buffer (default size 1), + // which leads to clearing of WF context. + shortCtx, cancel := context.WithTimeout(env.Context(), 100*time.Millisecond) + defer cancel() + queryResp, err := env.FrontendClient().QueryWorkflow(shortCtx, &workflowservice.QueryWorkflowRequest{ + Namespace: env.Namespace().String(), + Execution: env.Tv().WorkflowExecution(), + Query: &querypb.WorkflowQuery{ + QueryType: env.Tv().Any().String(), + }, + }) + resCh <- QueryResult{Resp: queryResp, Err: err} + } - // "query buffer is full" error clears WF context. If update registry is not cleared together with context (old behaviour), - // then update stays there but speculative WFT which supposed to deliver it, is cleared. - // Subsequent retry attempts of "UpdateWorkflowExecution" API wouldn't help, because update is deduped by registry, - // and new WFT is not created. Update is not delivered to the worker until new WFT is created. - // If registry is cleared together with WF context (current behaviour), retries of "UpdateWorkflowExecution" - // will create new update and WFT. + query1ResultCh := make(chan QueryResult) + query2ResultCh := make(chan QueryResult) + go queryFn(query1ResultCh) + go queryFn(query2ResultCh) + query1Res := <-query1ResultCh + query2Res := <-query2ResultCh + s.Error(query1Res.Err) + s.Error(query2Res.Err) + s.Nil(query1Res.Resp) + s.Nil(query2Res.Resp) + + var queryBufferFullErr *serviceerror.ResourceExhausted + if common.IsContextDeadlineExceededErr(query1Res.Err) { + s.True(common.IsContextDeadlineExceededErr(query1Res.Err), "one of query errors must be CDE") + s.ErrorAs(query2Res.Err, &queryBufferFullErr, "one of query errors must `query buffer is full`") + s.Contains(query2Res.Err.Error(), "query buffer is full", "one of query errors must `query buffer is full`") + } else { + s.ErrorAs(query1Res.Err, &queryBufferFullErr, "one of query errors must `query buffer is full`") + s.Contains(query1Res.Err.Error(), "query buffer is full", "one of query errors must `query buffer is full`") + s.True(common.IsContextDeadlineExceededErr(query2Res.Err), "one of query errors must be CDE") + } - // Wait to make sure that UpdateWorkflowExecution call is retried, update and speculative WFT are recreated. - time.Sleep(500 * time.Millisecond) //nolint:forbidigo + // "query buffer is full" error clears WF context. If update registry is not cleared together with context (old behaviour), + // then update stays there but speculative WFT which supposed to deliver it, is cleared. + // Subsequent retry attempts of "UpdateWorkflowExecution" API wouldn't help, because update is deduped by registry, + // and new WFT is not created. Update is not delivered to the worker until new WFT is created. + // If registry is cleared together with WF context (current behaviour), retries of "UpdateWorkflowExecution" + // will create new update and WFT. - // Process update in workflow. - res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResp := res.NewTask - updateResult := <-updateResultCh - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) - s.EqualValues(0, updateResp.ResetHistoryEventId) - - s.Equal(2, wtHandlerCalls) - s.Equal(2, msgHandlerCalls) - - events := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled // Was speculative WT... - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted // ...and events were written to the history when WT completes. - 8 WorkflowExecutionUpdateAccepted - 9 WorkflowExecutionUpdateCompleted -`, events) - }) + // Wait to make sure that UpdateWorkflowExecution call is retried, update and speculative WFT are recreated. + time.Sleep(500 * time.Millisecond) //nolint:forbidigo - t.Run("UpdatesAreSentToWorkerInOrderOfAdmission", func(t *testing.T) { - s := testcore.NewEnv(t) - // If our implementation is not in fact ordering updates correctly, then it may be ordering them - // non-deterministically. This number should be high enough that the false-negative rate of the test is low, but - // must not exceed our limit on number of in-flight updates. If we were picking a random ordering then the - // false-negative rate would be 1/(nUpdates!). - nUpdates := 10 - - mustStartWorkflow(s, s.Tv()) - for i := 0; i < nUpdates; i++ { - // Sequentially send updates one by one. - sendUpdateNoError(s, s.Tv().WithUpdateIDNumber(i)) - } + // Process update in workflow. + res, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResp := res.NewTask + updateResult := <-updateResultCh + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.EqualValues(0, updateResp.ResetHistoryEventId) - wtHandlerCalls := 0 - msgHandlerCalls := 0 - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - msgHandlerCalls++ - var commands []*commandpb.Command - for i := range task.Messages { - commands = append(commands, s.UpdateAcceptCompleteCommands(s.Tv().WithMessageIDNumber(i))...) - } - return commands, nil - }, - MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - wtHandlerCalls++ - s.Len(task.Messages, nUpdates) - var messages []*protocolpb.Message - // Updates were sent in sequential order of updateId => messages must be ordered in the same way. - for i, m := range task.Messages { - s.Equal(s.Tv().WithUpdateIDNumber(i).UpdateID(), m.ProtocolInstanceId) - messages = append(messages, s.UpdateAcceptCompleteMessages(s.Tv().WithMessageIDNumber(i), m)...) - } - return messages, nil - }, - Logger: s.Logger, - T: s.T(), - } - _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - s.Equal(1, wtHandlerCalls) - s.Equal(1, msgHandlerCalls) - - expectedHistory := ` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted -` - for i := 0; i < nUpdates; i++ { - tvi := s.Tv().WithUpdateIDNumber(i) - expectedHistory += fmt.Sprintf(` - %d WorkflowExecutionUpdateAccepted {"AcceptedRequest":{"Meta": {"UpdateId": "%s"}}} - %d WorkflowExecutionUpdateCompleted {"Meta": {"UpdateId": "%s"}}`, - 5+2*i, tvi.UpdateID(), - 6+2*i, tvi.UpdateID()) - } + s.Equal(2, wtHandlerCalls) + s.Equal(2, msgHandlerCalls) - history := s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution()) - s.EqualHistoryEvents(expectedHistory, history) - }) + events := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) - t.Run("WaitAccepted_GotCompleted", func(t *testing.T) { - s := testcore.NewEnv(t) - mustStartWorkflow(s, s.Tv()) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled // Was speculative WT... + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted // ...and events were written to the history when WT completes. + 8 WorkflowExecutionUpdateAccepted + 9 WorkflowExecutionUpdateCompleted + `, events) +} - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - return s.UpdateAcceptCompleteCommands(s.Tv()), nil - }, - MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - return s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), nil - }, - Logger: s.Logger, - T: s.T(), - } +func (s *WorkflowUpdateSuite) TestUpdatesAreSentToWorkerInOrderOfAdmission() { + env := testcore.NewEnv(s.T()) + // If our implementation is not in fact ordering updates correctly, then it may be ordering them + // non-deterministically. This number should be high enough that the false-negative rate of the test is low, but + // must not exceed our limit on number of in-flight updates. If we were picking a random ordering then the + // false-negative rate would be 1/(nUpdates!). + nUpdates := 10 + + mustStartWorkflow(env, env.Tv()) + for i := range nUpdates { + // Sequentially send updates one by one. + sendUpdateNoError(env, env.Tv().WithUpdateIDNumber(i)) + } - // Send Update with intent to wait for Accepted stage only, - updateResultCh := sendUpdateNoErrorWaitPolicyAccepted(s, s.Tv()) - _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) - s.NoError(err) - updateResult := <-updateResultCh - // but Update was accepted and completed on the same WFT, and outcome was returned. - s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, updateResult.GetStage()) - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + wtHandlerCalls := 0 + msgHandlerCalls := 0 + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + msgHandlerCalls++ + var commands []*commandpb.Command + for i := range task.Messages { + commands = append(commands, env.UpdateAcceptCompleteCommands(env.Tv().WithMessageIDNumber(i))...) + } + return commands, nil + }, + MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + wtHandlerCalls++ + s.Len(task.Messages, nUpdates) + var messages []*protocolpb.Message + // Updates were sent in sequential order of updateId => messages must be ordered in the same way. + for i, m := range task.Messages { + s.Equal(env.Tv().WithUpdateIDNumber(i).UpdateID(), m.ProtocolInstanceId) + messages = append(messages, env.UpdateAcceptCompleteMessages(env.Tv().WithMessageIDNumber(i), m)...) + } + return messages, nil + }, + Logger: env.Logger, + T: s.T(), + } + _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + s.Equal(1, wtHandlerCalls) + s.Equal(1, msgHandlerCalls) + + expectedHistory := ` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + ` + for i := range nUpdates { + tvi := env.Tv().WithUpdateIDNumber(i) + expectedHistory += fmt.Sprintf(` + %d WorkflowExecutionUpdateAccepted {"AcceptedRequest":{"Meta": {"UpdateId": "%s"}}} + %d WorkflowExecutionUpdateCompleted {"Meta": {"UpdateId": "%s"}}`, + 5+2*i, tvi.UpdateID(), + 6+2*i, tvi.UpdateID()) + } - s.EqualHistoryEvents(` + history := env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution()) + s.EqualHistoryEvents(expectedHistory, history) +} + +func (s *WorkflowUpdateSuite) TestWaitAccepted_GotCompleted() { + env := testcore.NewEnv(s.T()) + mustStartWorkflow(env, env.Tv()) + + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + return env.UpdateAcceptCompleteCommands(env.Tv()), nil + }, + MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + return env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), nil + }, + Logger: env.Logger, + T: s.T(), + } + + // Send Update with intent to wait for Accepted stage only, + updateResultCh := sendUpdateNoErrorWaitPolicyAccepted(env, env.Tv()) + _, err := poller.PollAndProcessWorkflowTask(testcore.WithoutRetries) + s.NoError(err) + updateResult := <-updateResultCh + // but Update was accepted and completed on the same WFT, and outcome was returned. + s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, updateResult.GetStage()) + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + + s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted 4 WorkflowTaskCompleted 5 WorkflowExecutionUpdateAccepted 6 WorkflowExecutionUpdateCompleted - `, s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution())) - }) + `, env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution())) +} - t.Run("ContinueAsNew_UpdateIsNotCarriedOver", func(t *testing.T) { - s := testcore.NewEnv(t) - firstRunID := mustStartWorkflow(s, s.Tv()) - tv1 := s.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1) - tv2 := s.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2) +func (s *WorkflowUpdateSuite) TestContinueAsNew_UpdateIsNotCarriedOver() { + env := testcore.NewEnv(s.T()) + firstRunID := mustStartWorkflow(env, env.Tv()) + tv1 := env.Tv().WithUpdateIDNumber(1).WithMessageIDNumber(1) + tv2 := env.Tv().WithUpdateIDNumber(2).WithMessageIDNumber(2) + + /* + 1st Update goes to the 1st run and accepted (but not completed) by Workflow. + While this WFT is running, 2nd Update is sent, and WFT is completing with CAN for the 1st run. + There are 2 Updates in the registry of the 1st run: 1st is accepted and 2nd is admitted. + Both of them are aborted but with different errors: + - Admitted Update is aborted with retryable "workflow is closing" error. SDK should retry this error + and new attempt should land on the new run. + - Accepted Update is aborted with update failure. + */ + + var update2ResponseCh <-chan updateResponseErr + + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller1 := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + // Send 2nd Update while WFT is running. + update2ResponseCh = sendUpdate(env.Context(), env, tv2) + canCommand := &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: env.Tv().WorkflowType(), + TaskQueue: env.Tv().WithTaskQueueNumber(2).TaskQueue(), + }}, + } + return append(env.UpdateAcceptCommands(tv1), canCommand), nil + }, + MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + return env.UpdateAcceptMessages(tv1, task.Messages[0]), nil + }, + Logger: env.Logger, + T: s.T(), + } - /* - 1st Update goes to the 1st run and accepted (but not completed) by Workflow. - While this WFT is running, 2nd Update is sent, and WFT is completing with CAN for the 1st run. - There are 2 Updates in the registry of the 1st run: 1st is accepted and 2nd is admitted. - Both of them are aborted but with different errors: - - Admitted Update is aborted with retryable "workflow is closing" error. SDK should retry this error - and new attempt should land on the new run. - - Accepted Update is aborted with update failure. - */ + //nolint:staticcheck // SA1019 TaskPoller replacement needed + poller2 := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), + TaskQueue: env.Tv().WithTaskQueueNumber(2).TaskQueue(), + Identity: env.Tv().WorkerIdentity(), + WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + return nil, nil + }, + MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { + s.Empty(task.Messages, "no Updates should be carried over to the 2nd run") + return nil, nil + }, + Logger: env.Logger, + T: s.T(), + } - var update2ResponseCh <-chan updateResponseErr + update1ResponseCh := sendUpdate(env.Context(), env, tv1) + _, err := poller1.PollAndProcessWorkflowTask() + s.NoError(err) - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller1 := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - // Send 2nd Update while WFT is running. - update2ResponseCh = sendUpdate(context.Background(), s, tv2) - canCommand := &commandpb.Command{ - CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ - WorkflowType: s.Tv().WorkflowType(), - TaskQueue: s.Tv().WithTaskQueueNumber(2).TaskQueue(), - }}, - } - return append(s.UpdateAcceptCommands(tv1), canCommand), nil - }, - MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - return s.UpdateAcceptMessages(tv1, task.Messages[0]), nil - }, - Logger: s.Logger, - T: s.T(), - } + _, err = poller2.PollAndProcessWorkflowTask() + s.NoError(err) - //nolint:staticcheck // SA1019 TaskPoller replacement needed - poller2 := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), - TaskQueue: s.Tv().WithTaskQueueNumber(2).TaskQueue(), - Identity: s.Tv().WorkerIdentity(), - WorkflowTaskHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { - return nil, nil - }, - MessageHandler: func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*protocolpb.Message, error) { - s.Empty(task.Messages, "no Updates should be carried over to the 2nd run") - return nil, nil - }, - Logger: s.Logger, - T: s.T(), - } + update1Response := <-update1ResponseCh + s.NoError(update1Response.err) + s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", update1Response.response.GetOutcome().GetFailure().GetMessage()) - update1ResponseCh := sendUpdate(context.Background(), s, tv1) - _, err := poller1.PollAndProcessWorkflowTask() - s.NoError(err) + update2Response := <-update2ResponseCh + s.Error(update2Response.err) + var resourceExhausted *serviceerror.ResourceExhausted + s.ErrorAs(update2Response.err, &resourceExhausted) + s.Equal("workflow operation can not be applied because workflow is closing", update2Response.err.Error()) - _, err = poller2.PollAndProcessWorkflowTask() - s.NoError(err) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 WorkflowExecutionContinuedAsNew`, env.GetHistory(env.Namespace().String(), env.Tv().WithRunID(firstRunID).WorkflowExecution())) - update1Response := <-update1ResponseCh - s.NoError(update1Response.err) - s.Equal("Workflow Update failed because the Workflow completed before the Update completed.", update1Response.response.GetOutcome().GetFailure().GetMessage()) - - update2Response := <-update2ResponseCh - s.Error(update2Response.err) - var resourceExhausted *serviceerror.ResourceExhausted - s.ErrorAs(update2Response.err, &resourceExhausted) - s.Equal("workflow operation can not be applied because workflow is closing", update2Response.err.Error()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 WorkflowExecutionContinuedAsNew`, s.GetHistory(s.Namespace().String(), s.Tv().WithRunID(firstRunID).WorkflowExecution())) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted`, s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution())) - }) + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted`, env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution())) +} - t.Run("ContinueAsNew_Suggestion", func(t *testing.T) { - // setup CAN suggestion to be at 2nd Update - s := testcore.NewEnv(t, - testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxTotalUpdates, 3), - testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxTotalUpdatesSuggestContinueAsNewThreshold, 0.5), - ) +func (s *WorkflowUpdateSuite) TestContinueAsNew_Suggestion() { + // setup CAN suggestion to be at 2nd Update + env := testcore.NewEnv(s.T(), + testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxTotalUpdates, 3), + testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxTotalUpdatesSuggestContinueAsNewThreshold, 0.5), + ) + + // start workflow + mustStartWorkflow(env, env.Tv()) + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + // send Update #1 - no CAN suggested + tv1 := env.Tv().WithUpdateIDNumber(1) + updateResultCh := sendUpdateNoError(env, tv1) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(tv1, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted {"SuggestContinueAsNew": false} + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted {"SuggestContinueAsNew": false}`, task.History.Events) + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(tv1, task.Messages[0]), + }, nil + }) + s.NoError(err) + <-updateResultCh + + // send Update #2 - CAN suggested + tv2 := env.Tv().WithUpdateIDNumber(2) + updateResultCh = sendUpdateNoError(env, tv2) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(tv2, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.EqualHistoryEventsSuffix(` + WorkflowTaskStarted {"SuggestContinueAsNew": true}`, task.History.Events) + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(tv2, task.Messages[0]), + }, nil + }) + s.NoError(err) + <-updateResultCh +} - // start workflow - mustStartWorkflow(s, s.Tv()) - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) +type UpdateWithStartSuite struct { + parallelsuite.Suite[*UpdateWithStartSuite] +} - // send Update #1 - no CAN suggested - tv1 := s.Tv().WithUpdateIDNumber(1) - updateResultCh := sendUpdateNoError(s, tv1) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(tv1, - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted {"SuggestContinueAsNew": false} - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted {"SuggestContinueAsNew": false}`, task.History.Events) +func TestUpdateWithStartSuite(t *testing.T) { + parallelsuite.Run(t, &UpdateWithStartSuite{}) +} - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(tv1, task.Messages[0]), - }, nil +type multiopsResponseErr struct { + response *workflowservice.ExecuteMultiOperationResponse + err error +} + +func (s *UpdateWithStartSuite) sendUpdateWithStart(env *testcore.TestEnv, startReq *workflowservice.StartWorkflowExecutionRequest, updateReq *workflowservice.UpdateWorkflowExecutionRequest) chan multiopsResponseErr { + ctx := testcore.NewContext(env.Context()) + capture := env.StartNamespaceMetricCapture() + + retCh := make(chan multiopsResponseErr) + go func() { + resp, err := env.FrontendClient().ExecuteMultiOperation( + ctx, + &workflowservice.ExecuteMultiOperationRequest{ + Namespace: env.Namespace().String(), + Operations: []*workflowservice.ExecuteMultiOperationRequest_Operation{ + { + Operation: &workflowservice.ExecuteMultiOperationRequest_Operation_StartWorkflow{ + StartWorkflow: startReq, + }, + }, + { + Operation: &workflowservice.ExecuteMultiOperationRequest_Operation_UpdateWorkflow{ + UpdateWorkflow: updateReq, + }, + }, + }, }) - s.NoError(err) - <-updateResultCh - // send Update #2 - CAN suggested - tv2 := s.Tv().WithUpdateIDNumber(2) - updateResultCh = sendUpdateNoError(s, tv2) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(tv2, - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - s.EqualHistoryEventsSuffix(` - WorkflowTaskStarted {"SuggestContinueAsNew": true}`, task.History.Events) + if err == nil { + completedStage := updateReq.WaitPolicy != nil && + updateReq.WaitPolicy.LifecycleStage == enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(tv2, task.Messages[0]), - }, nil - }) - s.NoError(err) - <-updateResultCh - }) + var startResp *workflowservice.StartWorkflowExecutionResponse + var updateResp *workflowservice.UpdateWorkflowExecutionResponse + if resp != nil && len(resp.Responses) == 2 { + startResp = resp.Responses[0].GetStartWorkflow() + updateResp = resp.Responses[1].GetUpdateWorkflow() + } - t.Run("UpdateWithStart", func(t *testing.T) { - type multiopsResponseErr struct { - response *workflowservice.ExecuteMultiOperationResponse - err error + switch { + case len(capture.Metric(metrics.TaskWorkflowBusyCounter.Name())) != 0: + err = fmt.Errorf("expected no %q metrics", metrics.TaskWorkflowBusyCounter.Name()) + case resp == nil: + err = errors.New("expected ExecuteMultiOperation response") + case len(resp.Responses) != 2: + err = fmt.Errorf("expected 2 ExecuteMultiOperation responses, got %d", len(resp.Responses)) + case startResp == nil: + err = errors.New("expected start workflow response") + case startResp.RunId == "": + err = errors.New("expected non-empty start workflow run ID") + case updateResp == nil: + err = errors.New("expected update workflow response") + case completedStage && updateResp.Outcome == nil: + err = errors.New("expected completed update outcome") + case completedStage && updateResp.Outcome.String() == "": + err = errors.New("expected non-empty completed update outcome") + default: + } } - sendUpdateWithStart := func(s testcore.Env, ctx context.Context, startReq *workflowservice.StartWorkflowExecutionRequest, updateReq *workflowservice.UpdateWorkflowExecutionRequest) chan multiopsResponseErr { - capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture() - defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture) + retCh <- multiopsResponseErr{ + response: resp, + err: err, + } + }() + return retCh +} - retCh := make(chan multiopsResponseErr) - go func() { - resp, err := s.FrontendClient().ExecuteMultiOperation( - ctx, - &workflowservice.ExecuteMultiOperationRequest{ - Namespace: s.Namespace().String(), - Operations: []*workflowservice.ExecuteMultiOperationRequest_Operation{ - { - Operation: &workflowservice.ExecuteMultiOperationRequest_Operation_StartWorkflow{ - StartWorkflow: startReq, - }, - }, - { - Operation: &workflowservice.ExecuteMultiOperationRequest_Operation_UpdateWorkflow{ - UpdateWorkflow: updateReq, - }, - }, - }, - }) +func (s *UpdateWithStartSuite) updateWithStartReq(env testcore.Env, tv *testvars.TestVars) *workflowservice.StartWorkflowExecutionRequest { + return &workflowservice.StartWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + WorkflowId: tv.WorkflowID(), + WorkflowType: tv.WorkflowType(), + TaskQueue: tv.TaskQueue(), + Identity: tv.WorkerIdentity(), + } +} - if err == nil { - // Use assert (not require) in goroutine - require calls t.FailNow() which panics - //nolint:testifylint // intentional use of assert in goroutine - assert.Len(s.T(), resp.Responses, 2) +func (s *UpdateWithStartSuite) TestWorkflowIsNotRunning() { + for _, p := range []enumspb.WorkflowIdConflictPolicy{ + enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } { + s.Run(fmt.Sprintf("start workflow and send update (with conflict policy %v)", p), func(s *UpdateWithStartSuite) { + s.Run("and accept", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = p + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) - startRes := resp.Responses[0].Response.(*workflowservice.ExecuteMultiOperationResponse_Response_StartWorkflow).StartWorkflow - assert.NotEmpty(s.T(), startRes.RunId) + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil + }) + s.NoError(err) - updateRes := resp.Responses[1].Response.(*workflowservice.ExecuteMultiOperationResponse_Response_UpdateWorkflow).UpdateWorkflow - if updateReq.WaitPolicy.LifecycleStage == enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED { - assert.NotNil(s.T(), updateRes.Outcome) - assert.NotEmpty(s.T(), updateRes.Outcome.String()) - } - } + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + startResp := uwsRes.response.Responses[0].GetStartWorkflow() + updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() + requireStartedAndRunning(s.T(), startResp) + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) - // make sure there's no lock contention - //nolint:testifylint // intentional use of assert in goroutine - assert.Empty(s.T(), capture.Snapshot()[metrics.TaskWorkflowBusyCounter.Name()]) + // poll update to ensure same outcome is returned + pollRes, err := pollUpdate(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + s.NoError(err) + s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) - retCh <- multiopsResponseErr{resp, err} - }() - return retCh - } + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowExecutionUpdateAccepted + 6 WorkflowExecutionUpdateCompleted`, env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution())) + }) - startWorkflowReq := func(s testcore.Env, tv *testvars.TestVars) *workflowservice.StartWorkflowExecutionRequest { - return &workflowservice.StartWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowId: tv.WorkflowID(), - WorkflowType: tv.WorkflowType(), - TaskQueue: tv.TaskQueue(), - Identity: tv.WorkerIdentity(), - } - } + s.Run("and reject", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + startReq := s.updateWithStartReq(env, env.Tv()) + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) - t.Run("workflow is not running", func(t *testing.T) { - for _, p := range []enumspb.WorkflowIdConflictPolicy{ - enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, - } { - t.Run(fmt.Sprintf("start workflow and send update (with conflict policy %v)", p), func(t *testing.T) { - t.Run("and accept", func(t *testing.T) { - s := testcore.NewEnv(t) - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = p - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - - uwsRes := <-uwsCh - s.NoError(err) - startResp := uwsRes.response.Responses[0].GetStartWorkflow() - updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() - requireStartedAndRunning(s.T(), startResp) - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) - - // poll update to ensure same outcome is returned - pollRes, err := pollUpdate(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - s.NoError(err) - s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowExecutionUpdateAccepted - 6 WorkflowExecutionUpdateCompleted`, s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution())) + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateRejectMessages(env.Tv(), task.Messages[0]), + }, nil }) + s.NoError(err) - t.Run("and reject", func(t *testing.T) { - s := testcore.NewEnv(t) - startReq := startWorkflowReq(s, s.Tv()) - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateRejectMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - - uwsRes := <-uwsCh - s.NoError(uwsRes.err) - startResp := uwsRes.response.Responses[0].GetStartWorkflow() - updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() - requireStartedAndRunning(s.T(), startResp) - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateRep.GetOutcome().GetFailure().GetMessage()) - - // poll update to ensure same outcome is returned - _, err = pollUpdate(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - s.Error(err) - s.ErrorAs(err, new(*serviceerror.NotFound)) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted`, s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution())) - }) - }) - } - }) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + startResp := uwsRes.response.Responses[0].GetStartWorkflow() + updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() + requireStartedAndRunning(s.T(), startResp) + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateRep.GetOutcome().GetFailure().GetMessage()) - t.Run("workflow is running", func(t *testing.T) { - t.Run("workflow id conflict policy use-existing: only send update", func(t *testing.T) { - t.Run("and accept", func(t *testing.T) { - s := testcore.NewEnv(t) - // start workflow - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) - - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) - - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - - uwsRes := <-uwsCh - s.NoError(uwsRes.err) - startResp := uwsRes.response.Responses[0].GetStartWorkflow() - updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() - requireNotStartedButRunning(s.T(), startResp) - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) - - // poll update to ensure same outcome is returned - pollRes, err := pollUpdate(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - s.Nil(err) - s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) - - s.EqualHistoryEvents(` + // poll update to ensure same outcome is returned + _, err = pollUpdate(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + s.Error(err) + s.ErrorAs(err, new(*serviceerror.NotFound)) + + s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - 5 WorkflowTaskScheduled - 6 WorkflowTaskStarted - 7 WorkflowTaskCompleted - 8 WorkflowExecutionUpdateAccepted - 9 WorkflowExecutionUpdateCompleted`, s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution())) + 4 WorkflowTaskCompleted`, env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution())) + }) + }) + } +} + +func (s *UpdateWithStartSuite) TestWorkflowIsRunning() { + s.Run("workflow id conflict policy use-existing: only send update", func(s *UpdateWithStartSuite) { + s.Run("and accept", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + + // start workflow + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) + + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil }) + s.NoError(err) + + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + startResp := uwsRes.response.Responses[0].GetStartWorkflow() + updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() + requireNotStartedButRunning(s.T(), startResp) + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) + + // poll update to ensure same outcome is returned + pollRes, err := pollUpdate(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + s.NoError(err) + s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted + 7 WorkflowTaskCompleted + 8 WorkflowExecutionUpdateAccepted + 9 WorkflowExecutionUpdateCompleted`, env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution())) + }) - t.Run("and reject", func(t *testing.T) { - s := testcore.NewEnv(t) - // start workflow - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) + s.Run("and reject", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) + // start workflow + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateRejectMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - - uwsRes := <-uwsCh - s.NoError(uwsRes.err) - startResp := uwsRes.response.Responses[0].GetStartWorkflow() - updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() - requireNotStartedButRunning(s.T(), startResp) - s.Equal("rejection-of-"+s.Tv().UpdateID(), updateRep.GetOutcome().GetFailure().GetMessage()) - - // poll update to ensure same outcome is returned - _, err = pollUpdate(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - s.Error(err) - s.ErrorAs(err, new(*serviceerror.NotFound)) - - s.EqualHistoryEvents(` - 1 WorkflowExecutionStarted - 2 WorkflowTaskScheduled - 3 WorkflowTaskStarted - 4 WorkflowTaskCompleted - `, s.GetHistory(s.Namespace().String(), s.Tv().WorkflowExecution())) + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateRejectMessages(env.Tv(), task.Messages[0]), + }, nil }) - }) + s.NoError(err) - t.Run("workflow id conflict policy terminate-existing", func(t *testing.T) { - t.Run("terminate workflow first, then start and update", func(t *testing.T) { - s := testcore.NewEnv(t) - // start workflow - firstWF, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + startResp := uwsRes.response.Responses[0].GetStartWorkflow() + updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() + requireNotStartedButRunning(s.T(), startResp) + s.Equal("rejection-of-"+env.Tv().UpdateID(), updateRep.GetOutcome().GetFailure().GetMessage()) + + // poll update to ensure same outcome is returned + _, err = pollUpdate(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + s.Error(err) + s.ErrorAs(err, new(*serviceerror.NotFound)) + + s.EqualHistoryEvents(` + 1 WorkflowExecutionStarted + 2 WorkflowTaskScheduled + 3 WorkflowTaskStarted + 4 WorkflowTaskCompleted + `, env.GetHistory(env.Namespace().String(), env.Tv().WorkflowExecution())) + }) + }) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) + s.Run("workflow id conflict policy terminate-existing", func(s *UpdateWithStartSuite) { + s.Run("terminate workflow first, then start and update", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) + // start workflow + firstWF, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - - uwsRes := <-uwsCh - s.NoError(uwsRes.err) - startResp := uwsRes.response.Responses[0].GetStartWorkflow() - updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() - requireStartedAndRunning(s.T(), startResp) - s.Equal(startResp.RunId, updateRep.UpdateRef.GetWorkflowExecution().RunId) - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) - - // ensure workflow was terminated - descResp, err := s.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(), - &workflowservice.DescribeWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - Execution: &commonpb.WorkflowExecution{WorkflowId: startReq.WorkflowId, RunId: firstWF.RunId}, - }) - s.NoError(err) - s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, descResp.WorkflowExecutionInfo.Status) - - // poll update to ensure same outcome is returned - pollRes, err := pollUpdate(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - s.NoError(err) - s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil }) + s.NoError(err) - t.Run("given an accepted update, attach to it", func(t *testing.T) { - s := testcore.NewEnv(t) - // 1st update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) - uwsCh1 := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - // accept the update - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - - uwsRes1 := <-uwsCh1 - s.NoError(uwsRes1.err) - startResp1 := uwsRes1.response.Responses[0].GetStartWorkflow() - updateRep1 := uwsRes1.response.Responses[1].GetUpdateWorkflow() - s.True(startResp1.Started) - s.Equal(startResp1.RunId, updateRep1.UpdateRef.GetWorkflowExecution().RunId) - s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, updateRep1.Stage) - - // 2nd update-with-start: attaches to update instead of terminating workflow - uwsCh2 := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - uwsRes2 := <-uwsCh2 - s.NoError(uwsRes2.err) - startResp2 := uwsRes2.response.Responses[0].GetStartWorkflow() - updateRep2 := uwsRes2.response.Responses[1].GetUpdateWorkflow() - s.False(startResp2.Started) - s.Equal(startResp2.RunId, startResp1.RunId) // no termination - s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, updateRep2.Stage) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + startResp := uwsRes.response.Responses[0].GetStartWorkflow() + updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() + requireStartedAndRunning(s.T(), startResp) + s.Equal(startResp.RunId, updateRep.UpdateRef.GetWorkflowExecution().RunId) + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) + + // ensure workflow was terminated + descResp, err := env.FrontendClient().DescribeWorkflowExecution(testcore.NewContext(env.Context()), + &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + Execution: &commonpb.WorkflowExecution{WorkflowId: startReq.WorkflowId, RunId: firstWF.RunId}, }) - }) + s.NoError(err) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, descResp.WorkflowExecutionInfo.Status) - t.Run("workflow id conflict policy fail: abort multi operation", func(t *testing.T) { - s := testcore.NewEnv(t) - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) + // poll update to ensure same outcome is returned + pollRes, err := pollUpdate(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + s.NoError(err) + s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) + }) - // start workflow - startWorkflowReq(s, s.Tv()) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) + s.Run("given an accepted update, attach to it", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL - updateReq := updateWorkflowRequest(s, s.Tv(), &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - uwsRes := <-uwsCh - s.Error(uwsRes.err) - s.Equal("Update-with-Start could not be executed.", uwsRes.err.Error()) - errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() - s.Len(errs, 2) - var alreadyStartedErr *serviceerror.WorkflowExecutionAlreadyStarted - s.ErrorAs(errs[0], &alreadyStartedErr) - s.Equal("Operation was aborted.", errs[1].Error()) - }) + // 1st update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) + uwsCh1 := s.sendUpdateWithStart(env, startReq, updateReq) - t.Run("receive completed update result", func(t *testing.T) { - _ = testcore.NewEnv(t) // unused s - for _, p := range []enumspb.WorkflowIdConflictPolicy{ - enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, - } { - t.Run(fmt.Sprintf("for workflow id conflict policy %v", p), func(t *testing.T) { - s := testcore.NewEnv(t) - startReq := startWorkflowReq(s, s.Tv()) - updReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - - // 1st update-with-start - uwsCh1 := sendUpdateWithStart(s, testcore.NewContext(), startReq, updReq) - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - uwsRes1 := <-uwsCh1 - s.NoError(uwsRes1.err) - - // 2nd update-with-start: using *same* UpdateID - but *different* RequestID - uwsRes2 := <-sendUpdateWithStart(s, testcore.NewContext(), startReq, updReq) - s.NoError(uwsRes2.err) - - s.Equal(uwsRes1.response.Responses[0].GetStartWorkflow().RunId, uwsRes2.response.Responses[0].GetStartWorkflow().RunId) - s.Equal(uwsRes1.response.Responses[1].GetUpdateWorkflow().Outcome.String(), uwsRes2.response.Responses[1].GetUpdateWorkflow().Outcome.String()) - - // poll update to ensure same outcome is returned - pollRes, err := pollUpdate(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - s.NoError(err) - s.Equal(uwsRes1.response.Responses[1].GetUpdateWorkflow().Outcome.String(), pollRes.Outcome.String()) - }) - } - }) + // accept the update + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptMessages(env.Tv(), task.Messages[0]), + }, nil + }) + s.NoError(err) - t.Run("dedupes start", func(t *testing.T) { - _ = testcore.NewEnv(t) // unused s - for _, p := range []enumspb.WorkflowIdConflictPolicy{ - enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, - } { - t.Run(fmt.Sprintf("for workflow id conflict policy %v", p), func(t *testing.T) { - s := testcore.NewEnv(t) - startReq := startWorkflowReq(s, s.Tv()) - startReq.RequestId = "request_id" - startReq.WorkflowIdConflictPolicy = p - updReq1 := updateWorkflowRequest(s, s.Tv().WithUpdateIDNumber(1), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - - // 1st update-with-start - uwsCh1 := sendUpdateWithStart(s, testcore.NewContext(), startReq, updReq1) - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - uwsRes1 := <-uwsCh1 - s.NoError(uwsRes1.err) - - // 2nd update-with-start: using *same* RequestID - but *different* UpdateID - updReq2 := updateWorkflowRequest(s, s.Tv().WithUpdateIDNumber(2), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh2 := sendUpdateWithStart(s, testcore.NewContext(), startReq, updReq2) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - uwsRes2 := <-uwsCh2 - s.NoError(uwsRes1.err) - - s.Equal(uwsRes1.response.Responses[0].GetStartWorkflow().RunId, uwsRes2.response.Responses[0].GetStartWorkflow().RunId) - }) - } - }) + uwsRes1 := <-uwsCh1 + s.NoError(uwsRes1.err) + startResp1 := uwsRes1.response.Responses[0].GetStartWorkflow() + updateRep1 := uwsRes1.response.Responses[1].GetUpdateWorkflow() + s.True(startResp1.Started) + s.Equal(startResp1.RunId, updateRep1.UpdateRef.GetWorkflowExecution().RunId) + s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, updateRep1.Stage) + + // 2nd update-with-start: attaches to update instead of terminating workflow + uwsCh2 := s.sendUpdateWithStart(env, startReq, updateReq) + + uwsRes2 := <-uwsCh2 + s.NoError(uwsRes2.err) + startResp2 := uwsRes2.response.Responses[0].GetStartWorkflow() + updateRep2 := uwsRes2.response.Responses[1].GetUpdateWorkflow() + s.False(startResp2.Started) + s.Equal(startResp2.RunId, startResp1.RunId) // no termination + s.Equal(enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, updateRep2.Stage) }) + }) - t.Run("workflow is closed", func(t *testing.T) { - t.Run("workflow id reuse policy allow-duplicate", func(t *testing.T) { - s := testcore.NewEnv(t) - // start and terminate workflow - initialWorkflow, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) + s.Run("workflow id conflict policy fail: abort multi operation", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) + // start workflow + s.updateWithStartReq(env, env.Tv()) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) - _, err = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), - &workflowservice.TerminateWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: s.Tv().WorkflowExecution(), - Reason: s.Tv().Any().String(), - }) - s.NoError(err) + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL + updateReq := updateWorkflowRequest(env, env.Tv(), &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + uwsRes := <-uwsCh + s.Error(uwsRes.err) + s.Equal("Update-with-Start could not be executed.", uwsRes.err.Error()) + errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() + s.Len(errs, 2) + var alreadyStartedErr *serviceerror.WorkflowExecutionAlreadyStarted + s.ErrorAs(errs[0], &alreadyStartedErr) + s.Equal("Operation was aborted.", errs[1].Error()) + }) - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdReusePolicy = enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE - updateReq := updateWorkflowRequest(s, s.Tv(), + s.Run("receive completed update result", func(s *UpdateWithStartSuite) { + _ = testcore.NewEnv(s.T()) // unused s + for _, p := range []enumspb.WorkflowIdConflictPolicy{ + enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } { + s.Run(fmt.Sprintf("for workflow id conflict policy %v", p), func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + startReq := s.updateWithStartReq(env, env.Tv()) + updReq := updateWorkflowRequest(env, env.Tv(), &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), + // 1st update-with-start + uwsCh1 := s.sendUpdateWithStart(env, startReq, updReq) + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), }, nil }) s.NoError(err) + uwsRes1 := <-uwsCh1 + s.NoError(uwsRes1.err) - uwsRes := <-uwsCh - s.NoError(uwsRes.err) - startResp := uwsRes.response.Responses[0].GetStartWorkflow() - updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() - requireStartedAndRunning(s.T(), startResp) - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) + // 2nd update-with-start: using *same* UpdateID - but *different* RequestID + uwsRes2 := <-s.sendUpdateWithStart(env, startReq, updReq) + s.NoError(uwsRes2.err) - // ensure terminated workflow is not locked by update-with-start - err = s.SendSignal(s.Namespace().String(), &commonpb.WorkflowExecution{ - WorkflowId: s.Tv().WorkflowID(), - RunId: initialWorkflow.RunId, - }, s.Tv().Any().String(), s.Tv().Any().Payloads(), s.Tv().Any().String()) - s.ErrorContains(err, "workflow execution already completed") + s.Equal(uwsRes1.response.Responses[0].GetStartWorkflow().RunId, uwsRes2.response.Responses[0].GetStartWorkflow().RunId) + s.Equal(uwsRes1.response.Responses[1].GetUpdateWorkflow().Outcome.String(), uwsRes2.response.Responses[1].GetUpdateWorkflow().Outcome.String()) // poll update to ensure same outcome is returned - pollRes, err := pollUpdate(s, s.Tv(), + pollRes, err := pollUpdate(env, env.Tv(), &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) s.NoError(err) - s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) + s.Equal(uwsRes1.response.Responses[1].GetUpdateWorkflow().Outcome.String(), pollRes.Outcome.String()) }) + } + }) - t.Run("workflow id reuse policy reject-duplicate", func(t *testing.T) { - s := testcore.NewEnv(t) - // start and terminate workflow - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) - - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) + s.Run("dedupes start", func(s *UpdateWithStartSuite) { + _ = testcore.NewEnv(s.T()) // unused s + for _, p := range []enumspb.WorkflowIdConflictPolicy{ + enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } { + s.Run(fmt.Sprintf("for workflow id conflict policy %v", p), func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.RequestId = "request_id" + startReq.WorkflowIdConflictPolicy = p + updReq1 := updateWorkflowRequest(env, env.Tv().WithUpdateIDNumber(1), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - _, err = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), - &workflowservice.TerminateWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: s.Tv().WorkflowExecution(), - Reason: s.Tv().Any().String(), + // 1st update-with-start + uwsCh1 := s.sendUpdateWithStart(env, startReq, updReq1) + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil }) s.NoError(err) + uwsRes1 := <-uwsCh1 + s.NoError(uwsRes1.err) - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdReusePolicy = enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE - updateReq := updateWorkflowRequest(s, s.Tv(), + // 2nd update-with-start: using *same* RequestID - but *different* UpdateID + updReq2 := updateWorkflowRequest(env, env.Tv().WithUpdateIDNumber(2), &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) + uwsCh2 := s.sendUpdateWithStart(env, startReq, updReq2) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil + }) + s.NoError(err) + uwsRes2 := <-uwsCh2 + s.NoError(uwsRes2.err) - uwsRes := <-uwsCh - s.Error(uwsRes.err) - s.Equal("Update-with-Start could not be executed.", uwsRes.err.Error()) - errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() - s.Len(errs, 2) - s.Contains(errs[0].Error(), "Workflow execution already finished") - var alreadyStartedErr *serviceerror.WorkflowExecutionAlreadyStarted - s.ErrorAs(errs[0], &alreadyStartedErr) - s.Equal("Operation was aborted.", errs[1].Error()) + s.Equal(uwsRes1.response.Responses[0].GetStartWorkflow().RunId, uwsRes2.response.Responses[0].GetStartWorkflow().RunId) }) + } + }) +} - t.Run("receive completed update result", func(t *testing.T) { - _ = testcore.NewEnv(t) // unused s - for _, p := range []enumspb.WorkflowIdConflictPolicy{ - enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, - enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, - } { - t.Run(fmt.Sprintf("for workflow id conflict policy %v", p), func(t *testing.T) { - s := testcore.NewEnv(t) - // 1st update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = p - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - Commands: s.UpdateAcceptCompleteCommands(s.Tv()), - }, nil - }) - s.NoError(err) - - uwsRes := <-uwsCh - s.NoError(uwsRes.err) - startResp1 := uwsRes.response.Responses[0].GetStartWorkflow() - _ = uwsRes.response.Responses[1].GetUpdateWorkflow() - requireStartedAndRunning(s.T(), startResp1) - - // terminate workflow - _, err = s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), - &workflowservice.TerminateWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: s.Tv().WorkflowExecution(), - Reason: s.Tv().Any().String(), - }) - s.NoError(err) - - // 2nd update-with-start (using the same Update ID but different Request ID) - uwsRes = <-sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - s.NoError(uwsRes.err) - startResp := uwsRes.response.Responses[0].GetStartWorkflow() - updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() - s.False(startResp.Started) - s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, startResp.Status) - // TODO: check startResp.Running - s.Equal("success-result-of-"+s.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) - }) - } +func (s *UpdateWithStartSuite) TestWorkflowIsClosed() { + s.Run("workflow id reuse policy allow-duplicate", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + + // start and terminate workflow + initialWorkflow, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) + + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + _, err = env.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(env.Context()), + &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + WorkflowExecution: env.Tv().WorkflowExecution(), + Reason: env.Tv().Any().String(), }) - }) + s.NoError(err) - t.Run("workflow start conflict", func(t *testing.T) { - t.Run("workflow id conflict policy fail: use-existing", func(t *testing.T) { - // Uses InjectHook which requires a dedicated cluster to avoid conflicts with other tests. - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdReusePolicy = enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) - // simulate a race condition - s.InjectHook(testhooks.UpdateWithStartInBetweenLockAndStart, func() { - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startReq) - s.NoError(err) - }) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil + }) + s.NoError(err) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + startResp := uwsRes.response.Responses[0].GetStartWorkflow() + updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() + requireStartedAndRunning(s.T(), startResp) + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) + + // ensure terminated workflow is not locked by update-with-start + err = env.SendSignal(env.Namespace().String(), &commonpb.WorkflowExecution{ + WorkflowId: env.Tv().WorkflowID(), + RunId: initialWorkflow.RunId, + }, env.Tv().Any().String(), env.Tv().Any().Payloads(), env.Tv().Any().String()) + s.ErrorContains(err, "workflow execution already completed") + + // poll update to ensure same outcome is returned + pollRes, err := pollUpdate(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + s.NoError(err) + s.Equal(updateRep.Outcome.String(), pollRes.Outcome.String()) + }) - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{}, nil - }) - s.NoError(err) + s.Run("workflow id reuse policy reject-duplicate", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) + // start and terminate workflow + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) + + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) - <-uwsCh + _, err = env.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(env.Context()), + &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + WorkflowExecution: env.Tv().WorkflowExecution(), + Reason: env.Tv().Any().String(), }) - }) + s.NoError(err) - t.Run("update is aborted by closing workflow", func(t *testing.T) { - t.Run("retry request once when workflow was not started", func(t *testing.T) { - s := testcore.NewEnv(t) - // start workflow - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdReusePolicy = enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + + uwsRes := <-uwsCh + s.Error(uwsRes.err) + s.Equal("Update-with-Start could not be executed.", uwsRes.err.Error()) + errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() + s.Len(errs, 2) + s.Contains(errs[0].Error(), "Workflow execution already finished") + var alreadyStartedErr *serviceerror.WorkflowExecutionAlreadyStarted + s.ErrorAs(errs[0], &alreadyStartedErr) + s.Equal("Operation was aborted.", errs[1].Error()) + }) - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) + s.Run("receive completed update result", func(s *UpdateWithStartSuite) { + _ = testcore.NewEnv(s.T()) // unused s + for _, p := range []enumspb.WorkflowIdConflictPolicy{ + enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, + enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + } { + s.Run(fmt.Sprintf("for workflow id conflict policy %v", p), func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + // 1st update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = p + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) - // wait until the update is admitted - then complete workflow - waitUpdateAdmitted(s, s.Tv()) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Commands: []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ - CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, - }, - }, - }, + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + Commands: env.UpdateAcceptCompleteCommands(env.Tv()), }, nil }) s.NoError(err) - // update-with-start will do a server-side retry + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + startResp1 := uwsRes.response.Responses[0].GetStartWorkflow() + _ = uwsRes.response.Responses[1].GetUpdateWorkflow() + requireStartedAndRunning(s.T(), startResp1) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), - }, nil + // terminate workflow + _, err = env.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(env.Context()), + &workflowservice.TerminateWorkflowExecutionRequest{ + Namespace: env.Namespace().String(), + WorkflowExecution: env.Tv().WorkflowExecution(), + Reason: env.Tv().Any().String(), }) s.NoError(err) - uwsRes := <-uwsCh + // 2nd update-with-start (using the same Update ID but different Request ID) + uwsRes = <-s.sendUpdateWithStart(env, startReq, updateReq) + s.NoError(uwsRes.err) + startResp := uwsRes.response.Responses[0].GetStartWorkflow() + updateRep := uwsRes.response.Responses[1].GetUpdateWorkflow() + s.False(startResp.Started) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, startResp.Status) + // TODO: check startResp.Running + s.Equal("success-result-of-"+env.Tv().UpdateID(), testcore.DecodeString(s.T(), updateRep.GetOutcome().GetSuccess())) }) + } + }) +} - t.Run("return retryable error after retry", func(t *testing.T) { - // Uses InjectHook which requires a dedicated cluster to avoid conflicts with other tests. - s := testcore.NewEnv(t, testcore.WithDedicatedCluster()) - // start workflow - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) - _, err = s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), taskpoller.DrainWorkflowTask) - s.NoError(err) +func (s *UpdateWithStartSuite) TestWorkflowStartConflict() { + s.Run("workflow id conflict policy fail: use-existing", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED}) + + // simulate a race condition + env.InjectHook(testhooks.NewHook(testhooks.UpdateWithStartInBetweenLockAndStart, func() { + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), startReq) + s.NoError(err) + })) + + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + + // Two orderings are possible depending on lock timing: + // A) UWS retry acquires the workflow lock before RecordWorkflowTaskStarted: update is admitted + // while WFT #1 is still scheduled, so it attaches to WFT #1 messages when WFT #1 starts. + // B) RecordWorkflowTaskStarted wins the lock first: WFT #1 starts with no messages, completes, + // then UWS retry runs with no pending WFT and creates a speculative WFT #2 for the update. + updateHandled := false + for range 2 { + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + if len(task.Messages) == 0 { + return &workflowservice.RespondWorkflowTaskCompletedRequest{}, nil + } + updateHandled = true + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil + }) + s.NoError(err) + if updateHandled { + break + } + } + s.True(updateHandled, "update not processed in either WFT") - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + }) +} - // wait until the update is admitted - waitUpdateAdmitted(s, s.Tv()) +func (s *UpdateWithStartSuite) TestUpdateIsAbortedByClosingWorkflow() { + s.Run("retry request once when workflow was not started", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) - s.InjectHook(testhooks.UpdateWithStartOnClosingWorkflowRetry, func() { - _, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), startWorkflowReq(s, s.Tv())) - s.NoError(err) - }) + // start workflow + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) - // complete workflow (twice including retry) - for i := 0; i < 2; i++ { - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Commands: []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ - CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, - }, - }, - }, - }, nil - }) - s.NoError(err) - } + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) - // ensure update-with-start returns retryable error - uwsRes := <-uwsCh - s.Error(uwsRes.err) - errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() - s.Len(errs, 2) - s.Equal("Operation was aborted.", errs[0].Error()) - s.ErrorContains(errs[1], update.AbortedByWorkflowClosingErr.Error()) - s.ErrorAs(errs[1], new(*serviceerror.Aborted)) + // wait until the update is admitted - then complete workflow + waitUpdateAdmitted(env, env.Tv()) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, + }, + }, + }, + }, nil }) + s.NoError(err) - t.Run("do not retry when workflow was started", func(t *testing.T) { - s := testcore.NewEnv(t) - // update-with-start - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING - updateReq := updateWorkflowRequest(s, s.Tv(), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) - uwsCh := sendUpdateWithStart(s, testcore.NewContext(), startReq, updateReq) - - // wait until the update is admitted - then complete workflow - waitUpdateAdmitted(s, s.Tv()) - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Commands: []*commandpb.Command{ - { - CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, - Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ - CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, - }, - }, - }, - }, nil - }) - s.NoError(err) + // update-with-start will do a server-side retry - uwsRes := <-uwsCh - s.Error(uwsRes.err) - errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() - s.Len(errs, 2) - s.ErrorContains(errs[1], update.AbortedByWorkflowClosingErr.Error()) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil }) - }) + s.NoError(err) - t.Run("return update rate limit error", func(t *testing.T) { - // lower maximum total number of updates for testing purposes - s := testcore.NewEnv(t, - testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxTotalUpdates, 1), - ) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + }) - ctx := testcore.NewContext() - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + s.Run("return retryable error after retry", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) - // allows 1st - updateReq := updateWorkflowRequest(s, s.Tv().WithUpdateIDNumber(0), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) - uwsCh := sendUpdateWithStart(s, ctx, startReq, updateReq) - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), + // start workflow + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) + _, err = env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), taskpoller.DrainWorkflowTask) + s.NoError(err) + + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + + // wait until the update is admitted + waitUpdateAdmitted(env, env.Tv()) + + env.InjectHook(testhooks.NewHook(testhooks.UpdateWithStartOnClosingWorkflowRetry, func() { + _, err := env.FrontendClient().StartWorkflowExecution(testcore.NewContext(env.Context()), s.updateWithStartReq(env, env.Tv())) + s.NoError(err) + })) + + // complete workflow (twice including retry) + for range 2 { + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptCompleteMessages(s.Tv(), task.Messages[0]), + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, + }, + }, + }, }, nil }) s.NoError(err) - uwsRes := <-uwsCh - s.NoError(uwsRes.err) + } - // denies 2nd - updateReq = updateWorkflowRequest(s, s.Tv().WithUpdateIDNumber(1), updateReq.WaitPolicy) - select { - case <-sendUpdateWithStart(s, ctx, startReq, updateReq): - err = (<-sendUpdateWithStart(s, ctx, startReq, updateReq)).err - s.Error(err) - errs := err.(*serviceerror.MultiOperationExecution).OperationErrors() - s.Len(errs, 2) - s.Equal("Operation was aborted.", errs[0].Error()) - s.Contains(errs[1].Error(), "limit on the total number of distinct updates in this workflow has been reached") - case <-ctx.Done(): - s.Fail("timed out waiting for update") - } - }) + // ensure update-with-start returns retryable error + uwsRes := <-uwsCh + s.Error(uwsRes.err) + errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() + s.Len(errs, 2) + s.Equal("Operation was aborted.", errs[0].Error()) + s.ErrorContains(errs[1], update.AbortedByWorkflowClosingErr.Error()) + s.ErrorAs(errs[1], new(*serviceerror.Aborted)) + }) - t.Run("return update in-flight limit error", func(t *testing.T) { - // lower maximum in-flight updates for testing purposes - maxInFlight := 1 - s := testcore.NewEnv(t, - testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxInFlightUpdates, maxInFlight), - ) + s.Run("do not retry when workflow was started", func(s *UpdateWithStartSuite) { + env := testcore.NewEnv(s.T()) - ctx := testcore.NewContext() - startReq := startWorkflowReq(s, s.Tv()) - startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + // update-with-start + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + updateReq := updateWorkflowRequest(env, env.Tv(), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) - // Start workflow and admit 1st update (but don't complete it) - updateReq := updateWorkflowRequest(s, s.Tv().WithUpdateIDNumber(0), - &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) - uwsCh := sendUpdateWithStart(s, ctx, startReq, updateReq) + // wait until the update is admitted - then complete workflow + waitUpdateAdmitted(env, env.Tv()) + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{}, + }, + }, + }, + }, nil + }) + s.NoError(err) - // Poll workflow task but only accept, don't complete the update - _, err := s.TaskPoller().PollAndHandleWorkflowTask(s.Tv(), - func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { - return &workflowservice.RespondWorkflowTaskCompletedRequest{ - Messages: s.UpdateAcceptMessages(s.Tv(), task.Messages[0]), - }, nil - }) - s.NoError(err) - uwsRes := <-uwsCh - s.NoError(uwsRes.err) + uwsRes := <-uwsCh + s.Error(uwsRes.err) + errs := uwsRes.err.(*serviceerror.MultiOperationExecution).OperationErrors() + s.Len(errs, 2) + s.ErrorContains(errs[1], update.AbortedByWorkflowClosingErr.Error()) + }) +} - // Try to send 2nd update-with-start while 1st is still in-flight (not completed) - updateReq = updateWorkflowRequest(s, s.Tv().WithUpdateIDNumber(1), updateReq.WaitPolicy) - uwsCh = sendUpdateWithStart(s, ctx, startReq, updateReq) - select { - case uwsRes := <-uwsCh: - err = uwsRes.err - s.Error(err) +func (s *UpdateWithStartSuite) TestReturnUpdateRateLimitError() { + // lower maximum total number of updates for testing purposes + env := testcore.NewEnv(s.T(), + testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxTotalUpdates, 1), + ) + + ctx := testcore.NewContext(env.Context()) + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + + // allows 1st + updateReq := updateWorkflowRequest(env, env.Tv().WithUpdateIDNumber(0), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptCompleteMessages(env.Tv(), task.Messages[0]), + }, nil + }) + s.NoError(err) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + + // denies 2nd + updateReq = updateWorkflowRequest(env, env.Tv().WithUpdateIDNumber(1), updateReq.WaitPolicy) + select { + case <-s.sendUpdateWithStart(env, startReq, updateReq): + err = (<-s.sendUpdateWithStart(env, startReq, updateReq)).err + s.Error(err) + errs := err.(*serviceerror.MultiOperationExecution).OperationErrors() + s.Len(errs, 2) + s.Equal("Operation was aborted.", errs[0].Error()) + s.Contains(errs[1].Error(), "limit on the total number of distinct updates in this workflow has been reached") + case <-ctx.Done(): + s.Fail("timed out waiting for update") + } +} - var multiOpsErr *serviceerror.MultiOperationExecution - s.ErrorAs(err, &multiOpsErr) - - errs := multiOpsErr.OperationErrors() - s.Len(errs, 2) - s.Equal("Operation was aborted.", errs[0].Error()) - s.Contains(errs[1].Error(), "limit on number of concurrent in-flight updates has been reached") - - // Verify ResourceExhausted error is accessible with all details preserved - var resExhausted *serviceerror.ResourceExhausted - s.ErrorAs(errs[1], &resExhausted) - s.Equal(enumspb.RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT, resExhausted.Cause) - s.Equal(enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, resExhausted.Scope) - s.Contains(resExhausted.Message, "limit on number of concurrent in-flight updates") - case <-ctx.Done(): - s.Fail("timed out waiting for update") - } +func (s *UpdateWithStartSuite) TestReturnUpdateInFlightLimitError() { + // lower maximum in-flight updates for testing purposes + maxInFlight := 1 + env := testcore.NewEnv(s.T(), + testcore.WithDynamicConfig(dynamicconfig.WorkflowExecutionMaxInFlightUpdates, maxInFlight), + ) + + ctx := testcore.NewContext(env.Context()) + startReq := s.updateWithStartReq(env, env.Tv()) + startReq.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + + // Start workflow and admit 1st update (but don't complete it) + updateReq := updateWorkflowRequest(env, env.Tv().WithUpdateIDNumber(0), + &updatepb.WaitPolicy{LifecycleStage: enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}) + uwsCh := s.sendUpdateWithStart(env, startReq, updateReq) + + // Poll workflow task but only accept, don't complete the update + _, err := env.TaskPoller().PollAndHandleWorkflowTask(env.Tv(), + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Messages: env.UpdateAcceptMessages(env.Tv(), task.Messages[0]), + }, nil }) - }) + s.NoError(err) + uwsRes := <-uwsCh + s.NoError(uwsRes.err) + + // Try to send 2nd update-with-start while 1st is still in-flight (not completed) + updateReq = updateWorkflowRequest(env, env.Tv().WithUpdateIDNumber(1), updateReq.WaitPolicy) + uwsCh = s.sendUpdateWithStart(env, startReq, updateReq) + select { + case uwsRes := <-uwsCh: + err = uwsRes.err + s.Error(err) + + var multiOpsErr *serviceerror.MultiOperationExecution + s.ErrorAs(err, &multiOpsErr) + + errs := multiOpsErr.OperationErrors() + s.Len(errs, 2) + s.Equal("Operation was aborted.", errs[0].Error()) + s.Contains(errs[1].Error(), "limit on number of concurrent in-flight updates has been reached") + + // Verify ResourceExhausted error is accessible with all details preserved + var resExhausted *serviceerror.ResourceExhausted + s.ErrorAs(errs[1], &resExhausted) + s.Equal(enumspb.RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT, resExhausted.Cause) + s.Equal(enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, resExhausted.Scope) + s.Contains(resExhausted.Message, "limit on number of concurrent in-flight updates") + case <-ctx.Done(): + s.Fail("timed out waiting for update") + } } diff --git a/tests/user_timers_test.go b/tests/user_timers_test.go index 9bdb1cefe0b..5a660c85a45 100644 --- a/tests/user_timers_test.go +++ b/tests/user_timers_test.go @@ -59,7 +59,7 @@ func (s *UserTimersTestSuite) TestUserTimers_Sequential() { if timerCounter < timerCount { timerCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, timerCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, timerCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_START_TIMER, Attributes: &commandpb.Command_StartTimerCommandAttributes{StartTimerCommandAttributes: &commandpb.StartTimerCommandAttributes{ @@ -89,7 +89,7 @@ func (s *UserTimersTestSuite) TestUserTimers_Sequential() { T: s.T(), } - for i := 0; i < 4; i++ { + for range 4 { _, err := poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask: completed") s.NoError(err) diff --git a/tests/versioning_3_test.go b/tests/versioning_3_test.go index 9db11a5ec87..d3e7db86795 100644 --- a/tests/versioning_3_test.go +++ b/tests/versioning_3_test.go @@ -18,6 +18,7 @@ import ( commonpb "go.temporal.io/api/common/v1" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" historypb "go.temporal.io/api/history/v1" nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/serviceerror" @@ -37,7 +38,7 @@ import ( "go.temporal.io/server/common" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/primitives/timestamp" - "go.temporal.io/server/common/searchattribute" + "go.temporal.io/server/common/searchattribute/sadefs" "go.temporal.io/server/common/testing/protoutils" "go.temporal.io/server/common/testing/taskpoller" "go.temporal.io/server/common/testing/testhooks" @@ -48,6 +49,7 @@ import ( "go.temporal.io/server/service/worker/workerdeployment" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/fieldmaskpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -74,37 +76,20 @@ const ( type Versioning3Suite struct { testcore.FunctionalTestBase - useV32 bool deploymentWorkflowVersion workerdeployment.DeploymentWorkflowVersion - useRevisionNumbers bool - useNewDeploymentData bool } -func TestVersioning3FunctionalSuiteV0(t *testing.T) { - t.Parallel() - suite.Run(t, &Versioning3Suite{ - deploymentWorkflowVersion: workerdeployment.InitialVersion, - useV32: true, - useNewDeploymentData: false, - useRevisionNumbers: false, - }) -} - -func TestVersioning3FunctionalSuiteV2(t *testing.T) { +func TestVersioning3FunctionalSuite(t *testing.T) { t.Parallel() suite.Run(t, &Versioning3Suite{ deploymentWorkflowVersion: workerdeployment.VersionDataRevisionNumber, - useV32: true, - useRevisionNumbers: true, - useNewDeploymentData: true, }) } func (s *Versioning3Suite) SetupSuite() { dynamicConfigOverrides := map[dynamicconfig.Key]any{ - dynamicconfig.MatchingDeploymentWorkflowVersion.Key(): int(s.deploymentWorkflowVersion), - dynamicconfig.UseRevisionNumberForWorkerVersioning.Key(): s.useRevisionNumbers, - dynamicconfig.MatchingForwarderMaxChildrenPerNode.Key(): partitionTreeDegree, + dynamicconfig.MatchingDeploymentWorkflowVersion.Key(): int(s.deploymentWorkflowVersion), + dynamicconfig.MatchingForwarderMaxChildrenPerNode.Key(): partitionTreeDegree, // Make sure we don't hit the rate limiter in tests dynamicconfig.FrontendGlobalNamespaceNamespaceReplicationInducingAPIsRPS.Key(): 1000, @@ -118,11 +103,6 @@ func (s *Versioning3Suite) SetupSuite() { // Overriding the number of deployments that can be registered in a single namespace. Done only for this test suite // since it creates a large number of unique deployments in the test suite's namespace. dynamicconfig.MatchingMaxDeployments.Key(): 1000, - - // Use new matcher for versioning tests. Ideally we would run everything with old and new, - // but for now we pick a subset of tests. Versioning tests exercise the most features of - // matching so they're a good condidate. - dynamicconfig.MatchingUseNewMatcher.Key(): true, } s.FunctionalTestBase.SetupSuiteWithCluster(testcore.WithDynamicConfigOverrides(dynamicConfigOverrides)) } @@ -145,7 +125,7 @@ func (s *Versioning3Suite) TestPinnedTask_NoProperPoller() { // Cancel the poller after condition is met cancelPoller() - s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32)) + s.startWorkflow(tv, tv.VersioningOverridePinned()) s.idlePollWorkflow(context.Background(), tv, false, ver3MinPollTime, "unversioned worker should not receive pinned task") // Sleeping to let the pollers arrive to server before ending the test. @@ -167,69 +147,43 @@ func (s *Versioning3Suite) TestUnpinnedTask_NonCurrentDeployment() { } func (s *Versioning3Suite) TestUnpinnedTask_OldDeployment() { - if s.useNewDeploymentData == true { - s.RunTestWithMatchingBehavior( - func() { - tv := testvars.New(s) - tvOldDeployment := tv.WithBuildIDNumber(1) - tvNewDeployment := tv.WithBuildIDNumber(2) - - // previous current deployment - s.updateTaskQueueDeploymentDataWithRoutingConfig(tvOldDeployment, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvOldDeployment.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvOldDeployment.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf) - - // current deployment - s.updateTaskQueueDeploymentDataWithRoutingConfig(tvNewDeployment, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvNewDeployment.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvNewDeployment.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf) - - s.startWorkflow(tv, nil) - - s.idlePollWorkflow( - context.Background(), - tvOldDeployment, - true, - ver3MinPollTime, - "old deployment should not receive unpinned task", - ) - // Sleeping to let the pollers arrive to server before ending the test. - time.Sleep(200 * time.Millisecond) //nolint:forbidigo - }, - ) - } else { - s.RunTestWithMatchingBehavior( - func() { - tv := testvars.New(s) - tvOldDeployment := tv.WithBuildIDNumber(1) - tvNewDeployment := tv.WithBuildIDNumber(2) - // previous current deployment - s.updateTaskQueueDeploymentData(tvOldDeployment, true, 0, false, time.Minute, tqTypeWf) - // current deployment - s.updateTaskQueueDeploymentData(tvNewDeployment, true, 0, false, 0, tqTypeWf) - - s.startWorkflow(tv, nil) - - s.idlePollWorkflow( - context.Background(), - tvOldDeployment, - true, - ver3MinPollTime, - "old deployment should not receive unpinned task", - ) - // Sleeping to let the pollers arrive to server before ending the test. - time.Sleep(200 * time.Millisecond) //nolint:forbidigo - }, - ) - } + s.RunTestWithMatchingBehavior( + func() { + tv := testvars.New(s) + tvOldDeployment := tv.WithBuildIDNumber(1) + tvNewDeployment := tv.WithBuildIDNumber(2) + + // previous current deployment + s.updateTaskQueueDeploymentDataWithRoutingConfig(tvOldDeployment, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvOldDeployment.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 1, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvOldDeployment.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf) + + // current deployment + s.updateTaskQueueDeploymentDataWithRoutingConfig(tvNewDeployment, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvNewDeployment.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 2, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvNewDeployment.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf) + + s.startWorkflow(tv, nil) + + s.idlePollWorkflow( + context.Background(), + tvOldDeployment, + true, + ver3MinPollTime, + "old deployment should not receive unpinned task", + ) + // Sleeping to let the pollers arrive to server before ending the test. + time.Sleep(200 * time.Millisecond) //nolint:forbidigo + }, + ) } func (s *Versioning3Suite) TestSessionActivityResourceSpecificTaskQueueNotRegisteredInVersion() { @@ -360,24 +314,24 @@ func (s *Versioning3Suite) testWorkflowWithPinnedOverride(sticky bool) { // Wait for the version to be present in the task queue. Version existence is required before it can be set as an override. s.validatePinnedVersionExistsInTaskQueue(tv) - runID := s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32)) + runID := s.startWorkflow(tv, tv.VersioningOverridePinned()) s.WaitForChannel(ctx, wftCompleted) - s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil) + s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(), nil) s.verifyVersioningSAs(tv, vbPinned, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, tv) if sticky { s.verifyWorkflowStickyQueue(tv.WithRunID(runID)) } s.WaitForChannel(ctx, actCompleted) - s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil) + s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(), nil) s.pollWftAndHandle(tv, sticky, nil, func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { s.NotNil(task) return respondCompleteWorkflow(tv, vbUnpinned), nil }) - s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil) + s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(), nil) } func (s *Versioning3Suite) TestQueryWithPinnedOverride_NoSticky() { @@ -448,9 +402,9 @@ func (s *Versioning3Suite) testPinnedQuery_DrainedVersion(pollersPresent bool, r return respondCompleteWorkflow(tv, vbPinned), nil }) - s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32)) + s.startWorkflow(tv, tv.VersioningOverridePinned()) s.WaitForChannel(ctx, wftCompleted) - s.verifyWorkflowVersioning(s.Assertions, tv, vbPinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil) + s.verifyWorkflowVersioning(s.Assertions, tv, vbPinned, tv.Deployment(), tv.VersioningOverridePinned(), nil) // create version v2 and make it current which shall make v1 go from current -> draining/drained idlePollerDone = make(chan struct{}) @@ -543,10 +497,10 @@ func (s *Versioning3Suite) testQueryWithPinnedOverride(sticky bool) { a.True(resp.GetIsMember()) }, 10*time.Second, 100*time.Millisecond) - runID := s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32)) + runID := s.startWorkflow(tv, tv.VersioningOverridePinned()) s.WaitForChannel(ctx, wftCompleted) - s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil) + s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(), nil) if sticky { s.verifyWorkflowStickyQueue(tv.WithRunID(runID)) } @@ -660,7 +614,7 @@ func (s *Versioning3Suite) testPinnedWorkflowWithLateActivityPoller() { }) s.waitForDeploymentDataPropagation(tv, versionStatusInactive, false, tqTypeWf) - override := tv.VersioningOverridePinned(s.useV32) + override := tv.VersioningOverridePinned() s.startWorkflow(tv, override) s.WaitForChannel(ctx, wftCompleted) @@ -770,7 +724,7 @@ func (s *Versioning3Suite) TestSearchByUsedVersion() { }) s.waitForDeploymentDataPropagation(tv, versionStatusInactive, false, tqTypeWf) - s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32)) + s.startWorkflow(tv, tv.VersioningOverridePinned()) <-wftCompleted s.verifyVersioningSAs(tv, vbPinned, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, tv) @@ -871,13 +825,14 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_SuccessfulUpdate_TransitionsToNe 6 WorkflowTaskStarted `, task.History) - // Verify that events from the speculative task are *not* written to the workflow history before being processed by the poller events := s.GetHistory(s.Namespace().String(), execution) s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted `, events) // VersioningInfo should not have changed before the update has been processed by the poller. @@ -897,7 +852,7 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_SuccessfulUpdate_TransitionsToNe }) updateResult := <-updateResultCh - s.EqualValues("success-result-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) + s.Equal("success-result-of-"+tv2.UpdateID(), testcore.DecodeString(s.T(), updateResult.GetOutcome().GetSuccess())) // Verify that events from the speculative task are written to the history since the update was accepted events := s.GetHistory(s.Namespace().String(), execution) @@ -918,7 +873,7 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_SuccessfulUpdate_TransitionsToNe Namespace: s.Namespace().String(), Execution: execution, }) - s.Nil(err) + s.NoError(err) s.NotNil(describeCall) // Since the poller accepted the update, the Worker Deployment Version that completed the last workflow task @@ -957,13 +912,14 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_FailedUpdate_DoesNotTransitionTo 6 WorkflowTaskStarted `, task.History) - // Verify that events from the speculative task are *not* written to the workflow history before being processed by the poller events := s.GetHistory(s.Namespace().String(), execution) s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted 4 WorkflowTaskCompleted + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted `, events) // VersioningInfo should not have changed before the update has been processed by the poller. @@ -1315,20 +1271,20 @@ func (s *Versioning3Suite) testUnpinnedWorkflowWithRamp(toUnversioned bool) { numTests := 50 counter := make(map[string]int) runs := make([]sdkclient.WorkflowRun, numTests) - for i := 0; i < numTests; i++ { + for i := range numTests { run, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tv1.TaskQueue().GetName()}, "wf") s.NoError(err) runs[i] = run } - for i := 0; i < numTests; i++ { + for i := range numTests { var out string s.NoError(runs[i].Get(ctx, &out)) counter[out]++ } // both versions should've got executions - s.Greater(counter["v1"], 0) - s.Greater(counter["v2"], 0) + s.Positive(counter["v1"]) + s.Positive(counter["v2"]) s.Equal(numTests, counter["v1"]+counter["v2"]) } @@ -1358,17 +1314,13 @@ func (s *Versioning3Suite) testTransitionFromWft(sticky bool, toUnversioned bool s.warmUpSticky(tv1) } - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv1, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 1, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf, tqTypeAct) runID := s.startWorkflow(tv1, nil) s.pollWftAndHandle(tv1, false, nil, @@ -1392,15 +1344,11 @@ func (s *Versioning3Suite) testTransitionFromWft(sticky bool, toUnversioned bool if toUnversioned { // unset A as current - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: nil, - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv1, false, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: nil, + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 2, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{}, []string{}, tqTypeWf, tqTypeAct) s.unversionedPollWftAndHandle(tv1, false, nil, func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { @@ -1413,19 +1361,15 @@ func (s *Versioning3Suite) testTransitionFromWft(sticky bool, toUnversioned bool } else { // Set B as the current deployment - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }, tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv2, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 2, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }, tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + }}, []string{}, tqTypeWf, tqTypeAct) s.pollWftAndHandle(tv2, false, nil, func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { @@ -1484,17 +1428,13 @@ func (s *Versioning3Suite) testDoubleTransition(unversionedSrc bool, signal bool if !unversionedSrc { // sourceV is v1, set current version to it - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv1, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 1, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf, tqTypeAct) } s.doPollWftAndHandle(tv1, !unversionedSrc, false, nil, @@ -1511,21 +1451,17 @@ func (s *Versioning3Suite) testDoubleTransition(unversionedSrc bool, signal bool } // set current version to v2 - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }, tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv2, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 2, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }, tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + }}, []string{}, tqTypeWf, tqTypeAct) // poll activity from v2 worker, this should start a transition but should not immediately start the activity. - go s.idlePollActivity(tv2, true, time.Minute, "v2 worker should not receive the activity") + go s.idlePollActivity(context.Background(), tv2, true, time.Minute, "v2 worker should not receive the activity") s.EventuallyWithT(func(t *assert.CollectT) { dwf, err := s.FrontendClient().DescribeWorkflowExecution( @@ -1543,29 +1479,21 @@ func (s *Versioning3Suite) testDoubleTransition(unversionedSrc bool, signal bool // Back to sourceV if unversionedSrc { - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: nil, - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 3, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv2, false, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: nil, + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 3, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{}, []string{}, tqTypeWf, tqTypeAct) } else { - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 3, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }, tv2.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv1, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 3, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }, tv2.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + }}, []string{}, tqTypeWf, tqTypeAct) } // Now poll for wf task from sourceV while there is a transition to v2 @@ -1587,19 +1515,15 @@ func (s *Versioning3Suite) testDoubleTransition(unversionedSrc bool, signal bool }) // Set v2 as the current version again - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 4, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }, tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv2, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 4, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }, tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + }}, []string{}, tqTypeWf, tqTypeAct) s.pollWftAndHandle(tv2, false, nil, func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { @@ -1635,35 +1559,27 @@ func (s *Versioning3Suite) nexusTaskStaysOnCurrentDeployment() { } // current deployment is -> tv1 - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeNexus) - } else { - s.updateTaskQueueDeploymentData(tv1, true, 0, false, 0, tqTypeNexus) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 1, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeNexus) // local poller with deployment A receives task s.pollAndDispatchNexusTask(tv1, nexusRequest) // current deployment is now -> tv2 - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }, tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - }}, []string{}, tqTypeNexus) - } else { - s.updateTaskQueueDeploymentData(tv2, true, 0, false, 0, tqTypeNexus) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 2, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }, tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + }}, []string{}, tqTypeNexus) // Pollers of tv1 are there but should not get any task go s.idlePollNexus(tv1, true, ver3MinPollTime, "nexus task should not go to the old deployment") @@ -1696,17 +1612,13 @@ func (s *Versioning3Suite) TestEagerActivity() { s.OverrideDynamicConfig(dynamicconfig.EnableActivityEagerExecution, true) tv := testvars.New(s) - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 1, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf, tqTypeAct) s.startWorkflow(tv, nil) poller, resp := s.pollWftAndHandle(tv, false, nil, @@ -1715,11 +1627,17 @@ func (s *Versioning3Suite) TestEagerActivity() { s.verifyWorkflowVersioning(s.Assertions, tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition()) resp := respondWftWithActivities(tv, tv, true, vbUnpinned, "5") resp.Commands[0].GetScheduleActivityTaskCommandAttributes().RequestEagerExecution = true + resp.Commands[0].GetScheduleActivityTaskCommandAttributes().Priority = &commonpb.Priority{ + FairnessKey: "fairness-key", + PriorityKey: 2, + } return resp, nil }) s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil) s.NotEmpty(resp.GetActivityTasks()) + s.Equal("fairness-key", resp.GetActivityTasks()[0].GetPriority().GetFairnessKey()) + s.EqualValues(2, resp.GetActivityTasks()[0].GetPriority().GetPriorityKey()) _, err := poller.HandleActivityTask(tv, resp.GetActivityTasks()[0], func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) { @@ -1759,7 +1677,7 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) { // 8. WFT completes and the transition completes. // 9. All the 3 remaining activities are now dispatched and completed. - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() tv1 := testvars.New(s).WithBuildIDNumber(1) @@ -1768,17 +1686,13 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) { s.warmUpSticky(tv1) } - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv1, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv1, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv1.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 1, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf, tqTypeAct) runID := s.startWorkflow(tv1, nil) s.pollWftAndHandle(tv1, false, nil, @@ -1807,7 +1721,11 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) { s.Logger.Info(fmt.Sprintf("Activity 1 started ID: %s", task.ActivityId)) close(act1Started) // block until the transition WFT starts - <-transitionStarted + select { + case <-transitionStarted: + case <-ctx.Done(): + return nil, fmt.Errorf("context expired waiting for transitionStarted in act1: %w", ctx.Err()) + } // 6. the 1st act completes during transition s.Logger.Info(fmt.Sprintf("Activity 1 completed ID: %s", task.ActivityId)) return respondActivity(), nil @@ -1820,7 +1738,11 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) { s.Logger.Info(fmt.Sprintf("Activity 2 started ID: %s", task.ActivityId)) close(act2Started) // block until the transition WFT starts - <-transitionStarted + select { + case <-transitionStarted: + case <-ctx.Done(): + return nil, fmt.Errorf("context expired waiting for transitionStarted in act2: %w", ctx.Err()) + } // 7. 2nd activity fails. Respond with error so it is retried. s.Logger.Info(fmt.Sprintf("Activity 2 failed ID: %s", task.ActivityId)) return nil, errors.New("intentional activity failure") @@ -1830,19 +1752,15 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) { s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil) // 2. Set d2 as the current deployment - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }, tv1.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - }}, []string{}, tqTypeWf, tqTypeAct) - } else { - s.updateTaskQueueDeploymentData(tv2, true, 0, false, 0, tqTypeWf, tqTypeAct) - } + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 2, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }, tv1.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + }}, []string{}, tqTypeWf, tqTypeAct) // Although updateTaskQueueDeploymentData waits for deployment data to reach the TQs, backlogged // tasks might still be waiting behind the old deployment's poll channel. Partition manage should // immediately react to the deployment data changes, but there still is a race possible and the @@ -1850,7 +1768,7 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) { time.Sleep(time.Millisecond * 200) //nolint:forbidigo // Pollers of d1 are there, but should not get any task - go s.idlePollActivity(tv1, true, ver3MinPollTime, "activities should not go to the old deployment") + go s.idlePollActivity(ctx, tv1, true, ver3MinPollTime, "activities should not go to the old deployment") go func() { for i := 2; i <= 4; i++ { @@ -1877,8 +1795,16 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) { close(transitionStarted) s.Logger.Info("Transition wft started") // 8. Complete the transition after act1 completes and act2's first attempt fails. - <-act1Completed - <-act2Failed + select { + case <-act1Completed: + case <-ctx.Done(): + s.FailNow("context expired waiting for act1 to complete") + } + select { + case <-act2Failed: + case <-ctx.Done(): + s.FailNow("context expired waiting for act2 to fail") + } transitionCompleted.Store(true) s.Logger.Info("Transition wft completed") return respondEmptyWft(tv2, sticky, vbUnpinned), nil @@ -1926,30 +1852,23 @@ func (s *Versioning3Suite) testIndependentActivity(behavior enumspb.VersioningBe tvAct := testvars.New(s).WithDeploymentSeriesNumber(2).WithTaskQueueNumber(2) // Set current deployment for each TQ - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tvWf, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvWf.DeploymentVersionString()), + s.updateTaskQueueDeploymentDataWithRoutingConfig(tvWf, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvWf.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 1, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvWf.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf) + + if !unversionedActivity { + // Different deployment here for the activity TQ. + s.updateTaskQueueDeploymentDataWithRoutingConfig(tvAct, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvAct.DeploymentVersionString()), CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvWf.DeploymentVersion().GetBuildId(): { + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvAct.DeploymentVersion().GetBuildId(): { Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf) - - if !unversionedActivity { - // Different deployment here for the activity TQ. - s.updateTaskQueueDeploymentDataWithRoutingConfig(tvAct, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tvAct.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 1, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tvAct.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeAct) - } - } else { - s.updateTaskQueueDeploymentData(tvWf, true, 0, false, 0, tqTypeWf) - if !unversionedActivity { - s.updateTaskQueueDeploymentData(tvAct, true, 0, false, 0, tqTypeAct) - } + }}, []string{}, tqTypeAct) } s.startWorkflow(tvWf, nil) @@ -2015,7 +1934,7 @@ func (s *Versioning3Suite) testChildWorkflowInheritance_ExpectInherit(crossTq bo var override *workflowpb.VersioningOverride if withOverride { - override = tv1.VersioningOverridePinned(s.useV32) + override = tv1.VersioningOverridePinned() } // This is the registered behavior which can be unpinned, but only if withOverride. We want @@ -2102,29 +2021,21 @@ func (s *Versioning3Suite) testChildWorkflowInheritance_ExpectInherit(crossTq bo close(wfStarted) // force panic if replayed // make v2 current for both parent and child and unblock the wf to start the child - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2.DeploymentVersionString()), + CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), + RevisionNumber: 2, + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf) + if crossTq { + s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2Child, &deploymentpb.RoutingConfig{ + CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2Child.DeploymentVersionString()), CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2.DeploymentVersion().GetBuildId(): { + }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2Child.DeploymentVersion().GetBuildId(): { Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, }}, []string{}, tqTypeWf) - } else { - s.updateTaskQueueDeploymentData(tv2, true, 0, false, 0, tqTypeWf) - } - if crossTq { - if s.useNewDeploymentData { - s.updateTaskQueueDeploymentDataWithRoutingConfig(tv2Child, &deploymentpb.RoutingConfig{ - CurrentDeploymentVersion: worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(tv2Child.DeploymentVersionString()), - CurrentVersionChangedTime: timestamp.TimePtr(time.Now()), - RevisionNumber: 2, - }, map[string]*deploymentspb.WorkerDeploymentVersionData{tv2Child.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf) - } else { - s.updateTaskQueueDeploymentData(tv2Child, true, 0, false, 0, tqTypeWf) - } } currentChanged <- struct{}{} @@ -2445,8 +2356,7 @@ func (s *Versioning3Suite) testPinnedCaNUpgradeOnCaN(normalTask, speculativeTask s.verifySpeculativeTask(execution) } else if transientTask { s.verifyTransientTask(task) - // Get events from server-side history instead of task.History.Events, because task.History.Events has an - // extra started event with the CaN suggestion in the transient task case + // Get events from server-side history, this includes transient events. historyEvents = s.GetHistory(s.Namespace().String(), execution) } @@ -2456,19 +2366,32 @@ func (s *Versioning3Suite) testPinnedCaNUpgradeOnCaN(normalTask, speculativeTask wfTaskStartedEvents = append(wfTaskStartedEvents, event) } } - if enableSendTargetVersionChanged { - // Verify ContinueAsNewSuggested and reasons were sent on the last WFT started event (but not the earlier ones). - s.Greater(len(wfTaskStartedEvents), 2) // make sure there are at least 2 WFT started events + if enableSendTargetVersionChanged && !pinnedOverride { + // Verify TargetWorkerDeploymentVersionChanged was sent on WFT started events after deployment change. + // Events BEFORE deployment change (events 3, 7) should NOT have the flag. + // Events AFTER deployment change (all subsequent WFTs) SHOULD have the flag, regardless of success/failure. + // The flag is recomputed on every WFT, so both failed attempts and retries will have it if conditions persist. + s.Greater(len(wfTaskStartedEvents), 2) // make sure there are at least 3 WFT started events + + // In this test, deployment is changed after event 7 (`s.setCurrentDeployment(tv2)`). + // So the first 2 WFT started events should NOT have the flag, + // and all subsequent events SHOULD have the flag. + eventsBeforeDeploymentChange := 2 // Events 3 and 7 + for i, event := range wfTaskStartedEvents { attr := event.GetWorkflowTaskStartedEventAttributes() - if i == len(wfTaskStartedEvents)-1 { // last event + if i < eventsBeforeDeploymentChange { + // Events before deployment change should NOT have the flag s.False(attr.GetSuggestContinueAsNew()) s.Require().Empty(attr.GetSuggestContinueAsNewReasons()) - s.True(attr.GetTargetWorkerDeploymentVersionChanged()) - } else { // earlier events + s.False(attr.GetTargetWorkerDeploymentVersionChanged(), + "Event %d should not have flag (before deployment change)", event.GetEventId()) + } else { + // Events after deployment change SHOULD have the flag (including failed attempts and transient retries) s.False(attr.GetSuggestContinueAsNew()) s.Require().Empty(attr.GetSuggestContinueAsNewReasons()) - s.False(attr.GetTargetWorkerDeploymentVersionChanged()) + s.True(attr.GetTargetWorkerDeploymentVersionChanged(), + "Event %d should have flag (after deployment change)", event.GetEventId()) } } } else { @@ -2578,7 +2501,6 @@ func (s *Versioning3Suite) triggerTransientWFT(ctx context.Context, tv *testvars // Verify this is a speculative task - events not yet in persisted history func (s *Versioning3Suite) verifySpeculativeTask(execution *commonpb.WorkflowExecution) { - // use history events instead of task events because some of the task.History.Events aren't persisted yet events := s.GetHistory(s.Namespace().String(), execution) s.EqualHistoryEvents(` 1 WorkflowExecutionStarted @@ -2589,6 +2511,8 @@ func (s *Versioning3Suite) verifySpeculativeTask(execution *commonpb.WorkflowExe 6 WorkflowTaskScheduled 7 WorkflowTaskStarted 8 WorkflowTaskCompleted + 9 WorkflowTaskScheduled + 10 WorkflowTaskStarted `, events) } @@ -2911,13 +2835,9 @@ func (s *Versioning3Suite) TestDescribeTaskQueueVersioningInfo() { RevisionNumber: revisionNumber, } - if s.useNewDeploymentData { - s.syncTaskQueueDeploymentDataWithRoutingConfig(tv, newRoutingConfig, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeWf) - } else { - s.syncTaskQueueDeploymentData(tv, false, 20, false, t1, tqTypeWf) - } + s.syncTaskQueueDeploymentDataWithRoutingConfig(tv, newRoutingConfig, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeWf) wfInfo, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: tv.TaskQueue(), @@ -2939,13 +2859,9 @@ func (s *Versioning3Suite) TestDescribeTaskQueueVersioningInfo() { CurrentVersionChangedTime: timestamp.TimePtr(t1), RevisionNumber: revisionNumber, } - if s.useNewDeploymentData { - s.syncTaskQueueDeploymentDataWithRoutingConfig(tv, newRoutingConfig, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeAct) - } else { - s.syncTaskQueueDeploymentData(tv, true, 0, false, t1, tqTypeAct) - } + s.syncTaskQueueDeploymentDataWithRoutingConfig(tv, newRoutingConfig, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeAct) actInfo, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{ Namespace: s.Namespace().String(), @@ -2969,13 +2885,9 @@ func (s *Versioning3Suite) TestDescribeTaskQueueVersioningInfo() { RampingVersionPercentageChangedTime: timestamp.TimePtr(t2), RevisionNumber: 2, } - if s.useNewDeploymentData { - s.syncTaskQueueDeploymentDataWithRoutingConfig(tv, newRoutingConfig, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { - Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, - }}, []string{}, tqTypeAct) - } else { - s.syncTaskQueueDeploymentData(tv, false, 10, true, t2, tqTypeAct) - } + s.syncTaskQueueDeploymentDataWithRoutingConfig(tv, newRoutingConfig, map[string]*deploymentspb.WorkerDeploymentVersionData{tv.DeploymentVersion().GetBuildId(): { + Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, + }}, []string{}, tqTypeAct) s.waitForDeploymentDataPropagation(tv, versionStatusNil, true, tqTypeAct) actInfo, err = s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{ @@ -2995,9 +2907,6 @@ func (s *Versioning3Suite) TestDescribeTaskQueueVersioningInfo() { } func (s *Versioning3Suite) TestSyncDeploymentUserDataWithRoutingConfig_Update() { - if s.useNewDeploymentData == false { - s.T().Skip() - } tv := testvars.New(s) data := s.getTaskQueueDeploymentData(tv, tqTypeAct) @@ -3173,96 +3082,6 @@ func (s *Versioning3Suite) TestSyncDeploymentUserDataWithRoutingConfig_Update() } -func (s *Versioning3Suite) TestSyncDeploymentUserData_Update() { - if s.useNewDeploymentData == true { - s.T().Skip() - } - tv := testvars.New(s) - - data := s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.Nil(data) - data = s.getTaskQueueDeploymentData(tv, tqTypeWf) - s.Nil(data) - - t1 := time.Now() - tv1 := tv.WithBuildIDNumber(1) - - s.syncTaskQueueDeploymentData(tv1, true, 0, false, t1, tqTypeAct) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv1.DeploymentVersion(), CurrentSinceTime: timestamp.TimePtr(t1), RoutingUpdateTime: timestamp.TimePtr(t1)}, - }}, data) - data = s.getTaskQueueDeploymentData(tv, tqTypeWf) - s.Nil(data) - - // Changing things with an older timestamp should not have effect. - t0 := t1.Add(-time.Second) - s.syncTaskQueueDeploymentData(tv1, false, 0, false, t0, tqTypeAct) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv1.DeploymentVersion(), CurrentSinceTime: timestamp.TimePtr(t1), RoutingUpdateTime: timestamp.TimePtr(t1)}, - }}, data) - - // Changing things with a newer timestamp should apply - t2 := t1.Add(time.Second) - s.syncTaskQueueDeploymentData(tv1, false, 20, false, t2, tqTypeAct) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv1.DeploymentVersion(), CurrentSinceTime: nil, RampingSinceTime: timestamp.TimePtr(t2), RampPercentage: 20, RoutingUpdateTime: timestamp.TimePtr(t2)}, - }}, data) - - // Add another version, this time to both tq types - tv2 := tv.WithBuildIDNumber(2) - s.syncTaskQueueDeploymentData(tv2, false, 10, false, t1, tqTypeAct, tqTypeWf) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv1.DeploymentVersion(), CurrentSinceTime: nil, RampingSinceTime: timestamp.TimePtr(t2), RampPercentage: 20, RoutingUpdateTime: timestamp.TimePtr(t2)}, - {Version: tv2.DeploymentVersion(), CurrentSinceTime: nil, RampingSinceTime: timestamp.TimePtr(t1), RampPercentage: 10, RoutingUpdateTime: timestamp.TimePtr(t1)}, - }}, data) - data = s.getTaskQueueDeploymentData(tv, tqTypeWf) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv2.DeploymentVersion(), CurrentSinceTime: nil, RampingSinceTime: timestamp.TimePtr(t1), RampPercentage: 10, RoutingUpdateTime: timestamp.TimePtr(t1)}, - }}, data) - - // Make v2 current - s.syncTaskQueueDeploymentData(tv2, true, 0, false, t2, tqTypeAct) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv1.DeploymentVersion(), CurrentSinceTime: nil, RampingSinceTime: timestamp.TimePtr(t2), RampPercentage: 20, RoutingUpdateTime: timestamp.TimePtr(t2)}, - {Version: tv2.DeploymentVersion(), CurrentSinceTime: timestamp.TimePtr(t2), RoutingUpdateTime: timestamp.TimePtr(t2)}, - }}, data) - - // Forget v1 - s.forgetTaskQueueDeploymentVersion(tv1, tqTypeAct, false) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv2.DeploymentVersion(), CurrentSinceTime: timestamp.TimePtr(t2), RoutingUpdateTime: timestamp.TimePtr(t2)}, - }}, data) - - // Forget v1 again should be a noop - s.forgetTaskQueueDeploymentVersion(tv1, tqTypeAct, false) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv2.DeploymentVersion(), CurrentSinceTime: timestamp.TimePtr(t2), RoutingUpdateTime: timestamp.TimePtr(t2)}, - }}, data) - - // Ramp unversioned - s.syncTaskQueueDeploymentData(tv2, false, 90, true, t2, tqTypeAct) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{Versions: []*deploymentspb.DeploymentVersionData{ - {Version: tv2.DeploymentVersion(), CurrentSinceTime: timestamp.TimePtr(t2), RoutingUpdateTime: timestamp.TimePtr(t2)}, - }, - UnversionedRampData: &deploymentspb.DeploymentVersionData{RampingSinceTime: timestamp.TimePtr(t2), RampPercentage: 90, RoutingUpdateTime: timestamp.TimePtr(t2)}, - }, data) - - // Forget v2 - s.forgetTaskQueueDeploymentVersion(tv2, tqTypeAct, false) - data = s.getTaskQueueDeploymentData(tv, tqTypeAct) - s.ProtoEqual(&persistencespb.DeploymentData{ - UnversionedRampData: &deploymentspb.DeploymentVersionData{RampingSinceTime: timestamp.TimePtr(t2), RampPercentage: 90, RoutingUpdateTime: timestamp.TimePtr(t2)}, - }, data) -} - func (s *Versioning3Suite) setCurrentDeployment(tv *testvars.TestVars) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() @@ -3271,11 +3090,7 @@ func (s *Versioning3Suite) setCurrentDeployment(tv *testvars.TestVars) { Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), } - if s.useV32 { - req.BuildId = tv.BuildID() - } else { - req.Version = tv.DeploymentVersionString() //nolint:staticcheck // SA1019: worker versioning v0.31 - } + req.BuildId = tv.BuildID() _, err := s.FrontendClient().SetWorkerDeploymentCurrentVersion(ctx, req) var notFound *serviceerror.NotFound if errors.As(err, ¬Found) || (err != nil && strings.Contains(err.Error(), serviceerror.NewFailedPreconditionf(workerdeployment.ErrCurrentVersionDoesNotHaveAllTaskQueues, tv.DeploymentVersionStringV32()).Error())) { @@ -3317,10 +3132,8 @@ func (s *Versioning3Suite) setRampingDeployment( ) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - v := tv.DeploymentVersionString() bid := tv.BuildID() if rampUnversioned { - v = "__unversioned__" bid = "" } @@ -3330,11 +3143,7 @@ func (s *Versioning3Suite) setRampingDeployment( DeploymentName: tv.DeploymentSeries(), Percentage: percentage, } - if s.useV32 { - req.BuildId = bid - } else { - req.Version = v //nolint:staticcheck // SA1019: worker versioning v0.31 - } + req.BuildId = bid _, err := s.FrontendClient().SetWorkerDeploymentRampingVersion(ctx, req) var notFound *serviceerror.NotFound if errors.As(err, ¬Found) || (err != nil && strings.Contains(err.Error(), serviceerror.NewFailedPreconditionf(workerdeployment.ErrRampingVersionDoesNotHaveAllTaskQueues, tv.DeploymentVersionStringV32()).Error())) { @@ -3466,7 +3275,7 @@ func (s *Versioning3Suite) rollbackTaskQueueToVersion( tv *testvars.TestVars, ) { - cleanup := s.InjectHook(testhooks.MatchingIgnoreRoutingConfigRevisionCheck, true) + cleanup := s.InjectHook(testhooks.NewHook(testhooks.MatchingIgnoreRoutingConfigRevisionCheck, true)) defer cleanup() rc := &deploymentpb.RoutingConfig{ @@ -3621,28 +3430,13 @@ func (s *Versioning3Suite) verifyWorkflowVersioning( )) } - if s.useV32 { - // v0.32 override - a.Equal(override.GetAutoUpgrade(), versioningInfo.GetVersioningOverride().GetAutoUpgrade()) - a.Equal(override.GetPinned().GetVersion().GetBuildId(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetBuildId()) - a.Equal(override.GetPinned().GetVersion().GetDeploymentName(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetDeploymentName()) - a.Equal(override.GetPinned().GetBehavior(), versioningInfo.GetVersioningOverride().GetPinned().GetBehavior()) - if worker_versioning.OverrideIsPinned(override) { - a.Equal(override.GetPinned().GetVersion().GetDeploymentName(), dwf.WorkflowExecutionInfo.GetWorkerDeploymentName()) - } - } else { - // v0.31 override - a.Equal(override.GetBehavior().String(), versioningInfo.GetVersioningOverride().GetBehavior().String()) //nolint:staticcheck // SA1019: worker versioning v0.31 - if actualOverrideDeployment := versioningInfo.GetVersioningOverride().GetPinnedVersion(); override.GetPinnedVersion() != actualOverrideDeployment { //nolint:staticcheck // SA1019: worker versioning v0.31 - a.Fail(fmt.Sprintf("pinned override mismatch. expected: {%s}, actual: {%s}", - override.GetPinnedVersion(), //nolint:staticcheck // SA1019: worker versioning v0.31 - actualOverrideDeployment, - )) - } - if worker_versioning.OverrideIsPinned(override) { - d, _ := worker_versioning.WorkerDeploymentVersionFromStringV31(override.GetPinnedVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31 - a.Equal(d.GetDeploymentName(), dwf.WorkflowExecutionInfo.GetWorkerDeploymentName()) - } + // v0.32 override + a.Equal(override.GetAutoUpgrade(), versioningInfo.GetVersioningOverride().GetAutoUpgrade()) + a.Equal(override.GetPinned().GetVersion().GetBuildId(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetBuildId()) + a.Equal(override.GetPinned().GetVersion().GetDeploymentName(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetDeploymentName()) + a.Equal(override.GetPinned().GetBehavior(), versioningInfo.GetVersioningOverride().GetPinned().GetBehavior()) + if worker_versioning.OverrideIsPinned(override) { + a.Equal(override.GetPinned().GetVersion().GetDeploymentName(), dwf.WorkflowExecutionInfo.GetWorkerDeploymentName()) } if !versioningInfo.GetVersionTransition().Equal(transition) { @@ -3844,25 +3638,25 @@ func (s *Versioning3Suite) doPollWftAndHandle( handler func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error), ) (*taskpoller.TaskPoller, *workflowservice.RespondWorkflowTaskCompletedResponse) { poller := taskpoller.New(s.T(), s.FrontendClient(), s.Namespace().String()) - f := func() *workflowservice.RespondWorkflowTaskCompletedResponse { + f := func() (*workflowservice.RespondWorkflowTaskCompletedResponse, error) { tq := tv.TaskQueue() if sticky { tq = tv.StickyTaskQueue() } - resp, err := poller.PollWorkflowTask( + return poller.PollWorkflowTask( &workflowservice.PollWorkflowTaskQueueRequest{ DeploymentOptions: tv.WorkerDeploymentOptions(versioned), TaskQueue: tq, }, ).HandleTask(tv, handler, taskpoller.WithTimeout(30*time.Second)) - s.NoError(err) - return resp } if async == nil { - return poller, f() + resp, err := f() + s.NoError(err) + return poller, resp } else { go func() { - f() + _, _ = f() // errors are surfaced via test context timeout on WaitForChannel close(async) }() } @@ -3876,25 +3670,25 @@ func (s *Versioning3Suite) pollWftAndHandleQueries( handler func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondQueryTaskCompletedRequest, error), ) (*taskpoller.TaskPoller, *workflowservice.RespondQueryTaskCompletedResponse) { poller := taskpoller.New(s.T(), s.FrontendClient(), s.Namespace().String()) - f := func() *workflowservice.RespondQueryTaskCompletedResponse { + f := func() (*workflowservice.RespondQueryTaskCompletedResponse, error) { tq := tv.TaskQueue() if sticky { tq = tv.StickyTaskQueue() } - resp, err := poller.PollWorkflowTask( + return poller.PollWorkflowTask( &workflowservice.PollWorkflowTaskQueueRequest{ DeploymentOptions: tv.WorkerDeploymentOptions(true), TaskQueue: tq, }, ).HandleLegacyQuery(tv, handler) - s.NoError(err) - return resp } if async == nil { - return poller, f() + resp, err := f() + s.NoError(err) + return poller, resp } go func() { - f() + _, _ = f() // errors are surfaced via test context timeout on WaitForChannel close(async) }() return nil, nil @@ -3907,25 +3701,25 @@ func (s *Versioning3Suite) pollNexusTaskAndHandle( handler func(task *workflowservice.PollNexusTaskQueueResponse) (*workflowservice.RespondNexusTaskCompletedRequest, error), ) (*taskpoller.TaskPoller, *workflowservice.RespondNexusTaskCompletedResponse) { poller := taskpoller.New(s.T(), s.FrontendClient(), s.Namespace().String()) - f := func() *workflowservice.RespondNexusTaskCompletedResponse { + f := func() (*workflowservice.RespondNexusTaskCompletedResponse, error) { tq := tv.TaskQueue() if sticky { tq = tv.StickyTaskQueue() } - resp, err := poller.PollNexusTask( + return poller.PollNexusTask( &workflowservice.PollNexusTaskQueueRequest{ DeploymentOptions: tv.WorkerDeploymentOptions(true), TaskQueue: tq, }, ).HandleTask(tv, handler, taskpoller.WithTimeout(10*time.Second)) - s.NoError(err) - return resp } if async == nil { - return poller, f() + resp, err := f() + s.NoError(err) + return poller, resp } go func() { - f() + _, _ = f() // errors are surfaced via test context timeout on WaitForChannel close(async) }() return nil, nil @@ -3954,19 +3748,19 @@ func (s *Versioning3Suite) doPollActivityAndHandle( handler func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error), ) { poller := taskpoller.New(s.T(), s.FrontendClient(), s.Namespace().String()) - f := func() { + f := func() error { _, err := poller.PollActivityTask( &workflowservice.PollActivityTaskQueueRequest{ DeploymentOptions: tv.WorkerDeploymentOptions(versioned), }, ).HandleTask(tv, handler, taskpoller.WithTimeout(time.Minute)) - s.NoError(err) + return err } if async == nil { - f() + s.NoError(f()) } else { go func() { - f() + _ = f() // errors are surfaced via test context timeout on WaitForChannel close(async) }() } @@ -4020,6 +3814,7 @@ func (s *Versioning3Suite) idlePollUnversionedActivity( } func (s *Versioning3Suite) idlePollActivity( + ctx context.Context, tv *testvars.TestVars, versioned bool, timeout time.Duration, @@ -4035,11 +3830,13 @@ func (s *Versioning3Suite) idlePollActivity( func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) { if task != nil { s.Logger.Error(fmt.Sprintf("Unexpected activity task received, ID: %s", task.ActivityId)) - s.Fail(unexpectedTaskMessage) + a := s.Assert() + a.Fail(unexpectedTaskMessage) } return nil, nil }, taskpoller.WithTimeout(timeout), + taskpoller.WithContext(ctx), ) } @@ -4119,7 +3916,7 @@ func (s *Versioning3Suite) waitForDeploymentDataPropagation( tp enumspb.TaskQueueType } remaining := make(map[partAndType]struct{}) - for i := 0; i < partitionCount; i++ { + for i := range partitionCount { for _, tqt := range tqTypes { remaining[partAndType{i, tqt}] = struct{}{} } @@ -4243,14 +4040,14 @@ func (s *Versioning3Suite) verifyVersioningSAs( Query: query, }) a := assert.New(t) - a.Nil(err) - a.Greater(len(resp.GetExecutions()), 0) + a.NoError(err) + a.NotEmpty(resp.GetExecutions()) if a.NotEmpty(resp.GetExecutions()) { w := resp.GetExecutions()[0] if behavior == vbPinned { payload, ok := w.GetSearchAttributes().GetIndexedFields()["BuildIds"] a.True(ok) - searchAttrAny, err := searchattribute.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) + searchAttrAny, err := sadefs.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) a.NoError(err) var searchAttr []string if searchAttrAny != nil { @@ -4265,7 +4062,7 @@ func (s *Versioning3Suite) verifyVersioningSAs( // Validate TemporalUsedWorkerDeploymentVersions search attribute versionPayload, ok := w.GetSearchAttributes().GetIndexedFields()["TemporalUsedWorkerDeploymentVersions"] a.True(ok) - versionAttrAny, err := searchattribute.DecodeValue(versionPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) + versionAttrAny, err := sadefs.DecodeValue(versionPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) a.NoError(err) var versionAttr []string if versionAttrAny != nil { @@ -4282,10 +4079,6 @@ func (s *Versioning3Suite) verifyVersioningSAs( } func (s *Versioning3Suite) TestAutoUpgradeWorkflows_NoBouncingBetweenVersions() { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) @@ -4339,9 +4132,6 @@ func (s *Versioning3Suite) TestAutoUpgradeWorkflows_NoBouncingBetweenVersions() } func (s *Versioning3Suite) TestWorkflowTQLags_DependentActivityStartsTransition() { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } /* The aim of this test is to show the following does not occur when using revisionNumber mechanics: - If the workflow TQ lags behind the activity TQ, with respect to the current version of a deployment, the activity should not be @@ -4433,9 +4223,6 @@ func (s *Versioning3Suite) TestWorkflowTQLags_DependentActivityStartsTransition( } func (s *Versioning3Suite) TestActivityTQLags_DependentActivityCompletesOnTheNewVersion() { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } /* The aim of this test is to show the following does not occur when using revisionNumber mechanics: - If the activity TQ lags behind the workflow TQ, with respect to the current version of a deployment, the activity should not be @@ -4515,7 +4302,7 @@ func (s *Versioning3Suite) TestActivityTQLags_DependentActivityCompletesOnTheNew // Start an idle activity poller on v0. This poller should not receive any activity tasks //nolint:testifylint - go s.idlePollActivity(tv0, true, ver3MinPollTime, "activity should not go to the old deployment") + go s.idlePollActivity(ctx, tv0, true, ver3MinPollTime, "activity should not go to the old deployment") // Start a poller on v1 activityTaskCh := make(chan struct{}, 1) @@ -4538,10 +4325,6 @@ func (s *Versioning3Suite) TestActivityTQLags_DependentActivityCompletesOnTheNew // the test is present to show that revision number mechanics work as expected even when the task-queue // partitions have a more updated view of the current version than the mutable state of a workflow. func (s *Versioning3Suite) TestChildStartsWithParentRevision_SameTQ_TQAhead() { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) @@ -4670,10 +4453,6 @@ func (s *Versioning3Suite) TestVersionedPoller_FailsWithEmptyNormalName() { } func (s *Versioning3Suite) TestChildStartsWithParentRevision_SameTQ_TQLags() { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) @@ -4770,10 +4549,6 @@ func (s *Versioning3Suite) TestChildStartsWithParentRevision_SameTQ_TQLags() { // TestChildStartsWithNoInheritedAutoUpgradeInfo_CrossTQ demonstrates that a child workflow of an AutoUpgrade parent, not sharing // the same task queue, starts with no inherited auto upgrade info. func (s *Versioning3Suite) TestChildStartsWithNoInheritedAutoUpgradeInfo_CrossTQ() { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) @@ -4859,10 +4634,6 @@ func (s *Versioning3Suite) TestChildStartsWithNoInheritedAutoUpgradeInfo_CrossTQ // Tests testing continue-as-new of an AutoUpgrade workflow using revision number mechanics. func (s *Versioning3Suite) TestContinueAsNewOfAutoUpgradeWorkflow_RevisionNumberMechanics() { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) @@ -4954,10 +4725,6 @@ func (s *Versioning3Suite) TestContinueAsNewOfAutoUpgradeWorkflow_RevisionNumber // If testContinueAsNew is true, tests a ContinueAsNew followed by retry; otherwise tests a direct retry of a workflow. // If testChildWorkflow is true, tests that a child workflow's retry doesn't bounce back (child spawned by parent with retry policy). func (s *Versioning3Suite) testRetryNoBounceBack(testContinueAsNew bool, testChildWorkflow bool) { - if !s.useRevisionNumbers { - s.T().Skip("This test is only supported on revision number mechanics") - } - s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) @@ -5555,8 +5322,12 @@ func (s *Versioning3Suite) testTransitionDuringTransientTask(withSignal bool) { // Create test vars for v1 tv1 := testvars.New(s).WithBuildIDNumber(1) - s.idlePollWorkflow(ctx, tv1, true, ver3MinPollTime, "should not get any tasks yet") - s.idlePollActivity(tv1, true, ver3MinPollTime, "should not get any tasks yet") + // We need to keep pollers until the task queues are properly registered + pollCtx, pollCtxCancel := context.WithTimeout(ctx, 60*time.Second) + go s.idlePollWorkflow(pollCtx, tv1, true, ver3MinPollTime, "should not get any tasks yet") + go s.idlePollActivity(pollCtx, tv1, true, ver3MinPollTime, "should not get any tasks yet") + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf, tqTypeAct) + pollCtxCancel() // Start the workflow execution := &commonpb.WorkflowExecution{ @@ -5606,7 +5377,7 @@ func (s *Versioning3Suite) testTransitionDuringTransientTask(withSignal bool) { } // Poll the second activity to cause transition to v1. - s.idlePollActivity(tv1, true, ver3MinPollTime, "should not get the activity because it started a transition") + s.idlePollActivity(ctx, tv1, true, ver3MinPollTime, "should not get the activity because it started a transition") s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnspecified, nil, nil, tv1.DeploymentVersionTransition()) // Print workflow describe and history @@ -5685,8 +5456,1018 @@ func (s *Versioning3Suite) testTransitionDuringTransientTask(withSignal bool) { s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil) } -func (s *Versioning3Suite) skipBeforeVersion(version workerdeployment.DeploymentWorkflowVersion) { - if s.deploymentWorkflowVersion < version { - s.T().Skipf("test supports workflow version %v and newer", version) - } +// TestPinnedCaN_NoAUOnCaN_NoInfiniteLoop tests that a pinned workflow that CAN's +// without AUTO_UPGRADE as the InitialVersioningBehavior does not get stuck in an infinite CAN loop. +func (s *Versioning3Suite) TestPinnedCaN_NoAUOnCaN_NoInfiniteLoop() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + tv2 := tv1.WithBuildIDNumber(2) + + // Start async poller for v1 that will handle the first WFT and declare pinned behavior + wftCompleted := make(chan struct{}) + s.pollWftAndHandle(tv1, false, wftCompleted, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Wait for v1 to be registered, then set it as current + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf) + s.setCurrentDeployment(tv1) + + // Start workflow — first WFT is handled by the async poller above (pinned on v1) + runID := s.startWorkflow(tv1, nil) + execution := tv1.WithRunID(runID).WorkflowExecution() + s.WaitForChannel(ctx, wftCompleted) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) + + // Register v2 poller before setting it as current + s.idlePollWorkflow(ctx, tv2, true, ver3MinPollTime, "should not get any tasks yet") + s.setCurrentDeployment(tv2) + + // Trigger WFT via signal + s.triggerNormalWFT(ctx, tv1, execution) + + // Process the WFT: verify targetWorkerDeploymentVersionChanged=true, then continue-as-new without AUTO_UPGRADE as the InitialVersioningBehavior + // so that the new run starts on v1 (pinned). + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + + historyEvents := task.History.GetEvents() + wfTaskStartedEvents := make([]*historypb.HistoryEvent, 0) + for _, event := range historyEvents { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + wfTaskStartedEvents = append(wfTaskStartedEvents, event) + } + } + s.NotEmpty(wfTaskStartedEvents) + lastStarted := wfTaskStartedEvents[len(wfTaskStartedEvents)-1] + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "expected targetWorkerDeploymentVersionChanged=true after v2 becomes current") + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ + ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv1.WorkflowType(), + TaskQueue: tv1.TaskQueue(), + Input: tv1.Any().Payloads(), + }, + }, + }, + }, + VersioningBehavior: vbPinned, + DeploymentOptions: tv1.WorkerDeploymentOptions(true), + }, nil + }) + + // New run starts on v1 (pinned). Verify targetVersionChanged=false on first WFT — loop is broken. + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + s.NotEqual(execution.RunId, task.WorkflowExecution.RunId, + "CAN should have created a new run with a different run ID") // This is purely to verify that a new run was created + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + s.False(event.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "new run should NOT have targetWorkerDeploymentVersionChanged=true (loop should be broken)") + } + } + return respondCompleteWorkflow(tv1, vbPinned), nil + }) + + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) +} + +// TestOverride_SuppressesTargetVersionChangedSignal tests that a versioning override +// suppresses the target_worker_deployment_version_changed signal. When an operator +// explicitly pins a workflow via override, routing changes should be irrelevant. +func (s *Versioning3Suite) TestOverride_SuppressesTargetVersionChangedSignal() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + tv2 := tv1.WithBuildIDNumber(2) + + // Start async poller for v1 that will handle the first WFT and declare pinned behavior + wftCompleted := make(chan struct{}) + s.pollWftAndHandle(tv1, false, wftCompleted, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Wait for v1 to be registered, then set it as current + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf) + s.setCurrentDeployment(tv1) + + // Start workflow — first WFT is handled by the async poller above (pinned on v1) + s.startWorkflow(tv1, nil) + execution := tv1.WorkflowExecution() + s.WaitForChannel(ctx, wftCompleted) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) + + // Apply pinned override to v1 via UpdateWorkflowExecutionOptions + override := s.makePinnedOverride(tv1) + _, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: execution, + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{VersioningOverride: override}, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + }) + s.NoError(err) + + // Register v2 poller before setting it as current + s.idlePollWorkflow(ctx, tv2, true, ver3MinPollTime, "should not get any tasks yet") + s.setCurrentDeployment(tv2) + + // Trigger WFT via signal + s.triggerNormalWFT(ctx, tv1, execution) + + // Verify signal=false: override suppresses the target version changed signal + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + s.False(event.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "override is active — signal should be suppressed") + } + } + return respondCompleteWorkflow(tv1, vbPinned), nil + }) +} + +// TestAutoUpgrade_SuppressesTargetVersionChangedSignal tests that an AutoUpgrade +// workflow does NOT receive targetWorkerDeploymentVersionChanged=true when the +// routing target changes. AutoUpgrade workflows will naturally transition to the +// new version on the next WFT, so signaling CaN is unnecessary. +func (s *Versioning3Suite) TestAutoUpgrade_SuppressesTargetVersionChangedSignal() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + tv2 := tv1.WithBuildIDNumber(2) + + // Start async poller for v1 that will handle the first WFT and declare AutoUpgrade behavior + wftCompleted := make(chan struct{}) + s.pollWftAndHandle(tv1, false, wftCompleted, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbUnpinned), nil + }) + + // Wait for v1 to be registered, then set it as current + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf) + s.setCurrentDeployment(tv1) + + // Start workflow — first WFT is handled by the async poller above (AutoUpgrade on v1) + runID := s.startWorkflow(tv1, nil) + execution := tv1.WithRunID(runID).WorkflowExecution() + s.WaitForChannel(ctx, wftCompleted) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil) + + // Register v2 poller before setting it as current + s.idlePollWorkflow(ctx, tv2, true, ver3MinPollTime, "should not get any tasks yet") + s.setCurrentDeployment(tv2) + + // Trigger WFT via signal + s.triggerNormalWFT(ctx, tv1, execution) + + // AutoUpgrade workflow transitions to v2, so the WFT is dispatched to v2 pollers. + // Verify signal=false: AutoUpgrade suppresses the target version changed signal. + s.pollWftAndHandle(tv2, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + s.False(event.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "AutoUpgrade workflow should NOT get targetWorkerDeploymentVersionChanged=true") + } + } + return respondCompleteWorkflow(tv2, vbUnpinned), nil + }) +} + +// TestPinnedCaN_TargetChangesAgain_SignalsTrue tests that when a pinned workflow +// does CaN (declining upgrade from v2), and then the routing target changes again +// to v3, the new run correctly receives targetWorkerDeploymentVersionChanged=true. +// This ensures condition 4 only suppresses the signal when the target is unchanged, +// not when a genuinely new target appears. +func (s *Versioning3Suite) TestPinnedCaN_TargetChangesAgain_SignalsTrue() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + tv2 := tv1.WithBuildIDNumber(2) + tv3 := tv1.WithBuildIDNumber(3) + + // Start async poller for v1 that will handle the first WFT and declare pinned behavior + wftCompleted := make(chan struct{}) + s.pollWftAndHandle(tv1, false, wftCompleted, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Wait for v1 to be registered, then set it as current + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf) + s.setCurrentDeployment(tv1) + + // Start workflow — first WFT is handled by the async poller above (pinned on v1) + runID := s.startWorkflow(tv1, nil) + execution := tv1.WithRunID(runID).WorkflowExecution() + s.WaitForChannel(ctx, wftCompleted) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) + + // Register v2 poller before setting it as current + s.idlePollWorkflow(ctx, tv2, true, ver3MinPollTime, "should not get any tasks yet") + s.setCurrentDeployment(tv2) + + // Trigger WFT via signal + s.triggerNormalWFT(ctx, tv1, execution) + + // Process the WFT: verify targetWorkerDeploymentVersionChanged=true, then CaN without AU + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + historyEvents := task.History.GetEvents() + wfTaskStartedEvents := make([]*historypb.HistoryEvent, 0) + for _, event := range historyEvents { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + wfTaskStartedEvents = append(wfTaskStartedEvents, event) + } + } + s.NotEmpty(wfTaskStartedEvents) + lastStarted := wfTaskStartedEvents[len(wfTaskStartedEvents)-1] + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "expected targetWorkerDeploymentVersionChanged=true after v2 becomes current") + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ + ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv1.WorkflowType(), + TaskQueue: tv1.TaskQueue(), + Input: tv1.Any().Payloads(), + }, + }, + }, + }, + VersioningBehavior: vbPinned, + DeploymentOptions: tv1.WorkerDeploymentOptions(true), + }, nil + }) + + // Now change current to v3 (target changes from v2 to v3) + s.idlePollWorkflow(ctx, tv3, true, ver3MinPollTime, "should not get any tasks yet") + s.setCurrentDeployment(tv3) + s.waitForDeploymentDataPropagation(tv3, versionStatusCurrent, false, tqTypeWf) + + // New run starts on v1 (pinned). Since target changed from v2→v3, + // condition 4 should NOT suppress — targetVersionChanged should be true. + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + s.NotEqual(execution.RunId, task.WorkflowExecution.RunId, + "CAN should have created a new run with a different run ID") + historyEvents := task.History.GetEvents() + wfTaskStartedEvents := make([]*historypb.HistoryEvent, 0) + for _, event := range historyEvents { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + wfTaskStartedEvents = append(wfTaskStartedEvents, event) + } + } + s.NotEmpty(wfTaskStartedEvents) + lastStarted := wfTaskStartedEvents[len(wfTaskStartedEvents)-1] + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "target changed from v2 to v3 — should signal true even though this is a CaN-inherited-pinned run") + return respondCompleteWorkflow(tv1, vbPinned), nil + }) +} + +// TestRemoveOverride_ClearsDeclinedState tests that when a workflow has a stale +// declined value from a previous CaN, and an override is set then removed, the +// declined state is cleared so the signal fires correctly. +// +// Flow: +// 1. Start pinned on v1, set v2 as current → signal → CaN without AU (decline v2) +// 2. CaN run: target=v2, declined=v2 → no signal (case 4) +// 3. Set override on the CaN run +// 4. Remove override — target is still v2 +// 5. Signal → assert true (declined should have been cleared by override) +func (s *Versioning3Suite) TestRemoveOverride_ClearsDeclinedState() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + tv2 := tv1.WithBuildIDNumber(2) + + // Start async poller for v1 that will handle the first WFT and declare pinned behavior + wftCompleted := make(chan struct{}) + s.pollWftAndHandle(tv1, false, wftCompleted, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Wait for v1 to be registered, then set it as current + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf) + s.setCurrentDeployment(tv1) + + // Start workflow — first WFT is handled by the async poller above (pinned on v1) + runID := s.startWorkflow(tv1, nil) + execution := tv1.WithRunID(runID).WorkflowExecution() + s.WaitForChannel(ctx, wftCompleted) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) + + // Set v2 as current, trigger signal + s.idlePollWorkflow(ctx, tv2, true, ver3MinPollTime, "should not get any tasks yet") + s.setCurrentDeployment(tv2) + s.triggerNormalWFT(ctx, tv1, execution) + + // Run 1: targetDeploymentVersionChanged=true → CaN without AU (decline v2) + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "expected true after v2 becomes current") + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ + ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv1.WorkflowType(), + TaskQueue: tv1.TaskQueue(), + Input: tv1.Any().Payloads(), + }, + }, + }, + }, + VersioningBehavior: vbPinned, + DeploymentOptions: tv1.WorkerDeploymentOptions(true), + }, nil + }) + + // CaN run: declined=v2, target=v2 → case 4 suppresses + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + s.NotEqual(execution.RunId, task.WorkflowExecution.RunId, + "CaN should have created a new run") + execution = task.WorkflowExecution + + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.False(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "declined=v2 == target=v2 — case 4 suppresses") + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Set override on the CaN run + override := s.makePinnedOverride(tv1) + _, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: execution, + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{VersioningOverride: override}, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + }) + s.NoError(err) + + // Trigger a WFT while override is active so case 1 fires (and should clear declined) + s.triggerNormalWFT(ctx, tv1, execution) + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.False(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "override active — signal suppressed") + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Remove the override + _, err = s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: execution, + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{VersioningOverride: nil}, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + }) + s.NoError(err) + + // Trigger WFT — target is still v2, but declined should have been cleared + s.triggerNormalWFT(ctx, tv1, execution) + + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "override removed — declined should have been cleared, signal should fire") + return respondCompleteWorkflow(tv1, vbPinned), nil + }) +} + +// TestRetryOfDeclinedCaN_SignalsOnNewTarget verifies that when a CaN'd run +// ,which declined to upgrade, fails and is retried by the server, the retry +// run inherits NotificationSuppressedTargetVersion from the original CaN +// decision. This ensures that if the target has not changed since the original +// CaN decision, the retry correctly does not set targetDeploymentVersionChanged=true +// on the WFT started event. +// +// Flow: +// 1. Start pinned workflow on v1, set v2 as current +// 2. Signal → workflow CaNs without AU (declines upgrade to v2) +// 3. CaN run starts on v1: OnStart=v2, target=v2 → does not set targetVersionChanged=true +// 4. Signal → CaN run fails → server retries +// 5. Retry run: OnStart=v2 (preserved from CaN decision), target=v2 +// 6. Assert targetDeploymentVersionChanged=false (v2 == v2, decline is preserved) +func (s *Versioning3Suite) TestRetryOfDeclinedCaN_SignalsOnNewTarget() { + s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1) + s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1) + + tv1 := testvars.New(s).WithBuildIDNumber(1) + tv2 := tv1.WithBuildIDNumber(2) + numPollers := 4 + + // Workflow: CaN without AU on first run, fail on CaN run, complete on retry. + // Uses runCount (workflow arg) to distinguish first run vs CaN run, + // and Attempt to distinguish CaN run vs its retry. + wf := func(ctx workflow.Context, runCount int) (string, error) { + info := workflow.GetInfo(ctx) + if runCount == 0 { + // First run: wait for signal then CaN without AU (decline upgrade). + workflow.GetSignalChannel(ctx, "proceed").Receive(ctx, nil) + return "", workflow.NewContinueAsNewError(ctx, "wf", 1) + } + if info.Attempt == 1 { + // CaN run (first attempt): wait for signal then fail to trigger retry. + workflow.GetSignalChannel(ctx, "proceed").Receive(ctx, nil) + return "", errors.New("explicit failure to trigger retry") + } + // Retry run (attempt > 1): just complete. + return "done", nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + w1 := worker.New(s.SdkClient(), tv1.TaskQueue().GetName(), worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + Version: tv1.SDKDeploymentVersion(), + UseVersioning: true, + }, + MaxConcurrentWorkflowTaskPollers: numPollers, + }) + w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ + Name: "wf", VersioningBehavior: workflow.VersioningBehaviorPinned, + }) + s.NoError(w1.Start()) + defer w1.Stop() + + // Set v1 as current. + s.setCurrentDeployment(tv1) + s.waitForDeploymentDataPropagation(tv1, versionStatusCurrent, false, tqTypeWf) + + // Start workflow with retry policy. + run0, err := s.SdkClient().ExecuteWorkflow(ctx, + sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().GetName(), + RetryPolicy: &temporal.RetryPolicy{ + InitialInterval: time.Second, + }, + }, + "wf", 0, + ) + s.NoError(err) + wfID := run0.GetID() + + // Wait for workflow to be running on v1. + s.Eventually(func() bool { + desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, wfID, run0.GetRunID()) + if err != nil { + return false + } + return desc.GetWorkflowExecutionInfo().GetVersioningInfo().GetDeploymentVersion().GetBuildId() == tv1.BuildID() + }, 10*time.Second, 100*time.Millisecond) + + // Set v2 as current, signal workflow to CaN without AU (decline upgrade). + s.idlePollWorkflow(ctx, tv2, true, ver3MinPollTime, "v2 idle poll") + s.setCurrentDeployment(tv2) + s.waitForDeploymentDataPropagation(tv2, versionStatusCurrent, false, tqTypeWf) + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfID, run0.GetRunID(), "proceed", nil)) + + // Wait for CaN to happen — new run on v1. + var canRunID string + s.Eventually(func() bool { + desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, wfID, "") + if err != nil { + return false + } + canRunID = desc.GetWorkflowExecutionInfo().GetExecution().GetRunId() + return canRunID != run0.GetRunID() && + desc.GetWorkflowExecutionInfo().GetVersioningInfo().GetDeploymentVersion().GetBuildId() == tv1.BuildID() + }, 10*time.Second, 100*time.Millisecond) + + // Signal CaN run to fail (triggers server retry). Target remains v2. + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfID, canRunID, "proceed", nil)) + + // Wait for CaN run to fail. + s.Eventually(func() bool { + desc, err := s.SdkClient().DescribeWorkflow(ctx, wfID, canRunID) + if err != nil { + return false + } + return desc.Status == enumspb.WORKFLOW_EXECUTION_STATUS_FAILED + }, 10*time.Second, 100*time.Millisecond) + + // Wait for retry run to complete. + var retryRunID string + s.Eventually(func() bool { + desc, err := s.SdkClient().DescribeWorkflowExecution(ctx, wfID, "") + if err != nil { + return false + } + retryRunID = desc.GetWorkflowExecutionInfo().GetExecution().GetRunId() + return retryRunID != canRunID && + desc.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 10*time.Second, 100*time.Millisecond) + + // Verify: retry run's WFT started should have targetDeploymentVersionChanged=false + // because OnStart=v2 (preserved from CaN decision) == target=v2 (still current). + retryHistory := s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ + WorkflowId: wfID, + RunId: retryRunID, + }) + foundWFTStarted := false + for _, event := range retryHistory { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + s.False(event.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "retry run should have targetDeploymentVersionChanged=false (OnStart=v2 == target=v2, decline preserved)") + foundWFTStarted = true + } + } + s.True(foundWFTStarted, "should have found at least one WFT started event in retry run") +} + +// TestPinnedCaN_RollbackResetsDeclined tests the rollback scenario: when a pinned +// workflow declines upgrade to v2 via CaN, and then the target rolls back to v1 +// (matching effective), the declined state is cleared. When v2 becomes current +// again, the signal fires again. +// +// Flow: +// 1. Start pinned workflow on v1, v1 is current +// 2. Set v2 as current → signal → CaN without AU (decline upgrade to v2) +// 3. CaN run on v1: target=v2, declined=v2 → no signal (case 4) → respond empty +// 4. Set v1 as current (rollback) → signal → target=v1, effective=v1 → no signal (case 3) +// - rollback clears declined → respond empty +// +// 5. Set v2 as current again → signal → target=v2, no declined → signal fires (assert true) +func (s *Versioning3Suite) TestPinnedCaN_RollbackResetsDeclined() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + tv2 := tv1.WithBuildIDNumber(2) + + // Start async poller for v1 that will handle the first WFT and declare pinned behavior + wftCompleted := make(chan struct{}) + s.pollWftAndHandle(tv1, false, wftCompleted, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Wait for v1 to be registered, then set it as current + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf) + s.setCurrentDeployment(tv1) + + // Start workflow — first WFT is handled by the async poller above (pinned on v1) + runID := s.startWorkflow(tv1, nil) + execution := tv1.WithRunID(runID).WorkflowExecution() + s.WaitForChannel(ctx, wftCompleted) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) + + // Register v2 poller before setting it as current + s.idlePollWorkflow(ctx, tv2, true, ver3MinPollTime, "should not get any tasks yet") + s.setCurrentDeployment(tv2) + + // Trigger WFT via signal + s.triggerNormalWFT(ctx, tv1, execution) + + // Process the WFT: verify targetDeploymentVersionChanged=true, then CaN without AU + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "expected targetDeploymentVersionChanged=true after v2 becomes current") + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ + ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv1.WorkflowType(), + TaskQueue: tv1.TaskQueue(), + Input: tv1.Any().Payloads(), + }, + }, + }, + }, + VersioningBehavior: vbPinned, + DeploymentOptions: tv1.WorkerDeploymentOptions(true), + }, nil + }) + + // Step 3: CaN run starts on v1. Target=v2, declined=v2 → case 4 suppresses signal. + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + s.NotEqual(execution.RunId, task.WorkflowExecution.RunId, + "CaN should have created a new run") + // Update execution to track the new run + execution = task.WorkflowExecution + + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.False(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "declined=v2 == target=v2 — signal should be suppressed (case 4)") + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Step 4: Rollback — set v1 as current again. Target becomes v1. + s.setCurrentDeployment(tv1) + s.waitForDeploymentDataPropagation(tv1, versionStatusCurrent, false, tqTypeWf) + + // Trigger WFT via signal so case 3 executes (effective=v1 == target=v1) + s.triggerNormalWFT(ctx, tv1, execution) + + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.False(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "effective=v1 == target=v1 — no signal needed") + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Step 5: Set v2 as current again. Target goes back to v2. + s.setCurrentDeployment(tv2) + s.waitForDeploymentDataPropagation(tv2, versionStatusCurrent, false, tqTypeWf) + + // Trigger WFT via signal + s.triggerNormalWFT(ctx, tv1, execution) + + // This is the key assertion: declined should have been cleared in step 4, + // so target=v2 should fire the signal again. + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "declined was cleared by rollback — target=v2 should fire signal again") + return respondCompleteWorkflow(tv1, vbPinned), nil + }) +} + +// TestPinnedCaN_NeverSignaled_NewRunGetsSignalForUnversioned verifies that when a +// pinned workflow does CaN without ever being signaled about a target change +// (LastNotifiedTargetVersion wrapper is nil), the new run's +// NotificationSuppressedTargetVersion is nil. When the target later becomes +// unversioned (nil), the new run correctly receives +// targetWorkerDeploymentVersionChanged=true. +// +// This specifically tests the LastNotifiedTargetVersion wrapper message semantics: +// nil wrapper means "never signaled" (not "signaled about unversioned target"). +// +// Flow: +// 1. Start pinned workflow on v1, v1 is current +// 2. CaN without AU (no target change, no signal fires) +// 3. Unset current deployment → target becomes nil +// 4. New run's WFT: targetDeploymentVersionChanged=true (nil target != v1) +func (s *Versioning3Suite) TestPinnedCaN_NeverSignaled_NewRunGetsSignalForUnversioned() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + + // Start async poller for v1 that will handle the first WFT and declare pinned behavior + wftCompleted := make(chan struct{}) + s.pollWftAndHandle(tv1, false, wftCompleted, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbPinned), nil + }) + + // Wait for v1 to be registered, then set it as current + s.waitForDeploymentDataPropagation(tv1, versionStatusInactive, false, tqTypeWf) + s.setCurrentDeployment(tv1) + + // Start workflow — first WFT is handled by the async poller above (pinned on v1) + runID := s.startWorkflow(tv1, nil) + execution := tv1.WithRunID(runID).WorkflowExecution() + s.WaitForChannel(ctx, wftCompleted) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) + + // Trigger a WFT and CaN without any target change — no signal fires. + // This means LastNotifiedTargetVersion is never set (nil wrapper). + s.triggerNormalWFT(ctx, tv1, execution) + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + // Verify no signal — target hasn't changed. + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + s.False(event.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "no target change happened — signal should be false") + } + } + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ + ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv1.WorkflowType(), + TaskQueue: tv1.TaskQueue(), + Input: tv1.Any().Payloads(), + }, + }, + }, + }, + VersioningBehavior: vbPinned, + DeploymentOptions: tv1.WorkerDeploymentOptions(true), + }, nil + }) + + // Unset the current deployment — target becomes nil (unversioned). + s.unsetCurrentDeployment(tv1) + + // New run starts on v1 (pinned, inherited). Since LastNotifiedTargetVersion + // was nil (wrapper absent) on the previous run, NotificationSuppressedTargetVersion + // on this run is nil. Target=nil != effective=v1 → signal fires. + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + s.NotEqual(execution.RunId, task.WorkflowExecution.RunId, + "CaN should have created a new run with a different run ID") + + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "target changed to unversioned and previous run was never signaled — should signal true") + return respondCompleteWorkflow(tv1, vbPinned), nil + }) +} + +// TestPinnedCaN_UpgradeToUnversioned verifies that a pinned workflow can CaN +// with AUTO_UPGRADE initial behavior when the current deployment is unset +// (unversioned target). Matching sends nil as the target version, and history +// correctly signals the SDK to CaN. +// +// Flow: +// 1. Start pinned workflow on v1, set v1 as current +// 2. Unset current deployment (target becomes nil/unversioned) +// 3. Signal → WFT has targetDeploymentVersionChanged=true (v1 != nil) +// 4. CaN with AUTO_UPGRADE → new run picked up by unversioned poller → completes +func (s *Versioning3Suite) TestPinnedCaN_UpgradeToUnversioned() { + s.RunTestWithMatchingBehavior(func() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + tv1 := testvars.New(s).WithBuildIDNumber(1) + execution, _ := s.drainWorkflowTaskAfterSetCurrent(tv1) + + // Trigger a normal WFT and declare pinned behavior to make the workflow pinned on v1. + s.triggerNormalWFT(ctx, tv1, execution) + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return respondEmptyWft(tv1, false, vbPinned), nil + }) + s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil) + + // Unset the current deployment — target becomes nil (unversioned). + s.unsetCurrentDeployment(tv1) + + // Signal to trigger a new WFT. + s.triggerNormalWFT(ctx, tv1, execution) + + // Poll as v1 (pinned workflow still dispatched to v1 poller), verify the signal, + // and issue CaN with AUTO_UPGRADE initial behavior. + s.pollWftAndHandle(tv1, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + + // Verify targetDeploymentVersionChanged=true on the latest WFT started event. + var lastStarted *historypb.HistoryEvent + for _, event := range task.History.GetEvents() { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + lastStarted = event + } + } + s.NotNil(lastStarted) + s.True(lastStarted.GetWorkflowTaskStartedEventAttributes().GetTargetWorkerDeploymentVersionChanged(), + "should signal targetDeploymentVersionChanged=true when target is unversioned (nil != v1)") + + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ + ContinueAsNewWorkflowExecutionCommandAttributes: &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: tv1.WorkflowType(), + TaskQueue: tv1.TaskQueue(), + Input: tv1.Any().Payloads(), + InitialVersioningBehavior: enumspb.CONTINUE_AS_NEW_VERSIONING_BEHAVIOR_AUTO_UPGRADE, + }, + }, + }, + }, + ForceCreateNewWorkflowTask: false, + VersioningBehavior: vbPinned, + DeploymentOptions: tv1.WorkerDeploymentOptions(true), + }, nil + }) + + // Poll unversioned to pick up the CaN run's first WFT and complete the workflow. + wftNewRunDone := make(chan struct{}) + s.unversionedPollWftAndHandle(tv1, false, wftNewRunDone, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + s.Equal(execution.GetWorkflowId(), task.WorkflowExecution.GetWorkflowId(), + "CaN run should have the same workflow ID") + s.NotEqual(execution.GetRunId(), task.WorkflowExecution.GetRunId(), + "CaN run should have a different run ID") + return respondCompleteWorkflowUnversioned(tv1), nil + }) + s.WaitForChannel(ctx, wftNewRunDone) + }) +} + +func (s *Versioning3Suite) TestVersioning3_NoWorkerVersionOnStartedEvents() { + tv := testvars.New(s) + tvUpd1 := tv.WithUpdateIDNumber(1).WithMessageIDNumber(1) + tvUpd2 := tv.WithUpdateIDNumber(2) + stamp := &commonpb.WorkerVersionStamp{UseVersioning: true, BuildId: tv.BuildID()} + + // Start workflow and drain first WFT. + execution, _ := s.drainWorkflowTaskAfterSetCurrent(tv) + + // Send first update. + updateResultCh := sendUpdateNoError(s, tvUpd1) + + // Poll and handle: accept + complete the first update. + s.pollWftAndHandle(tv, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + s.NotEmpty(task.Messages) + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: s.UpdateAcceptCompleteCommands(tvUpd1), + Messages: s.UpdateAcceptCompleteMessages(tvUpd1, task.Messages[0]), + WorkerVersionStamp: stamp, + VersioningBehavior: vbUnpinned, + DeploymentOptions: &deploymentpb.WorkerDeploymentOptions{ + BuildId: tv.BuildID(), + DeploymentName: tv.DeploymentSeries(), + WorkerVersioningMode: enumspb.WORKER_VERSIONING_MODE_VERSIONED, + }, + }, nil + }) + <-updateResultCh + + // Send second update. + updateResultCh2 := sendUpdate(context.Background(), s, tvUpd2) + + // Poll and fail the WFT with deployment options to trigger a transient retry. + failCtx := testcore.NewContext() + pollResp, err := s.FrontendClient().PollWorkflowTaskQueue(failCtx, &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: s.Namespace().String(), + TaskQueue: tv.TaskQueue(), + Identity: tv.WorkerIdentity(), + DeploymentOptions: tv.WorkerDeploymentOptions(true), + }) + s.NoError(err) + s.NotEmpty(pollResp.GetTaskToken()) + _, err = s.FrontendClient().RespondWorkflowTaskFailed(failCtx, &workflowservice.RespondWorkflowTaskFailedRequest{ + Namespace: s.Namespace().String(), + TaskToken: pollResp.TaskToken, + Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + Identity: tv.WorkerIdentity(), + WorkerVersion: stamp, + DeploymentOptions: tv.WorkerDeploymentOptions(true), + }) + s.NoError(err) + + // Poll the retried (transient) WFT and fail the workflow. + s.pollWftAndHandle(tv, false, nil, + func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) { + s.NotNil(task) + return &workflowservice.RespondWorkflowTaskCompletedRequest{ + Commands: []*commandpb.Command{ + { + CommandType: enumspb.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_FailWorkflowExecutionCommandAttributes{ + FailWorkflowExecutionCommandAttributes: &commandpb.FailWorkflowExecutionCommandAttributes{ + Failure: &failurepb.Failure{Message: "workflow failed intentionally"}, + }, + }, + }, + }, + WorkerVersionStamp: stamp, + VersioningBehavior: vbUnpinned, + DeploymentOptions: &deploymentpb.WorkerDeploymentOptions{ + BuildId: tv.BuildID(), + DeploymentName: tv.DeploymentSeries(), + WorkerVersioningMode: enumspb.WORKER_VERSIONING_MODE_VERSIONED, + }, + }, nil + }) + + <-updateResultCh2 + + // Verify: no WorkflowTaskStarted event should have WorkerVersion set. + events := s.GetHistory(s.Namespace().String(), execution) + for _, event := range events { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_STARTED { + s.Nil( + //nolint:staticcheck // SA1019 + event.GetWorkflowTaskStartedEventAttributes().GetWorkerVersion(), + "WorkflowTaskStarted event %d should not have WorkerVersion set in V3", + event.GetEventId(), + ) + } + } +} + +func (s *Versioning3Suite) skipBeforeVersion(version workerdeployment.DeploymentWorkflowVersion) { + if s.deploymentWorkflowVersion < version { + s.T().Skipf("test supports workflow version %v and newer", version) + } +} + +func (s *Versioning3Suite) Context() context.Context { + return s.T().Context() } diff --git a/tests/versioning_test.go b/tests/versioning_test.go index 3887457b755..12af7a19503 100644 --- a/tests/versioning_test.go +++ b/tests/versioning_test.go @@ -31,7 +31,7 @@ import ( taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/common/dynamicconfig" - "go.temporal.io/server/common/searchattribute" + "go.temporal.io/server/common/searchattribute/sadefs" "go.temporal.io/server/common/tqid" "go.temporal.io/server/common/worker_versioning" "go.temporal.io/server/tests/testcore" @@ -99,11 +99,6 @@ func (s *VersioningIntegSuite) SetupSuite() { // this is overridden since we don't want caching to be enabled while testing DescribeTaskQueue // behaviour related to versioning dynamicconfig.TaskQueueInfoByBuildIdTTL.Key(): 0 * time.Second, - - // Use new matcher for versioning tests. Ideally we would run everything with old and new, - // but for now we pick a subset of tests. Versioning tests exercise the most features of - // matching so they're a good condidate. - dynamicconfig.MatchingUseNewMatcher.Key(): true, } s.FunctionalTestBase.SetupSuiteWithCluster(testcore.WithDynamicConfigOverrides(dynamicConfigOverrides)) } @@ -222,7 +217,7 @@ func (s *VersioningIntegSuite) TestAssignmentRuleDelete() { // success cT = s.deleteAssignmentRule(ctx, tq, 0, cT, true) res := s.getVersioningRules(ctx, tq) - s.Equal(1, len(res.GetAssignmentRules())) + s.Len(res.GetAssignmentRules(), 1) // failure due to requirement that once a fully-ramped rule exists, at least one must always exist s.deleteAssignmentRule(ctx, tq, 0, cT, false) @@ -233,7 +228,7 @@ func (s *VersioningIntegSuite) TestAssignmentRuleDelete() { // delete again, success s.deleteAssignmentRule(ctx, tq, 0, cT, true) - s.Equal(1, len(res.GetAssignmentRules())) + s.Len(res.GetAssignmentRules(), 1) } @@ -250,7 +245,7 @@ func (s *VersioningIntegSuite) TestRedirectRuleInsert() { res := s.getVersioningRules(ctx, tq) rulesMap := mkRedirectRulesMap(res.GetCompatibleRedirectRules()) s.Contains(rulesMap, "1") - s.Equal(rulesMap["1"], "0") + s.Equal("0", rulesMap["1"]) // failure due to cycle s.insertRedirectRule(ctx, tq, "0", "1", cT, false) @@ -274,7 +269,7 @@ func (s *VersioningIntegSuite) TestRedirectRuleReplace() { res := s.getVersioningRules(ctx, tq) rulesMap := mkRedirectRulesMap(res.GetCompatibleRedirectRules()) s.Contains(rulesMap, "1") - s.Equal(rulesMap["1"], "2") + s.Equal("2", rulesMap["1"]) // failure due to source not found s.replaceRedirectRule(ctx, tq, "10", "3", cT, false) @@ -297,7 +292,7 @@ func (s *VersioningIntegSuite) TestRedirectRuleDelete() { // success cT = s.deleteRedirectRule(ctx, tq, "1", cT, true) res := s.getVersioningRules(ctx, tq) - s.Equal(1, len(res.GetCompatibleRedirectRules())) + s.Len(res.GetCompatibleRedirectRules(), 1) // failure due to source not found s.deleteRedirectRule(ctx, tq, "1", cT, false) @@ -321,8 +316,8 @@ func (s *VersioningIntegSuite) TestCommitBuildID() { // no recent poller + force --> success cT = s.commitBuildId(ctx, tq, "1", true, cT, true) res := s.getVersioningRules(ctx, tq) - s.Equal(1, len(res.GetAssignmentRules())) - s.Equal(0, len(res.GetCompatibleRedirectRules())) + s.Len(res.GetAssignmentRules(), 1) + s.Empty(res.GetCompatibleRedirectRules()) s.Equal("1", res.GetAssignmentRules()[0].GetRule().GetTargetBuildId()) s.Equal(float32(100), res.GetAssignmentRules()[0].GetRule().GetPercentageRamp().GetRampPercentage()) @@ -338,8 +333,8 @@ func (s *VersioningIntegSuite) TestCommitBuildID() { s.registerWorkflowAndPollVersionedTaskQueue(tq, "2", true) s.commitBuildId(ctx, tq, "2", false, cT, true) res = s.getVersioningRules(ctx, tq) - s.Equal(1, len(res.GetAssignmentRules())) - s.Equal(0, len(res.GetCompatibleRedirectRules())) + s.Len(res.GetAssignmentRules(), 1) + s.Empty(res.GetCompatibleRedirectRules()) s.Equal("2", res.GetAssignmentRules()[0].GetRule().GetTargetBuildId()) s.Equal(float32(100), res.GetAssignmentRules()[0].GetRule().GetPercentageRamp().GetRampPercentage()) } @@ -373,7 +368,7 @@ func (s *VersioningIntegSuite) TestSeriesOfUpdates() { ctx := testcore.NewContext() tq := "functional-versioning-series" - for i := 0; i < 10; i++ { + for i := range 10 { s.addNewDefaultBuildId(ctx, tq, s.prefixed(fmt.Sprintf("foo-%d", i))) } s.addCompatibleBuildId(ctx, tq, s.prefixed("foo-2.1"), s.prefixed("foo-2"), false) @@ -596,7 +591,7 @@ func (s *VersioningIntegSuite) TestDispatchNewWorkflowWithRamp() { defer w2.Stop() counter := make(map[string]int) - for i := 0; i < 50; i++ { + for range 50 { run, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf") s.NoError(err) var out string @@ -605,8 +600,8 @@ func (s *VersioningIntegSuite) TestDispatchNewWorkflowWithRamp() { } // both builds should've got executions - s.Greater(counter["done v1!"], 0) - s.Greater(counter["done v2!"], 0) + s.Positive(counter["done v1!"]) + s.Positive(counter["done v2!"]) s.Equal(50, counter["done v1!"]+counter["done v2!"]) } @@ -681,7 +676,7 @@ func (s *VersioningIntegSuite) workflowStaysInBuildId() { dw, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) - s.Equal(1, len(dw.GetPendingActivities())) + s.Len(dw.GetPendingActivities(), 1) s.NotNil(dw.GetPendingActivities()[0].GetUseWorkflowBuildId()) close(rulesUpdated) @@ -763,7 +758,7 @@ func (s *VersioningIntegSuite) unversionedWorkflowStaysUnversioned() { dw, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) - s.Equal(1, len(dw.GetPendingActivities())) + s.Len(dw.GetPendingActivities(), 1) s.Nil(dw.GetPendingActivities()[0].GetAssignedBuildId()) close(rulesUpdated) @@ -1070,7 +1065,7 @@ func (s *VersioningIntegSuite) independentActivityTaskAssignmentSpooled(versione s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId()) } else { - s.Equal("", dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) + s.Empty(dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) //nolint:staticcheck s.False(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetUseVersioning()) } return v1 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId() @@ -1106,7 +1101,7 @@ func (s *VersioningIntegSuite) independentActivityTaskAssignmentSpooled(versione func() bool { dw, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) - s.Equal(1, len(dw.GetPendingActivities())) + s.Len(dw.GetPendingActivities(), 1) return v2 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId() }, 10*time.Second, @@ -1145,7 +1140,7 @@ func (s *VersioningIntegSuite) independentActivityTaskAssignmentSpooled(versione func() bool { dw, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) - s.Equal(1, len(dw.GetPendingActivities())) + s.Len(dw.GetPendingActivities(), 1) return v3 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId() }, 10*time.Second, @@ -1277,7 +1272,7 @@ func (s *VersioningIntegSuite) independentActivityTaskAssignmentSyncMatch(versio s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId()) } else { - s.Equal("", dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) + s.Empty(dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) //nolint:staticcheck s.False(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetUseVersioning()) } return v1 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId() @@ -1318,7 +1313,7 @@ func (s *VersioningIntegSuite) independentActivityTaskAssignmentSyncMatch(versio func() bool { dw, err := s.SdkClient().DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) - s.Equal(1, len(dw.GetPendingActivities())) + s.Len(dw.GetPendingActivities(), 1) return v2 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId() }, 10*time.Second, @@ -1398,7 +1393,7 @@ func (s *VersioningIntegSuite) testWorkflowTaskRedirectInRetry(firstTask bool) { wf1 := func(ctx workflow.Context) (string, error) { if !firstTask { // add an activity to move workflow past first WFT - var out interface{} + var out any err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ StartToCloseTimeout: 1 * time.Second}), act).Get(ctx, &out) s.NoError(err) @@ -1445,7 +1440,7 @@ func (s *VersioningIntegSuite) testWorkflowTaskRedirectInRetry(firstTask bool) { wf11 := func(ctx workflow.Context) (string, error) { if !firstTask { // add an activity to move workflow past first WFT - var out interface{} + var out any err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ StartToCloseTimeout: 1 * time.Second}), act).Get(ctx, &out) s.NoError(err) @@ -1483,7 +1478,7 @@ func (s *VersioningIntegSuite) testWorkflowTaskRedirectInRetry(firstTask bool) { wf12 := func(ctx workflow.Context) (string, error) { if !firstTask { // add an activity to move workflow past first WFT - var out interface{} + var out any err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ StartToCloseTimeout: 1 * time.Second}), act).Get(ctx, &out) s.NoError(err) @@ -2075,34 +2070,34 @@ func (s *VersioningIntegSuite) TestDispatchActivityUpgrade() { s.WaitForChannel(ctx, startedWf) rule2 := s.addRedirectRule(ctx, tq, v1, v11) s.waitForRedirectRulePropagation(ctx, tq, rule2) - proceedWf <- struct{}{} + s.SendToChannel(ctx, proceedWf) s.WaitForChannel(ctx, started11) // wf assigned build ID should be updated by activity redirect s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, true, v1, "", []string{v1}) // let activity finish - proceed11 <- struct{}{} + s.SendToChannel(ctx, proceed11) // wf replays on 1.1 so need to unblock it an extra time s.WaitForChannel(ctx, startedWf) - proceedWf <- struct{}{} + s.SendToChannel(ctx, proceedWf) s.WaitForChannel(ctx, startedWf) rule2 = s.addRedirectRule(ctx, tq, v11, v12) s.waitForRedirectRulePropagation(ctx, tq, rule2) - proceedWf <- struct{}{} + s.SendToChannel(ctx, proceedWf) s.WaitForChannel(ctx, started12) // wf assigned build ID should not be updated by independent activity redirect s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, true, v11, "", []string{v1}) // let activity finish - proceed12 <- struct{}{} + s.SendToChannel(ctx, proceed12) // wf replays on 1.2 so need to unblock it two extra times s.WaitForChannel(ctx, startedWf) - proceedWf <- struct{}{} + s.SendToChannel(ctx, proceedWf) s.WaitForChannel(ctx, startedWf) - proceedWf <- struct{}{} + s.SendToChannel(ctx, proceedWf) var out string s.NoError(run.Get(ctx, &out)) @@ -2229,7 +2224,7 @@ func (s *VersioningIntegSuite) TestRedirectWithConcurrentActivities() { s.NoError(err) res = append(res, activityVersion) // The output of a newer build ID should never be sent to a wf worker of an older build ID - s.Assert().GreaterOrEqual(wfVersion, activityVersion) + s.GreaterOrEqual(wfVersion, activityVersion) // TODO: uncomment this check once workflow.GetInfo(wfCtx).GetCurrentBuildID() returns correct value // based on last started task build ID, not last completed task build ID. // s.Assert().GreaterOrEqual(workflow.GetInfo(wfCtx).GetCurrentBuildID(), activityVersion) @@ -2285,7 +2280,7 @@ func (s *VersioningIntegSuite) TestRedirectWithConcurrentActivities() { var maxStartedTimestamp time.Time for wh.HasNext() { he, err := wh.Next() - s.Nil(err) + s.NoError(err) var taskStartedStamp *commonpb.WorkerVersionStamp var taskRedirectCounter int64 var buildId string @@ -2483,7 +2478,7 @@ func (s *VersioningIntegSuite) TestDispatchActivityEager() { }, }) s.Require().NoError(err) - s.Require().Equal(1, len(completionResponse.ActivityTasks)) + s.Require().Len(completionResponse.ActivityTasks, 1) s.Require().Equal("compatible", completionResponse.ActivityTasks[0].ActivityId) } @@ -4001,7 +3996,7 @@ func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_Versioned_Reachabil Namespace: s.Namespace().String(), Query: queryARunning, }) - s.Nil(err) + s.NoError(err) return resp.GetCount() > 0 }, 5*time.Second, 50*time.Millisecond) @@ -4069,7 +4064,7 @@ func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_Versioned_BasicReac Namespace: s.Namespace().String(), Query: queryARunning, }) - s.Nil(err) + s.NoError(err) return resp.GetCount() > 0 }, 3*time.Second, 50*time.Millisecond) @@ -4103,7 +4098,7 @@ func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_Unversioned() { workerN := 3 workerMap := make(map[string]worker.Worker) - for i := 0; i < workerN; i++ { + for range workerN { wId := testcore.RandomizeStr("id") w := worker.New(s.SdkClient(), tq, worker.Options{ UseBuildIDForVersioning: false, @@ -4128,9 +4123,9 @@ func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_Unversioned() { }) s.NoError(err) s.NotNil(resp) - s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue") + s.Len(resp.GetVersionsInfo(), 1, "should be 1 because only default/unversioned queue") //nolint:staticcheck versionInfo := resp.GetVersionsInfo()[""] - s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability()) + s.Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability()) var pollersInfo []*taskqueuepb.PollerInfo for _, t := range versionInfo.GetTypesInfo() { pollersInfo = append(pollersInfo, t.GetPollers()...) @@ -4178,9 +4173,9 @@ func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_ReportFlags() { }) s.NoError(err) s.NotNil(resp) - s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue") + s.Len(resp.GetVersionsInfo(), 1, "should be 1 because only default/unversioned queue") //nolint:staticcheck versionInfo := resp.GetVersionsInfo()[""] - s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability()) + s.Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability()) var pollersInfo []*taskqueuepb.PollerInfo for _, t := range versionInfo.GetTypesInfo() { pollersInfo = append(pollersInfo, t.GetPollers()...) @@ -4206,11 +4201,11 @@ func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_ReportFlags() { }) s.NoError(err) s.NotNil(resp) - s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue") + s.Len(resp.GetVersionsInfo(), 1, "should be 1 because only default/unversioned queue") //nolint:staticcheck versionInfo := resp.GetVersionsInfo()[""] - s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability()) + s.Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability()) for _, t := range versionInfo.GetTypesInfo() { - s.Zero(len(t.GetPollers()), "poller info should not be reported") + s.Empty(t.GetPollers(), "poller info should not be reported") } // ask for pollers only @@ -4224,11 +4219,11 @@ func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_ReportFlags() { }) s.NoError(err) s.NotNil(resp) - s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue") + s.Len(resp.GetVersionsInfo(), 1, "should be 1 because only default/unversioned queue") //nolint:staticcheck versionInfo = resp.GetVersionsInfo()[""] - s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_UNSPECIFIED, versionInfo.GetTaskReachability()) + s.Equal(enumspb.BUILD_ID_TASK_REACHABILITY_UNSPECIFIED, versionInfo.GetTaskReachability()) for _, t := range versionInfo.GetTypesInfo() { - s.Equal(1, len(t.GetPollers()), "only one poller info should be reported") + s.Len(t.GetPollers(), 1, "only one poller info should be reported") } } @@ -4449,7 +4444,7 @@ func (s *VersioningIntegSuite) insertAssignmentRule( if expectSuccess { s.NoError(err) s.NotNil(res) - s.Assert().Equal(newBuildId, res.GetAssignmentRules()[idx].GetRule().GetTargetBuildId()) + s.Equal(newBuildId, res.GetAssignmentRules()[idx].GetRule().GetTargetBuildId()) return res.GetConflictToken() } else { s.Error(err) @@ -4479,7 +4474,7 @@ func (s *VersioningIntegSuite) replaceAssignmentRule( if expectSuccess { s.NoError(err) s.NotNil(res) - s.Assert().Equal(newBuildId, res.GetAssignmentRules()[idx].GetRule().GetTargetBuildId()) + s.Equal(newBuildId, res.GetAssignmentRules()[idx].GetRule().GetTargetBuildId()) return res.GetConflictToken() } else { s.Error(err) @@ -4525,7 +4520,7 @@ func (s *VersioningIntegSuite) deleteAssignmentRule( break } } - s.Assert().False(found) + s.False(found) return res.GetConflictToken() } else { s.Error(err) @@ -4562,7 +4557,7 @@ func (s *VersioningIntegSuite) insertRedirectRule( break } } - s.Assert().True(found) + s.True(found) return res.GetConflictToken() } else { s.Error(err) @@ -4599,7 +4594,7 @@ func (s *VersioningIntegSuite) replaceRedirectRule( break } } - s.Assert().True(found) + s.True(found) return res.GetConflictToken() } else { s.Error(err) @@ -4633,7 +4628,7 @@ func (s *VersioningIntegSuite) deleteRedirectRule( break } } - s.Assert().False(found) + s.False(found) return res.GetConflictToken() } else { s.Error(err) @@ -4664,7 +4659,7 @@ func (s *VersioningIntegSuite) commitBuildId( // 1. Adds a fully-ramped assignment rule for the target Build ID at the end of the list. endIdx := len(res.GetAssignmentRules()) - 1 addedRule := res.GetAssignmentRules()[endIdx].GetRule() - s.Assert().Equal(targetBuildId, addedRule.GetTargetBuildId()) + s.Equal(targetBuildId, addedRule.GetTargetBuildId()) s.Assert().Equal(float32(100), addedRule.GetPercentageRamp().GetRampPercentage()) foundOtherAssignmentRuleForTarget := false @@ -4678,9 +4673,9 @@ func (s *VersioningIntegSuite) commitBuildId( } } // 2. Removes all previously added assignment rules to the given target Build ID (if any). - s.Assert().False(foundOtherAssignmentRuleForTarget) + s.False(foundOtherAssignmentRuleForTarget) // 3. Removes any fully-ramped assignment rule for other Build IDs. - s.Assert().False(foundFullyRampedAssignmentRuleForOtherTarget) + s.False(foundFullyRampedAssignmentRuleForOtherTarget) return res.GetConflictToken() } else { s.Error(err) @@ -4725,8 +4720,8 @@ func (s *VersioningIntegSuite) getBuildIdReachability( s.NotNil(resp) for buildId, vi := range resp.GetVersionsInfo() { expected, ok := expectedReachability[buildId] - s.Assert().True(ok, "build id %s was not expected", buildId) - s.Assert().Equal(expected, vi.GetTaskReachability(), "build id %s has unexpected reachability", buildId) + s.True(ok, "build id %s was not expected", buildId) + s.Equal(expected, vi.GetTaskReachability(), "build id %s has unexpected reachability", buildId) } } @@ -5028,7 +5023,7 @@ func (s *VersioningIntegSuite) validateWorkflowBuildIds( dw, err := s.SdkClient().DescribeWorkflowExecution(ctx, wfId, runId) s.NoError(err) saPayload := dw.GetWorkflowExecutionInfo().GetSearchAttributes().GetIndexedFields()["BuildIds"] - searchAttrAny, err := searchattribute.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) + searchAttrAny, err := sadefs.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true) var searchAttr []string if searchAttrAny != nil { searchAttr = searchAttrAny.([]string) @@ -5038,12 +5033,12 @@ func (s *VersioningIntegSuite) validateWorkflowBuildIds( if expectedStampBuildId != "" { s.NotNil(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId()) s.False(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetUseVersioning()) - s.Equal(2+len(extraSearchAttrBuildIds), len(searchAttr)) + s.Len(searchAttr, 2+len(extraSearchAttrBuildIds)) s.Equal(worker_versioning.UnversionedSearchAttribute, searchAttr[0]) s.True(strings.HasPrefix(searchAttr[1], worker_versioning.UnversionedSearchAttribute)) } else { s.Nil(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp()) - s.Equal(0, len(searchAttr)) + s.Empty(searchAttr) } } else { if expectedStampBuildId != "" { @@ -5054,16 +5049,16 @@ func (s *VersioningIntegSuite) validateWorkflowBuildIds( } if newVersioning { s.Equal(expectedBuildId, dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) - s.Equal(2+len(extraSearchAttrBuildIds), len(searchAttr)) + s.Len(searchAttr, 2+len(extraSearchAttrBuildIds)) s.Equal(worker_versioning.AssignedBuildIdSearchAttribute(expectedBuildId), searchAttr[0]) s.Contains(searchAttr, worker_versioning.VersionedBuildIdSearchAttribute(expectedBuildId)) } else { - s.Equal("", dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) + s.Empty(dw.GetWorkflowExecutionInfo().GetAssignedBuildId()) //nolint:staticcheck if expectedStampBuildId != "" { - s.Equal(1+len(extraSearchAttrBuildIds), len(searchAttr)) + s.Len(searchAttr, 1+len(extraSearchAttrBuildIds)) s.Contains(searchAttr, worker_versioning.VersionedBuildIdSearchAttribute(expectedBuildId)) } else { - s.Equal(0, len(searchAttr)) + s.Empty(searchAttr) } } } @@ -5088,7 +5083,7 @@ func (s *VersioningIntegSuite) validateWorkflowEventsVersionStamps( checkedInheritedBuildId := false for wh.HasNext() { he, err := wh.Next() - s.Nil(err) + s.NoError(err) if !checkedInheritedBuildId { // first event checkedInheritedBuildId = true diff --git a/tests/worker_deployment_test.go b/tests/worker_deployment_test.go index 3fead8f2210..2f2ad94bc01 100644 --- a/tests/worker_deployment_test.go +++ b/tests/worker_deployment_test.go @@ -2,7 +2,6 @@ package tests import ( "context" - "errors" "fmt" "strings" "testing" @@ -39,12 +38,7 @@ type ( } ) -func TestWorkerDeploymentSuiteV0(t *testing.T) { - t.Parallel() - suite.Run(t, &WorkerDeploymentSuite{workflowVersion: workerdeployment.InitialVersion}) -} - -func TestWorkerDeploymentSuiteV2(t *testing.T) { +func TestWorkerDeploymentSuite(t *testing.T) { t.Parallel() suite.Run(t, &WorkerDeploymentSuite{workflowVersion: workerdeployment.VersionDataRevisionNumber}) } @@ -116,7 +110,7 @@ func (s *WorkerDeploymentSuite) ensureCreateVersionWithExpectedTaskQueues(ctx co Version: tv.DeploymentVersionString(), }) - a.Equal(expectedTaskQueues, len(respV.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos())) + a.Len(respV.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos(), expectedTaskQueues) }, 5*time.Minute, 500*time.Millisecond) } @@ -313,6 +307,7 @@ func (s *WorkerDeploymentSuite) TestDeploymentVersionLimits() { func (s *WorkerDeploymentSuite) TestNamespaceDeploymentsLimit() { // TODO (carly): check the error messages that poller receives in each case and make sense they are informative and appropriate (e.g. do not expose internal stuff) + // Also in TestCreateWorkerDeployment_MaxDeploymentsLimit s.T().Skip() // Need to separate this test so other tests do not create deployment in the same NS s.OverrideDynamicConfig(dynamicconfig.MatchingMaxDeployments, 1) @@ -326,6 +321,9 @@ func (s *WorkerDeploymentSuite) TestNamespaceDeploymentsLimit() { go s.pollFromDeployment(ctx, tv) s.ensureCreateVersionInDeployment(tv) + // wait for all existing deployments to show up in visibility + s.validateWorkerDeploymentCount(ctx, &workflowservice.ListWorkerDeploymentsRequest{Namespace: s.Namespace().String()}, 1) + // pollers of the second deployment version should be rejected s.pollFromDeploymentExpectFail(ctx, tv.WithDeploymentSeriesNumber(2), "reached maximum deployments in namespace (1)") } @@ -341,13 +339,12 @@ func (s *WorkerDeploymentSuite) TestDescribeWorkerDeployment_TwoVersions_Sorted( go s.pollFromDeployment(ctx, firstVersion) - // waiting for 1ms to start the second version later. - startTime := time.Now() - waitTime := 1 * time.Millisecond - s.EventuallyWithT(func(t *assert.CollectT) { - a := require.New(t) - a.Greater(time.Since(startTime), waitTime) - }, 10*time.Second, 1000*time.Millisecond) + // Wait until the first version is registered in the deployment before starting the second. + // This ensures that both versions get distinct CreateTime values in the deployment workflow + // (each processed in a separate workflow task), so that the descending-by-CreateTime sort + // produces a deterministic order. A wall-clock wait is not sufficient because in V2 mode + // both registrations can queue up and be processed within the same workflow task millisecond. + s.ensureCreateVersionInDeployment(firstVersion) go s.pollFromDeployment(ctx, secondVersion) @@ -363,7 +360,7 @@ func (s *WorkerDeploymentSuite) TestDescribeWorkerDeployment_TwoVersions_Sorted( a.Equal(tv.DeploymentSeries(), resp.GetWorkerDeploymentInfo().GetName()) a.NotNil(resp.GetWorkerDeploymentInfo().GetVersionSummaries()) - a.Equal(2, len(resp.GetWorkerDeploymentInfo().GetVersionSummaries())) + a.Len(resp.GetWorkerDeploymentInfo().GetVersionSummaries(), 2) // Verify that the version summaries are non-nil and sorted. versionSummaries := resp.GetWorkerDeploymentInfo().GetVersionSummaries() @@ -390,7 +387,7 @@ func (s *WorkerDeploymentSuite) TestDescribeWorkerDeployment_MultipleVersions_So numVersions := 10 - for i := 0; i < numVersions; i++ { + for i := range numVersions { go s.pollFromDeployment(ctx, tv.WithBuildIDNumber(i)) // waiting for 1ms to start the next version later. @@ -411,7 +408,7 @@ func (s *WorkerDeploymentSuite) TestDescribeWorkerDeployment_MultipleVersions_So a.NoError(err) a.NotNil(resp.GetWorkerDeploymentInfo().GetVersionSummaries()) - a.Equal(numVersions, len(resp.GetWorkerDeploymentInfo().GetVersionSummaries())) + a.Len(resp.GetWorkerDeploymentInfo().GetVersionSummaries(), numVersions) // Verify that the version summaries are sorted. versionSummaries := resp.GetWorkerDeploymentInfo().GetVersionSummaries() @@ -456,7 +453,7 @@ func (s *WorkerDeploymentSuite) TestConflictToken_Describe_SetCurrent_SetRamping Version: firstVersion.DeploymentVersionString(), ConflictToken: cT, }) - s.Nil(err) + s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { a := require.New(t) @@ -480,7 +477,7 @@ func (s *WorkerDeploymentSuite) TestConflictToken_Describe_SetCurrent_SetRamping ConflictToken: cT, IgnoreMissingTaskQueues: true, // here until we have 'has version started' safeguard in place }) - s.Nil(err) + s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { a := require.New(t) @@ -525,7 +522,7 @@ func (s *WorkerDeploymentSuite) TestConflictToken_SetCurrent_SetRamping_Wrong() Version: firstVersion.DeploymentVersionString(), ConflictToken: cTWrong, }) - s.Equal(err.Error(), expectedError) + s.Equal(expectedError, err.Error()) // Set first version as ramping version with wrong token _, err = s.FrontendClient().SetWorkerDeploymentRampingVersion(ctx, &workflowservice.SetWorkerDeploymentRampingVersionRequest{ @@ -535,7 +532,7 @@ func (s *WorkerDeploymentSuite) TestConflictToken_SetCurrent_SetRamping_Wrong() Percentage: 5, ConflictToken: cTWrong, }) - s.Equal(err.Error(), expectedError) + s.Equal(expectedError, err.Error()) } // Testing ListWorkerDeployments @@ -818,7 +815,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Ramping_Wi DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -857,7 +854,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Ramping_Wi DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -907,7 +904,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Ramping_Wi DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -960,7 +957,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Ramping_Wi DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1022,7 +1019,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_DuplicateR DeploymentName: rampingVersionVars.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: rampingVersionVars.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1071,7 +1068,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Invalid_Se DeploymentName: currentVersionVars.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: currentVersionVars.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1109,7 +1106,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Invalid_Se DeploymentName: currentVersionVars.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: currentVersionVars.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1174,7 +1171,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_ModifyExis DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1213,7 +1210,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_ModifyExis DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1268,7 +1265,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_WithCurren DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: version1CreateTime, @@ -1318,7 +1315,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_WithCurren DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: version1CreateTime, @@ -1381,7 +1378,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_SetRamping DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1429,12 +1426,12 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Batching() defer cancel() tv := testvars.New(s) - s.InjectHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, 1) + s.InjectHook(testhooks.NewHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, 1)) // registering 5 task-queues in the version which would result in the creation of 5 batches, each with 1 task-queue, during the SyncState call. versionCreateTime := timestamppb.Now() taskQueues := 5 - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { go s.pollFromDeploymentWithTaskQueueNumber(ctx, tv, i) } @@ -1443,7 +1440,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Batching() s.ensureCreateVersionWithExpectedTaskQueues(ctx, tv, taskQueues) // verify that all the registered task-queues have "" set as their ramping version - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { s.verifyTaskQueueVersioningInfo(ctx, tv.WithTaskQueueNumber(i).TaskQueue(), worker_versioning.UnversionedVersionId, "", 0) } @@ -1452,7 +1449,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Batching() s.setAndVerifyRampingVersion(ctx, tv, false, 50, true, "") // verify the task queues have new ramping version - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { s.verifyTaskQueueVersioningInfo(ctx, tv.WithTaskQueueNumber(i).TaskQueue(), worker_versioning.UnversionedVersionId, tv.DeploymentVersionString(), 50) } @@ -1461,10 +1458,10 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Batching() Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), }) - s.Nil(err) + s.NoError(err) s.Equal(tv.DeploymentVersionString(), resp.GetWorkerDeploymentInfo().GetRoutingConfig().GetRampingVersion()) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1502,12 +1499,12 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Unversione defer cancel() tv := testvars.New(s) - s.InjectHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, 1) + s.InjectHook(testhooks.NewHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, 1)) // registering 5 task-queues in the version which would result in the creation of 5 batches, each with 1 task-queue, during the SyncState call. versionCreateTime := timestamppb.Now() taskQueues := 5 - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { go s.pollFromDeploymentWithTaskQueueNumber(ctx, tv, i) } @@ -1524,7 +1521,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Unversione s.setAndVerifyRampingVersionUnversionedOption(ctx, tv, true, false, 75, true, false, true, "") // check that the current version's task queues have ramping version == __unversioned__ - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { s.verifyTaskQueueVersioningInfo(ctx, tv.WithTaskQueueNumber(i).TaskQueue(), tv.DeploymentVersionString(), worker_versioning.UnversionedVersionId, 75) } @@ -1533,12 +1530,12 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Unversione Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), }) - s.Nil(err) + s.NoError(err) // nolint:staticcheck // SA1019: old worker versioning s.Equal(worker_versioning.UnversionedVersionId, resp.GetWorkerDeploymentInfo().GetRoutingConfig().GetRampingVersion()) s.Nil(resp.GetWorkerDeploymentInfo().GetRoutingConfig().GetRampingDeploymentVersion()) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1607,7 +1604,7 @@ func (s *WorkerDeploymentSuite) TestDescribeWorkerDeployment_SetCurrentVersion() DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: version1CreateTime, @@ -1648,7 +1645,7 @@ func (s *WorkerDeploymentSuite) TestDescribeWorkerDeployment_SetCurrentVersion() DeploymentName: tv.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: version1CreateTime, @@ -1693,12 +1690,12 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Batching() { defer cancel() tv := testvars.New(s) - s.InjectHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, 1) + s.InjectHook(testhooks.NewHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, 1)) // registering 5 task-queues in the version which would result in the creation of 5 batches, each with 1 task-queue, during the SyncState call. versionCreateTime := timestamppb.Now() taskQueues := 5 - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { go s.pollFromDeploymentWithTaskQueueNumber(ctx, tv, i) } @@ -1707,7 +1704,7 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Batching() { s.ensureCreateVersionWithExpectedTaskQueues(ctx, tv, taskQueues) // verify that all the registered task-queues have "__unversioned__" as their current version - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { s.verifyTaskQueueVersioningInfo(ctx, tv.WithTaskQueueNumber(i).TaskQueue(), worker_versioning.UnversionedVersionId, "", 0) } @@ -1716,7 +1713,7 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Batching() { s.setCurrentVersion(ctx, tv, true, "") // verify the current version has propogated to all the registered task-queues userData - for i := 0; i < taskQueues; i++ { + for i := range taskQueues { s.verifyTaskQueueVersioningInfo(ctx, tv.WithTaskQueueNumber(i).TaskQueue(), tv.DeploymentVersionString(), "", 0) } @@ -1725,9 +1722,9 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Batching() { Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), }) - s.Nil(err) + s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1910,8 +1907,8 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Unversioned_NoRamp() { Namespace: s.Namespace().String(), DeploymentName: currentVars.DeploymentSeries(), }) - s.Nil(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.NoError(err) + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: currentVars.DeploymentSeries(), CreateTime: versionCreateTime, @@ -1972,12 +1969,12 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Concurrent_DifferentVersio errChan := make(chan error) versions := 10 - for i := 0; i < versions; i++ { + for i := range versions { s.startVersionWorkflow(ctx, tv.WithBuildIDNumber(i)) } // Concurrently set 10 different versions as current version - for i := 0; i < versions; i++ { + for i := range versions { go func() { _, err := s.FrontendClient().SetWorkerDeploymentCurrentVersion(ctx, &workflowservice.SetWorkerDeploymentCurrentVersionRequest{ Namespace: s.Namespace().String(), @@ -1990,7 +1987,7 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Concurrent_DifferentVersio }() } - for i := 0; i < versions; i++ { + for range versions { err := <-errChan if err != nil { switch err.(type) { @@ -2025,7 +2022,7 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Concurrent_SameVersion_NoU s.startVersionWorkflow(ctx, tv) // create version // Concurrently set the same version as current version 10 times. - for i := 0; i < 10; i++ { + for range 10 { go func() { _, err := s.FrontendClient().SetWorkerDeploymentCurrentVersion(ctx, &workflowservice.SetWorkerDeploymentCurrentVersionRequest{ Namespace: s.Namespace().String(), @@ -2038,7 +2035,7 @@ func (s *WorkerDeploymentSuite) TestSetCurrentVersion_Concurrent_SameVersion_NoU }() } - for i := 0; i < 10; i++ { + for range 10 { err := <-errChan if err != nil { switch err.(type) { @@ -2071,15 +2068,29 @@ func (s *WorkerDeploymentSuite) TestConcurrentPollers_DifferentTaskQueues_SameVe tv := testvars.New(s) tqs := 10 - for i := 0; i < tqs; i++ { - go s.startVersionWorkflow(ctx, tv.WithTaskQueueNumber(i)) + // Start all pollers concurrently (pollFromDeployment has no assertions, so it's safe to call from goroutines) + for i := range tqs { + go s.pollFromDeployment(ctx, tv.WithTaskQueueNumber(i)) + } + // Wait for all version workflows to appear (must run in the test goroutine due to assertions) + for i := range tqs { + tvI := tv.WithTaskQueueNumber(i) + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + resp, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, &workflowservice.DescribeWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + Version: tvI.DeploymentVersionString(), + }) + a.NoError(err) + a.Equal(tvI.ExternalDeploymentVersion(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion()) + }, time.Minute, time.Second) } // set this version as current version s.setCurrentVersion(ctx, tv, false, "") // verify that the task queues, eventually, have this version as the current version in their versioning info - for i := 0; i < tqs; i++ { + for i := range tqs { s.verifyTaskQueueVersioningInfo(ctx, tv.WithTaskQueueNumber(i).TaskQueue(), tv.DeploymentVersionString(), "", 0) } } @@ -2094,12 +2105,12 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_Concurrent_DifferentVersio errChan := make(chan error) versions := 10 - for i := 0; i < versions; i++ { + for i := range versions { s.startVersionWorkflow(ctx, tv.WithBuildIDNumber(i)) } // Concurrently set 10 different versions as ramping version - for i := 0; i < versions; i++ { + for i := range versions { go func() { _, err := s.FrontendClient().SetWorkerDeploymentRampingVersion(ctx, &workflowservice.SetWorkerDeploymentRampingVersionRequest{ Namespace: s.Namespace().String(), @@ -2113,7 +2124,7 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_Concurrent_DifferentVersio }() } - for i := 0; i < versions; i++ { + for range versions { err := <-errChan if err != nil { switch err.(type) { @@ -2148,7 +2159,7 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_Concurrent_SameVersion_NoU s.startVersionWorkflow(ctx, tv) // create version // Concurrently set the same version as ramping version 10 times. - for i := 0; i < 10; i++ { + for range 10 { go func() { _, err := s.FrontendClient().SetWorkerDeploymentRampingVersion(ctx, &workflowservice.SetWorkerDeploymentRampingVersionRequest{ Namespace: s.Namespace().String(), @@ -2162,7 +2173,7 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_Concurrent_SameVersion_NoU }() } - for i := 0; i < 10; i++ { + for range 10 { err := <-errChan if err != nil { switch err.(type) { @@ -2202,8 +2213,8 @@ func (s *WorkerDeploymentSuite) TestConcurrentPollers_ManyTaskQueues_RapidRoutin numOperations := 20 s.OverrideDynamicConfig(dynamicconfig.MatchingMaxTaskQueuesInDeploymentVersion, numTaskQueues) - s.InjectHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, syncBatchSize) - s.InjectHook(testhooks.MatchingDeploymentRegisterErrorBackoff, time.Millisecond*500) + s.InjectHook(testhooks.NewHook(testhooks.TaskQueuesInDeploymentSyncBatchSize, syncBatchSize)) + s.InjectHook(testhooks.NewHook(testhooks.MatchingDeploymentRegisterErrorBackoff, time.Millisecond*500)) // Need to increase max pending activities because it is set only to 10 for functional tests. it's 2000 by default. s.OverrideDynamicConfig(dynamicconfig.NumPendingActivitiesLimitError, numOperations) @@ -2213,11 +2224,11 @@ func (s *WorkerDeploymentSuite) TestConcurrentPollers_ManyTaskQueues_RapidRoutin start := time.Now() // For each version send pollers regularly until all TQs are registered from DescribeVersion POV - for i := 0; i < numVersions; i++ { + for i := range numVersions { pollCtx, cancelPollers := context.WithTimeout(context.Background(), 5*time.Minute) sendPollers := func() { - for j := 0; j < numTaskQueues; j++ { + for j := range numTaskQueues { go s.pollFromDeployment(pollCtx, tv.WithBuildIDNumber(i).WithTaskQueueNumber(j)) } } @@ -2273,7 +2284,7 @@ func (s *WorkerDeploymentSuite) TestConcurrentPollers_ManyTaskQueues_RapidRoutin defer cancel() // Rapidly perform 20 setCurrent and setRamping operations, each targeting one of the 3 versions - for i := 0; i < numOperations; i++ { + for i := range numOperations { // Alternate between setCurrent and setRamping targetVersion := i % numVersions versionTV := tv.WithBuildIDNumber(targetVersion) @@ -2331,7 +2342,7 @@ func (s *WorkerDeploymentSuite) TestConcurrentPollers_ManyTaskQueues_RapidRoutin // Verify that the routing info revision number in each of the task queues matches the latest revision number // Note: The public API doesn't expose revision numbers at the task queue level, so we verify that the // versioning info has been propagated correctly by checking the current/ramping versions - for j := 0; j < numTaskQueues; j++ { + for j := range numTaskQueues { tqTV := tv.WithTaskQueueNumber(j) tqUD, err := s.GetTestCluster().MatchingClient().GetTaskQueueUserData(ctx, &matchingservice.GetTaskQueueUserDataRequest{ NamespaceId: s.NamespaceID().String(), @@ -2356,7 +2367,7 @@ func (s *WorkerDeploymentSuite) TestResourceExhaustedErrors_Converted_To_Readabl errChan := make(chan error, versions) // Start all version workflows first - for i := 0; i < versions; i++ { + for i := range versions { s.startVersionWorkflow(ctx, tv.WithBuildIDNumber(i)) } @@ -2409,14 +2420,14 @@ func (s *WorkerDeploymentSuite) testConcurrentRequestsResourceExhausted( requestFn func(int) error, ) { // Launch concurrent requests - for i := 0; i < versions; i++ { + for i := range versions { go func(i int) { errChan <- requestFn(i) }(i) } // Expect ResourceExhausted errors to be converted to Internal errors with the appropriate message - for i := 0; i < versions; i++ { + for range versions { err := <-errChan if err != nil { switch err.(type) { @@ -2462,7 +2473,7 @@ func (s *WorkerDeploymentSuite) TestSetWorkerDeploymentRampingVersion_Unversione Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), }) - s.Nil(err) + s.NoError(err) s.Equal(worker_versioning.UnversionedVersionId, resp.GetWorkerDeploymentInfo().GetRoutingConfig().GetRampingVersion()) // check that the current version's task queues have ramping version == __unversioned__ @@ -2606,7 +2617,7 @@ func (s *WorkerDeploymentSuite) verifyTaskQueueVersioningInfo(ctx context.Contex TaskQueue: tq, }) a := require.New(t) - a.Nil(err) + a.NoError(err) a.Equal(worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(expectedCurrentVersion), tqDesc.GetVersioningInfo().GetCurrentDeploymentVersion()) a.Equal(worker_versioning.ExternalWorkerDeploymentVersionFromStringV31(expectedRampingVersion), tqDesc.GetVersioningInfo().GetRampingDeploymentVersion()) a.Equal(expectedCurrentVersion, tqDesc.GetVersioningInfo().GetCurrentVersion()) //nolint:staticcheck // SA1019: old worker versioning @@ -2641,7 +2652,7 @@ func (s *WorkerDeploymentSuite) TestDrainRollbackedVersion() { DeploymentName: tv1.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv1.DeploymentSeries(), CreateTime: v1CreateTime, @@ -2679,7 +2690,7 @@ func (s *WorkerDeploymentSuite) TestDrainRollbackedVersion() { }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv2.DeploymentSeries(), CreateTime: v1CreateTime, @@ -2734,12 +2745,13 @@ func (s *WorkerDeploymentSuite) TestDrainRollbackedVersion() { // Verify that the drainageStatus of v1 has been updated in the VersionSummaries s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) resp, err = s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv2.DeploymentSeries(), }) - s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + a.NoError(err) + s.verifyDescribeWorkerDeployment(a, resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv2.DeploymentSeries(), CreateTime: v1CreateTime, @@ -2788,12 +2800,13 @@ func (s *WorkerDeploymentSuite) TestDrainRollbackedVersion() { // verify if the right information is set in the DescribeWorkerDeployment response s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) resp, err = s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv2.DeploymentSeries(), }) - s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + a.NoError(err) + s.verifyDescribeWorkerDeployment(a, resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv2.DeploymentSeries(), CreateTime: v1CreateTime, @@ -2853,12 +2866,13 @@ func (s *WorkerDeploymentSuite) TestDrainRollbackedVersion() { // verify if the right information is set in the DescribeWorkerDeployment response s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) resp, err = s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv1.DeploymentSeries(), }) - s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + a.NoError(err) + s.verifyDescribeWorkerDeployment(a, resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv1.DeploymentSeries(), CreateTime: v1CreateTime, @@ -2922,12 +2936,13 @@ func (s *WorkerDeploymentSuite) TestDrainRollbackedVersion() { // Verify that v1, which was rolled back to being current previously, is drained with it's information present // in the deployment workflow. s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) resp, err = s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv1.DeploymentSeries(), }) - s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + a.NoError(err) + s.verifyDescribeWorkerDeployment(a, resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv1.DeploymentSeries(), CreateTime: v1CreateTime, @@ -3004,7 +3019,7 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_AfterDrained() { DeploymentName: tv1.DeploymentSeries(), }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv1.DeploymentSeries(), CreateTime: v1CreateTime, @@ -3042,7 +3057,7 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_AfterDrained() { }) s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + s.verifyDescribeWorkerDeployment(s.Require(), resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv2.DeploymentSeries(), CreateTime: v1CreateTime, @@ -3097,12 +3112,13 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_AfterDrained() { // Verify that the drainageStatus of v1 has been updated in the VersionSummaries s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) resp, err = s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv2.DeploymentSeries(), }) - s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + a.NoError(err) + s.verifyDescribeWorkerDeployment(a, resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv2.DeploymentSeries(), CreateTime: v1CreateTime, @@ -3151,12 +3167,13 @@ func (s *WorkerDeploymentSuite) TestSetRampingVersion_AfterDrained() { // verify if the right information is set in the DescribeWorkerDeployment response s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) resp, err = s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv2.DeploymentSeries(), }) - s.NoError(err) - s.verifyDescribeWorkerDeployment(resp, &workflowservice.DescribeWorkerDeploymentResponse{ + a.NoError(err) + s.verifyDescribeWorkerDeployment(a, resp, &workflowservice.DescribeWorkerDeploymentResponse{ WorkerDeploymentInfo: &deploymentpb.WorkerDeploymentInfo{ Name: tv2.DeploymentSeries(), CreateTime: v1CreateTime, @@ -3237,7 +3254,7 @@ func (s *WorkerDeploymentSuite) TestDeleteWorkerDeployment_ValidDelete() { } err = s.SendSignal(s.Namespace().String(), workflowExecution, workerdeployment.SyncDrainageSignalName, signalPayload, tv1.ClientIdentity()) - s.Nil(err) + s.NoError(err) // Stop the poller so it doesn't keep polling pollerCancel() @@ -3267,7 +3284,7 @@ func (s *WorkerDeploymentSuite) TestDeleteWorkerDeployment_ValidDelete() { for _, vs := range resp.GetWorkerDeploymentInfo().GetVersionSummaries() { //nolint:staticcheck // SA1019 deprecated Version will clean up later a.NotEqual(tv1.DeploymentVersionString(), vs.Version) - s.False(proto.Equal(tv1.ExternalDeploymentVersion(), vs.GetDeploymentVersion())) + a.False(proto.Equal(tv1.ExternalDeploymentVersion(), vs.GetDeploymentVersion())) } }, time.Second*5, time.Millisecond*200) @@ -3277,7 +3294,7 @@ func (s *WorkerDeploymentSuite) TestDeleteWorkerDeployment_ValidDelete() { DeploymentName: tv1.DeploymentSeries(), Identity: tv1.ClientIdentity(), }) - s.Nil(err) + s.NoError(err) // Describe Worker Deployment should give not found s.EventuallyWithT(func(t *assert.CollectT) { @@ -3288,7 +3305,7 @@ func (s *WorkerDeploymentSuite) TestDeleteWorkerDeployment_ValidDelete() { }) a.Error(err) var nfe *serviceerror.NotFound - a.True(errors.As(err, &nfe)) + a.ErrorAs(err, &nfe) }, time.Second*5, time.Millisecond*200) // ListDeployments should not show the closed/deleted Worker Deployment @@ -3297,7 +3314,7 @@ func (s *WorkerDeploymentSuite) TestDeleteWorkerDeployment_ValidDelete() { listResp, err := s.FrontendClient().ListWorkerDeployments(ctx, &workflowservice.ListWorkerDeploymentsRequest{ Namespace: s.Namespace().String(), }) - a.Nil(err) + a.NoError(err) for _, dInfo := range listResp.GetWorkerDeployments() { a.NotEqual(tv1.DeploymentSeries(), dInfo.GetName()) } @@ -3370,7 +3387,7 @@ func (s *WorkerDeploymentSuite) tryDeleteVersion( Identity: tv.ClientIdentity(), }) if expectedError == "" { - s.Nil(err) + s.NoError(err) } else { s.Error(err) s.Contains(err.Error(), expectedError) @@ -3431,9 +3448,9 @@ func (s *WorkerDeploymentSuite) verifyRoutingConfig(a *require.Assertions, expec } func (s *WorkerDeploymentSuite) verifyWorkerDeploymentInfo(a *require.Assertions, expected, actual *deploymentpb.WorkerDeploymentInfo) { - a.True((actual == nil) == (expected == nil)) + a.Equal((actual == nil), (expected == nil)) a.Equal(expected.GetName(), actual.GetName()) - a.True((actual.GetRoutingConfig() == nil) == (expected.GetRoutingConfig() == nil)) + a.Equal((actual.GetRoutingConfig() == nil), (expected.GetRoutingConfig() == nil)) a.Equal(expected.GetLastModifierIdentity(), actual.GetLastModifierIdentity()) s.verifyTimestampWithinRange(a, expected.GetCreateTime(), actual.GetCreateTime(), @@ -3455,11 +3472,16 @@ func (s *WorkerDeploymentSuite) verifyWorkerDeploymentInfo(a *require.Assertions } func (s *WorkerDeploymentSuite) verifyDescribeWorkerDeployment( + a *require.Assertions, actualResp *workflowservice.DescribeWorkerDeploymentResponse, expectedResp *workflowservice.DescribeWorkerDeploymentResponse, ) { - s.True((actualResp == nil) == (expectedResp == nil)) - s.verifyWorkerDeploymentInfo(s.Assertions, expectedResp.GetWorkerDeploymentInfo(), actualResp.GetWorkerDeploymentInfo()) + if expectedResp == nil { + a.Nil(actualResp) + } else { + a.NotNil(actualResp) + } + s.verifyWorkerDeploymentInfo(a, expectedResp.GetWorkerDeploymentInfo(), actualResp.GetWorkerDeploymentInfo()) } func (s *WorkerDeploymentSuite) setAndVerifyRampingVersion( @@ -3619,7 +3641,7 @@ func (s *WorkerDeploymentSuite) setAndValidateManagerIdentity(ctx context.Contex func (s *WorkerDeploymentSuite) createVersionsInDeployments(ctx context.Context, tv *testvars.TestVars, n int) []*workflowservice.ListWorkerDeploymentsResponse_WorkerDeploymentSummary { var expectedDeploymentSummaries []*workflowservice.ListWorkerDeploymentsResponse_WorkerDeploymentSummary - for i := 0; i < n; i++ { + for i := range n { deployment := tv.WithDeploymentSeriesNumber(i) version := deployment.WithBuildIDNumber(i) @@ -3720,6 +3742,7 @@ func (s *WorkerDeploymentSuite) startAndValidateWorkerDeployments( actualDeploymentSummaries, err := s.listWorkerDeployments(ctx, request) a.NoError(err) if len(actualDeploymentSummaries) < len(expectedDeploymentSummaries) { + a.Failf("not enough deployment summaries", "got %d, want at least %d", len(actualDeploymentSummaries), len(expectedDeploymentSummaries)) return } @@ -3740,6 +3763,19 @@ func (s *WorkerDeploymentSuite) startAndValidateWorkerDeployments( }, time.Second*10, time.Millisecond*1000) } +func (s *WorkerDeploymentSuite) validateWorkerDeploymentCount( + ctx context.Context, + request *workflowservice.ListWorkerDeploymentsRequest, + expectedCount int, +) { + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + actualDeploymentSummaries, err := s.listWorkerDeployments(ctx, request) + a.NoError(err) + a.Len(actualDeploymentSummaries, expectedCount) + }, time.Second*5, time.Millisecond*200) +} + func (s *WorkerDeploymentSuite) buildWorkerDeploymentSummary( deploymentName string, createTime *timestamppb.Timestamp, routingConfig *deploymentpb.RoutingConfig, @@ -3757,6 +3793,266 @@ func (s *WorkerDeploymentSuite) buildWorkerDeploymentSummary( } } +func (s *WorkerDeploymentSuite) TestCreateWorkerDeployment_Success() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + tv := testvars.New(s) + + deploymentName := tv.DeploymentSeries() + requestID := tv.Any().String() + identity := tv.Any().String() + + // Create a new worker deployment + resp, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + Identity: identity, + RequestId: requestID, + }) + + s.NoError(err) + s.NotNil(resp) + s.NotEmpty(resp.ConflictToken) + + // Verify the deployment was created + descResp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + }) + s.NoError(err) + s.NotNil(descResp) + s.NotNil(descResp.WorkerDeploymentInfo) + s.Equal(deploymentName, descResp.WorkerDeploymentInfo.Name) + s.Equal(identity, descResp.WorkerDeploymentInfo.LastModifierIdentity) + s.NotNil(descResp.WorkerDeploymentInfo.CreateTime) + s.Empty(descResp.WorkerDeploymentInfo.VersionSummaries) // No versions initially +} + +func (s *WorkerDeploymentSuite) TestCreateWorkerDeployment_Idempotent() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + tv := testvars.New(s) + + deploymentName := tv.DeploymentSeries() + requestID := tv.Any().String() + + // Create a worker deployment + resp1, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: requestID, + }) + s.NoError(err) + s.NotNil(resp1) + token1 := resp1.ConflictToken + + // Create the same deployment again with same request ID - should be idempotent + resp2, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: requestID, + }) + s.NoError(err) + s.NotNil(resp2) + s.Equal(token1, resp2.ConflictToken) // Should get the same conflict token +} + +func (s *WorkerDeploymentSuite) TestCreateWorkerDeployment_AlreadyExists_DifferentRequestID() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + tv := testvars.New(s) + + deploymentName := tv.DeploymentSeries() + requestID1 := tv.Any().String() + requestID2 := tv.Any().String() + + // Create a worker deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: requestID1, + }) + s.NoError(err) + + // Try to create the same deployment with different request ID - should fail + _, err = s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: requestID2, + }) + s.Error(err) + var alreadyExists *serviceerror.AlreadyExists + s.ErrorAs(err, &alreadyExists) + s.Contains(alreadyExists.Message, deploymentName) +} + +func (s *WorkerDeploymentSuite) TestCreateWorkerDeployment_AutoCreatedByPoller_ConflictWithExplicitCreate() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + tv := testvars.New(s) + + // First, create deployment via polling + go s.pollFromDeployment(ctx, tv) + s.ensureCreateDeployment(tv) + + // Try to explicitly create the same deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + RequestId: tv.Any().String(), + }) + s.Error(err) + var alreadyExists *serviceerror.AlreadyExists + s.ErrorAs(err, &alreadyExists) + s.Contains(alreadyExists.Message, "auto-created from worker polls") +} + +func (s *WorkerDeploymentSuite) TestCreateWorkerDeployment_InvalidDeploymentName() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + testCases := []struct { + name string + deploymentName string + expectedError string + }{ + { + name: "empty name", + deploymentName: "", + expectedError: "deployment name cannot be empty", + }, + { + name: "name with dot", + deploymentName: "test.deployment", + expectedError: "worker deployment name cannot contain '.'", + }, + { + name: "name starting with __", + deploymentName: "__reserved", + expectedError: "WorkerDeploymentName cannot start with '__'", + }, + { + name: "name too long", + deploymentName: strings.Repeat("a", 1001), + expectedError: "size of WorkerDeploymentName larger than the maximum allowed", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tc.deploymentName, + RequestId: testvars.New(s).Any().String(), + }) + s.Error(err) + var invalidArg *serviceerror.InvalidArgument + s.ErrorAs(err, &invalidArg) + s.Contains(invalidArg.Message, tc.expectedError) + }) + } +} + +func (s *WorkerDeploymentSuite) TestCreateWorkerDeployment_MaxDeploymentsLimit() { + // TODO (carly): check the error messages that poller receives in each case and make sense they are informative and appropriate (e.g. do not expose internal stuff) + // Also in TestNamespaceDeploymentsLimit + s.T().Skip() // Need to separate this test so other tests do not create deployment in the same NS + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Override the max deployments limit for this test + s.OverrideDynamicConfig(dynamicconfig.MatchingMaxDeployments, 2) + + tv := testvars.New(s) + + // Create first deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries() + "_1", + RequestId: tv.Any().String(), + }) + s.NoError(err) + + // Create second deployment + _, err = s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries() + "_2", + RequestId: tv.Any().String(), + }) + s.NoError(err) + + // wait for all existing deployments to show up in visibility + s.validateWorkerDeploymentCount(ctx, &workflowservice.ListWorkerDeploymentsRequest{Namespace: s.Namespace().String()}, 2) + + // Try to create third deployment - should fail + _, err = s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries() + "_3", + RequestId: tv.Any().String(), + }) + s.Error(err) + var resourceExhausted *serviceerror.ResourceExhausted + s.ErrorAs(err, &resourceExhausted) + s.Contains(resourceExhausted.Message, "reached maximum deployments in namespace") + s.Equal(enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, resourceExhausted.Scope) + s.Equal(enumspb.RESOURCE_EXHAUSTED_CAUSE_WORKER_DEPLOYMENT_LIMITS, resourceExhausted.Cause) +} + +func (s *WorkerDeploymentSuite) TestCreateWorkerDeployment_AfterDelete_CanRecreate() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + tv := testvars.New(s) + + deploymentName := tv.DeploymentSeries() + requestID1 := tv.Any().String() + requestID2 := tv.Any().String() + + // Create a worker deployment + resp1, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: requestID1, + }) + s.NoError(err) + s.NotNil(resp1) + + // Delete the deployment + _, err = s.FrontendClient().DeleteWorkerDeployment(ctx, &workflowservice.DeleteWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + Identity: "test", + }) + s.NoError(err) + + // Should be able to create a deployment with the same name again + resp2, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: requestID2, + }) + s.NoError(err) + s.NotNil(resp2) + s.NotEqual(resp1.ConflictToken, resp2.ConflictToken) // Should be a new deployment with different token + + // Verify the deployment was created + descResp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + }) + s.NoError(err) + s.NotNil(descResp) + s.NotNil(descResp.WorkerDeploymentInfo) + s.Equal(deploymentName, descResp.WorkerDeploymentInfo.Name) + s.NotNil(descResp.WorkerDeploymentInfo.CreateTime) + s.Empty(descResp.WorkerDeploymentInfo.VersionSummaries) // No versions initially +} + // Name is used by testvars. We use a shortened test name in variables so that physical task queue IDs // do not grow larger than DB column limit (currently as low as 272 chars). func (s *WorkerDeploymentSuite) Name() string { diff --git a/tests/worker_deployment_version_test.go b/tests/worker_deployment_version_test.go index 2bc3b97c1a8..25b0bcca71b 100644 --- a/tests/worker_deployment_version_test.go +++ b/tests/worker_deployment_version_test.go @@ -17,15 +17,19 @@ import ( "github.com/stretchr/testify/suite" batchpb "go.temporal.io/api/batch/v1" commonpb "go.temporal.io/api/common/v1" + computepb "go.temporal.io/api/compute/v1" deploymentpb "go.temporal.io/api/deployment/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" + computeprovider "go.temporal.io/auto-scaled-workers/wci/workflow/compute_provider" sdkclient "go.temporal.io/sdk/client" "go.temporal.io/sdk/worker" "go.temporal.io/sdk/workflow" deploymentspb "go.temporal.io/server/api/deployment/v1" + "go.temporal.io/server/api/matchingservice/v1" + persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/testing/testhooks" "go.temporal.io/server/common/testing/testvars" @@ -39,10 +43,15 @@ import ( ) const ( - maxConcurrentBatchOperations = 3 - testVersionDrainageRefreshInterval = 3 * time.Second - testVersionDrainageVisibilityGracePeriod = 3 * time.Second - testMaxVersionsInDeployment = 5 + maxConcurrentBatchOperations = 3 + testVersionDrainageRefreshInterval = 3 * time.Second + testVersionDrainageVisibilityGracePeriod = 3 * time.Second + testLongVersionDrainageRefreshInterval = 10 * time.Second + testLongVersionDrainageVisibilityGracePeriod = 10 * time.Second + testExtraLongVersionDrainageRefreshInterval = 30 * time.Second + testVersionMembershipCacheTTL = 5 * time.Second + testLongVersionReactivationCacheTTL = 5 * time.Minute + testMaxVersionsInDeployment = 4 ) type ( @@ -58,12 +67,7 @@ var ( testRandomMetadataValue = []byte("random metadata value") ) -func TestDeploymentVersionSuiteV0(t *testing.T) { - t.Parallel() - suite.Run(t, &DeploymentVersionSuite{workflowVersion: workerdeployment.InitialVersion, useV32: true}) -} - -func TestDeploymentVersionSuiteV2(t *testing.T) { +func TestDeploymentVersionSuite(t *testing.T) { t.Parallel() suite.Run(t, &DeploymentVersionSuite{workflowVersion: workerdeployment.VersionDataRevisionNumber, useV32: true}) } @@ -84,8 +88,10 @@ func (s *DeploymentVersionSuite) SetupSuite() { dynamicconfig.VersionDrainageStatusRefreshInterval.Key(): testVersionDrainageRefreshInterval, dynamicconfig.VersionDrainageStatusVisibilityGracePeriod.Key(): testVersionDrainageVisibilityGracePeriod, - dynamicconfig.VersionMembershipCacheTTL.Key(): 5 * time.Second, - dynamicconfig.VersionReactivationSignalCacheTTL.Key(): 5 * time.Minute, // Large TTL for deduplication test + dynamicconfig.VersionMembershipCacheTTL.Key(): testVersionMembershipCacheTTL, + + // Large TTL for deduplication test. Must be set at suite level for cache initialization to work. + dynamicconfig.VersionReactivationSignalCacheTTL.Key(): testLongVersionReactivationCacheTTL, })) } @@ -143,7 +149,9 @@ func (s *DeploymentVersionSuite) startVersionWorkflow(ctx context.Context, tv *t s.EventuallyWithT(func(t *assert.CollectT) { a := assert.New(t) resp, err := s.describeVersion(tv) - a.NoError(err) + if !a.NoError(err) { + return + } // regardless of s.useV32, we want to read both version formats a.Equal(tv.DeploymentVersionString(), resp.GetWorkerDeploymentVersionInfo().GetVersion()) a.Equal(tv.ExternalDeploymentVersion().GetDeploymentName(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetDeploymentName()) @@ -154,7 +162,9 @@ func (s *DeploymentVersionSuite) startVersionWorkflow(ctx context.Context, tv *t Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), }) - a.NoError(err) + if !a.NoError(err) { + return + } var versionSummaryNames []string var versionSummaryVersions []*deploymentpb.WorkerDeploymentVersion for _, versionSummary := range newResp.GetWorkerDeploymentInfo().GetVersionSummaries() { @@ -177,7 +187,9 @@ func (s *DeploymentVersionSuite) startVersionWorkflowExpectFailAddVersion(ctx co Identity: "random", DeploymentOptions: tv.WorkerDeploymentOptions(true), }) - s.Error(err, serviceerror.NewUnavailable("cannot add version, already at max versions 4")) + var resourceExhausted *serviceerror.ResourceExhausted + s.ErrorAs(err, &resourceExhausted) + s.Contains(resourceExhausted.Message, "maximum number of versions") } func (s *DeploymentVersionSuite) TestForceCAN_NoOpenWFS() { @@ -213,8 +225,8 @@ func (s *DeploymentVersionSuite) TestForceCAN_NoOpenWFS() { a.Equal(tv.ExternalDeploymentVersion().GetDeploymentName(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetDeploymentName()) a.Equal(tv.ExternalDeploymentVersion().GetBuildId(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetBuildId()) - a.Equal(1, len(resp.GetVersionTaskQueues())) - a.Equal(1, len(resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos())) + a.Len(resp.GetVersionTaskQueues(), 1) + a.Len(resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos(), 1) // verify that the version state is intact even after a CAN a.Equal(tv.TaskQueue().GetName(), resp.GetVersionTaskQueues()[0].Name) @@ -291,7 +303,9 @@ func (s *DeploymentVersionSuite) TestForceCAN_WithOverrideState() { // Verify the metadata from override state is present entries := resp.GetWorkerDeploymentVersionInfo().GetMetadata().GetEntries() - a.Len(entries, 1) + if !a.Len(entries, 1) { + return + } a.Equal([]byte("override-value"), entries["override-key"].Data) }, time.Second*10, time.Millisecond*1000) } @@ -318,10 +332,10 @@ func (s *DeploymentVersionSuite) TestDescribeVersion_RegisterTaskQueue() { a.Equal(tv.ExternalDeploymentVersion().GetBuildId(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetBuildId()) a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, resp.GetWorkerDeploymentVersionInfo().GetStatus()) - a.Equal(numberOfDeployments, len(resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos())) + a.Len(resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos(), numberOfDeployments) a.Equal(tv.TaskQueue().GetName(), resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos()[0].Name) - a.Equal(numberOfDeployments, len(resp.GetVersionTaskQueues())) + a.Len(resp.GetVersionTaskQueues(), numberOfDeployments) a.Equal(tv.TaskQueue().GetName(), resp.GetVersionTaskQueues()[0].Name) }, time.Second*5, time.Millisecond*200) } @@ -334,9 +348,9 @@ func (s *DeploymentVersionSuite) TestDescribeVersion_RegisterTaskQueue_Concurren root, err := tqid.PartitionFromProto(tv.TaskQueue(), s.Namespace().String(), enumspb.TASK_QUEUE_TYPE_WORKFLOW) s.NoError(err) // Making concurrent polls to 4 partitions, 3 polls to each - for p := 0; p < 4; p++ { + for p := range 4 { tv2 := tv.WithTaskQueue(root.TaskQueue().NormalPartition(p).RpcName()) - for i := 0; i < 3; i++ { + for range 3 { go s.pollFromDeployment(ctx, tv2) go s.pollActivityFromDeployment(ctx, tv2) } @@ -354,9 +368,9 @@ func (s *DeploymentVersionSuite) TestDescribeVersion_RegisterTaskQueue_Concurren a.Equal(tv.ExternalDeploymentVersion().GetDeploymentName(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetDeploymentName()) a.Equal(tv.ExternalDeploymentVersion().GetBuildId(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetBuildId()) a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, resp.GetWorkerDeploymentVersionInfo().GetStatus()) - a.Equal(2, len(resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos())) + a.Len(resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos(), 2) a.Equal(tv.TaskQueue().GetName(), resp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos()[0].Name) - a.Equal(2, len(resp.GetVersionTaskQueues())) + a.Len(resp.GetVersionTaskQueues(), 2) a.Equal(tv.TaskQueue().GetName(), resp.GetVersionTaskQueues()[0].Name) }, time.Second*10, time.Millisecond*1000) } @@ -389,32 +403,32 @@ func (s *DeploymentVersionSuite) TestDrainageStatus_SetCurrentVersion_NoOpenWFs( s.startVersionWorkflow(ctx, tv2) // non-current deployments have never been used and have no drainage info - s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, false, false) - s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, false, false) + s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, 0) + s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, 0) // SetCurrent tv1 err := s.setCurrent(tv1, true) - s.Nil(err) + s.NoError(err) // Both versions have no drainage info and tv1 has it's status updated to current - s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, false, false) - s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, false, false) + s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, 0) + s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, 0) baseTime := time.Now() // SetCurrent tv2 --> tv1 starts the child drainage workflow err = s.setCurrent(tv2, true) - s.Nil(err) + s.NoError(err) changed1, checked1 := s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{ Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, - }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, false, false) + }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, 0) s.Greater(changed1, baseTime) s.GreaterOrEqual(checked1, changed1) // tv1 should now be "drained" changed2, checked2 := s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{ Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, - }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, true, false) + }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, testVersionDrainageVisibilityGracePeriod) s.Greater(changed2, changed1) s.GreaterOrEqual(checked2, changed2) } @@ -432,16 +446,16 @@ func (s *DeploymentVersionSuite) TestDrainageStatus_SetCurrentVersion_YesOpenWFs s.startVersionWorkflow(ctx, tv2) // non-current deployments have never been used and have no drainage info - s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, false, false) - s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, false, false) + s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, 0) + s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, 0) // SetCurrent tv1 err := s.setCurrent(tv1, true) - s.Nil(err) + s.NoError(err) // both versions have no drainage info and tv1 has it's status updated to current - s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, false, false) - s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, false, false) + s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, 0) + s.checkVersionDrainageAndVersionStatus(ctx, tv2, &deploymentpb.VersionDrainageInfo{}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, 0) // start a pinned workflow on v1 run := s.startPinnedWorkflow(ctx, tv1) @@ -449,26 +463,26 @@ func (s *DeploymentVersionSuite) TestDrainageStatus_SetCurrentVersion_YesOpenWFs baseTime := time.Now() // SetCurrent tv2 --> tv1 starts the child drainage workflow err = s.setCurrent(tv2, true) - s.Nil(err) + s.NoError(err) // tv1 should now be "draining" for visibilityGracePeriod duration changed1, checked1 := s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{ Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, - }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, false, false) + }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, 0) s.Greater(changed1, baseTime) s.GreaterOrEqual(checked1, changed1) // tv1 should still be "draining" for visibilityGracePeriod duration changed2, checked2 := s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{ Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, - }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, true, false) + }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, testVersionDrainageVisibilityGracePeriod) s.Equal(changed2, changed1) s.Greater(checked2, checked1) // tv1 should still be "draining" after a refresh intervals changed3, checked3 := s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{ Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, - }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, false, true) + }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, testVersionDrainageRefreshInterval) s.Equal(changed3, changed1) s.Greater(checked3, checked2) @@ -482,12 +496,12 @@ func (s *DeploymentVersionSuite) TestDrainageStatus_SetCurrentVersion_YesOpenWFs Reason: "test", Identity: tv1.ClientIdentity(), }) - s.Nil(err) + s.NoError(err) // tv1 should now be "drained" changed4, checked4 := s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{ Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, - }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, false, false) + }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, 0) s.Greater(changed4, changed3) s.GreaterOrEqual(checked4, changed4) } @@ -537,7 +551,7 @@ func (s *DeploymentVersionSuite) TestVersionIgnoresDrainageSignalWhenCurrentOrRa // Make it current err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // Signal it to be drained. Only do this in tests. versionWorkflowID := workerdeployment.GenerateVersionWorkflowID(tv1.DeploymentSeries(), tv1.BuildID()) @@ -562,7 +576,7 @@ func (s *DeploymentVersionSuite) TestVersionIgnoresDrainageSignalWhenCurrentOrRa }, } err = s.SendSignal(s.Namespace().String(), workflowExecution, workerdeployment.SyncDrainageSignalName, signalPayload, tv1.ClientIdentity()) - s.Nil(err) + s.NoError(err) // describe version and confirm that it is not drained // add a 3s time requirement so that it does not succeed immediately @@ -591,7 +605,7 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_DeleteCurrentVersion() { // Set version as current err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // Deleting this version should fail since the version is current s.tryDeleteVersion(ctx, tv1, fmt.Sprintf(workerdeployment.ErrVersionIsCurrentOrRamping, tv1.DeploymentVersionStringV32()), false) @@ -621,7 +635,7 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_DeleteRampedVersion() { // Set version as ramping err := s.setRamping(tv1, 0) - s.Nil(err) + s.NoError(err) // Deleting this version should fail since the version is ramping s.tryDeleteVersion(ctx, tv1, fmt.Sprintf(workerdeployment.ErrVersionIsCurrentOrRamping, tv1.DeploymentVersionStringV32()), false) @@ -683,7 +697,7 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_DrainingVersion() { // Make the version current err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // Start another version workflow tv2 := testvars.New(s).WithBuildIDNumber(2) @@ -691,14 +705,14 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_DrainingVersion() { // Setting this version to current should start the drainage workflow for version1 and make it draining err = s.setCurrent(tv2, true) - s.Nil(err) + s.NoError(err) // Version should be draining s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{ Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, LastChangedTime: nil, // don't test this now LastCheckedTime: nil, // don't test this now - }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, false, false) + }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, 0) // delete should fail s.tryDeleteVersion(ctx, tv1, fmt.Sprintf(workerdeployment.ErrVersionIsDraining, tv1.DeploymentVersionStringV32()), false) @@ -715,7 +729,7 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_Drained_But_Pollers_Exist() { // Make the version current err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // Start another version workflow tv2 := testvars.New(s).WithBuildIDNumber(2) @@ -723,7 +737,7 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_Drained_But_Pollers_Exist() { // Setting this version to current should start the drainage workflow for version1 err = s.setCurrent(tv2, true) - s.Nil(err) + s.NoError(err) // Signal the first version to be drained. Only do this in tests. s.signalAndWaitForDrained(ctx, tv1) @@ -755,7 +769,7 @@ func (s *DeploymentVersionSuite) signalAndWaitForDrained(ctx context.Context, tv }, } err = s.SendSignal(s.Namespace().String(), workflowExecution, workerdeployment.SyncDrainageSignalName, signalPayload, tv.ClientIdentity()) - s.Nil(err) + s.NoError(err) // wait for drained s.EventuallyWithT(func(t *assert.CollectT) { @@ -822,35 +836,34 @@ func (s *DeploymentVersionSuite) waitForNoPollers(ctx context.Context, tv *testv } func (s *DeploymentVersionSuite) TestVersionScavenger_DeleteOnAdd() { - testMaxVersionsInDeployment := 4 s.OverrideDynamicConfig(dynamicconfig.PollerHistoryTTL, 3*time.Second) s.OverrideDynamicConfig(dynamicconfig.MatchingMaxVersionsInDeployment, testMaxVersionsInDeployment) // we don't want the version to drain in this test s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 60*time.Second) s.OverrideDynamicConfig(dynamicconfig.TaskQueueInfoByBuildIdTTL, 0) // Set deployment register error backoff to zero so to speed up the test. - s.InjectHook(testhooks.MatchingDeploymentRegisterErrorBackoff, 0*time.Second) + s.InjectHook(testhooks.NewHook(testhooks.MatchingDeploymentRegisterErrorBackoff, 0*time.Second)) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() tvs := make([]*testvars.TestVars, testMaxVersionsInDeployment) // max out the versions - for i := 0; i < testMaxVersionsInDeployment; i++ { + for i := range testMaxVersionsInDeployment { tvs[i] = testvars.New(s).WithBuildIDNumber(i) s.startVersionWorkflow(ctx, tvs[i]) } // Make tvs[0] current err := s.setCurrent(tvs[0], false) - s.Nil(err) + s.NoError(err) // Make tvs[1] current, hence tvs[0] should go to draining err = s.setCurrent(tvs[1], false) - s.Nil(err) + s.NoError(err) // CI can be slow, keep sending fresh polls to ensure that auto deletion logic sees them when we want to add tvMax so it can't add. pollContext, cancelPolls := context.WithTimeout(context.Background(), 3*time.Second) go func() { - for i := 0; i < testMaxVersionsInDeployment; i++ { + for i := range testMaxVersionsInDeployment { go s.pollFromDeployment(pollContext, tvs[i]) } @@ -860,7 +873,7 @@ func (s *DeploymentVersionSuite) TestVersionScavenger_DeleteOnAdd() { case <-pollContext.Done(): return case <-t.C: - for i := 0; i < testMaxVersionsInDeployment; i++ { + for i := range testMaxVersionsInDeployment { go s.pollFromDeployment(pollContext, tvs[i]) } } @@ -940,6 +953,12 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_ValidDelete() { s.tryDeleteVersion(ctx, tv1, "", false) } +func (s *DeploymentVersionSuite) skipBeforeVersion(version workerdeployment.DeploymentWorkflowVersion) { + if s.workflowVersion < version { + s.T().Skipf("test supports version %v and newer", version) + } +} + func (s *DeploymentVersionSuite) TestDeleteVersion_ValidDelete_SkipDrainage() { s.OverrideDynamicConfig(dynamicconfig.PollerHistoryTTL, 500*time.Millisecond) @@ -991,7 +1010,7 @@ func (s *DeploymentVersionSuite) TestDeleteVersion_ValidDelete_SkipDrainage() { _, err := s.describeVersion(tv1) a.Error(err) var nfe *serviceerror.NotFound - a.True(errors.As(err, &nfe)) + a.ErrorAs(err, &nfe) }, time.Second*5, time.Millisecond*200) } @@ -1063,7 +1082,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_InvalidSetCurrentV // SetCurrent so that the task queue puts the version in its versions info err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // new version with a different registered task-queue tv2 := testvars.New(s).WithBuildIDNumber(2).WithTaskQueue(testvars.New(s.T()).Any().String()) @@ -1073,7 +1092,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_InvalidSetCurrentV pollerCancel1() // Start a workflow on task_queue_1 to increase the add rate - s.startWorkflow(tv1, tv1.VersioningOverridePinned(s.useV32)) + s.startWorkflow(tv1, tv1.VersioningOverridePinned()) // SetCurrent tv2 err = s.setCurrent(tv2, false) @@ -1093,7 +1112,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_ValidSetCurrentVer // SetCurrent so that the task queue puts the version in its versions info err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // new version with a different registered task-queue tv2 := tv.WithBuildIDNumber(2).WithTaskQueue(tv.Any().String()) @@ -1103,7 +1122,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_ValidSetCurrentVer err = s.setCurrent(tv2, false) // SetCurrent tv2 should succeed as task_queue_1, despite missing from the new current version, has no backlogged tasks/add-rate > 0 - s.Nil(err) + s.NoError(err) } func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_InvalidSetRampingVersion() { @@ -1121,7 +1140,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_InvalidSetRampingV // SetCurrent so that the task queue puts the version in its versions info err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // new version with a different registered task-queue tv2 := tv.WithBuildIDNumber(2).WithTaskQueue(tv.Any().String()) @@ -1131,7 +1150,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_InvalidSetRampingV pollerCancel1() // Start a workflow on task_queue_1 to increase the add rate - s.startWorkflow(tv1, tv1.VersioningOverridePinned(s.useV32)) + s.startWorkflow(tv1, tv1.VersioningOverridePinned()) // SetRampingVersion to tv2 err = s.setRamping(tv2, 0) @@ -1152,7 +1171,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_ValidSetRampingVer // SetCurrent so that the task queue puts the version in its versions info err := s.setCurrent(tv1, false) - s.Nil(err) + s.NoError(err) // new version with a different registered task-queue tv2 := tv.WithBuildIDNumber(2).WithTaskQueue(tv.Any().String()) @@ -1162,7 +1181,7 @@ func (s *DeploymentVersionSuite) TestVersionMissingTaskQueues_ValidSetRampingVer err = s.setRamping(tv2, 0) // SetRampingVersion to tv2 should succeed as task_queue_1, despite missing from the new current version, has no backlogged tasks/add-rate > 0 - s.Nil(err) + s.NoError(err) } func (s *DeploymentVersionSuite) TestUpdateVersionMetadata() { @@ -1196,10 +1215,21 @@ func (s *DeploymentVersionSuite) TestUpdateVersionMetadata() { resp, err = s.describeVersion(tv1) s.NoError(err) entries = resp.GetWorkerDeploymentVersionInfo().GetMetadata().GetEntries() - s.Equal(0, len(entries)) + s.Empty(entries) - // update metadata for the second time - _, err = s.updateMetadata(tv1, metadata, nil) + // update metadata for the second time with an explicit identity + metadataIdentity := tv1.Any().String() + metadataReq := &workflowservice.UpdateWorkerDeploymentVersionMetadataRequest{ + Namespace: s.Namespace().String(), + UpsertEntries: metadata, + Identity: metadataIdentity, + } + if s.useV32 { + metadataReq.DeploymentVersion = tv1.ExternalDeploymentVersion() + } else { + metadataReq.Version = tv1.DeploymentVersionString() //nolint:staticcheck // SA1019: worker versioning v0.31 + } + _, err = s.FrontendClient().UpdateWorkerDeploymentVersionMetadata(ctx, metadataReq) s.NoError(err) resp, err = s.describeVersion(tv1) @@ -1210,442 +1240,868 @@ func (s *DeploymentVersionSuite) TestUpdateVersionMetadata() { s.Len(entries, 2) s.Equal(testRandomMetadataValue, entries["key1"].Data) s.Equal(testRandomMetadataValue, entries["key2"].Data) + + // LastModifierIdentity should match the identity provided in the metadata update + s.Equal(metadataIdentity, resp.GetWorkerDeploymentVersionInfo().GetLastModifierIdentity()) } -func (s *DeploymentVersionSuite) checkVersionDrainageAndVersionStatus( +func (s *DeploymentVersionSuite) createDeploymentAndVersion( ctx context.Context, tv *testvars.TestVars, - expectedDrainageInfo *deploymentpb.VersionDrainageInfo, - expectedStatus enumspb.WorkerDeploymentVersionStatus, - addGracePeriod, addRefreshInterval bool, -) (changedTime, checkedTime time.Time) { - var waitFor time.Duration - if addGracePeriod { - waitFor += testVersionDrainageVisibilityGracePeriod - } - if addRefreshInterval { - waitFor += testVersionDrainageRefreshInterval - } - if waitFor > 0 { - // wait for the requested duration before looking at the result ( +1 sec for system latency) - time.Sleep(waitFor + 1*time.Second) //nolint:forbidigo - } + identity string, + computeConfig *computepb.ComputeConfig, +) { + s.T().Helper() + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + RequestId: tv.Any().String(), + }) + s.NoError(err) + + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + Identity: identity, + RequestId: tv.Any().String(), + ComputeConfig: computeConfig, + }) + s.NoError(err) + // Wait for version to be created. s.EventuallyWithT(func(t *assert.CollectT) { - a := assert.New(t) - resp, err := s.describeVersion(tv) + a := require.New(t) + descResp, err := s.describeVersion(tv) a.NoError(err) - dInfo := resp.GetWorkerDeploymentVersionInfo().GetDrainageInfo() - a.Equal(expectedDrainageInfo.Status, dInfo.GetStatus()) - if expectedDrainageInfo.LastCheckedTime != nil { - a.Equal(expectedDrainageInfo.LastCheckedTime, dInfo.GetLastCheckedTime()) - } - if expectedDrainageInfo.LastChangedTime != nil { - a.Equal(expectedDrainageInfo.LastChangedTime, dInfo.GetLastChangedTime()) - } - a.Equal(expectedStatus, resp.GetWorkerDeploymentVersionInfo().GetStatus()) - changedTime = dInfo.GetLastChangedTime().AsTime() - checkedTime = dInfo.GetLastCheckedTime().AsTime() - }, 15*time.Second, time.Second) - return changedTime, checkedTime + a.NotNil(descResp.GetWorkerDeploymentVersionInfo()) + }, 10*time.Second, 500*time.Millisecond) } -func (s *DeploymentVersionSuite) checkVersionStatusInDeployment( - ctx context.Context, - tv *testvars.TestVars, - expectedStatus enumspb.WorkerDeploymentVersionStatus, -) { +func (s *DeploymentVersionSuite) TestUpdateComputeConfig_Success() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + tv := testvars.New(s) + createIdentity := tv.Any().String() + validProvider := computeprovider.TestInvokeComputeProviderValidComputeProvider() + + s.createDeploymentAndVersion(ctx, tv, createIdentity, &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {Provider: validProvider}, + }, + }) + + // Update compute config with a different identity and a new scaling group. + updateIdentity := tv.Any().String() + _, err := s.FrontendClient().UpdateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg2": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + Provider: validProvider, + }, + }, + }, + Identity: updateIdentity, + RequestId: tv.Any().String(), + }) + s.NoError(err) + + // Verify both scaling groups exist and LastModifierIdentity is updated. s.EventuallyWithT(func(t *assert.CollectT) { - a := assert.New(t) - resp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + a := require.New(t) + descResp, err := s.describeVersion(tv) + a.NoError(err) + info := descResp.GetWorkerDeploymentVersionInfo() + a.Equal(updateIdentity, info.GetLastModifierIdentity()) + a.True(proto.Equal(&computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {Provider: validProvider}, + "sg2": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + Provider: validProvider, + }, + }, + }, info.GetComputeConfig())) + }, 10*time.Second, 500*time.Millisecond) + + // Verify the compute config summary is reflected in DescribeWorkerDeployment version summaries. + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descDeployResp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), }) a.NoError(err) - found := false - for _, versionSummary := range resp.GetWorkerDeploymentInfo().GetVersionSummaries() { - if versionSummary.GetVersion() == tv.DeploymentVersionString() { //nolint:staticcheck // SA1019: worker versioning v0.31 - a.Equal(expectedStatus, versionSummary.GetStatus(), - "DescribeWorkerDeployment should show version %s as %s", tv.DeploymentVersionString(), expectedStatus) - found = true + var versionSummary *deploymentpb.WorkerDeploymentInfo_WorkerDeploymentVersionSummary + for _, vs := range descDeployResp.GetWorkerDeploymentInfo().GetVersionSummaries() { + if vs.GetVersion() == tv.DeploymentVersionString() { //nolint:staticcheck // SA1019: worker versioning v0.31 + versionSummary = vs break } } - a.True(found, "Version %s should be found in DescribeWorkerDeployment response", tv.DeploymentVersionString()) + a.NotNil(versionSummary, "version %s not found in DescribeWorkerDeployment", tv.DeploymentVersionString()) + a.True(proto.Equal(&computepb.ComputeConfigSummary{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroupSummary{ + "sg1": { + ProviderType: validProvider.GetType(), + }, + "sg2": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + ProviderType: validProvider.GetType(), + }, + }, + }, versionSummary.GetComputeConfig())) }, 10*time.Second, 500*time.Millisecond) -} -func (s *DeploymentVersionSuite) checkDescribeWorkflowAfterOverride( - ctx context.Context, - wf *commonpb.WorkflowExecution, - expectedOverride *workflowpb.VersioningOverride, -) { + // Verify the compute config summary is reflected in ListWorkerDeployments latest version summary. s.EventuallyWithT(func(t *assert.CollectT) { a := require.New(t) - resp, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + listResp, err := s.FrontendClient().ListWorkerDeployments(ctx, &workflowservice.ListWorkerDeploymentsRequest{ Namespace: s.Namespace().String(), - Execution: wf, }) a.NoError(err) - a.NotNil(resp) - a.NotNil(resp.GetWorkflowExecutionInfo()) - actualOverride := resp.GetWorkflowExecutionInfo().GetVersioningInfo().GetVersioningOverride() - - if s.useV32 { - // v0.32 override - a.Equal(expectedOverride.GetAutoUpgrade(), actualOverride.GetAutoUpgrade()) - a.Equalf(expectedOverride.GetPinned().GetVersion().GetBuildId(), actualOverride.GetPinned().GetVersion().GetBuildId(), - "expected pinned version build id %v, got %v", expectedOverride.GetPinned().GetVersion().GetBuildId(), actualOverride.GetPinned().GetVersion().GetBuildId()) - a.Equalf(expectedOverride.GetPinned().GetVersion().GetDeploymentName(), actualOverride.GetPinned().GetVersion().GetDeploymentName(), - "expected pinned version deployment name %v, got %v", expectedOverride.GetPinned().GetVersion().GetDeploymentName(), actualOverride.GetPinned().GetVersion().GetDeploymentName()) - a.Equalf(expectedOverride.GetPinned().GetBehavior(), actualOverride.GetPinned().GetBehavior(), - "expected pinned override behavior %v, got %v", expectedOverride.GetPinned().GetBehavior(), actualOverride.GetPinned().GetBehavior()) - if worker_versioning.OverrideIsPinned(expectedOverride) { - a.Equal(expectedOverride.GetPinned().GetVersion().GetDeploymentName(), resp.GetWorkflowExecutionInfo().GetWorkerDeploymentName()) - } - } else { - // v0.31 override - a.Equal(expectedOverride.GetBehavior().String(), actualOverride.GetBehavior().String()) //nolint:staticcheck // SA1019: worker versioning v0.31 - if actualOverrideDeployment := actualOverride.GetPinnedVersion(); expectedOverride.GetPinnedVersion() != actualOverrideDeployment { //nolint:staticcheck // SA1019: worker versioning v0.31 - a.Fail(fmt.Sprintf("pinned override mismatch. expected: {%s}, actual: {%s}", - expectedOverride.GetPinnedVersion(), //nolint:staticcheck // SA1019: worker versioning v0.31 - actualOverrideDeployment, - )) - } - if worker_versioning.OverrideIsPinned(expectedOverride) { - d, _ := worker_versioning.WorkerDeploymentVersionFromStringV31(expectedOverride.GetPinnedVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31 - a.Equal(d.GetDeploymentName(), resp.GetWorkflowExecutionInfo().GetWorkerDeploymentName()) + var found *workflowservice.ListWorkerDeploymentsResponse_WorkerDeploymentSummary + for _, d := range listResp.GetWorkerDeployments() { + if d.GetName() == tv.DeploymentSeries() { + found = d + break } } - }, 10*time.Second, 50*time.Millisecond) + a.NotNil(found, "deployment %s not found in ListWorkerDeployments", tv.DeploymentSeries()) + a.True(proto.Equal(&computepb.ComputeConfigSummary{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroupSummary{ + "sg1": { + ProviderType: validProvider.GetType(), + }, + "sg2": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + ProviderType: validProvider.GetType(), + }, + }, + }, found.GetLatestVersionSummary().GetComputeConfig())) + }, 60*time.Second, 500*time.Millisecond) } -func (s *DeploymentVersionSuite) checkWorkflowUpdateOptionsEventIdentity( - ctx context.Context, - wf *commonpb.WorkflowExecution, - expectedIdentity string, -) { +func (s *DeploymentVersionSuite) TestUpdateComputeConfig_UpdateExistingGroup() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + tv := testvars.New(s) + validProvider := computeprovider.TestInvokeComputeProviderValidComputeProvider() + + s.createDeploymentAndVersion(ctx, tv, tv.Any().String(), &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW}, + Provider: validProvider, + }, + }, + }) + + // Partially update sg1's task queue types via field mask. + _, err := s.FrontendClient().UpdateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg1": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"task_queue_types"}}, + }, + }, + Identity: tv.Any().String(), + RequestId: tv.Any().String(), + }) + s.NoError(err) + + // Verify task queue types changed but provider is preserved. s.EventuallyWithT(func(t *assert.CollectT) { a := require.New(t) - resp, err := s.FrontendClient().GetWorkflowExecutionHistory(ctx, &workflowservice.GetWorkflowExecutionHistoryRequest{ - Namespace: s.Namespace().String(), - Execution: wf, - }) + descResp, err := s.describeVersion(tv) a.NoError(err) - a.NotNil(resp) - events := resp.GetHistory().GetEvents() - for resp.NextPageToken != nil { // probably there won't ever be more than one page of events in these tests - resp, err = s.FrontendClient().GetWorkflowExecutionHistory(ctx, &workflowservice.GetWorkflowExecutionHistoryRequest{ - Namespace: s.Namespace().String(), - Execution: wf, - NextPageToken: resp.NextPageToken, - }) - a.NoError(err) - a.NotNil(resp) - events = append(events, resp.GetHistory().GetEvents()...) - } - for _, event := range events { - if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED { - a.Equal(expectedIdentity, event.GetWorkflowExecutionOptionsUpdatedEventAttributes().GetIdentity()) - } - } - }, 10*time.Second, 50*time.Millisecond) + a.True(proto.Equal(&computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + Provider: validProvider, + }, + }, + }, descResp.GetWorkerDeploymentVersionInfo().GetComputeConfig())) + }, 10*time.Second, 500*time.Millisecond) } -func (s *DeploymentVersionSuite) checkVersionIsCurrent(ctx context.Context, tv *testvars.TestVars) { - // Querying the Deployment Version - s.EventuallyWithT(func(t *assert.CollectT) { - a := assert.New(t) - resp, err := s.describeVersion(tv) - if !a.NoError(err) { - return - } - a.Equal(tv.DeploymentVersionString(), resp.GetWorkerDeploymentVersionInfo().GetVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31 - a.Equal(tv.ExternalDeploymentVersion().GetDeploymentName(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetDeploymentName()) - a.Equal(tv.ExternalDeploymentVersion().GetBuildId(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetBuildId()) +func (s *DeploymentVersionSuite) TestUpdateComputeConfig_RemoveScalingGroup() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() - a.NotNil(resp.GetWorkerDeploymentVersionInfo().GetCurrentSinceTime()) - a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, resp.GetWorkerDeploymentVersionInfo().GetStatus()) - }, time.Second*10, time.Millisecond*1000) -} + tv := testvars.New(s) + validProvider := computeprovider.TestInvokeComputeProviderValidComputeProvider() + + s.createDeploymentAndVersion(ctx, tv, tv.Any().String(), &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {Provider: validProvider}, + "sg2": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + Provider: validProvider, + }, + }, + }) -func (s *DeploymentVersionSuite) checkVersionIsRamping(ctx context.Context, tv *testvars.TestVars) { - // Querying the Deployment Version - s.EventuallyWithT(func(t *assert.CollectT) { - a := assert.New(t) - resp, err := s.describeVersion(tv) - if !a.NoError(err) { - return - } - a.Equal(tv.DeploymentVersionString(), resp.GetWorkerDeploymentVersionInfo().GetVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31 - a.Equal(tv.ExternalDeploymentVersion().GetDeploymentName(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetDeploymentName()) - a.Equal(tv.ExternalDeploymentVersion().GetBuildId(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetBuildId()) + // Remove sg1. + _, err := s.FrontendClient().UpdateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + RemoveComputeConfigScalingGroups: []string{"sg1"}, + Identity: tv.Any().String(), + RequestId: tv.Any().String(), + }) + s.NoError(err) - a.NotNil(resp.GetWorkerDeploymentVersionInfo().GetRampingSinceTime()) - a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING, resp.GetWorkerDeploymentVersionInfo().GetStatus()) - }, time.Second*10, time.Millisecond*1000) + // Verify only sg2 remains. + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descResp, err := s.describeVersion(tv) + a.NoError(err) + a.True(proto.Equal(&computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg2": { + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + Provider: validProvider, + }, + }, + }, descResp.GetWorkerDeploymentVersionInfo().GetComputeConfig())) + }, 10*time.Second, 500*time.Millisecond) } -func (s *DeploymentVersionSuite) setCurrent(tv *testvars.TestVars, ignoreMissingTQs bool) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +func (s *DeploymentVersionSuite) TestUpdateComputeConfig_VersionNotFound() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - req := &workflowservice.SetWorkerDeploymentCurrentVersionRequest{ - Namespace: s.Namespace().String(), - DeploymentName: tv.DeploymentSeries(), - IgnoreMissingTaskQueues: ignoreMissingTQs, - Identity: tv.ClientIdentity(), - } - if s.useV32 { - req.BuildId = tv.BuildID() - } else { - req.Version = tv.DeploymentVersionString() //nolint:staticcheck // SA1019: worker versioning v0.31 - } - _, err := s.FrontendClient().SetWorkerDeploymentCurrentVersion(ctx, req) - if err == nil { - s.checkVersionIsCurrent(ctx, tv) - } - return err -} -func (s *DeploymentVersionSuite) setRamping( - tv *testvars.TestVars, - percentage float32, -) error { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - v := tv.DeploymentVersionString() - bid := tv.BuildID() - req := &workflowservice.SetWorkerDeploymentRampingVersionRequest{ + tv := testvars.New(s) + + // Create deployment but no version. + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ Namespace: s.Namespace().String(), DeploymentName: tv.DeploymentSeries(), - Percentage: percentage, - Identity: tv.ClientIdentity(), - } - if s.useV32 { - req.BuildId = bid - } else { - req.Version = v //nolint:staticcheck // SA1019: worker versioning v0.31 - } - _, err := s.FrontendClient().SetWorkerDeploymentRampingVersion(ctx, req) - if err == nil { - s.checkVersionIsRamping(ctx, tv) - } - return err -} - -func (s *DeploymentVersionSuite) startWorkflow( - tv *testvars.TestVars, - override *workflowpb.VersioningOverride, -) string { - request := &workflowservice.StartWorkflowExecutionRequest{ - RequestId: tv.Any().String(), - Namespace: s.Namespace().String(), - WorkflowId: tv.WorkflowID(), - WorkflowType: tv.WorkflowType(), - TaskQueue: tv.TaskQueue(), - Identity: tv.WorkerIdentity(), - VersioningOverride: override, - } - - we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) - s.NoError(err0) - return we.GetRunId() -} - -func (s *DeploymentVersionSuite) tryDeleteVersion( - ctx context.Context, - tv *testvars.TestVars, - expectedError string, - skipDrainage bool, -) { - req := &workflowservice.DeleteWorkerDeploymentVersionRequest{ - Namespace: s.Namespace().String(), - SkipDrainage: skipDrainage, - } - if s.useV32 { - req.DeploymentVersion = tv.ExternalDeploymentVersion() - } else { - req.Version = tv.DeploymentVersionString() //nolint:staticcheck // SA1019: worker versioning v0.31 - } - _, err := s.FrontendClient().DeleteWorkerDeploymentVersion(ctx, req) - if expectedError == "" { - s.Nil(err) - } else { - s.EqualErrorf(err, expectedError, err.Error()) - } -} - -func (s *DeploymentVersionSuite) setAndCheckOverride(ctx context.Context, tv *testvars.TestVars, override *workflowpb.VersioningOverride) { - s.setAndCheckOverrideWithExpectedOutput(ctx, tv, override, override) -} - -func (s *DeploymentVersionSuite) setAndCheckOverrideWithExpectedOutput(ctx context.Context, tv *testvars.TestVars, inputOverride, expectedOutputOverride *workflowpb.VersioningOverride) { - optsIn := &workflowpb.WorkflowExecutionOptions{VersioningOverride: inputOverride} - optsOut := &workflowpb.WorkflowExecutionOptions{VersioningOverride: expectedOutputOverride} - // Set input override --> describe workflow shows the expected output override - updateResp, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: tv.WorkflowExecution(), - WorkflowExecutionOptions: optsIn, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - Identity: tv.ClientIdentity(), + RequestId: tv.Any().String(), }) s.NoError(err) - s.True(proto.Equal(updateResp.GetWorkflowExecutionOptions(), optsOut)) - s.checkDescribeWorkflowAfterOverride(ctx, tv.WorkflowExecution(), expectedOutputOverride) - s.checkWorkflowUpdateOptionsEventIdentity(ctx, tv.WorkflowExecution(), tv.ClientIdentity()) -} -// The following tests test the VersioningOverride functionality when passed via the UpdateWorkflowExecutionOptions API. -func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetPinned_CacheMissAndHits() { + _, err = s.FrontendClient().UpdateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg1": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider(), + }, + }, + }, + Identity: tv.Any().String(), + RequestId: tv.Any().String(), + }) + s.Error(err) + var notFound *serviceerror.NotFound + s.ErrorAs(err, ¬Found) +} - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) +func (s *DeploymentVersionSuite) TestUpdateComputeConfig_InvalidProvider() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() - tv := testvars.New(s) - - // start an unversioned workflow - s.startWorkflow(tv, nil) - - opts := &workflowpb.WorkflowExecutionOptions{VersioningOverride: s.makePinnedOverride(tv)} - // Setting a pinned override should fail since the version does not exist - resp, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: tv.WorkflowExecution(), - WorkflowExecutionOptions: opts, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - Identity: tv.ClientIdentity(), + tv := testvars.New(s) + s.createDeploymentAndVersion(ctx, tv, tv.Any().String(), &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider()}, + }, }) - s.Error(err) - s.Nil(resp) - - // Start a versioned poller which shall create a version; however, the cache TTL is not expired yet. This would result in a cache hit which would return - // a stale value for the version presence in the task queue. - s.startVersionWorkflow(ctx, tv) - // Setting a pinned override should fail since the stale cache entry is returned. - resp, err = s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: tv.WorkflowExecution(), - WorkflowExecutionOptions: opts, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - Identity: tv.ClientIdentity(), + _, err := s.FrontendClient().UpdateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg2": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, + Provider: &computepb.ComputeProvider{Type: "invalid-provider"}, + }, + }, + }, + Identity: tv.Any().String(), + RequestId: tv.Any().String(), }) s.Error(err) - s.Nil(resp) + var invalidArg *serviceerror.InvalidArgument + s.ErrorAs(err, &invalidArg) + s.Contains(invalidArg.Message, "invalid compute provider type") - // Wait for the cache TTL to expire - s.Eventually(func() bool { - _, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: tv.WorkflowExecution(), - WorkflowExecutionOptions: opts, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - Identity: tv.ClientIdentity(), + // Verify compute config summary is unchanged — the failed update should not have added sg2. + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descDeployResp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), }) - return err == nil + a.NoError(err) + var versionSummary *deploymentpb.WorkerDeploymentInfo_WorkerDeploymentVersionSummary + for _, vs := range descDeployResp.GetWorkerDeploymentInfo().GetVersionSummaries() { + if vs.GetVersion() == tv.DeploymentVersionString() { //nolint:staticcheck // SA1019: worker versioning v0.31 + versionSummary = vs + break + } + } + a.NotNil(versionSummary, "version %s not found in deployment summaries", tv.DeploymentVersionString()) + a.True(proto.Equal(&computepb.ComputeConfigSummary{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroupSummary{ + "sg1": { + ProviderType: computeprovider.TestInvokeComputeProviderValidComputeProvider().GetType(), + }, + }, + }, versionSummary.GetComputeConfig())) }, 10*time.Second, 500*time.Millisecond) - - // The Pinned Override should have now succeeded with no error. Verify that the - // the workflow shows the override. - s.checkDescribeWorkflowAfterOverride(ctx, tv.WorkflowExecution(), opts.VersioningOverride) } -func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetUnpinnedThenUnset() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) +func (s *DeploymentVersionSuite) TestUpdateComputeConfig_DeletedVersion() { + s.OverrideDynamicConfig(dynamicconfig.PollerHistoryTTL, 500*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() - tv := testvars.New(s) - - // start an unversioned workflow - s.startWorkflow(tv, nil) - - // 1. Set unpinned override --> describe workflow shows the override - s.setAndCheckOverride(ctx, tv, s.makeAutoUpgradeOverride()) - - // 2. Unset using empty update opts with mutation mask --> describe workflow shows no more override - s.setAndCheckOverride(ctx, tv, nil) -} -func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetPinnedThenUnset() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() tv := testvars.New(s) + s.createDeploymentAndVersion(ctx, tv, tv.Any().String(), nil) - // Start a versioned poller which shall create a version; the version must be present before it can be set as an override. - s.startVersionWorkflow(ctx, tv) - - // start an unversioned workflow - s.startWorkflow(tv, nil) - - // 1. Set pinned override on our new unversioned workflow --> describe workflow shows the override - s.setAndCheckOverride(ctx, tv, s.makePinnedOverride(tv)) + // Delete the version (skip drainage, no pollers since we created via CreateWorkerDeploymentVersion). + s.tryDeleteVersion(ctx, tv, "", true) - // 2. Unset using empty update opts with mutation mask --> describe workflow shows no more override - s.setAndCheckOverride(ctx, tv, nil) + // Try to update compute config on the deleted version. + _, err := s.FrontendClient().UpdateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg1": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider(), + }, + }, + }, + Identity: tv.Any().String(), + RequestId: tv.Any().String(), + }) + s.Error(err) } -func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_EmptyFields() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) +func (s *DeploymentVersionSuite) TestValidateComputeConfig_Valid() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() - tv := testvars.New(s) - - // Start a versioned poller which shall create a version; the version must be present before it can be set as an override. - s.startVersionWorkflow(ctx, tv) - // start an unversioned workflow - s.startWorkflow(tv, nil) + tv := testvars.New(s) + s.createDeploymentAndVersion(ctx, tv, tv.Any().String(), nil) - // 1. Pinned update with empty mask --> describe workflow shows no change - updateResp, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + _, err := s.FrontendClient().ValidateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest{ Namespace: s.Namespace().String(), - WorkflowExecution: tv.WorkflowExecution(), - WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ - VersioningOverride: s.makePinnedOverride(tv), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg1": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider(), + }, + }, }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, + Identity: tv.Any().String(), }) s.NoError(err) - s.True(proto.Equal(updateResp.GetWorkflowExecutionOptions(), &workflowpb.WorkflowExecutionOptions{})) - s.checkDescribeWorkflowAfterOverride(ctx, tv.WorkflowExecution(), nil) + + // Verify the validation had no side effects — no compute config should exist. + descResp, err := s.describeVersion(tv) + s.NoError(err) + s.Nil(descResp.GetWorkerDeploymentVersionInfo().GetComputeConfig()) } -func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetPinnedSetPinned() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) +func (s *DeploymentVersionSuite) TestValidateComputeConfig_InvalidProvider() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() - tv := testvars.New(s) - tv1 := tv.WithBuildIDNumber(1) - tv2 := tv.WithBuildIDNumber(2) - - // Start a versioned poller which shall create the two versions; the versions must be present before they can be set as overrides. - s.startVersionWorkflow(ctx, tv1) - s.startVersionWorkflow(ctx, tv2) - - // start an unversioned workflow - s.startWorkflow(tv, nil) - // 1. Set pinned override 1 --> describe workflow shows the override - s.setAndCheckOverride(ctx, tv, s.makePinnedOverride(tv1)) + tv := testvars.New(s) + s.createDeploymentAndVersion(ctx, tv, tv.Any().String(), nil) - // 3. Set pinned override 2 --> describe workflow shows the override - s.setAndCheckOverride(ctx, tv, s.makePinnedOverride(tv2)) + _, err := s.FrontendClient().ValidateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg1": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: &computepb.ComputeProvider{Type: "invalid-provider"}, + }, + }, + }, + Identity: tv.Any().String(), + }) + s.Error(err) + var invalidArg *serviceerror.InvalidArgument + s.ErrorAs(err, &invalidArg) + s.Contains(invalidArg.Message, "invalid compute provider type") } -func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetImpliedPinnedSuccess() { - if !s.useV32 { - s.T().Skip("Implied pinned overrides are only supported in v3.2+") - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) +func (s *DeploymentVersionSuite) TestValidateComputeConfig_VersionNotFound() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - tv1 := testvars.New(s).WithBuildIDNumber(1) - // Start a versioned poller which shall create the two versions; the versions must be present before they can be set as overrides. - s.startVersionWorkflow(ctx, tv1) + tv := testvars.New(s) - // Set tv1 to current, so that the test workflow will be naturally pinned to tv1. - err := s.setCurrent(tv1, true) + // No deployment or version created — validate should still work since + // the WCI validates independently. + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + RequestId: tv.Any().String(), + }) s.NoError(err) - // Start a workflow pinned to tv1. + _, err = s.FrontendClient().ValidateWorkerDeploymentVersionComputeConfig(ctx, &workflowservice.ValidateWorkerDeploymentVersionComputeConfigRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: tv.ExternalDeploymentVersion(), + ComputeConfigScalingGroups: map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "sg1": { + ScalingGroup: &computepb.ComputeConfigScalingGroup{ + Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider(), + }, + }, + }, + Identity: tv.Any().String(), + }) + s.NoError(err) +} + +func (s *DeploymentVersionSuite) checkVersionDrainageAndVersionStatus( + ctx context.Context, + tv *testvars.TestVars, + expectedDrainageInfo *deploymentpb.VersionDrainageInfo, + expectedStatus enumspb.WorkerDeploymentVersionStatus, + waitFor time.Duration, +) (changedTime, checkedTime time.Time) { + if waitFor > 0 { + // wait for the requested duration before looking at the result ( +1 sec for system latency) + time.Sleep(waitFor + 1*time.Second) //nolint:forbidigo + } + + s.EventuallyWithT(func(t *assert.CollectT) { + a := assert.New(t) + resp, err := s.describeVersion(tv) + a.NoError(err) + dInfo := resp.GetWorkerDeploymentVersionInfo().GetDrainageInfo() + a.Equal(expectedDrainageInfo.Status, dInfo.GetStatus()) + if expectedDrainageInfo.LastCheckedTime != nil { + a.Equal(expectedDrainageInfo.LastCheckedTime, dInfo.GetLastCheckedTime()) + } + if expectedDrainageInfo.LastChangedTime != nil { + a.Equal(expectedDrainageInfo.LastChangedTime, dInfo.GetLastChangedTime()) + } + a.Equal(expectedStatus, resp.GetWorkerDeploymentVersionInfo().GetStatus()) + changedTime = dInfo.GetLastChangedTime().AsTime() + checkedTime = dInfo.GetLastCheckedTime().AsTime() + }, 15*time.Second, time.Second) + return changedTime, checkedTime +} + +func (s *DeploymentVersionSuite) checkVersionStatusInDeployment( + ctx context.Context, + tv *testvars.TestVars, + expectedStatus enumspb.WorkerDeploymentVersionStatus, +) { + s.EventuallyWithT(func(t *assert.CollectT) { + a := assert.New(t) + resp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + }) + a.NoError(err) + found := false + for _, versionSummary := range resp.GetWorkerDeploymentInfo().GetVersionSummaries() { + if versionSummary.GetVersion() == tv.DeploymentVersionString() { //nolint:staticcheck // SA1019: worker versioning v0.31 + a.Equal(expectedStatus, versionSummary.GetStatus(), + "DescribeWorkerDeployment should show version %s as %s", tv.DeploymentVersionString(), expectedStatus) + found = true + break + } + } + a.True(found, "Version %s should be found in DescribeWorkerDeployment response", tv.DeploymentVersionString()) + }, 10*time.Second, 500*time.Millisecond) +} + +func (s *DeploymentVersionSuite) checkDescribeWorkflowAfterOverride( + ctx context.Context, + wf *commonpb.WorkflowExecution, + expectedOverride *workflowpb.VersioningOverride, +) { + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + resp, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + Execution: wf, + }) + a.NoError(err) + a.NotNil(resp) + a.NotNil(resp.GetWorkflowExecutionInfo()) + actualOverride := resp.GetWorkflowExecutionInfo().GetVersioningInfo().GetVersioningOverride() + + if s.useV32 { + // v0.32 override + a.Equal(expectedOverride.GetAutoUpgrade(), actualOverride.GetAutoUpgrade()) + a.Equalf(expectedOverride.GetPinned().GetVersion().GetBuildId(), actualOverride.GetPinned().GetVersion().GetBuildId(), + "expected pinned version build id %v, got %v", expectedOverride.GetPinned().GetVersion().GetBuildId(), actualOverride.GetPinned().GetVersion().GetBuildId()) + a.Equalf(expectedOverride.GetPinned().GetVersion().GetDeploymentName(), actualOverride.GetPinned().GetVersion().GetDeploymentName(), + "expected pinned version deployment name %v, got %v", expectedOverride.GetPinned().GetVersion().GetDeploymentName(), actualOverride.GetPinned().GetVersion().GetDeploymentName()) + a.Equalf(expectedOverride.GetPinned().GetBehavior(), actualOverride.GetPinned().GetBehavior(), + "expected pinned override behavior %v, got %v", expectedOverride.GetPinned().GetBehavior(), actualOverride.GetPinned().GetBehavior()) + if worker_versioning.OverrideIsPinned(expectedOverride) { + a.Equal(expectedOverride.GetPinned().GetVersion().GetDeploymentName(), resp.GetWorkflowExecutionInfo().GetWorkerDeploymentName()) + } + } else { + // v0.31 override + a.Equal(expectedOverride.GetBehavior().String(), actualOverride.GetBehavior().String()) //nolint:staticcheck // SA1019: worker versioning v0.31 + if actualOverrideDeployment := actualOverride.GetPinnedVersion(); expectedOverride.GetPinnedVersion() != actualOverrideDeployment { //nolint:staticcheck // SA1019: worker versioning v0.31 + a.Fail(fmt.Sprintf("pinned override mismatch. expected: {%s}, actual: {%s}", + expectedOverride.GetPinnedVersion(), //nolint:staticcheck // SA1019: worker versioning v0.31 + actualOverrideDeployment, + )) + } + if worker_versioning.OverrideIsPinned(expectedOverride) { + d, _ := worker_versioning.WorkerDeploymentVersionFromStringV31(expectedOverride.GetPinnedVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31 + a.Equal(d.GetDeploymentName(), resp.GetWorkflowExecutionInfo().GetWorkerDeploymentName()) + } + } + }, 10*time.Second, 50*time.Millisecond) +} + +func (s *DeploymentVersionSuite) checkWorkflowUpdateOptionsEventIdentity( + ctx context.Context, + wf *commonpb.WorkflowExecution, + expectedIdentity string, +) { + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + resp, err := s.FrontendClient().GetWorkflowExecutionHistory(ctx, &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: s.Namespace().String(), + Execution: wf, + }) + a.NoError(err) + a.NotNil(resp) + events := resp.GetHistory().GetEvents() + for resp.NextPageToken != nil { // probably there won't ever be more than one page of events in these tests + resp, err = s.FrontendClient().GetWorkflowExecutionHistory(ctx, &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: s.Namespace().String(), + Execution: wf, + NextPageToken: resp.NextPageToken, + }) + a.NoError(err) + a.NotNil(resp) + events = append(events, resp.GetHistory().GetEvents()...) + } + for _, event := range events { + if event.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED { + a.Equal(expectedIdentity, event.GetWorkflowExecutionOptionsUpdatedEventAttributes().GetIdentity()) + } + } + }, 10*time.Second, 50*time.Millisecond) +} + +func (s *DeploymentVersionSuite) checkVersionIsCurrent(ctx context.Context, tv *testvars.TestVars) { + // Querying the Deployment Version + s.EventuallyWithT(func(t *assert.CollectT) { + a := assert.New(t) + resp, err := s.describeVersion(tv) + if !a.NoError(err) { + return + } + a.Equal(tv.DeploymentVersionString(), resp.GetWorkerDeploymentVersionInfo().GetVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31 + a.Equal(tv.ExternalDeploymentVersion().GetDeploymentName(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetDeploymentName()) + a.Equal(tv.ExternalDeploymentVersion().GetBuildId(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetBuildId()) + + a.NotNil(resp.GetWorkerDeploymentVersionInfo().GetCurrentSinceTime()) + a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, resp.GetWorkerDeploymentVersionInfo().GetStatus()) + }, time.Second*10, time.Millisecond*1000) +} + +func (s *DeploymentVersionSuite) checkVersionIsRamping(ctx context.Context, tv *testvars.TestVars) { + // Querying the Deployment Version + s.EventuallyWithT(func(t *assert.CollectT) { + a := assert.New(t) + resp, err := s.describeVersion(tv) + if !a.NoError(err) { + return + } + a.Equal(tv.DeploymentVersionString(), resp.GetWorkerDeploymentVersionInfo().GetVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31 + a.Equal(tv.ExternalDeploymentVersion().GetDeploymentName(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetDeploymentName()) + a.Equal(tv.ExternalDeploymentVersion().GetBuildId(), resp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion().GetBuildId()) + + a.NotNil(resp.GetWorkerDeploymentVersionInfo().GetRampingSinceTime()) + a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING, resp.GetWorkerDeploymentVersionInfo().GetStatus()) + }, time.Second*10, time.Millisecond*1000) +} + +func (s *DeploymentVersionSuite) setCurrent(tv *testvars.TestVars, ignoreMissingTQs bool) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + req := &workflowservice.SetWorkerDeploymentCurrentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + IgnoreMissingTaskQueues: ignoreMissingTQs, + Identity: tv.ClientIdentity(), + } + if s.useV32 { + req.BuildId = tv.BuildID() + } else { + req.Version = tv.DeploymentVersionString() //nolint:staticcheck // SA1019: worker versioning v0.31 + } + _, err := s.FrontendClient().SetWorkerDeploymentCurrentVersion(ctx, req) + if err == nil { + s.checkVersionIsCurrent(ctx, tv) + } + return err +} + +func (s *DeploymentVersionSuite) setRamping( + tv *testvars.TestVars, + percentage float32, +) error { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + v := tv.DeploymentVersionString() + bid := tv.BuildID() + req := &workflowservice.SetWorkerDeploymentRampingVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + Percentage: percentage, + Identity: tv.ClientIdentity(), + } + if s.useV32 { + req.BuildId = bid + } else { + req.Version = v //nolint:staticcheck // SA1019: worker versioning v0.31 + } + _, err := s.FrontendClient().SetWorkerDeploymentRampingVersion(ctx, req) + if err == nil { + s.checkVersionIsRamping(ctx, tv) + } + return err +} + +func (s *DeploymentVersionSuite) startWorkflow( + tv *testvars.TestVars, + override *workflowpb.VersioningOverride, +) string { + request := &workflowservice.StartWorkflowExecutionRequest{ + RequestId: tv.Any().String(), + Namespace: s.Namespace().String(), + WorkflowId: tv.WorkflowID(), + WorkflowType: tv.WorkflowType(), + TaskQueue: tv.TaskQueue(), + Identity: tv.WorkerIdentity(), + VersioningOverride: override, + } + + we, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + s.NoError(err0) + return we.GetRunId() +} + +func (s *DeploymentVersionSuite) tryDeleteVersion( + ctx context.Context, + tv *testvars.TestVars, + expectedError string, + skipDrainage bool, +) { + req := &workflowservice.DeleteWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + SkipDrainage: skipDrainage, + } + if s.useV32 { + req.DeploymentVersion = tv.ExternalDeploymentVersion() + } else { + req.Version = tv.DeploymentVersionString() //nolint:staticcheck // SA1019: worker versioning v0.31 + } + _, err := s.FrontendClient().DeleteWorkerDeploymentVersion(ctx, req) + if expectedError == "" { + s.NoError(err) + } else { + s.EqualErrorf(err, expectedError, err.Error()) + } +} + +func (s *DeploymentVersionSuite) setAndCheckOverride(ctx context.Context, tv *testvars.TestVars, override *workflowpb.VersioningOverride) { + s.setAndCheckOverrideWithExpectedOutput(ctx, tv, override, override) +} + +func (s *DeploymentVersionSuite) setAndCheckOverrideWithExpectedOutput(ctx context.Context, tv *testvars.TestVars, inputOverride, expectedOutputOverride *workflowpb.VersioningOverride) { + optsIn := &workflowpb.WorkflowExecutionOptions{VersioningOverride: inputOverride} + optsOut := &workflowpb.WorkflowExecutionOptions{VersioningOverride: expectedOutputOverride} + // Set input override --> describe workflow shows the expected output override + updateResp, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: tv.WorkflowExecution(), + WorkflowExecutionOptions: optsIn, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + Identity: tv.ClientIdentity(), + }) + s.NoError(err) + s.True(proto.Equal(updateResp.GetWorkflowExecutionOptions(), optsOut)) + s.checkDescribeWorkflowAfterOverride(ctx, tv.WorkflowExecution(), expectedOutputOverride) + s.checkWorkflowUpdateOptionsEventIdentity(ctx, tv.WorkflowExecution(), tv.ClientIdentity()) +} + +// The following tests test the VersioningOverride functionality when passed via the UpdateWorkflowExecutionOptions API. +func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetPinned_CacheMissAndHits() { + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tv := testvars.New(s) + + // start an unversioned workflow + s.startWorkflow(tv, nil) + + opts := &workflowpb.WorkflowExecutionOptions{VersioningOverride: s.makePinnedOverride(tv)} + + // Setting a pinned override should fail since the version does not exist + resp, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: tv.WorkflowExecution(), + WorkflowExecutionOptions: opts, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + Identity: tv.ClientIdentity(), + }) + s.Error(err) + s.Nil(resp) + + // Start a versioned poller which shall create a version; however, the cache TTL is not expired yet. This would result in a cache hit which would return + // a stale value for the version presence in the task queue. + s.startVersionWorkflow(ctx, tv) + + // Setting a pinned override should fail since the stale cache entry is returned. + resp, err = s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: tv.WorkflowExecution(), + WorkflowExecutionOptions: opts, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + Identity: tv.ClientIdentity(), + }) + s.Error(err) + s.Nil(resp) + + // Wait for the cache TTL to expire + s.Eventually(func() bool { + _, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: tv.WorkflowExecution(), + WorkflowExecutionOptions: opts, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + Identity: tv.ClientIdentity(), + }) + return err == nil + }, 10*time.Second, 500*time.Millisecond) + + // The Pinned Override should have now succeeded with no error. Verify that the + // the workflow shows the override. + s.checkDescribeWorkflowAfterOverride(ctx, tv.WorkflowExecution(), opts.VersioningOverride) +} + +func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetUnpinnedThenUnset() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tv := testvars.New(s) + + // start an unversioned workflow + s.startWorkflow(tv, nil) + + // 1. Set unpinned override --> describe workflow shows the override + s.setAndCheckOverride(ctx, tv, s.makeAutoUpgradeOverride()) + + // 2. Unset using empty update opts with mutation mask --> describe workflow shows no more override + s.setAndCheckOverride(ctx, tv, nil) +} + +func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetPinnedThenUnset() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tv := testvars.New(s) + + // Start a versioned poller which shall create a version; the version must be present before it can be set as an override. + s.startVersionWorkflow(ctx, tv) + + // start an unversioned workflow + s.startWorkflow(tv, nil) + + // 1. Set pinned override on our new unversioned workflow --> describe workflow shows the override + s.setAndCheckOverride(ctx, tv, s.makePinnedOverride(tv)) + + // 2. Unset using empty update opts with mutation mask --> describe workflow shows no more override + s.setAndCheckOverride(ctx, tv, nil) +} + +func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_EmptyFields() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tv := testvars.New(s) + + // Start a versioned poller which shall create a version; the version must be present before it can be set as an override. + s.startVersionWorkflow(ctx, tv) + + // start an unversioned workflow + s.startWorkflow(tv, nil) + + // 1. Pinned update with empty mask --> describe workflow shows no change + updateResp, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: tv.WorkflowExecution(), + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ + VersioningOverride: s.makePinnedOverride(tv), + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, + }) + s.NoError(err) + s.True(proto.Equal(updateResp.GetWorkflowExecutionOptions(), &workflowpb.WorkflowExecutionOptions{})) + s.checkDescribeWorkflowAfterOverride(ctx, tv.WorkflowExecution(), nil) +} + +func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetPinnedSetPinned() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tv := testvars.New(s) + tv1 := tv.WithBuildIDNumber(1) + tv2 := tv.WithBuildIDNumber(2) + + // Start a versioned poller which shall create the two versions; the versions must be present before they can be set as overrides. + s.startVersionWorkflow(ctx, tv1) + s.startVersionWorkflow(ctx, tv2) + + // start an unversioned workflow + s.startWorkflow(tv, nil) + + // 1. Set pinned override 1 --> describe workflow shows the override + s.setAndCheckOverride(ctx, tv, s.makePinnedOverride(tv1)) + + // 3. Set pinned override 2 --> describe workflow shows the override + s.setAndCheckOverride(ctx, tv, s.makePinnedOverride(tv2)) +} + +func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetImpliedPinnedSuccess() { + if !s.useV32 { + s.T().Skip("Implied pinned overrides are only supported in v3.2+") + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tv1 := testvars.New(s).WithBuildIDNumber(1) + + // Start a versioned poller which shall create the two versions; the versions must be present before they can be set as overrides. + s.startVersionWorkflow(ctx, tv1) + + // Set tv1 to current, so that the test workflow will be naturally pinned to tv1. + err := s.setCurrent(tv1, true) + s.NoError(err) + + // Start a workflow pinned to tv1. run := s.startPinnedWorkflow(ctx, tv1) noVersionPinnedOverride := &workflowpb.VersioningOverride{Override: &workflowpb.VersioningOverride_Pinned{ @@ -1758,8 +2214,8 @@ func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_SetPinnedSet } func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_ReactivateVersionOnPinned() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 10*time.Second) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testLongVersionDrainageRefreshInterval) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -1786,7 +2242,7 @@ func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_ReactivateVe Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) // wait for grace period and refresh interval + testLongVersionDrainageVisibilityGracePeriod+testLongVersionDrainageRefreshInterval) // wait for grace period and refresh interval wf := func(version string) func(ctx workflow.Context) (string, error) { return func(ctx workflow.Context) (string, error) { @@ -1876,7 +2332,7 @@ func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_ReactivateVe Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) // wait for grace period and refresh interval + 0) // Verify via DescribeWorkerDeployment that the version status is updated s.checkVersionStatusInDeployment(ctx, tv1, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING) @@ -1892,8 +2348,8 @@ func (s *DeploymentVersionSuite) TestUpdateWorkflowExecutionOptions_ReactivateVe } func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnPinned() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 10*time.Second) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testLongVersionDrainageRefreshInterval) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -1920,7 +2376,7 @@ func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnP Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) // wait for grace period and refresh interval + testLongVersionDrainageVisibilityGracePeriod+testLongVersionDrainageRefreshInterval) // wait for grace period and refresh interval wf := func(version string) func(ctx workflow.Context) (string, error) { return func(ctx workflow.Context) (string, error) { @@ -1980,7 +2436,7 @@ func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnP Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) // wait for grace period and refresh interval + 0) // Verify via DescribeWorkerDeployment that the version status is updated s.checkVersionStatusInDeployment(ctx, tv1, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING) @@ -1996,8 +2452,8 @@ func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnP } func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnPinned_WithConflictPolicy() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 10*time.Second) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testLongVersionDrainageRefreshInterval) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -2022,7 +2478,7 @@ func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnP Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + testLongVersionDrainageVisibilityGracePeriod+testLongVersionDrainageRefreshInterval) wf := func(version string) func(ctx workflow.Context) (string, error) { return func(ctx workflow.Context) (string, error) { @@ -2106,7 +2562,7 @@ func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnP Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) + 0) // Verify via DescribeWorkerDeployment that the version status is updated s.checkVersionStatusInDeployment(ctx, tv1, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING) @@ -2122,8 +2578,8 @@ func (s *DeploymentVersionSuite) TestStartWorkflowExecution_ReactivateVersionOnP } func (s *DeploymentVersionSuite) TestSignalWithStartWorkflowExecution_ReactivateVersionOnPinned() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 10*time.Second) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testLongVersionDrainageRefreshInterval) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -2150,7 +2606,7 @@ func (s *DeploymentVersionSuite) TestSignalWithStartWorkflowExecution_Reactivate Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) // wait for grace period and refresh interval + testLongVersionDrainageVisibilityGracePeriod+testLongVersionDrainageRefreshInterval) // wait for grace period and refresh interval wf := func(version string) func(ctx workflow.Context) (string, error) { return func(ctx workflow.Context) (string, error) { @@ -2216,7 +2672,7 @@ func (s *DeploymentVersionSuite) TestSignalWithStartWorkflowExecution_Reactivate Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) // wait for grace period and refresh interval + 0) // Verify via DescribeWorkerDeployment that the version status is updated s.checkVersionStatusInDeployment(ctx, tv1, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING) @@ -2232,8 +2688,8 @@ func (s *DeploymentVersionSuite) TestSignalWithStartWorkflowExecution_Reactivate } func (s *DeploymentVersionSuite) TestResetWorkflowExecution_ReactivateVersionOnPinned() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 10*time.Second) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testLongVersionDrainageRefreshInterval) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -2260,7 +2716,7 @@ func (s *DeploymentVersionSuite) TestResetWorkflowExecution_ReactivateVersionOnP Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) // wait for grace period and refresh interval + testLongVersionDrainageVisibilityGracePeriod+testLongVersionDrainageRefreshInterval) // wait for grace period and refresh interval // Workflow that waits for a signal, used for both versions. // Returns a string indicating which version completed it. @@ -2399,7 +2855,7 @@ func (s *DeploymentVersionSuite) TestResetWorkflowExecution_ReactivateVersionOnP Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING, }, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) // wait for grace period and refresh interval + 0) // Verify via DescribeWorkerDeployment that the version status is DRAINING s.checkVersionStatusInDeployment(ctx, tv1, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING) @@ -2432,7 +2888,7 @@ func (s *DeploymentVersionSuite) runBatchUpdateWorkflowExecutionOptionsTest(crea // start some unversioned workflows workflowType := "UpdateOptionsBatchTestFunc" workflows := make([]*commonpb.WorkflowExecution, 0) - for i := 0; i < 5; i++ { + for range 5 { run, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tv.TaskQueue().Name}, workflowType) s.NoError(err) workflows = append(workflows, &commonpb.WorkflowExecution{ @@ -2529,7 +2985,7 @@ func (s *DeploymentVersionSuite) checkListAndWaitForBatchCompletion(ctx context. Namespace: s.Namespace().String(), }) a.NoError(err) - a.Greater(len(listResp.GetOperationInfo()), 0) + a.NotEmpty(listResp.GetOperationInfo()) if len(listResp.GetOperationInfo()) > 0 { a.Equal(jobId, listResp.GetOperationInfo()[0].GetJobId()) } @@ -2542,7 +2998,7 @@ func (s *DeploymentVersionSuite) checkListAndWaitForBatchCompletion(ctx context. JobId: jobId, }) a.NoError(err) - a.NotEqual(enumspb.BATCH_OPERATION_STATE_FAILED, descResp.GetState(), fmt.Sprintf("batch operation failed. description: %+v", descResp)) + a.NotEqual(enumspb.BATCH_OPERATION_STATE_FAILED, descResp.GetState(), "batch operation failed. description: %+v", descResp) a.Equal(enumspb.BATCH_OPERATION_STATE_COMPLETED, descResp.GetState()) }, 10*time.Second, 50*time.Millisecond) } @@ -2675,46 +3131,162 @@ func (s *DeploymentVersionSuite) TestSignalWithStartWorkflowExecution_WithPinned s.checkDescribeWorkflowAfterOverride(ctx, wf, override) } -func (s *DeploymentVersionSuite) TestSignalWithStartWorkflowExecution_WithUnpinnedOverride() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - tv := testvars.New(s) +func (s *DeploymentVersionSuite) TestSignalWithStartWorkflowExecution_WithUnpinnedOverride() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tv := testvars.New(s) + + override := s.makeAutoUpgradeOverride() + + resp, err := s.FrontendClient().SignalWithStartWorkflowExecution(ctx, &workflowservice.SignalWithStartWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowId: tv.WorkflowID(), + WorkflowType: tv.WorkflowType(), + TaskQueue: tv.TaskQueue(), + Identity: tv.ClientIdentity(), + RequestId: tv.RequestID(), + SignalName: "test-signal", + SignalInput: nil, + VersioningOverride: override, + }) + s.NoError(err) + s.True(resp.GetStarted()) + + wf := &commonpb.WorkflowExecution{ + WorkflowId: tv.WorkflowID(), + RunId: resp.GetRunId(), + } + s.checkDescribeWorkflowAfterOverride(ctx, wf, override) +} + +// The following tests verify that the version_reactivation_signal_cache works as intended. +func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_StartWorkflow() { + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testExtraLongVersionDrainageRefreshInterval) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + // Use shorter, explicit deployment series names to avoid truncation issues + deploymentName := fmt.Sprintf("test-cache-dedup-wfv%d", s.workflowVersion) + tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-cache-dedup-tq") + tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-cache-dedup-tq") + + // Set version 1 as current + s.startVersionWorkflow(ctx, tv1) + err := s.setCurrent(tv1, true) + s.NoError(err) + + // Set version 2 as current → version 1 starts draining + s.startVersionWorkflow(ctx, tv2) + err = s.setCurrent(tv2, true) + s.NoError(err) + + // Wait for version 1 to become DRAINED + s.checkVersionDrainageAndVersionStatus(ctx, tv1, + &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) + + // Workflow that waits for a signal before completing + wf := func(ctx workflow.Context) (string, error) { + workflow.GetSignalChannel(ctx, "complete").Receive(ctx, nil) + return "done", nil + } + + // Register worker for version 1 + w1 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + Version: tv1.SDKDeploymentVersion(), + UseVersioning: true, + }, + }) + w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ + Name: "waitingWorkflow", + VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, + }) + s.NoError(w1.Start()) + defer w1.Stop() + + // === First workflow run: Should trigger reactivation signal to be sent (cache miss) === + wfTV1 := testvars.New(s) + var run1 sdkclient.WorkflowRun + s.Eventually(func() bool { + var startErr error + run1, startErr = s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().String(), + ID: wfTV1.WorkflowID(), + VersioningOverride: &sdkclient.PinnedVersioningOverride{ + Version: tv1.SDKDeploymentVersion(), + }, + }, "waitingWorkflow") + return startErr == nil + }, 10*time.Second, 500*time.Millisecond) + + // Version 1 should transition to DRAINING + s.checkVersionDrainageAndVersionStatus(ctx, tv1, + &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING}, + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + 0) + + // Signal workflow to complete. + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV1.WorkflowID(), run1.GetRunID(), "complete", nil)) + + // Wait for workflow to complete. + var result string + s.NoError(run1.Get(ctx, &result)) - override := s.makeAutoUpgradeOverride() + // Wait for version 1 to become Drained again + s.checkVersionDrainageAndVersionStatus(ctx, tv1, + &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) - resp, err := s.FrontendClient().SignalWithStartWorkflowExecution(ctx, &workflowservice.SignalWithStartWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowId: tv.WorkflowID(), - WorkflowType: tv.WorkflowType(), - TaskQueue: tv.TaskQueue(), - Identity: tv.ClientIdentity(), - RequestId: tv.RequestID(), - SignalName: "test-signal", - SignalInput: nil, - VersioningOverride: override, - }) - s.NoError(err) - s.True(resp.GetStarted()) + // === Second workflow run: Should NOT trigger reactivation signal to be sent (cache hit) === + wfTV2 := testvars.New(s) + var run2 sdkclient.WorkflowRun + s.Eventually(func() bool { + var startErr error + run2, startErr = s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().String(), + ID: wfTV2.WorkflowID(), + VersioningOverride: &sdkclient.PinnedVersioningOverride{ + Version: tv1.SDKDeploymentVersion(), + }, + }, "waitingWorkflow") + return startErr == nil + }, 10*time.Second, 500*time.Millisecond) - wf := &commonpb.WorkflowExecution{ - WorkflowId: tv.WorkflowID(), - RunId: resp.GetRunId(), - } - s.checkDescribeWorkflowAfterOverride(ctx, wf, override) + // Verify version stays Drained for several checks (even though there is a workflow running) + // Use Eventually with a counter to check multiple times that the reactivation signal was cached + drainedCheckCount := 0 + s.Eventually(func() bool { + resp, err := s.describeVersion(tv1) + s.NoError(err) + s.Equalf(enumspb.VERSION_DRAINAGE_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetDrainageInfo().GetStatus(), + "Version should remain DRAINED because reactivation signal was cached (check %d)", drainedCheckCount) + s.Equalf(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetStatus(), + "Version status should remain DRAINED because reactivation signal was cached (check %d)", drainedCheckCount) + drainedCheckCount++ + return drainedCheckCount >= 5 + }, 10*time.Second, 1*time.Second) + + // Signal the workflow to complete. + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV2.WorkflowID(), run2.GetRunID(), "complete", nil)) + s.NoError(run2.Get(ctx, &result)) } -// The following tests verify that the version_reactivation_signal_cache works as intended. -func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_StartWorkflow() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 30*time.Second) +func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_SignalWithStart() { + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testExtraLongVersionDrainageRefreshInterval) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() // Use shorter, explicit deployment series names to avoid truncation issues - deploymentName := fmt.Sprintf("test-cache-dedup-wfv%d", s.workflowVersion) - tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-cache-dedup-tq") - tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-cache-dedup-tq") + deploymentName := fmt.Sprintf("test-sws-cache-dedup-wfv%d", s.workflowVersion) + tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-sws-cache-dedup-tq") + tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-sws-cache-dedup-tq") // Set version 1 as current s.startVersionWorkflow(ctx, tv1) @@ -2730,7 +3302,7 @@ func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Start s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) // Workflow that waits for a signal before completing wf := func(ctx workflow.Context) (string, error) { @@ -2752,57 +3324,230 @@ func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Start s.NoError(w1.Start()) defer w1.Stop() - // === First workflow run: Should trigger reactivation signal to be sent (cache miss) === + // === FIRST SIGNAL WITH START: Should trigger reactivation (cache miss) === wfTV1 := testvars.New(s) var run1 sdkclient.WorkflowRun s.Eventually(func() bool { var startErr error - run1, startErr = s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ - TaskQueue: tv1.TaskQueue().String(), - ID: wfTV1.WorkflowID(), - VersioningOverride: &sdkclient.PinnedVersioningOverride{ - Version: tv1.SDKDeploymentVersion(), + run1, startErr = s.SdkClient().SignalWithStartWorkflow(ctx, + wfTV1.WorkflowID(), + "start-signal", + nil, + sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().String(), + VersioningOverride: &sdkclient.PinnedVersioningOverride{ + Version: tv1.SDKDeploymentVersion(), + }, }, - }, "waitingWorkflow") + "waitingWorkflow", + ) return startErr == nil }, 10*time.Second, 500*time.Millisecond) - // Version 1 should transition to DRAINING + // Version 1 should transition to DRAINING (reactivated) s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) + 0) - // Signal workflow to complete. + // Signal workflow to complete s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV1.WorkflowID(), run1.GetRunID(), "complete", nil)) - // Wait for workflow to complete. + // Wait for workflow to complete var result string s.NoError(run1.Get(ctx, &result)) - // Wait for version 1 to become Drained again + // Wait for version 1 to become DRAINED again (workflow completed) s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) - // === Second workflow run: Should NOT trigger reactivation signal to be sent (cache hit) === + // === SECOND SIGNAL WITH START: Should NOT trigger reactivation (cache hit) === wfTV2 := testvars.New(s) var run2 sdkclient.WorkflowRun s.Eventually(func() bool { var startErr error - run2, startErr = s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ - TaskQueue: tv1.TaskQueue().String(), - ID: wfTV2.WorkflowID(), - VersioningOverride: &sdkclient.PinnedVersioningOverride{ - Version: tv1.SDKDeploymentVersion(), + run2, startErr = s.SdkClient().SignalWithStartWorkflow(ctx, + wfTV2.WorkflowID(), + "start-signal", + nil, + sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().String(), + VersioningOverride: &sdkclient.PinnedVersioningOverride{ + Version: tv1.SDKDeploymentVersion(), + }, }, - }, "waitingWorkflow") + "waitingWorkflow", + ) return startErr == nil }, 10*time.Second, 500*time.Millisecond) - // Verify version stays Drained for several checks (even though there is a workflow running) - // Use Eventually with a counter to check multiple times that the reactivation signal was cached + // Verify version stays DRAINED for several checks (workflow is still running) + // Use Eventually with a counter to check multiple times that the signal was cached + drainedCheckCount := 0 + s.Eventually(func() bool { + resp, err := s.describeVersion(tv1) + s.NoError(err) + s.Equal(enumspb.VERSION_DRAINAGE_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetDrainageInfo().GetStatus(), + "Version should remain DRAINED because reactivation signal was cached") + s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetStatus(), + "Version status should remain DRAINED because reactivation signal was cached") + drainedCheckCount++ + return drainedCheckCount >= 5 + }, 10*time.Second, 1*time.Second) + + // Signal the workflow to complete + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV2.WorkflowID(), run2.GetRunID(), "complete", nil)) + s.NoError(run2.Get(ctx, &result)) +} + +func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_UpdateOptions() { + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testExtraLongVersionDrainageRefreshInterval) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + // Use shorter, explicit deployment series names to avoid truncation issues + deploymentName := fmt.Sprintf("test-opts-cache-dedup-wfv%d", s.workflowVersion) + tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-opts-cache-dedup-tq") + tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-opts-cache-dedup-tq") + + // Set version 1 as current + s.startVersionWorkflow(ctx, tv1) + err := s.setCurrent(tv1, true) + s.NoError(err) + + // Set version 2 as current → version 1 starts draining + s.startVersionWorkflow(ctx, tv2) + err = s.setCurrent(tv2, true) + s.NoError(err) + + // Wait for version 1 to become DRAINED + s.checkVersionDrainageAndVersionStatus(ctx, tv1, + &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) + + // Workflow that waits for a signal before completing + wf := func(ctx workflow.Context) (string, error) { + workflow.GetSignalChannel(ctx, "complete").Receive(ctx, nil) + return "done", nil + } + + // Register worker for version 1 (DRAINED) + w1 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + Version: tv1.SDKDeploymentVersion(), + UseVersioning: true, + }, + }) + w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ + Name: "waitingWorkflow", + VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, + }) + s.NoError(w1.Start()) + defer w1.Stop() + + // Register worker for version 2 (CURRENT) on the same task queue + w2 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + Version: tv2.SDKDeploymentVersion(), + UseVersioning: true, + }, + }) + w2.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ + Name: "waitingWorkflow", + VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, + }) + s.NoError(w2.Start()) + defer w2.Stop() + + s.waitForPollers(ctx, tv1, tv2) + + pinnedOverride := &workflowpb.VersioningOverride{ + Override: &workflowpb.VersioningOverride_Pinned{ + Pinned: &workflowpb.VersioningOverride_PinnedOverride{ + Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, + Version: tv1.ExternalDeploymentVersion(), + }, + }, + } + + // === FIRST UPDATE OPTIONS: Should trigger reactivation (cache miss) === + // Start workflow on v2 (current version) - waits for signal to complete + wfTV1 := testvars.New(s) + run1, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().String(), + ID: wfTV1.WorkflowID(), + }, "waitingWorkflow") + s.NoError(err) + + // Pin the workflow to v1 using UpdateWorkflowExecutionOptions + s.Eventually(func() bool { + _, err = s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, + &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: wfTV1.WorkflowID(), + RunId: run1.GetRunID(), + }, + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ + VersioningOverride: pinnedOverride, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + }) + return err == nil + }, 10*time.Second, 500*time.Millisecond) + + // Signal workflow to complete + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV1.WorkflowID(), run1.GetRunID(), "complete", nil)) + + // Wait for workflow to complete + var result string + s.NoError(run1.Get(ctx, &result)) + + // Version 1 should transition to DRAINING (reactivated) + s.checkVersionDrainageAndVersionStatus(ctx, tv1, + &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING}, + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, + 0) + + // Wait for version 1 to become DRAINED again (workflow completed) + s.checkVersionDrainageAndVersionStatus(ctx, tv1, + &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, + enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) + + // === SECOND UPDATE OPTIONS: Should NOT trigger reactivation (cache hit) === + // Start another workflow on v2 - waits for signal to complete + wfTV2 := testvars.New(s) + run2, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().String(), + ID: wfTV2.WorkflowID(), + }, "waitingWorkflow") + s.NoError(err) + + // Pin this workflow to v1 using UpdateWorkflowExecutionOptions (should be cached) + s.Eventually(func() bool { + _, err = s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, + &workflowservice.UpdateWorkflowExecutionOptionsRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: wfTV2.WorkflowID(), + RunId: run2.GetRunID(), + }, + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ + VersioningOverride: pinnedOverride, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + }) + return err == nil + }, 10*time.Second, 500*time.Millisecond) + + // Verify version stays DRAINED for several checks (workflow is still running) + // Use Eventually with a counter to check multiple times that the signal was cached drainedCheckCount := 0 s.Eventually(func() bool { resp, err := s.describeVersion(tv1) @@ -2815,22 +3560,25 @@ func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Start return drainedCheckCount >= 5 }, 10*time.Second, 1*time.Second) - // Signal the workflow to complete. + // Signal the workflow to complete s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV2.WorkflowID(), run2.GetRunID(), "complete", nil)) s.NoError(run2.Get(ctx, &result)) } -func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_SignalWithStart() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 30*time.Second) +// TestReactivationSignalCache_Deduplication_Reset verifies that the version reactivation signal cache +// deduplicates signals when ResetWorkflowExecution is called multiple times with a pinned override +// to a DRAINED version. +func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Reset() { + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, testLongVersionDrainageVisibilityGracePeriod) + s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, testExtraLongVersionDrainageRefreshInterval) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() // Use shorter, explicit deployment series names to avoid truncation issues - deploymentName := fmt.Sprintf("test-sws-cache-dedup-wfv%d", s.workflowVersion) - tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-sws-cache-dedup-tq") - tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-sws-cache-dedup-tq") + deploymentName := fmt.Sprintf("test-reset-cache-dedup-wfv%d", s.workflowVersion) + tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-reset-cache-dedup-tq") + tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-reset-cache-dedup-tq") // Set version 1 as current s.startVersionWorkflow(ctx, tv1) @@ -2846,7 +3594,7 @@ func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Signa s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) // Workflow that waits for a signal before completing wf := func(ctx workflow.Context) (string, error) { @@ -2854,7 +3602,7 @@ func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Signa return "done", nil } - // Register worker for version 1 + // Register worker for version 1 (DRAINED) w1 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ DeploymentOptions: worker.DeploymentOptions{ Version: tv1.SDKDeploymentVersion(), @@ -2868,66 +3616,156 @@ func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Signa s.NoError(w1.Start()) defer w1.Stop() - // === FIRST SIGNAL WITH START: Should trigger reactivation (cache miss) === + // Register worker for version 2 (CURRENT) on the same task queue + w2 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + Version: tv2.SDKDeploymentVersion(), + UseVersioning: true, + }, + }) + w2.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ + Name: "waitingWorkflow", + VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, + }) + s.NoError(w2.Start()) + defer w2.Stop() + + s.waitForPollers(ctx, tv1, tv2) + + pinnedOverride := &workflowpb.VersioningOverride{ + Override: &workflowpb.VersioningOverride_Pinned{ + Pinned: &workflowpb.VersioningOverride_PinnedOverride{ + Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, + Version: tv1.ExternalDeploymentVersion(), + }, + }, + } + + // Helper function to start a workflow, wait for task completion, and get reset event ID + startAndGetResetEventID := func(wfID string) (sdkclient.WorkflowRun, int64) { + run, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ + TaskQueue: tv1.TaskQueue().String(), + ID: wfID, + }, "waitingWorkflow") + s.NoError(err) + + // Wait for workflow task completion (creates a reset point) + s.Eventually(func() bool { + hist := s.SdkClient().GetWorkflowHistory(ctx, wfID, run.GetRunID(), false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for hist.HasNext() { + event, err := hist.Next() + if err != nil { + return false + } + if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED { + return true + } + } + return false + }, 10*time.Second, 200*time.Millisecond) + + // Find the reset event ID + var resetEventID int64 + hist := s.SdkClient().GetWorkflowHistory(ctx, wfID, run.GetRunID(), false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for hist.HasNext() { + event, err := hist.Next() + s.NoError(err) + if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED { + resetEventID = event.EventId + break + } + } + s.Positive(resetEventID) + return run, resetEventID + } + + // === FIRST RESET: Should trigger reactivation (cache miss) === wfTV1 := testvars.New(s) - var run1 sdkclient.WorkflowRun + run1, resetEventID1 := startAndGetResetEventID(wfTV1.WorkflowID()) + + // Reset with pinned override to v1 (DRAINED) + var resetResp1 *workflowservice.ResetWorkflowExecutionResponse s.Eventually(func() bool { - var startErr error - run1, startErr = s.SdkClient().SignalWithStartWorkflow(ctx, - wfTV1.WorkflowID(), - "start-signal", - nil, - sdkclient.StartWorkflowOptions{ - TaskQueue: tv1.TaskQueue().String(), - VersioningOverride: &sdkclient.PinnedVersioningOverride{ - Version: tv1.SDKDeploymentVersion(), + var resetErr error + resetResp1, resetErr = s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: wfTV1.WorkflowID(), + RunId: run1.GetRunID(), + }, + Reason: "testing-reset-cache-dedup-1", + RequestId: uuid.NewString(), + WorkflowTaskFinishEventId: resetEventID1, + PostResetOperations: []*workflowpb.PostResetOperation{ + { + Variant: &workflowpb.PostResetOperation_UpdateWorkflowOptions_{ + UpdateWorkflowOptions: &workflowpb.PostResetOperation_UpdateWorkflowOptions{ + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ + VersioningOverride: pinnedOverride, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + }, + }, }, }, - "waitingWorkflow", - ) - return startErr == nil + }) + return resetErr == nil }, 10*time.Second, 500*time.Millisecond) + // Signal the reset workflow to complete + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV1.WorkflowID(), resetResp1.RunId, "complete", nil)) + + // Wait for workflow to complete + resetRun1 := s.SdkClient().GetWorkflow(ctx, wfTV1.WorkflowID(), resetResp1.RunId) + var result string + s.NoError(resetRun1.Get(ctx, &result)) + // Version 1 should transition to DRAINING (reactivated) s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) - - // Signal workflow to complete - s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV1.WorkflowID(), run1.GetRunID(), "complete", nil)) - - // Wait for workflow to complete - var result string - s.NoError(run1.Get(ctx, &result)) + 0) // Wait for version 1 to become DRAINED again (workflow completed) s.checkVersionDrainageAndVersionStatus(ctx, tv1, &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + testLongVersionDrainageVisibilityGracePeriod+testExtraLongVersionDrainageRefreshInterval) - // === SECOND SIGNAL WITH START: Should NOT trigger reactivation (cache hit) === + // === SECOND RESET: Should NOT trigger reactivation (cache hit) === wfTV2 := testvars.New(s) - var run2 sdkclient.WorkflowRun + run2, resetEventID2 := startAndGetResetEventID(wfTV2.WorkflowID()) + + // Reset with pinned override to v1 (should be cached) + var resetResp2 *workflowservice.ResetWorkflowExecutionResponse s.Eventually(func() bool { - var startErr error - run2, startErr = s.SdkClient().SignalWithStartWorkflow(ctx, - wfTV2.WorkflowID(), - "start-signal", - nil, - sdkclient.StartWorkflowOptions{ - TaskQueue: tv1.TaskQueue().String(), - VersioningOverride: &sdkclient.PinnedVersioningOverride{ - Version: tv1.SDKDeploymentVersion(), + var resetErr error + resetResp2, resetErr = s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: s.Namespace().String(), + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: wfTV2.WorkflowID(), + RunId: run2.GetRunID(), + }, + Reason: "testing-reset-cache-dedup-2", + RequestId: uuid.NewString(), + WorkflowTaskFinishEventId: resetEventID2, + PostResetOperations: []*workflowpb.PostResetOperation{ + { + Variant: &workflowpb.PostResetOperation_UpdateWorkflowOptions_{ + UpdateWorkflowOptions: &workflowpb.PostResetOperation_UpdateWorkflowOptions{ + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ + VersioningOverride: pinnedOverride, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, + }, + }, }, }, - "waitingWorkflow", - ) - return startErr == nil + }) + return resetErr == nil }, 10*time.Second, 500*time.Millisecond) - // Verify version stays DRAINED for several checks (workflow is still running) + // Verify version stays DRAINED for several checks (reset workflow is still running) // Use Eventually with a counter to check multiple times that the signal was cached drainedCheckCount := 0 s.Eventually(func() bool { @@ -2941,393 +3779,637 @@ func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Signa return drainedCheckCount >= 5 }, 10*time.Second, 1*time.Second) - // Signal the workflow to complete - s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV2.WorkflowID(), run2.GetRunID(), "complete", nil)) - s.NoError(run2.Get(ctx, &result)) + // Signal the reset workflow to complete + s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV2.WorkflowID(), resetResp2.RunId, "complete", nil)) + + // Wait for workflow to complete + resetRun2 := s.SdkClient().GetWorkflow(ctx, wfTV2.WorkflowID(), resetResp2.RunId) + var result2 string + s.NoError(resetRun2.Get(ctx, &result2)) +} + +func (s *DeploymentVersionSuite) TestDeleteVersion_ThenRecreateByPolling() { + s.skipBeforeVersion(workerdeployment.VersionDataRevisionNumber) + s.OverrideDynamicConfig(dynamicconfig.PollerHistoryTTL, 500*time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + tv := testvars.New(s).WithBuildIDNumber(1) + + s.startVersionWorkflow(ctx, tv) + + vd := s.getTaskQueueVersionData(tv, enumspb.TASK_QUEUE_TYPE_WORKFLOW, tv.ExternalDeploymentVersion()) + s.Equal(int64(0), vd.GetRevisionNumber()) + s.False(vd.GetDeleted()) + + // Wait for pollers to go away + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{ + Namespace: s.Namespace().String(), + TaskQueue: tv.TaskQueue(), + TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW, + }) + require.NoError(t, err) + require.Empty(t, resp.Pollers) + }, 5*time.Second, time.Second) + + // Delete the version + s.tryDeleteVersion(ctx, tv, "", false) + // Verify the version is gone from the task queue + s.EventuallyWithT(func(t *assert.CollectT) { + vd = s.getTaskQueueVersionData(tv, enumspb.TASK_QUEUE_TYPE_WORKFLOW, tv.ExternalDeploymentVersion()) + require.New(t).Nil(vd) + }, time.Second*5, time.Millisecond*200) + + // Verify the version is gone from the deployment + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + resp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + }) + a.NoError(err) + for _, vs := range resp.GetWorkerDeploymentInfo().GetVersionSummaries() { + //nolint:staticcheck // SA1019 deprecated Version will clean up later + a.NotEqual(tv.DeploymentVersionString(), vs.Version) + } + }, time.Second*5, time.Millisecond*200) + + // Poll again to recreate the version + + s.startVersionWorkflow(ctx, tv) + + // Verify the version is back (undeleted) in the deployment + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + resp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: tv.DeploymentSeries(), + }) + a.NoError(err) + found := false + for _, vs := range resp.GetWorkerDeploymentInfo().GetVersionSummaries() { + //nolint:staticcheck // SA1019 deprecated Version will clean up later + if vs.Version == tv.DeploymentVersionString() { + found = true + } + } + a.True(found, "version should be recreated after polling") + }, time.Second*5, time.Millisecond*200) + + // Ensure the version data revived properly in the task queue + vd = s.getTaskQueueVersionData(tv, enumspb.TASK_QUEUE_TYPE_WORKFLOW, tv.ExternalDeploymentVersion()) + s.Equal(int64(0), vd.GetRevisionNumber()) + s.False(vd.GetDeleted()) +} + +// getTaskQueueDeploymentData gets the deployment data for a given TQ type. The data is always +// returned from the WF type root partition, so no need to wait for propagation before calling this +// function. +func (s *DeploymentVersionSuite) getTaskQueueDeploymentData( + tv *testvars.TestVars, + tqType enumspb.TaskQueueType, +) *persistencespb.DeploymentData { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + resp, err := s.GetTestCluster().MatchingClient().GetTaskQueueUserData( + ctx, + &matchingservice.GetTaskQueueUserDataRequest{ + NamespaceId: s.NamespaceID().String(), + TaskQueue: tv.TaskQueue().GetName(), + TaskQueueType: tqTypeWf, + }) + s.NoError(err) + return resp.GetUserData().GetData().GetPerType()[int32(tqType)].GetDeploymentData() } -func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_UpdateOptions() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 30*time.Second) +func (s *DeploymentVersionSuite) getTaskQueueVersionData( + tv *testvars.TestVars, + tqType enumspb.TaskQueueType, + version *deploymentpb.WorkerDeploymentVersion, +) *deploymentspb.WorkerDeploymentVersionData { + data := s.getTaskQueueDeploymentData(tv, tqType) + return data.GetDeploymentsData()[version.GetDeploymentName()].GetVersions()[version.GetBuildId()] +} - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_Success() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() - // Use shorter, explicit deployment series names to avoid truncation issues - deploymentName := fmt.Sprintf("test-opts-cache-dedup-wfv%d", s.workflowVersion) - tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-opts-cache-dedup-tq") - tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-opts-cache-dedup-tq") + tv := testvars.New(s) - // Set version 1 as current - s.startVersionWorkflow(ctx, tv1) - err := s.setCurrent(tv1, true) - s.NoError(err) + deploymentName := tv.DeploymentSeries() + buildID := tv.BuildID() + requestID := tv.Any().String() + identity := tv.Any().String() - // Set version 2 as current → version 1 starts draining - s.startVersionWorkflow(ctx, tv2) - err = s.setCurrent(tv2, true) + // First create the deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: tv.Any().String(), + }) s.NoError(err) - // Wait for version 1 to become DRAINED - s.checkVersionDrainageAndVersionStatus(ctx, tv1, - &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, - enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) - - // Workflow that waits for a signal before completing - wf := func(ctx workflow.Context) (string, error) { - workflow.GetSignalChannel(ctx, "complete").Receive(ctx, nil) - return "done", nil + computeConfig := &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": { + Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider(), + }, + }, } - // Register worker for version 1 (DRAINED) - w1 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ - DeploymentOptions: worker.DeploymentOptions{ - Version: tv1.SDKDeploymentVersion(), - UseVersioning: true, + // Create a version in the deployment + resp, err := s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, }, + Identity: identity, + RequestId: requestID, + ComputeConfig: computeConfig, }) - w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ - Name: "waitingWorkflow", - VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, + s.NoError(err) + s.NotNil(resp) + + // Verify the version exists via DescribeWorkerDeploymentVersion + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descResp, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, &workflowservice.DescribeWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + Version: tv.DeploymentVersionString(), + }) + a.NoError(err) + a.NotNil(descResp.GetWorkerDeploymentVersionInfo()) + a.Equal(tv.DeploymentVersionStringV32(), worker_versioning.ExternalWorkerDeploymentVersionToString(descResp.GetWorkerDeploymentVersionInfo().GetDeploymentVersion())) + a.NotNil(descResp.GetWorkerDeploymentVersionInfo().GetCreateTime()) + a.True(proto.Equal(computeConfig, descResp.GetWorkerDeploymentVersionInfo().GetComputeConfig())) + a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED, descResp.GetWorkerDeploymentVersionInfo().GetStatus()) + a.Equal(identity, descResp.GetWorkerDeploymentVersionInfo().GetLastModifierIdentity()) + }, 10*time.Second, 500*time.Millisecond) + + // Verify the version shows up in deployment's version summaries with CREATED status and correct compute config summary. + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descDeployResp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + }) + a.NoError(err) + a.Len(descDeployResp.GetWorkerDeploymentInfo().GetVersionSummaries(), 1) + versionSummary := descDeployResp.GetWorkerDeploymentInfo().GetVersionSummaries()[0] + a.Equal(tv.DeploymentVersionStringV32(), worker_versioning.ExternalWorkerDeploymentVersionToString(versionSummary.GetDeploymentVersion())) + a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED, versionSummary.GetStatus()) + a.True(proto.Equal(&computepb.ComputeConfigSummary{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroupSummary{ + "sg1": { + ProviderType: computeConfig.GetScalingGroups()["sg1"].GetProvider().GetType(), + }, + }, + }, versionSummary.GetComputeConfig())) + }, 10*time.Second, 500*time.Millisecond) + + // Verify the compute config summary is reflected in ListWorkerDeployments latest version summary. + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + listResp, err := s.FrontendClient().ListWorkerDeployments(ctx, &workflowservice.ListWorkerDeploymentsRequest{ + Namespace: s.Namespace().String(), + }) + a.NoError(err) + var found *workflowservice.ListWorkerDeploymentsResponse_WorkerDeploymentSummary + for _, d := range listResp.GetWorkerDeployments() { + if d.GetName() == deploymentName { + found = d + break + } + } + a.NotNil(found, "deployment %s not found in ListWorkerDeployments", deploymentName) + a.True(proto.Equal(&computepb.ComputeConfigSummary{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroupSummary{ + "sg1": { + ProviderType: computeConfig.GetScalingGroups()["sg1"].GetProvider().GetType(), + }, + }, + }, found.GetLatestVersionSummary().GetComputeConfig())) + }, 10*time.Second, 500*time.Millisecond) +} + +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_ThenPoll_TaskQueueInVersionInfo() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + tv := testvars.New(s) + + deploymentName := tv.DeploymentSeries() + buildID := tv.BuildID() + + // Create the deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: tv.Any().String(), }) - s.NoError(w1.Start()) - defer w1.Stop() + s.NoError(err) - // Register worker for version 2 (CURRENT) on the same task queue - w2 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ - DeploymentOptions: worker.DeploymentOptions{ - Version: tv2.SDKDeploymentVersion(), - UseVersioning: true, + // Create the version explicitly + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, }, + RequestId: tv.Any().String(), }) - w2.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ - Name: "waitingWorkflow", - VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, - }) - s.NoError(w2.Start()) - defer w2.Stop() + s.NoError(err) - s.waitForPollers(ctx, tv1, tv2) + // Verify the version starts with CREATED status + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descResp, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, &workflowservice.DescribeWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + Version: tv.DeploymentVersionString(), + }) + a.NoError(err) + a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CREATED, descResp.GetWorkerDeploymentVersionInfo().GetStatus()) + }, 10*time.Second, 500*time.Millisecond) - pinnedOverride := &workflowpb.VersioningOverride{ - Override: &workflowpb.VersioningOverride_Pinned{ - Pinned: &workflowpb.VersioningOverride_PinnedOverride{ - Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, - Version: tv1.ExternalDeploymentVersion(), - }, - }, - } + // Poll from the version to register a task queue + go s.pollFromDeployment(ctx, tv) - // === FIRST UPDATE OPTIONS: Should trigger reactivation (cache miss) === - // Start workflow on v2 (current version) - waits for signal to complete - wfTV1 := testvars.New(s) - run1, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ - TaskQueue: tv1.TaskQueue().String(), - ID: wfTV1.WorkflowID(), - }, "waitingWorkflow") - s.NoError(err) + // Verify the task queue shows up and status transitions to INACTIVE + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descResp, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, &workflowservice.DescribeWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + Version: tv.DeploymentVersionString(), + }) + a.NoError(err) + tqInfos := descResp.GetWorkerDeploymentVersionInfo().GetTaskQueueInfos() + a.GreaterOrEqual(len(tqInfos), 1) - // Pin the workflow to v1 using UpdateWorkflowExecutionOptions - s.Eventually(func() bool { - _, err = s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, - &workflowservice.UpdateWorkflowExecutionOptionsRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: &commonpb.WorkflowExecution{ - WorkflowId: wfTV1.WorkflowID(), - RunId: run1.GetRunID(), - }, - WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ - VersioningOverride: pinnedOverride, - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - }) - return err == nil + found := false + for _, tqInfo := range tqInfos { + if tqInfo.GetName() == tv.TaskQueue().GetName() { + found = true + break + } + } + a.True(found, "expected task queue %q in version info, got %v", tv.TaskQueue().GetName(), tqInfos) + a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, descResp.GetWorkerDeploymentVersionInfo().GetStatus()) + }, 30*time.Second, 500*time.Millisecond) + + // Verify the version shows up in deployment's version summaries with INACTIVE status + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descDeployResp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + }) + a.NoError(err) + a.Len(descDeployResp.GetWorkerDeploymentInfo().GetVersionSummaries(), 1) + a.Equal(tv.DeploymentVersionStringV32(), worker_versioning.ExternalWorkerDeploymentVersionToString(descDeployResp.GetWorkerDeploymentInfo().GetVersionSummaries()[0].GetDeploymentVersion())) + a.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_INACTIVE, descDeployResp.GetWorkerDeploymentInfo().GetVersionSummaries()[0].GetStatus()) }, 10*time.Second, 500*time.Millisecond) +} - // Signal workflow to complete - s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV1.WorkflowID(), run1.GetRunID(), "complete", nil)) +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_Idempotent() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - // Wait for workflow to complete - var result string - s.NoError(run1.Get(ctx, &result)) + tv := testvars.New(s) - // Version 1 should transition to DRAINING (reactivated) - s.checkVersionDrainageAndVersionStatus(ctx, tv1, - &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING}, - enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) + deploymentName := tv.DeploymentSeries() + buildID := tv.BuildID() + requestID := tv.Any().String() - // Wait for version 1 to become DRAINED again (workflow completed) - s.checkVersionDrainageAndVersionStatus(ctx, tv1, - &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, - enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + // First create the deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: tv.Any().String(), + }) + s.NoError(err) - // === SECOND UPDATE OPTIONS: Should NOT trigger reactivation (cache hit) === - // Start another workflow on v2 - waits for signal to complete - wfTV2 := testvars.New(s) - run2, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ - TaskQueue: tv1.TaskQueue().String(), - ID: wfTV2.WorkflowID(), - }, "waitingWorkflow") + // Create a version + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, + }, + RequestId: requestID, + }) s.NoError(err) - // Pin this workflow to v1 using UpdateWorkflowExecutionOptions (should be cached) - s.Eventually(func() bool { - _, err = s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, - &workflowservice.UpdateWorkflowExecutionOptionsRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: &commonpb.WorkflowExecution{ - WorkflowId: wfTV2.WorkflowID(), - RunId: run2.GetRunID(), - }, - WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ - VersioningOverride: pinnedOverride, - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - }) - return err == nil - }, 10*time.Second, 500*time.Millisecond) + // Create the same version again with same request ID - should be idempotent + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, + }, + RequestId: requestID, + }) + s.NoError(err) +} - // Verify version stays DRAINED for several checks (workflow is still running) - // Use Eventually with a counter to check multiple times that the signal was cached - drainedCheckCount := 0 - s.Eventually(func() bool { - resp, err := s.describeVersion(tv1) - s.NoError(err) - s.Equal(enumspb.VERSION_DRAINAGE_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetDrainageInfo().GetStatus(), - "Version should remain DRAINED because reactivation signal was cached") - s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetStatus(), - "Version status should remain DRAINED because reactivation signal was cached") - drainedCheckCount++ - return drainedCheckCount >= 5 - }, 10*time.Second, 1*time.Second) +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_AlreadyExists_DifferentRequestID() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - // Signal the workflow to complete - s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV2.WorkflowID(), run2.GetRunID(), "complete", nil)) - s.NoError(run2.Get(ctx, &result)) + tv := testvars.New(s) + + deploymentName := tv.DeploymentSeries() + buildID := tv.BuildID() + requestID1 := tv.Any().String() + requestID2 := tv.Any().String() + + // First create the deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: tv.Any().String(), + }) + s.NoError(err) + + // Create a version + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, + }, + RequestId: requestID1, + }) + s.NoError(err) + + // Try to create the same version with different request ID - should fail + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, + }, + RequestId: requestID2, + }) + s.Error(err) + var alreadyExists *serviceerror.AlreadyExists + s.ErrorAs(err, &alreadyExists) } -// TestReactivationSignalCache_Deduplication_Reset verifies that the version reactivation signal cache -// deduplicates signals when ResetWorkflowExecution is called multiple times with a pinned override -// to a DRAINED version. -func (s *DeploymentVersionSuite) TestReactivationSignalCache_Deduplication_Reset() { - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusVisibilityGracePeriod, 10*time.Second) - s.OverrideDynamicConfig(dynamicconfig.VersionDrainageStatusRefreshInterval, 30*time.Second) +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_DeploymentNotFound() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + tv := testvars.New(s) + + // Try to create a version for a deployment that doesn't exist + _, err := s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), + }, + RequestId: tv.Any().String(), + }) + s.Error(err) + var notFound *serviceerror.NotFound + s.ErrorAs(err, ¬Found) +} - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_InvalidArgs() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - // Use shorter, explicit deployment series names to avoid truncation issues - deploymentName := fmt.Sprintf("test-reset-cache-dedup-wfv%d", s.workflowVersion) - tv1 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v1").WithTaskQueue("test-reset-cache-dedup-tq") - tv2 := testvars.New(s).WithDeploymentSeries(deploymentName).WithBuildID(deploymentName + "-v2").WithTaskQueue("test-reset-cache-dedup-tq") + testCases := []struct { + name string + deploymentName string + buildID string + expectedError string + }{ + { + name: "empty deployment name", + deploymentName: "", + buildID: "build-1", + expectedError: "deployment name cannot be empty", + }, + { + name: "empty build ID", + deploymentName: "my-deployment", + buildID: "", + expectedError: "build ID cannot be empty", + }, + } - // Set version 1 as current - s.startVersionWorkflow(ctx, tv1) - err := s.setCurrent(tv1, true) - s.NoError(err) + for _, tc := range testCases { + s.Run(tc.name, func() { + _, err := s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: tc.deploymentName, + BuildId: tc.buildID, + }, + RequestId: testvars.New(s).Any().String(), + }) + s.Error(err) + var invalidArg *serviceerror.InvalidArgument + s.ErrorAs(err, &invalidArg) + s.Contains(invalidArg.Message, tc.expectedError) + }) + } +} - // Set version 2 as current → version 1 starts draining - s.startVersionWorkflow(ctx, tv2) - err = s.setCurrent(tv2, true) - s.NoError(err) +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_AutoCreatedByPoller_ConflictWithExplicitCreate() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - // Wait for version 1 to become DRAINED - s.checkVersionDrainageAndVersionStatus(ctx, tv1, - &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, - enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + tv := testvars.New(s) - // Workflow that waits for a signal before completing - wf := func(ctx workflow.Context) (string, error) { - workflow.GetSignalChannel(ctx, "complete").Receive(ctx, nil) - return "done", nil - } + // Create version via polling (auto-creates deployment and version) + s.startVersionWorkflow(ctx, tv) - // Register worker for version 1 (DRAINED) - w1 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ - DeploymentOptions: worker.DeploymentOptions{ - Version: tv1.SDKDeploymentVersion(), - UseVersioning: true, + // Try to explicitly create the same version with a different request ID + _, err := s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: tv.DeploymentSeries(), + BuildId: tv.BuildID(), }, + RequestId: tv.Any().String(), }) - w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ - Name: "waitingWorkflow", - VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, - }) - s.NoError(w1.Start()) - defer w1.Stop() + s.Error(err) + var alreadyExists *serviceerror.AlreadyExists + s.ErrorAs(err, &alreadyExists) +} - // Register worker for version 2 (CURRENT) on the same task queue - w2 := worker.New(s.SdkClient(), tv1.TaskQueue().String(), worker.Options{ - DeploymentOptions: worker.DeploymentOptions{ - Version: tv2.SDKDeploymentVersion(), - UseVersioning: true, - }, - }) - w2.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{ - Name: "waitingWorkflow", - VersioningBehavior: workflow.VersioningBehaviorAutoUpgrade, - }) - s.NoError(w2.Start()) - defer w2.Stop() +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_MultipleVersions() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - s.waitForPollers(ctx, tv1, tv2) + tv := testvars.New(s) - pinnedOverride := &workflowpb.VersioningOverride{ - Override: &workflowpb.VersioningOverride_Pinned{ - Pinned: &workflowpb.VersioningOverride_PinnedOverride{ - Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, - Version: tv1.ExternalDeploymentVersion(), + deploymentName := tv.DeploymentSeries() + + // First create the deployment + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: tv.Any().String(), + }) + s.NoError(err) + + computeConfig1 := &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": { + Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider(), + }, + }, + } + computeConfig2 := &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg2": { + Provider: computeprovider.TestInvokeComputeProviderValidComputeProvider(), }, }, } - // Helper function to start a workflow, wait for task completion, and get reset event ID - startAndGetResetEventID := func(wfID string) (sdkclient.WorkflowRun, int64) { - run, err := s.SdkClient().ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{ - TaskQueue: tv1.TaskQueue().String(), - ID: wfID, - }, "waitingWorkflow") - s.NoError(err) - - // Wait for workflow task completion (creates a reset point) - s.Eventually(func() bool { - hist := s.SdkClient().GetWorkflowHistory(ctx, wfID, run.GetRunID(), false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) - for hist.HasNext() { - event, err := hist.Next() - if err != nil { - return false - } - if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED { - return true - } - } - return false - }, 10*time.Second, 200*time.Millisecond) + // Create first version + tv1 := tv.WithBuildIDNumber(1) + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: tv1.BuildID(), + }, + RequestId: tv.Any().String(), + ComputeConfig: computeConfig1, + }) + s.NoError(err) - // Find the reset event ID - var resetEventID int64 - hist := s.SdkClient().GetWorkflowHistory(ctx, wfID, run.GetRunID(), false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) - for hist.HasNext() { - event, err := hist.Next() - s.NoError(err) - if event.EventType == enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED { - resetEventID = event.EventId - break - } - } - s.Positive(resetEventID) - return run, resetEventID - } + // Create second version + tv2 := tv.WithBuildIDNumber(2) + _, err = s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: tv2.BuildID(), + }, + RequestId: tv.Any().String(), + ComputeConfig: computeConfig2, + }) + s.NoError(err) - // === FIRST RESET: Should trigger reactivation (cache miss) === - wfTV1 := testvars.New(s) - run1, resetEventID1 := startAndGetResetEventID(wfTV1.WorkflowID()) + // Verify both versions show up in deployment's version summaries + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descResp, err := s.FrontendClient().DescribeWorkerDeployment(ctx, &workflowservice.DescribeWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + }) + a.NoError(err) + a.Len(descResp.GetWorkerDeploymentInfo().GetVersionSummaries(), 2) + }, 10*time.Second, 500*time.Millisecond) - // Reset with pinned override to v1 (DRAINED) - var resetResp1 *workflowservice.ResetWorkflowExecutionResponse - s.Eventually(func() bool { - var resetErr error - resetResp1, resetErr = s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ + // Verify compute configs via DescribeWorkerDeploymentVersion + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descV1, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, &workflowservice.DescribeWorkerDeploymentVersionRequest{ Namespace: s.Namespace().String(), - WorkflowExecution: &commonpb.WorkflowExecution{ - WorkflowId: wfTV1.WorkflowID(), - RunId: run1.GetRunID(), - }, - Reason: "testing-reset-cache-dedup-1", - RequestId: uuid.NewString(), - WorkflowTaskFinishEventId: resetEventID1, - PostResetOperations: []*workflowpb.PostResetOperation{ - { - Variant: &workflowpb.PostResetOperation_UpdateWorkflowOptions_{ - UpdateWorkflowOptions: &workflowpb.PostResetOperation_UpdateWorkflowOptions{ - WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ - VersioningOverride: pinnedOverride, - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - }, - }, - }, - }, + Version: tv1.DeploymentVersionString(), }) - return resetErr == nil + a.NoError(err) + a.True(proto.Equal(computeConfig1, descV1.GetWorkerDeploymentVersionInfo().GetComputeConfig())) }, 10*time.Second, 500*time.Millisecond) - // Signal the reset workflow to complete - s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV1.WorkflowID(), resetResp1.RunId, "complete", nil)) - - // Wait for workflow to complete - resetRun1 := s.SdkClient().GetWorkflow(ctx, wfTV1.WorkflowID(), resetResp1.RunId) - var result string - s.NoError(resetRun1.Get(ctx, &result)) + s.EventuallyWithT(func(t *assert.CollectT) { + a := require.New(t) + descV2, err := s.FrontendClient().DescribeWorkerDeploymentVersion(ctx, &workflowservice.DescribeWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + Version: tv2.DeploymentVersionString(), + }) + a.NoError(err) + a.True(proto.Equal(computeConfig2, descV2.GetWorkerDeploymentVersionInfo().GetComputeConfig())) + }, 10*time.Second, 500*time.Millisecond) +} - // Version 1 should transition to DRAINING (reactivated) - s.checkVersionDrainageAndVersionStatus(ctx, tv1, - &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINING}, - enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINING, - true, true) +func (s *DeploymentVersionSuite) TestCreateWorkerDeploymentVersion_InvalidScalingGroups() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - // Wait for version 1 to become DRAINED again (workflow completed) - s.checkVersionDrainageAndVersionStatus(ctx, tv1, - &deploymentpb.VersionDrainageInfo{Status: enumspb.VERSION_DRAINAGE_STATUS_DRAINED}, - enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, - true, true) + tv := testvars.New(s) + deploymentName := tv.DeploymentSeries() - // === SECOND RESET: Should NOT trigger reactivation (cache hit) === - wfTV2 := testvars.New(s) - run2, resetEventID2 := startAndGetResetEventID(wfTV2.WorkflowID()) + // Create the deployment first + _, err := s.FrontendClient().CreateWorkerDeployment(ctx, &workflowservice.CreateWorkerDeploymentRequest{ + Namespace: s.Namespace().String(), + DeploymentName: deploymentName, + RequestId: tv.Any().String(), + }) + s.NoError(err) - // Reset with pinned override to v1 (should be cached) - var resetResp2 *workflowservice.ResetWorkflowExecutionResponse - s.Eventually(func() bool { - var resetErr error - resetResp2, resetErr = s.FrontendClient().ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{ - Namespace: s.Namespace().String(), - WorkflowExecution: &commonpb.WorkflowExecution{ - WorkflowId: wfTV2.WorkflowID(), - RunId: run2.GetRunID(), + validProvider := computeprovider.TestInvokeComputeProviderValidComputeProvider() + + testCases := []struct { + name string + computeConfig *computepb.ComputeConfig + expectedError string + }{ + { + name: "invalid compute provider type", + computeConfig: &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {TaskQueueTypes: nil, Provider: &computepb.ComputeProvider{Type: "invalid-provider"}}, + }, }, - Reason: "testing-reset-cache-dedup-2", - RequestId: uuid.NewString(), - WorkflowTaskFinishEventId: resetEventID2, - PostResetOperations: []*workflowpb.PostResetOperation{ - { - Variant: &workflowpb.PostResetOperation_UpdateWorkflowOptions_{ - UpdateWorkflowOptions: &workflowpb.PostResetOperation_UpdateWorkflowOptions{ - WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ - VersioningOverride: pinnedOverride, - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}, - }, - }, + expectedError: "invalid compute provider type", + }, + { + name: "invalid compute provider details", + computeConfig: &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {TaskQueueTypes: nil, Provider: computeprovider.TestInvokeComputeProviderInvalidComputeProvider()}, }, }, - }) - return resetErr == nil - }, 10*time.Second, 500*time.Millisecond) - - // Verify version stays DRAINED for several checks (reset workflow is still running) - // Use Eventually with a counter to check multiple times that the signal was cached - drainedCheckCount := 0 - s.Eventually(func() bool { - resp, err := s.describeVersion(tv1) - s.NoError(err) - s.Equal(enumspb.VERSION_DRAINAGE_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetDrainageInfo().GetStatus(), - "Version should remain DRAINED because reactivation signal was cached") - s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_DRAINED, resp.GetWorkerDeploymentVersionInfo().GetStatus(), - "Version status should remain DRAINED because reactivation signal was cached") - drainedCheckCount++ - return drainedCheckCount >= 5 - }, 10*time.Second, 1*time.Second) - - // Signal the reset workflow to complete - s.NoError(s.SdkClient().SignalWorkflow(ctx, wfTV2.WorkflowID(), resetResp2.RunId, "complete", nil)) + expectedError: "illegal_field found in config", + }, + { + name: "two catch-all scaling groups", + computeConfig: &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {TaskQueueTypes: nil, Provider: validProvider}, + "sg2": {TaskQueueTypes: nil, Provider: validProvider}, + }, + }, + expectedError: "only one scaling group can have no task types defined", + }, + { + name: "overlapping workflow task queue type", + computeConfig: &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "sg1": {TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW}, Provider: validProvider}, + "sg2": {TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW, enumspb.TASK_QUEUE_TYPE_ACTIVITY}, Provider: validProvider}, + }, + }, + expectedError: "task type Workflow appears in more than one entry", + }, + } - // Wait for workflow to complete - resetRun2 := s.SdkClient().GetWorkflow(ctx, wfTV2.WorkflowID(), resetResp2.RunId) - var result2 string - s.NoError(resetRun2.Get(ctx, &result2)) + for _, tc := range testCases { + s.Run(tc.name, func() { + _, err := s.FrontendClient().CreateWorkerDeploymentVersion(ctx, &workflowservice.CreateWorkerDeploymentVersionRequest{ + Namespace: s.Namespace().String(), + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: testvars.New(s).BuildID(), + }, + RequestId: testvars.New(s).Any().String(), + ComputeConfig: tc.computeConfig, + }) + s.Error(err) + var invalidArg *serviceerror.InvalidArgument + s.ErrorAs(err, &invalidArg) + s.Contains(invalidArg.Message, tc.expectedError) + }) + } } diff --git a/tests/worker_registry_test.go b/tests/worker_registry_test.go index 71f2ba00cbf..df1769b7be0 100644 --- a/tests/worker_registry_test.go +++ b/tests/worker_registry_test.go @@ -2,6 +2,7 @@ package tests import ( "context" + "errors" "fmt" "testing" "time" @@ -29,7 +30,6 @@ func TestWorkerRegistryTestSuite(t *testing.T) { } func (s *WorkerRegistryTestSuite) SetupTest() { - s.OverrideDynamicConfig(dynamicconfig.ListWorkersEnabled, true) s.OverrideDynamicConfig(dynamicconfig.WorkerHeartbeatsEnabled, true) s.FunctionalTestBase.SetupTest() @@ -118,6 +118,37 @@ func (s *WorkerRegistryTestSuite) TestWorkerRegistry_DescribeWorker() { } } +func (s *WorkerRegistryTestSuite) TestWorkerRegistry_ListWorkersWithNoHeartbeats() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := s.FrontendClient().ListWorkers(ctx, &workflowservice.ListWorkersRequest{ + Namespace: s.Namespace().String(), + Query: "TaskQueue='no-heartbeats-recorded-on-this-queue'", + }) + s.NoError(err) + s.NotNil(resp) + s.Empty(resp.GetWorkersInfo()) //nolint:staticcheck // testing deprecated field + s.Empty(resp.GetWorkers()) +} + +func (s *WorkerRegistryTestSuite) TestWorkerRegistry_DescribeWorkerWithNoHeartbeats() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + _, err := s.FrontendClient().DescribeWorker(ctx, &workflowservice.DescribeWorkerRequest{ + Namespace: s.Namespace().String(), + WorkerInstanceKey: "nonexistent-worker", + }) + s.Require().Error(err) + var notFound *serviceerror.NotFound + var namespaceNotFound *serviceerror.NamespaceNotFound + s.True( + errors.As(err, ¬Found) || errors.As(err, &namespaceNotFound), + "expected NotFound or NamespaceNotFound, got: %v", err, + ) +} + func (s *WorkerRegistryTestSuite) TestWorkerRegistry_ListWorkers() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -154,10 +185,17 @@ func (s *WorkerRegistryTestSuite) TestWorkerRegistry_ListWorkers() { s.NotNil(resp) s.Len(resp.GetWorkersInfo(), 1) + // Verify deprecated WorkersInfo field (backward compatibility) workerHeartbeat := resp.GetWorkersInfo()[0].GetWorkerHeartbeat() s.Equal(worker1Key, workerHeartbeat.WorkerInstanceKey) s.Equal(sharedTaskQueue, workerHeartbeat.TaskQueue) s.Equal(int32(1), workerHeartbeat.TotalStickyCacheHit) + + // Verify new Workers field (WorkerListInfo) + s.Len(resp.GetWorkers(), 1) + workerListInfo := resp.GetWorkers()[0] + s.Equal(worker1Key, workerListInfo.GetWorkerInstanceKey()) + s.Equal(sharedTaskQueue, workerListInfo.GetTaskQueue()) } { resp, err := s.FrontendClient().ListWorkers(ctx, &workflowservice.ListWorkersRequest{ @@ -167,28 +205,40 @@ func (s *WorkerRegistryTestSuite) TestWorkerRegistry_ListWorkers() { s.NoError(err) s.NotNil(resp) + // Verify deprecated WorkersInfo field workers := resp.GetWorkersInfo() - // Collect workers by their instance key workersByKey := make(map[string]*workerpb.WorkerHeartbeat) for _, workerInfo := range workers { heartbeat := workerInfo.GetWorkerHeartbeat() workersByKey[heartbeat.WorkerInstanceKey] = heartbeat } - - // Verify we have exactly the workers we expect s.Len(workersByKey, 2) - // Verify worker1 worker1, exists := workersByKey[worker1Key] s.True(exists, "worker1 should exist") s.Equal(sharedTaskQueue, worker1.TaskQueue) s.Equal(int32(1), worker1.TotalStickyCacheHit) - // Verify worker2 worker2, exists := workersByKey[worker2Key] s.True(exists, "worker2 should exist") s.Equal(sharedTaskQueue, worker2.TaskQueue) s.Equal(int32(2), worker2.TotalStickyCacheHit) + + // Verify new Workers field (WorkerListInfo) + listInfos := resp.GetWorkers() + s.Len(listInfos, 2) + listInfoByKey := make(map[string]*workerpb.WorkerListInfo) + for _, info := range listInfos { + listInfoByKey[info.GetWorkerInstanceKey()] = info + } + + listInfo1, exists := listInfoByKey[worker1Key] + s.True(exists, "worker1 list info should exist") + s.Equal(sharedTaskQueue, listInfo1.GetTaskQueue()) + + listInfo2, exists := listInfoByKey[worker2Key] + s.True(exists, "worker2 list info should exist") + s.Equal(sharedTaskQueue, listInfo2.GetTaskQueue()) } { nonExistentWorkerKey := s.tv.WorkerIdentity() + "_nonexistent" @@ -198,7 +248,8 @@ func (s *WorkerRegistryTestSuite) TestWorkerRegistry_ListWorkers() { }) s.NoError(err) s.NotNil(resp) - s.Len(resp.GetWorkersInfo(), 0) + s.Empty(resp.GetWorkersInfo()) //nolint:staticcheck // testing deprecated field + s.Empty(resp.GetWorkers()) } } @@ -211,7 +262,7 @@ func (s *WorkerRegistryTestSuite) TestWorkerRegistry_ListWorkersPagination() { // Create 5 workers with predictable keys for pagination testing workerKeys := make([]string, 5) heartbeats := make([]*workerpb.WorkerHeartbeat, 5) - for i := 0; i < 5; i++ { + for i := range 5 { workerKeys[i] = fmt.Sprintf("%s_worker_%02d", s.tv.WorkerIdentity(), i) heartbeats[i] = &workerpb.WorkerHeartbeat{ WorkerInstanceKey: workerKeys[i], diff --git a/tests/workflow_alias_search_attribute_test.go b/tests/workflow_alias_search_attribute_test.go index c9607ce296b..557a8663070 100644 --- a/tests/workflow_alias_search_attribute_test.go +++ b/tests/workflow_alias_search_attribute_test.go @@ -36,7 +36,7 @@ func TestWorkflowAliasSearchAttributeTestSuite(t *testing.T) { func (s *WorkflowAliasSearchAttributeTestSuite) SetupTest() { s.FunctionalTestBase.SetupTest() - s.Worker().RegisterWorkflow(s.workflowFunc) + s.SdkWorker().RegisterWorkflow(s.workflowFunc) } func (s *WorkflowAliasSearchAttributeTestSuite) workflowFunc(ctx workflow.Context) (string, error) { @@ -104,7 +104,7 @@ func (s *WorkflowAliasSearchAttributeTestSuite) createWorkflow( WorkflowType: tv.WorkflowType(), TaskQueue: tv.TaskQueue(), Identity: tv.WorkerIdentity(), - VersioningOverride: tv.VersioningOverridePinned(true), + VersioningOverride: tv.VersioningOverridePinned(), SearchAttributes: sa, } return s.FrontendClient().StartWorkflowExecution(ctx, request) diff --git a/tests/workflow_buffered_events_test.go b/tests/workflow_buffered_events_test.go index 7547ac5a8c4..83392593f81 100644 --- a/tests/workflow_buffered_events_test.go +++ b/tests/workflow_buffered_events_test.go @@ -76,11 +76,11 @@ func (s *WorkflowBufferedEventsTestSuite) TestRateLimitBufferedEvents() { if !signalsSent { signalsSent = true // Buffered Signals - for i := 0; i < 100; i++ { + for i := range 100 { buf := new(bytes.Buffer) err := binary.Write(buf, binary.LittleEndian, byte(i)) s.NoError(err) - s.Nil(s.SendSignal(s.Namespace().String(), workflowExecution, "SignalName", payloads.EncodeBytes(buf.Bytes()), identity)) + s.NoError(s.SendSignal(s.Namespace().String(), workflowExecution, "SignalName", payloads.EncodeBytes(buf.Bytes()), identity)) } buf := new(bytes.Buffer) @@ -116,7 +116,7 @@ func (s *WorkflowBufferedEventsTestSuite) TestRateLimitBufferedEvents() { // first workflow task to send 101 signals, the last signal will force fail workflow task and flush buffered events. _, err := poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) - s.NotNil(err) + s.Error(err) s.IsType(&serviceerror.NotFound{}, err) s.Equal("Workflow task not found.", err.Error()) diff --git a/tests/workflow_delete_execution_test.go b/tests/workflow_delete_execution_test.go index 251b7065407..3753d44f072 100644 --- a/tests/workflow_delete_execution_test.go +++ b/tests/workflow_delete_execution_test.go @@ -53,7 +53,7 @@ func (s *WorkflowDeleteExecutionSuite) TestDeleteWorkflowExecution_CompetedWorkf var wes []*commonpb.WorkflowExecution // Start numExecutions workflow executions. - for i := 0; i < numExecutions; i++ { + for i := range numExecutions { we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), Namespace: s.Namespace().String(), @@ -197,7 +197,7 @@ func (s *WorkflowDeleteExecutionSuite) TestDeleteWorkflowExecution_RunningWorkfl var wes []*commonpb.WorkflowExecution // Start numExecutions workflow executions. - for i := 0; i < numExecutions; i++ { + for i := range numExecutions { we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), Namespace: s.Namespace().String(), @@ -312,7 +312,7 @@ func (s *WorkflowDeleteExecutionSuite) TestDeleteWorkflowExecution_JustTerminate var wes []*commonpb.WorkflowExecution // Start numExecutions workflow executions. - for i := 0; i < numExecutions; i++ { + for i := range numExecutions { we, err := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), Namespace: s.Namespace().String(), diff --git a/tests/workflow_failures_test.go b/tests/workflow_failures_test.go index 2571a1f145b..2570bfb8dde 100644 --- a/tests/workflow_failures_test.go +++ b/tests/workflow_failures_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -21,21 +20,22 @@ import ( "go.temporal.io/server/common/convert" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) -type WorkflowFailuresTestSuite struct { - testcore.FunctionalTestBase +type WorkflowFailuresSuite struct { + parallelsuite.Suite[*WorkflowFailuresSuite] } func TestWorkflowFailuresTestSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(WorkflowFailuresTestSuite)) + parallelsuite.Run(t, &WorkflowFailuresSuite{}) } -func (s *WorkflowFailuresTestSuite) TestWorkflowTimeout() { +func (suite *WorkflowFailuresSuite) TestWorkflowTimeout() { + s := testcore.NewEnv(suite.T()) startTime := time.Now().UTC() id := "functional-workflow-timeout" @@ -64,7 +64,7 @@ func (s *WorkflowFailuresTestSuite) TestWorkflowTimeout() { var historyEvents []*historypb.HistoryEvent GetHistoryLoop: - for i := 0; i < 10; i++ { + for range 10 { historyEvents = s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: id, RunId: we.RunId, @@ -91,7 +91,7 @@ GetHistoryLoop: closedCount := 0 ListClosedLoop: - for i := 0; i < 10; i++ { + for range 10 { resp, err3 := s.FrontendClient().ListClosedWorkflowExecutions(testcore.NewContext(), &workflowservice.ListClosedWorkflowExecutionsRequest{ Namespace: s.Namespace().String(), MaximumPageSize: 100, @@ -112,7 +112,8 @@ ListClosedLoop: s.Equal(1, closedCount) } -func (s *WorkflowFailuresTestSuite) TestWorkflowTaskFailed() { +func (suite *WorkflowFailuresSuite) TestWorkflowTaskFailed() { + s := testcore.NewEnv(suite.T()) id := "functional-workflowtask-failed-test" wt := "functional-workflowtask-failed-test-type" tl := "functional-workflowtask-failed-test-taskqueue" @@ -173,7 +174,7 @@ func (s *WorkflowFailuresTestSuite) TestWorkflowTaskFailed() { if !activityScheduled { activityScheduled = true buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityData)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityData)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -316,10 +317,11 @@ func (s *WorkflowFailuresTestSuite) TestWorkflowTaskFailed() { lastWorkflowTaskStartedEvent := events[25] s.Equal(lastWorkflowTaskTime, lastWorkflowTaskStartedEvent.GetEventTime().AsTime()) wfCompletedEvent := events[27] - s.True(wfCompletedEvent.GetEventTime().AsTime().Sub(lastWorkflowTaskTime) >= time.Second) + s.GreaterOrEqual(wfCompletedEvent.GetEventTime().AsTime().Sub(lastWorkflowTaskTime), time.Second) } -func (s *WorkflowFailuresTestSuite) TestRespondWorkflowTaskCompleted_ReturnsErrorIfInvalidArgument() { +func (suite *WorkflowFailuresSuite) TestRespondWorkflowTaskCompletedReturnsErrorIfInvalidArgument() { + s := testcore.NewEnv(suite.T()) id := "functional-respond-workflow-task-completed-test" wt := "functional-respond-workflow-task-completed-test-type" tq := "functional-respond-workflow-task-completed-test-taskqueue" @@ -378,5 +380,6 @@ func (s *WorkflowFailuresTestSuite) TestRespondWorkflowTaskCompleted_ReturnsErro 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, historyEvents) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled`, historyEvents) } diff --git a/tests/workflow_reset_test.go b/tests/workflow_reset_test.go index bdff6cec77c..5d942eba576 100644 --- a/tests/workflow_reset_test.go +++ b/tests/workflow_reset_test.go @@ -75,7 +75,7 @@ func (s *WorkflowResetSuite) TestNoBaseCurrentRunning() { }) s.NoError(err) s.Equal(currentMutableState.GetDatabaseMutableState().ExecutionInfo.ResetRunId, newRunID) - s.Equal(currentMutableState.GetDatabaseMutableState().ExecutionState.Status, enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, currentMutableState.GetDatabaseMutableState().ExecutionState.Status) } // No explicit base run provided. current run is closed. @@ -104,7 +104,7 @@ func (s *WorkflowResetSuite) TestNoBaseCurrentClosed() { }) s.NoError(err) s.Equal(currentMutableState.GetDatabaseMutableState().ExecutionInfo.ResetRunId, newRunID) - s.Equal(currentMutableState.GetDatabaseMutableState().ExecutionState.Status, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED) + s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, currentMutableState.GetDatabaseMutableState().ExecutionState.Status) } // Explicit base run is provided to be reset and its the same as currently running execution. @@ -204,7 +204,7 @@ func (s *WorkflowResetSuite) TestOriginalExecutionRunId() { runs := s.setupRuns(ctx, workflowID, 1, true, versioningConfig{}) baseRunID := runs[0] // Reset the current run repeatedly. Verify that each time the new run points to the original baseRunID - for i := 0; i < 5; i++ { + for range 5 { currentRunID := s.performReset(ctx, workflowID, baseRunID) baseMutableState, err := s.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{ Namespace: s.Namespace().String(), diff --git a/tests/workflow_reset_with_child_test.go b/tests/workflow_reset_with_child_test.go index b0174b26492..8a5530cae2a 100644 --- a/tests/workflow_reset_with_child_test.go +++ b/tests/workflow_reset_with_child_test.go @@ -49,12 +49,12 @@ func TestWorkflowResetWithChildTestSuite(t *testing.T) { func (s *WorkflowResetWithChildSuite) SetupTest() { s.FunctionalTestBase.SetupTest() - s.Worker().RegisterWorkflow(s.WorkflowWithChildren) - s.Worker().RegisterWorkflow(s.WorkflowWithWaitingChild) - s.Worker().RegisterWorkflow(child) - s.Worker().RegisterWorkflow(s.waitingChild) - s.Worker().RegisterActivity(simpleActivity) - err := s.Worker().Start() + s.SdkWorker().RegisterWorkflow(s.WorkflowWithChildren) + s.SdkWorker().RegisterWorkflow(s.WorkflowWithWaitingChild) + s.SdkWorker().RegisterWorkflow(child) + s.SdkWorker().RegisterWorkflow(s.waitingChild) + s.SdkWorker().RegisterActivity(simpleActivity) + err := s.SdkWorker().Start() s.NoError(err) } diff --git a/tests/workflow_task_reported_problems_test.go b/tests/workflow_task_reported_problems_test.go index 476ba6d77d8..63a73b6a11c 100644 --- a/tests/workflow_task_reported_problems_test.go +++ b/tests/workflow_task_reported_problems_test.go @@ -84,7 +84,7 @@ func (s *WFTFailureReportedProblemsTestSuite) TestWFTFailureReportedProblems_Set s.shouldFail.Store(true) - s.Worker().RegisterWorkflow(s.simpleWorkflowWithShouldFail) + s.SdkWorker().RegisterWorkflow(s.simpleWorkflowWithShouldFail) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), @@ -126,7 +126,7 @@ func (s *WFTFailureReportedProblemsTestSuite) TestWFTFailureReportedProblems_Not ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - s.Worker().RegisterWorkflow(s.workflowWithSignalsThatFails) + s.SdkWorker().RegisterWorkflow(s.workflowWithSignalsThatFails) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), @@ -193,8 +193,8 @@ func (s *WFTFailureReportedProblemsTestSuite) TestWFTFailureReportedProblems_Set s.shouldFail.Store(true) - s.Worker().RegisterWorkflow(s.workflowWithActivity) - s.Worker().RegisterActivity(s.simpleActivity) + s.SdkWorker().RegisterWorkflow(s.workflowWithActivity) + s.SdkWorker().RegisterActivity(s.simpleActivity) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), @@ -237,7 +237,7 @@ func (s *WFTFailureReportedProblemsTestSuite) TestWFTFailureReportedProblems_Dyn defer cleanup() s.shouldFail.Store(true) - s.Worker().RegisterWorkflow(s.simpleWorkflowWithShouldFail) + s.SdkWorker().RegisterWorkflow(s.simpleWorkflowWithShouldFail) workflowOptions := sdkclient.StartWorkflowOptions{ ID: testcore.RandomizeStr("wf_id-" + s.T().Name()), diff --git a/tests/workflow_task_test.go b/tests/workflow_task_test.go index 22eaf4b73a8..16b5e0983f1 100644 --- a/tests/workflow_task_test.go +++ b/tests/workflow_task_test.go @@ -77,7 +77,7 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTaskHeartbeatingWithEmptyResult() { taskToken := resp1.GetTaskToken() hbTimeout := 0 - for i := 0; i < 12; i++ { + for range 12 { resp2, err2 := s.FrontendClient().RespondWorkflowTaskCompleted(testcore.NewContext(), &workflowservice.RespondWorkflowTaskCompletedRequest{ Namespace: s.Namespace().String(), TaskToken: taskToken, @@ -620,7 +620,7 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalBeforeTransientWork 2 WorkflowTaskScheduled`, s.GetHistory(s.Namespace().String(), we)) cause := enumspb.WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE - for i := 0; i < 10; i++ { + for i := range 10 { resp1, err1 := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: taskQueue, @@ -649,7 +649,8 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalBeforeTransientWork 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), we)) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled`, s.GetHistory(s.Namespace().String(), we)) _, err0 = s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ Namespace: s.Namespace().String(), @@ -660,12 +661,14 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalBeforeTransientWork RequestId: uuid.NewString(), }) s.NoError(err0) + histAfterSignal := s.GetHistory(s.Namespace().String(), we) + s.GreaterOrEqual(len(histAfterSignal), 5, "Should have at least 5 events after signal") s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted 4 WorkflowTaskFailed - 5 WorkflowExecutionSignaled`, s.GetHistory(s.Namespace().String(), we)) + 5 WorkflowExecutionSignaled`, histAfterSignal[:5]) // start this transient workflow task, the attempt should be cleared and it becomes again a regular workflow task resp1, err1 := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ @@ -740,7 +743,7 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 2 WorkflowTaskScheduled`, s.GetHistory(s.Namespace().String(), we)) cause := enumspb.WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE - for i := 0; i < 10; i++ { + for i := range 10 { resp1, err1 := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: taskQueue, @@ -769,7 +772,8 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), we)) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled`, s.GetHistory(s.Namespace().String(), we)) // start workflow task to make signals into bufferedEvents _, err1 := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ @@ -783,7 +787,9 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), we)) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted`, s.GetHistory(s.Namespace().String(), we)) // this signal should be buffered _, err0 = s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ @@ -799,7 +805,9 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), we)) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted`, s.GetHistory(s.Namespace().String(), we)) // then terminate the workflow _, err := s.FrontendClient().TerminateWorkflowExecution(testcore.NewContext(), &workflowservice.TerminateWorkflowExecutionRequest{ @@ -853,7 +861,7 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 2 WorkflowTaskScheduled`, s.GetHistory(s.Namespace().String(), we)) cause := enumspb.WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE - for i := 0; i < 10; i++ { + for i := range 10 { resp1, err1 := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ Namespace: s.Namespace().String(), TaskQueue: taskQueue, @@ -882,7 +890,8 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), we)) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled`, s.GetHistory(s.Namespace().String(), we)) // start workflow task to make signals into bufferedEvents resp1, err1 := s.FrontendClient().PollWorkflowTaskQueue(testcore.NewContext(), &workflowservice.PollWorkflowTaskQueueRequest{ @@ -896,7 +905,9 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), we)) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted`, s.GetHistory(s.Namespace().String(), we)) // this signal should be buffered _, err0 = s.FrontendClient().SignalWorkflowExecution(testcore.NewContext(), &workflowservice.SignalWorkflowExecutionRequest{ @@ -912,7 +923,9 @@ func (s *WorkflowTaskTestSuite) TestWorkflowTerminationSignalAfterTransientWorkf 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled 3 WorkflowTaskStarted - 4 WorkflowTaskFailed`, s.GetHistory(s.Namespace().String(), we)) + 4 WorkflowTaskFailed + 5 WorkflowTaskScheduled + 6 WorkflowTaskStarted`, s.GetHistory(s.Namespace().String(), we)) // fail this workflow task to flush buffer _, err2 := s.FrontendClient().RespondWorkflowTaskFailed(testcore.NewContext(), &workflowservice.RespondWorkflowTaskFailedRequest{ diff --git a/tests/workflow_test.go b/tests/workflow_test.go index 00438248861..9d5f44046fc 100644 --- a/tests/workflow_test.go +++ b/tests/workflow_test.go @@ -873,7 +873,7 @@ func (s *WorkflowTestSuite) TestTerminateWorkflow() { if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -932,7 +932,7 @@ func (s *WorkflowTestSuite) TestTerminateWorkflow() { var historyEvents []*historypb.HistoryEvent GetHistoryLoop: - for i := 0; i < 10; i++ { + for range 10 { historyEvents = s.GetHistory(s.Namespace().String(), &commonpb.WorkflowExecution{ WorkflowId: tv.WorkflowID(), RunId: we.RunId, @@ -961,7 +961,7 @@ GetHistoryLoop: newExecutionStarted := false StartNewExecutionLoop: - for i := 0; i < 10; i++ { + for range 10 { request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), Namespace: s.Namespace().String(), @@ -1081,7 +1081,7 @@ func (s *WorkflowTestSuite) TestSequentialWorkflow() { if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -1109,7 +1109,7 @@ func (s *WorkflowTestSuite) TestSequentialWorkflow() { expectedActivity := int32(1) atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - s.EqualValues(tv.WorkflowID(), task.WorkflowExecution.WorkflowId) + s.Equal(tv.WorkflowID(), task.WorkflowExecution.WorkflowId) s.Equal(tv.ActivityType().Name, task.ActivityType.Name) s.Equal(tv.WithActivityIDNumber(int(expectedActivity)).ActivityID(), task.ActivityId) s.Equal(expectedActivity, s.DecodePayloadsByteSliceInt32(task.Input)) @@ -1131,7 +1131,7 @@ func (s *WorkflowTestSuite) TestSequentialWorkflow() { T: s.T(), } - for i := 0; i < 10; i++ { + for i := range 10 { _, err := poller.PollAndProcessWorkflowTask() s.Logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) s.NoError(err) @@ -1208,7 +1208,7 @@ func (s *WorkflowTestSuite) TestCompleteWorkflowTaskAndCreateNewOne() { s.Equal(int64(3), newTask.WorkflowTask.GetPreviousStartedEventId()) s.Equal(int64(7), newTask.WorkflowTask.GetStartedEventId()) - s.Equal(4, len(newTask.WorkflowTask.History.Events)) + s.Len(newTask.WorkflowTask.History.Events, 4) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_COMPLETED, newTask.WorkflowTask.History.Events[0].GetEventType()) s.Equal(enumspb.EVENT_TYPE_MARKER_RECORDED, newTask.WorkflowTask.History.Events[1].GetEventType()) s.Equal(enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED, newTask.WorkflowTask.History.Events[2].GetEventType()) @@ -1245,7 +1245,7 @@ func (s *WorkflowTestSuite) TestWorkflowTaskAndActivityTaskTimeoutsWorkflow() { if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -1274,9 +1274,9 @@ func (s *WorkflowTestSuite) TestWorkflowTaskAndActivityTaskTimeoutsWorkflow() { } atHandler := func(task *workflowservice.PollActivityTaskQueueResponse) (*commonpb.Payloads, bool, error) { - s.EqualValues(tv.WorkflowID(), task.WorkflowExecution.WorkflowId) + s.Equal(tv.WorkflowID(), task.WorkflowExecution.WorkflowId) s.Equal(tv.ActivityType().Name, task.ActivityType.Name) - s.Logger.Info("Activity ID", tag.WorkflowActivityID(task.ActivityId)) + s.Logger.Info("Activity ID", tag.ActivityID(task.ActivityId)) return payloads.EncodeString("Activity Result"), false, nil } @@ -1297,7 +1297,7 @@ func (s *WorkflowTestSuite) TestWorkflowTaskAndActivityTaskTimeoutsWorkflow() { const testTag = "[TestWorkflowTaskAndActivityTaskTimeoutsWorkflow] " testStart := time.Now() var lastDropTime time.Time - for i := 0; i < 8; i++ { + for i := range 8 { // Check if test context has been cancelled/timed out select { case <-ctx.Done(): @@ -1468,7 +1468,7 @@ func (s *WorkflowTestSuite) TestWorkflowRetry() { } // Check run id links - for i := 0; i < maximumAttempts; i++ { + for i := range maximumAttempts { events := s.GetHistory(s.Namespace().String(), executions[i]) if i == 0 { s.EqualHistoryEvents(fmt.Sprintf(` @@ -1726,7 +1726,7 @@ func (s *WorkflowTestSuite) TestStartWorkflowExecution_Invalid_DeploymentSearchA func (s *WorkflowTestSuite) TestStartWorkflowExecution_InternalTaskQueue() { tv := testvars.New(s.T()) - errorMessageKeyword := "internal per namespace task queue" + errorMessageKeyword := "internal per-namespace task queue" // Test StartWorkflowExecution with internal task queue s.Run("StartWorkflowExecution_PerNSWorkerTaskQueue", func() { @@ -1824,13 +1824,13 @@ func (s *WorkflowTestSuite) TestStartWorkflowExecution_InternalTaskQueue() { func requireNotStartedButRunning(t *testing.T, resp *workflowservice.StartWorkflowExecutionResponse) { t.Helper() require.False(t, resp.Started) - require.Equalf(t, resp.Status, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, + require.Equalf(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, resp.Status, "Expected workflow to be running, but got %s", resp.Status) } func requireStartedAndRunning(t *testing.T, resp *workflowservice.StartWorkflowExecutionResponse) { t.Helper() require.True(t, resp.Started) - require.Equalf(t, resp.Status, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, + require.Equalf(t, enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING, resp.Status, "Expected workflow to be running, but got %s", resp.Status) } diff --git a/tests/workflow_timer_test.go b/tests/workflow_timer_test.go index 31008ddfbf0..bb898aa649e 100644 --- a/tests/workflow_timer_test.go +++ b/tests/workflow_timer_test.go @@ -6,27 +6,28 @@ import ( "time" "github.com/google/uuid" - "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" ) type WorkflowTimerTestSuite struct { - testcore.FunctionalTestBase + parallelsuite.Suite[*WorkflowTimerTestSuite] } func TestWorkflowTimerTestSuite(t *testing.T) { - t.Parallel() - suite.Run(t, new(WorkflowTimerTestSuite)) + parallelsuite.Run(t, &WorkflowTimerTestSuite{}) } func (s *WorkflowTimerTestSuite) TestCancelTimer() { + env := testcore.NewEnv(s.T()) + id := "functional-cancel-timer-test" wt := "functional-cancel-timer-test-type" tl := "functional-cancel-timer-test-taskqueue" @@ -34,7 +35,7 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer() { request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -44,7 +45,7 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer() { Identity: identity, } - creatResp, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + creatResp, err0 := env.FrontendClient().StartWorkflowExecution(env.Context(), request) s.NoError(err0) workflowExecution := &commonpb.WorkflowExecution{ WorkflowId: id, @@ -68,7 +69,7 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer() { }}, nil } - historyEvents := s.GetHistory(s.Namespace().String(), workflowExecution) + historyEvents := env.GetHistory(env.Namespace().String(), workflowExecution) for _, event := range historyEvents { switch event.GetEventType() { // nolint:exhaustive case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED: @@ -100,35 +101,35 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer() { } poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: identity, WorkflowTaskHandler: wtHandler, ActivityTaskHandler: nil, - Logger: s.Logger, + Logger: env.Logger, T: s.T(), } // schedule the timer _, err := poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask: completed") + env.Logger.Info("PollAndProcessWorkflowTask: completed") s.NoError(err) - s.Nil(s.SendSignal(s.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) + s.NoError(env.SendSignal(env.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) // receive the signal & cancel the timer _, err = poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask: completed") + env.Logger.Info("PollAndProcessWorkflowTask: completed") s.NoError(err) - s.Nil(s.SendSignal(s.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) + s.NoError(env.SendSignal(env.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) // complete the workflow _, err = poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask: completed") + env.Logger.Info("PollAndProcessWorkflowTask: completed") s.NoError(err) - historyEvents := s.GetHistory(s.Namespace().String(), workflowExecution) + historyEvents := env.GetHistory(env.Namespace().String(), workflowExecution) s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled @@ -149,6 +150,8 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer() { } func (s *WorkflowTimerTestSuite) TestCancelTimer_CancelFiredAndBuffered() { + env := testcore.NewEnv(s.T()) + id := "functional-cancel-timer-fired-and-buffered-test" wt := "functional-cancel-timer-fired-and-buffered-test-type" tl := "functional-cancel-timer-fired-and-buffered-test-taskqueue" @@ -156,7 +159,7 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer_CancelFiredAndBuffered() { request := &workflowservice.StartWorkflowExecutionRequest{ RequestId: uuid.NewString(), - Namespace: s.Namespace().String(), + Namespace: env.Namespace().String(), WorkflowId: id, WorkflowType: &commonpb.WorkflowType{Name: wt}, TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -166,7 +169,7 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer_CancelFiredAndBuffered() { Identity: identity, } - creatResp, err0 := s.FrontendClient().StartWorkflowExecution(testcore.NewContext(), request) + creatResp, err0 := env.FrontendClient().StartWorkflowExecution(env.Context(), request) s.NoError(err0) workflowExecution := &commonpb.WorkflowExecution{ WorkflowId: id, @@ -190,7 +193,7 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer_CancelFiredAndBuffered() { }}, nil } - historyEvents := s.GetHistory(s.Namespace().String(), workflowExecution) + historyEvents := env.GetHistory(env.Namespace().String(), workflowExecution) for _, event := range historyEvents { switch event.GetEventType() { // nolint:exhaustive case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED: @@ -223,35 +226,35 @@ func (s *WorkflowTimerTestSuite) TestCancelTimer_CancelFiredAndBuffered() { } poller := &testcore.TaskPoller{ - Client: s.FrontendClient(), - Namespace: s.Namespace().String(), + Client: env.FrontendClient(), + Namespace: env.Namespace().String(), TaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, Identity: identity, WorkflowTaskHandler: wtHandler, ActivityTaskHandler: nil, - Logger: s.Logger, + Logger: env.Logger, T: s.T(), } // schedule the timer _, err := poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask: completed") + env.Logger.Info("PollAndProcessWorkflowTask: completed") s.NoError(err) - s.Nil(s.SendSignal(s.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) + s.NoError(env.SendSignal(env.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) // receive the signal & cancel the timer _, err = poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask: completed") + env.Logger.Info("PollAndProcessWorkflowTask: completed") s.NoError(err) - s.Nil(s.SendSignal(s.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) + s.NoError(env.SendSignal(env.Namespace().String(), workflowExecution, "random signal name", payloads.EncodeString("random signal payload"), identity)) // complete the workflow _, err = poller.PollAndProcessWorkflowTask() - s.Logger.Info("PollAndProcessWorkflowTask: completed") + env.Logger.Info("PollAndProcessWorkflowTask: completed") s.NoError(err) - historyEvents := s.GetHistory(s.Namespace().String(), workflowExecution) + historyEvents := env.GetHistory(env.Namespace().String(), workflowExecution) s.EqualHistoryEvents(` 1 WorkflowExecutionStarted 2 WorkflowTaskScheduled diff --git a/tests/workflow_visibility_test.go b/tests/workflow_visibility_test.go index b4445c655cd..4b86c35d541 100644 --- a/tests/workflow_visibility_test.go +++ b/tests/workflow_visibility_test.go @@ -90,7 +90,7 @@ func (s *WorkflowVisibilityTestSuite) TestVisibility() { HistoryEventFilterType: historyEventFilterType, NextPageToken: nextToken, }) - s.Nil(historyErr) + s.NoError(historyErr) if len(historyResponse.NextPageToken) == 0 { break } diff --git a/tests/xdc/activity_api_test.go b/tests/xdc/activity_api_test.go index 272eba3564d..f63d952af18 100644 --- a/tests/xdc/activity_api_test.go +++ b/tests/xdc/activity_api_test.go @@ -40,7 +40,7 @@ func TestActivityApiStateReplicationSuite(t *testing.T) { func (s *ActivityApiStateReplicationSuite) SetupSuite() { if s.dynamicConfigOverrides == nil { - s.dynamicConfigOverrides = make(map[dynamicconfig.Key]interface{}) + s.dynamicConfigOverrides = make(map[dynamicconfig.Key]any) } s.setupSuite() } @@ -123,7 +123,7 @@ func (s *ActivityApiStateReplicationSuite) TestPauseActivityFailover() { s.EventuallyWithT(func(t *assert.CollectT) { description, err := activeSDKClient.DescribeWorkflowExecution(ctx, workflowRun.GetID(), workflowRun.GetRunID()) require.NoError(t, err) - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) }, 5*time.Second, 200*time.Millisecond) @@ -152,7 +152,7 @@ func (s *ActivityApiStateReplicationSuite) TestPauseActivityFailover() { require.NoError(t, err) require.NotNil(t, description.GetPendingActivities()) if description.GetPendingActivities() != nil { - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) require.Equal(t, int64(2), description.PendingActivities[0].CurrentRetryInterval.GetSeconds()) } @@ -177,7 +177,7 @@ func (s *ActivityApiStateReplicationSuite) TestPauseActivityFailover() { require.NoError(t, err) require.NotNil(t, description.GetPendingActivities()) if description.GetPendingActivities() != nil { - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) require.Equal(t, int32(1), description.PendingActivities[0].Attempt) require.Equal(t, int64(2), description.PendingActivities[0].CurrentRetryInterval.GetSeconds()) @@ -204,7 +204,7 @@ func (s *ActivityApiStateReplicationSuite) TestPauseActivityFailover() { require.NoError(t, err) require.NotNil(t, description.GetPendingActivities()) if description.GetPendingActivities() != nil { - require.Equal(t, 1, len(description.PendingActivities)) + require.Len(t, description.PendingActivities, 1) require.True(t, description.PendingActivities[0].Paused) require.Equal(t, int64(2), description.PendingActivities[0].CurrentRetryInterval.GetSeconds()) require.Equal(t, int32(10), description.PendingActivities[0].MaximumAttempts) diff --git a/tests/xdc/base.go b/tests/xdc/base.go index c5a08460805..5702718689b 100644 --- a/tests/xdc/base.go +++ b/tests/xdc/base.go @@ -3,6 +3,7 @@ package xdc import ( "cmp" "context" + "errors" "sync" "time" @@ -10,12 +11,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - commonpb "go.temporal.io/api/common/v1" namespacepb "go.temporal.io/api/namespace/v1" + "go.temporal.io/api/operatorservice/v1" replicationpb "go.temporal.io/api/replication/v1" + "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" sdkclient "go.temporal.io/sdk/client" - "go.temporal.io/sdk/converter" sdkworker "go.temporal.io/sdk/worker" "go.temporal.io/server/api/adminservice/v1" "go.temporal.io/server/api/historyservice/v1" @@ -25,6 +26,7 @@ import ( "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/searchattribute" "go.temporal.io/server/common/testing/historyrequire" "go.temporal.io/server/common/testing/protorequire" "go.temporal.io/server/tests/testcore" @@ -52,7 +54,7 @@ type ( clusters []*testcore.TestCluster logger log.Logger - dynamicConfigOverrides map[dynamicconfig.Key]interface{} + dynamicConfigOverrides map[dynamicconfig.Key]any startTime time.Time onceClusterConnect sync.Once @@ -82,7 +84,7 @@ func (s *xdcBaseSuite) setupSuite(opts ...testcore.TestClusterOption) { s.logger = log.NewTestLogger() } if s.dynamicConfigOverrides == nil { - s.dynamicConfigOverrides = make(map[dynamicconfig.Key]interface{}) + s.dynamicConfigOverrides = make(map[dynamicconfig.Key]any) } s.dynamicConfigOverrides[dynamicconfig.ClusterMetadataRefreshInterval.Key()] = time.Second * 5 s.dynamicConfigOverrides[dynamicconfig.NamespaceCacheRefreshInterval.Key()] = testcore.NamespaceCacheRefreshInterval @@ -185,7 +187,7 @@ func (s *xdcBaseSuite) waitForClusterConnected( shard := resp.Shards[0] require.NotNil(c, shard) - require.Greater(c, shard.MaxReplicationTaskId, int64(0)) + require.Positive(c, shard.MaxReplicationTaskId) require.NotNil(c, shard.ShardLocalTime) require.WithinRange(c, shard.ShardLocalTime.AsTime(), s.startTime, time.Now()) require.NotNil(c, shard.RemoteClusters) @@ -231,6 +233,66 @@ func (s *xdcBaseSuite) createGlobalNamespace() string { return s.createNamespace(true, s.clusters) } +func (s *xdcBaseSuite) registerTestSearchAttributes(ns string) { + expectedSearchAttributes := searchattribute.TestSearchAttributesToRegister() + addSearchAttributes := func(cl *testcore.TestCluster) { + _, err := cl.OperatorClient().AddSearchAttributes(testcore.NewContext(), &operatorservice.AddSearchAttributesRequest{ + Namespace: ns, + SearchAttributes: expectedSearchAttributes, + }) + var alreadyExistsErr *serviceerror.AlreadyExists + if err != nil && !errors.As(err, &alreadyExistsErr) { + s.Require().NoError(err) + } + } + + waitForListSearchAttributes := func(cl *testcore.TestCluster) { + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := cl.OperatorClient().ListSearchAttributes(testcore.NewContext(), &operatorservice.ListSearchAttributesRequest{ + Namespace: ns, + }) + require.NoError(t, err) + for attrName, attrType := range expectedSearchAttributes { + gotType, ok := resp.GetCustomAttributes()[attrName] + require.True(t, ok, "expected search attribute %q to be registered", attrName) + require.Equal(t, attrType, gotType) + } + }, replicationWaitTime, replicationCheckInterval) + } + + waitForNamespaceAliasPropagation := func() { + for _, cl := range s.clusters { + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := cl.FrontendClient().DescribeNamespace(testcore.NewContext(), &workflowservice.DescribeNamespaceRequest{ + Namespace: ns, + }) + require.NoError(t, err) + + expectedAliases := resp.GetConfig().GetCustomSearchAttributeAliases() + for _, r := range cl.Host().NamespaceRegistries() { + cachedNS, err := r.GetNamespace(namespace.Name(ns)) + require.NoError(t, err) + require.NotNil(t, cachedNS) + mapper := cachedNS.CustomSearchAttributesMapper() + require.Equal(t, expectedAliases, mapper.FieldToAliasMap()) + } + }, namespaceCacheWaitTime, namespaceCacheCheckInterval) + } + } + + addSearchAttributes(s.clusters[0]) + waitForListSearchAttributes(s.clusters[0]) + waitForNamespaceAliasPropagation() + + for _, cl := range s.clusters[1:] { + addSearchAttributes(cl) + } + for _, cl := range s.clusters { + waitForListSearchAttributes(cl) + } + waitForNamespaceAliasPropagation() +} + // TODO (alex): rename this to createLocalNamespace, and everywhere where it is called with isGlobal == true, add call to promoteNamespace. func (s *xdcBaseSuite) createNamespaceInCluster0(isGlobal bool) string { return s.createNamespace(isGlobal, s.clusters[:1]) @@ -460,13 +522,6 @@ func (s *xdcBaseSuite) failover( s.waitForClusterSynced() } -func (s *xdcBaseSuite) mustToPayload(v any) *commonpb.Payload { - conv := converter.GetDefaultDataConverter() - payload, err := conv.ToPayload(v) - s.NoError(err) - return payload -} - func (s *xdcBaseSuite) newClientAndWorker(hostport, ns, taskqueue, identity string) (sdkclient.Client, sdkworker.Worker) { sdkClient, err := sdkclient.Dial(sdkclient.Options{ HostPort: hostport, @@ -480,3 +535,20 @@ func (s *xdcBaseSuite) newClientAndWorker(hostport, ns, taskqueue, identity stri return sdkClient, worker } + +// waitForVisibilityCount waits for the visibility store to index the expected number of workflow +// executions in the given namespace before proceeding. This is important before starting +// force-replication, which uses ListWorkflowExecutions with an empty query to discover all +// workflows in a namespace. +func (s *xdcBaseSuite) waitForVisibilityCount(ctx context.Context, ns string, expectedCount int64) { + frontendClient := s.clusters[0].FrontendClient() + s.Eventually(func() bool { + countResp, err := frontendClient.CountWorkflowExecutions(ctx, &workflowservice.CountWorkflowExecutionsRequest{ + Namespace: ns, + }) + if err != nil { + return false + } + return countResp.GetCount() == expectedCount + }, 15*time.Second, 200*time.Millisecond, "visibility should index %d workflow runs before force-replication", expectedCount) +} diff --git a/tests/xdc/chasm_test.go b/tests/xdc/chasm_test.go index d2de0d9e7a2..7660524ae3e 100644 --- a/tests/xdc/chasm_test.go +++ b/tests/xdc/chasm_test.go @@ -8,15 +8,20 @@ import ( "github.com/stretchr/testify/suite" commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" namespacepb "go.temporal.io/api/namespace/v1" "go.temporal.io/api/serviceerror" + taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/api/adminservice/v1" + taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/activity" "go.temporal.io/server/chasm/lib/tests" "go.temporal.io/server/common/debug" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/payloads" "go.temporal.io/server/common/testing/testvars" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" @@ -42,8 +47,11 @@ func TestChasmSuite(t *testing.T) { func (s *ChasmSuite) SetupSuite() { s.dynamicConfigOverrides = map[dynamicconfig.Key]any{ - dynamicconfig.EnableChasm.Key(): true, - dynamicconfig.NamespaceMinRetentionGlobal.Key(): 1 * time.Second, + dynamicconfig.EnableChasm.Key(): true, + activity.Enabled.Key(): true, + dynamicconfig.ChasmStandbyTaskDiscardDelay.Key(): 1 * time.Second, + dynamicconfig.TransferProcessorMaxPollInterval.Key(): 1 * time.Second, + dynamicconfig.NamespaceMinRetentionGlobal.Key(): 1 * time.Second, } s.setupSuite() } @@ -66,6 +74,83 @@ func (s *ChasmSuite) TearDownSuite() { s.tearDownSuite() } +func (s *ChasmSuite) TestDeleteExecution_RunningExecution() { + nsName := s.createGlobalNamespace() + + nsResp, err := s.clusters[0].FrontendClient().DescribeNamespace(testcore.NewContext(), &workflowservice.DescribeNamespaceRequest{ + Namespace: nsName, + }) + s.NoError(err) + nsID := nsResp.NamespaceInfo.GetId() + + tv := testvars.New(s.T()) + storeID := tv.Any().String() + + ctx, cancel := context.WithTimeout(s.chasmContext, chasmTestTimeout) + defer cancel() + + _, err = tests.NewPayloadStoreHandler( + ctx, + tests.NewPayloadStoreRequest{ + NamespaceID: namespace.ID(nsID), + StoreID: storeID, + IDReusePolicy: chasm.BusinessIDReusePolicyRejectDuplicate, + IDConflictPolicy: chasm.BusinessIDConflictPolicyFail, + }, + ) + s.NoError(err) + + chasmRegistry := s.clusters[0].Host().GetCHASMRegistry() + archetypeID, ok := chasmRegistry.ComponentIDFor(&tests.PayloadStore{}) + s.True(ok) + archetype, ok := chasmRegistry.ComponentFqnByID(archetypeID) + s.True(ok) + + describeExecutionRequest := &adminservice.DescribeMutableStateRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{ + WorkflowId: storeID, + }, + Archetype: archetype, + } + _, err = s.clusters[0].AdminClient().DescribeMutableState(testcore.NewContext(), describeExecutionRequest) + s.NoError(err) + + s.Eventually(func() bool { + _, err = s.clusters[1].AdminClient().DescribeMutableState(testcore.NewContext(), describeExecutionRequest) + return err == nil + }, 10*time.Second, 100*time.Millisecond) + + err = tests.DeletePayloadStoreHandler( + ctx, + tests.DeletePayloadStoreRequest{ + NamespaceID: namespace.ID(nsID), + StoreID: storeID, + Reason: "xdc test deletion", + Identity: "test-identity", + }, + ) + s.NoError(err) + + // Active cluster should fully delete the execution. + s.Eventually(func() bool { + _, err = s.clusters[0].AdminClient().DescribeMutableState(testcore.NewContext(), describeExecutionRequest) + return errors.As(err, new(*serviceerror.NotFound)) + }, 10*time.Second, 100*time.Millisecond) + + // Standby cluster receives the close via replication but won't process + // the DeleteExecutionTask itself; it will be cleaned up by the retention timer. + // Verify the execution is terminated on the standby. + s.Eventually(func() bool { + resp, err := s.clusters[1].AdminClient().DescribeMutableState(testcore.NewContext(), describeExecutionRequest) + if err != nil { + // Execution may already be gone if replication of delete happened. + return errors.As(err, new(*serviceerror.NotFound)) + } + return resp.GetDatabaseMutableState().GetExecutionState().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED + }, 10*time.Second, 100*time.Millisecond) +} + func (s *ChasmSuite) TestRetentionTimer() { nsName := s.createGlobalNamespace() @@ -155,3 +240,213 @@ func (s *ChasmSuite) TestRetentionTimer() { }, 10*time.Second, 100*time.Millisecond) } } + +// TestActivityDispatchTaskStandbySpillover verifies that the standby transfer queue executor's spillover path works +// end-to-end for ActivityDispatchTask. +// +// Test flow: +// 1. Start a standalone activity on cluster 0 (active). This generates an ActivityDispatchTask. +// 2. Wait for replication to cluster 1 (standby). +// 3. Verify Discard fired by checking that the activity task appears in cluster 1's matching backlog. +// 4. Failover namespace to cluster 1, as SAA tasks are only pollable on active clusters +// 5. Poll the activity task on cluster 1 (now active). +// 6. Complete the activity from cluster 1. +// 7. Verify completion status on both clusters. +func (s *ChasmSuite) TestActivityDispatchTaskStandbySpillover() { + nsName := s.createGlobalNamespace() + + tv := testvars.New(s.T()) + activityID := tv.Any().String() + taskQueue := tv.Any().String() + + ctx, cancel := context.WithTimeout(context.Background(), chasmTestTimeout) + defer cancel() + + // Start standalone activity on cluster 0 (active), creating an ActivityDispatchTask on immediate transfer queue. + startResp, err := s.clusters[0].FrontendClient().StartActivityExecution( + ctx, + &workflowservice.StartActivityExecutionRequest{ + Namespace: nsName, + ActivityId: activityID, + ActivityType: &commonpb.ActivityType{ + Name: "test-activity-type", + }, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + }, + Input: payloads.EncodeString("test-input"), + StartToCloseTimeout: durationpb.New(1 * time.Minute), + Identity: "test-worker", + }, + ) + s.NoError(err) + s.NotEmpty(startResp.GetRunId()) + + // Wait for replication to cluster 1 (standby). + describeExecutionRequest := &adminservice.DescribeMutableStateRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{ + WorkflowId: activityID, + }, + Archetype: activity.Archetype, + } + s.Eventually(func() bool { + _, err = s.clusters[1].AdminClient().DescribeMutableState(testcore.NewContext(), describeExecutionRequest) + return err == nil + }, 10*time.Second, 100*time.Millisecond) + + // Verify Discard fired on cluster 1 (standby) by checking that the activity task was pushed into cluster 1's + // matching backlog + s.Eventually(func() bool { + for partitionID := range int32(dynamicconfig.GlobalDefaultNumTaskQueuePartitions) { + res, err := s.clusters[1].AdminClient().DescribeTaskQueuePartition(ctx, + &adminservice.DescribeTaskQueuePartitionRequest{ + Namespace: nsName, + TaskQueuePartition: &taskqueuespb.TaskQueuePartition{ + TaskQueue: taskQueue, + TaskQueueType: enumspb.TASK_QUEUE_TYPE_ACTIVITY, + PartitionId: &taskqueuespb.TaskQueuePartition_NormalPartitionId{NormalPartitionId: partitionID}, + }, + BuildIds: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true}, + }) + if err != nil { + continue + } + for _, vi := range res.VersionsInfoInternal { + for _, st := range vi.PhysicalTaskQueueInfo.InternalTaskQueueStatus { + if st.ApproximateBacklogCount > 0 { + return true + } + } + } + } + return false + }, 5*time.Second, 200*time.Millisecond) + + // Failover namespace to cluster 1. Matching on standby only serves query + // tasks (not activity tasks), so the pre-positioned task only becomes + // pollable once cluster 1 becomes the new active. + s.failover(nsName, 0, s.clusters[1].ClusterName(), 2) + + // Poll activity task on cluster 1 (now active after failover). + var pollResp *workflowservice.PollActivityTaskQueueResponse + s.Eventually(func() bool { + pollCtx, pollCancel := context.WithTimeout(ctx, 3*time.Second) + defer pollCancel() + pollResp, err = s.clusters[1].FrontendClient().PollActivityTaskQueue( + pollCtx, + &workflowservice.PollActivityTaskQueueRequest{ + Namespace: nsName, + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + Identity: "standby-worker", + }, + ) + return err == nil && len(pollResp.GetTaskToken()) > 0 + }, 10*time.Second, 500*time.Millisecond) + + // Complete the activity from cluster 1. + _, err = s.clusters[1].FrontendClient().RespondActivityTaskCompleted( + ctx, + &workflowservice.RespondActivityTaskCompletedRequest{ + Namespace: nsName, + TaskToken: pollResp.GetTaskToken(), + Result: payloads.EncodeString("done"), + Identity: "standby-worker", + }, + ) + s.NoError(err) + + // Verify completion on both clusters. + for _, cluster := range s.clusters { + s.Eventually(func() bool { + resp, err := cluster.FrontendClient().DescribeActivityExecution( + ctx, + &workflowservice.DescribeActivityExecutionRequest{ + Namespace: nsName, + ActivityId: activityID, + RunId: startResp.GetRunId(), + }, + ) + if err != nil { + return false + } + return resp.GetInfo().GetStatus() == enumspb.ACTIVITY_EXECUTION_STATUS_COMPLETED + }, 10*time.Second, 100*time.Millisecond) + } +} + +// TestDeleteExecution_ReplicatedToStandby verifies that deleting a non-workflow Chasm execution +// (PayloadStore) on the active cluster replicates the deletion to the standby cluster. +func (s *ChasmSuite) TestDeleteExecution_ReplicatedToStandby() { + for _, cluster := range s.clusters { + cluster.OverrideDynamicConfig(s.T(), dynamicconfig.EnableDeleteWorkflowExecutionReplication, true) + } + + nsName := s.createGlobalNamespace() + + nsResp, err := s.clusters[0].FrontendClient().DescribeNamespace(testcore.NewContext(), &workflowservice.DescribeNamespaceRequest{ + Namespace: nsName, + }) + s.NoError(err) + nsID := nsResp.NamespaceInfo.GetId() + + tv := testvars.New(s.T()) + storeID := tv.Any().String() + + ctx, cancel := context.WithTimeout(s.chasmContext, chasmTestTimeout) + defer cancel() + + _, err = tests.NewPayloadStoreHandler( + ctx, + tests.NewPayloadStoreRequest{ + NamespaceID: namespace.ID(nsID), + StoreID: storeID, + IDReusePolicy: chasm.BusinessIDReusePolicyRejectDuplicate, + IDConflictPolicy: chasm.BusinessIDConflictPolicyFail, + }, + ) + s.NoError(err) + + chasmRegistry := s.clusters[0].Host().GetCHASMRegistry() + archetypeID, ok := chasmRegistry.ComponentIDFor(&tests.PayloadStore{}) + s.True(ok) + archetype, ok := chasmRegistry.ComponentFqnByID(archetypeID) + s.True(ok) + + describeExecutionRequest := &adminservice.DescribeMutableStateRequest{ + Namespace: nsName, + Execution: &commonpb.WorkflowExecution{ + WorkflowId: storeID, + }, + Archetype: archetype, + } + + s.Eventually(func() bool { + _, err = s.clusters[1].AdminClient().DescribeMutableState(testcore.NewContext(), describeExecutionRequest) + return err == nil + }, 10*time.Second, 100*time.Millisecond) + + err = tests.DeletePayloadStoreHandler( + ctx, + tests.DeletePayloadStoreRequest{ + NamespaceID: namespace.ID(nsID), + StoreID: storeID, + Reason: "xdc delete replication test", + Identity: "test-identity", + }, + ) + s.NoError(err) + + // Verify Chasm deletion on both clusters. + for _, cluster := range s.clusters { + s.Eventually(func() bool { + _, err = cluster.AdminClient().DescribeMutableState(testcore.NewContext(), describeExecutionRequest) + return errors.As(err, new(*serviceerror.NotFound)) + }, 10*time.Second, 100*time.Millisecond) + } +} + +// TODO Add test for ActivityDispatchTask timer task queue spillover when we have SAA scheduling support. diff --git a/tests/xdc/delete_execution_replication_test.go b/tests/xdc/delete_execution_replication_test.go new file mode 100644 index 00000000000..303d8241fd1 --- /dev/null +++ b/tests/xdc/delete_execution_replication_test.go @@ -0,0 +1,401 @@ +package xdc + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + commandpb "go.temporal.io/api/command/v1" + 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" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/api/historyservice/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/common" + "go.temporal.io/server/common/config" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/payloads" + "go.temporal.io/server/common/primitives" + "go.temporal.io/server/tests/testcore" + "go.uber.org/fx" + "google.golang.org/protobuf/types/known/durationpb" +) + +type deleteExecutionReplicationTestSuite struct { + xdcBaseSuite +} + +func TestDeleteExecutionReplicationTestSuite(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + enableTransitionHistory bool + }{ + { + name: "DisableTransitionHistory", + enableTransitionHistory: false, + }, + { + name: "EnableTransitionHistory", + enableTransitionHistory: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + s := &deleteExecutionReplicationTestSuite{} + s.enableTransitionHistory = tc.enableTransitionHistory + suite.Run(t, s) + }) + } +} + +func (s *deleteExecutionReplicationTestSuite) SetupSuite() { + s.dynamicConfigOverrides = map[dynamicconfig.Key]any{ + dynamicconfig.EnableReplicationStream.Key(): true, + dynamicconfig.EnableReplicationTaskBatching.Key(): true, + dynamicconfig.EnableDeleteWorkflowExecutionReplication.Key(): true, + dynamicconfig.EnableSeparateReplicationEnableFlag.Key(): true, + dynamicconfig.EnableWorkflowTaskStampIncrementOnFailure.Key(): true, + } + s.logger = log.NewTestLogger() + + s.setupSuite( + testcore.WithFxOptionsForService(primitives.AllServices, + fx.Decorate( + func(_ config.DCRedirectionPolicy) config.DCRedirectionPolicy { + return config.DCRedirectionPolicy{Policy: "noop"} + }, + ), + ), + ) +} + +func (s *deleteExecutionReplicationTestSuite) TearDownSuite() { + s.tearDownSuite() +} + +func (s *deleteExecutionReplicationTestSuite) SetupTest() { + s.setupTest() +} + +func (s *deleteExecutionReplicationTestSuite) TestDeleteClosedWorkflow_ReplicatedToPassiveCluster() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2) + defer cancel() + + // Create a global namespace on both clusters. + ns := s.createGlobalNamespace() + nsResp, err := s.clusters[0].FrontendClient().DescribeNamespace(ctx, &workflowservice.DescribeNamespaceRequest{ + Namespace: ns, + }) + s.Require().NoError(err) + nsID := nsResp.GetNamespaceInfo().GetId() + + workflowID := "test-delete-replication-" + uuid.NewString() + taskQueue := "test-delete-tq-" + uuid.NewString() + sourceClient := s.clusters[0].FrontendClient() + + // Start a workflow on the active cluster. + startResp, err := sourceClient.StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: ns, + WorkflowId: workflowID, + WorkflowType: &commonpb.WorkflowType{Name: "test-wf-type"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + WorkflowRunTimeout: durationpb.New(time.Minute), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + }) + s.Require().NoError(err) + runID := startResp.GetRunId() + + // Complete the workflow. + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ + Result: payloads.EncodeString("done"), + }, + }, + }}, nil + } + //nolint:staticcheck // TODO: replace with taskpoller.TaskPoller + poller := &testcore.TaskPoller{ + Client: sourceClient, + Namespace: ns, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue}, + Identity: "worker", + WorkflowTaskHandler: wtHandler, + Logger: s.logger, + T: s.T(), + } + _, err = poller.PollAndProcessWorkflowTask() + s.Require().NoError(err) + + // Wait for workflow to be completed on active cluster. + s.Eventually(func() bool { + resp, err := sourceClient.DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + if err != nil { + return false + } + return resp.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, time.Second*10, time.Second) + + // Wait for the workflow to be replicated to the passive cluster. + targetClient := s.clusters[1].FrontendClient() + s.Eventually(func() bool { + resp, err := targetClient.DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + if err != nil { + return false + } + return resp.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, replicationWaitTime, replicationCheckInterval, "Workflow should be replicated to passive cluster") + + // Delete the workflow on the active cluster. + _, err = sourceClient.DeleteWorkflowExecution(ctx, &workflowservice.DeleteWorkflowExecutionRequest{ + Namespace: ns, + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, + RunId: runID, + }, + }) + s.Require().NoError(err) + + // Verify the workflow is deleted on the active cluster. + s.Eventually(func() bool { + _, err := sourceClient.DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + if err == nil { + return false + } + var notFound *serviceerror.NotFound + return errors.As(err, ¬Found) + }, time.Second*10, time.Second, "Workflow should be deleted on active cluster") + + // Verify the workflow mutable state is deleted on the passive cluster via replication. + s.Eventually(func() bool { + _, err := s.clusters[1].HistoryClient().DescribeMutableState(ctx, &historyservice.DescribeMutableStateRequest{ + NamespaceId: nsID, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + ArchetypeId: chasm.WorkflowArchetypeID, + }) + if err == nil { + return false + } + var notFound *serviceerror.NotFound + return errors.As(err, ¬Found) + }, time.Second*30, replicationCheckInterval, "Workflow mutable state should be deleted on passive cluster via replication") +} + +func (s *deleteExecutionReplicationTestSuite) TestDeleteRunningWorkflow_ReplicatedToPassiveCluster() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + ns := s.createGlobalNamespace() + nsResp, err := s.clusters[0].FrontendClient().DescribeNamespace(ctx, &workflowservice.DescribeNamespaceRequest{ + Namespace: ns, + }) + s.Require().NoError(err) + nsID := nsResp.GetNamespaceInfo().GetId() + + workflowID := "test-delete-running-" + uuid.NewString() + taskQueue := "test-delete-running-tq-" + uuid.NewString() + sourceClient := s.clusters[0].FrontendClient() + + // Start a workflow on the active cluster (don't complete it — leave it running). + startResp, err := sourceClient.StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: ns, + WorkflowId: workflowID, + WorkflowType: &commonpb.WorkflowType{Name: "test-wf-type"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + WorkflowRunTimeout: durationpb.New(time.Minute), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + }) + s.Require().NoError(err) + runID := startResp.GetRunId() + + // Wait for the workflow to be replicated to the passive cluster. + s.Eventually(func() bool { + _, err := s.clusters[1].FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + return err == nil + }, replicationWaitTime, replicationCheckInterval, "Workflow should be replicated to passive cluster") + + // Delete the running workflow on the active cluster. + // This will terminate it first (deleteAfterTerminate=true), then delete. + _, err = sourceClient.DeleteWorkflowExecution(ctx, &workflowservice.DeleteWorkflowExecutionRequest{ + Namespace: ns, + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, + RunId: runID, + }, + }) + s.Require().NoError(err) + + // Verify the workflow is deleted on the active cluster. + s.Eventually(func() bool { + _, err := sourceClient.DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + if err == nil { + return false + } + var notFound *serviceerror.NotFound + return errors.As(err, ¬Found) + }, time.Second*10, time.Second, "Workflow should be deleted on active cluster") + + // Verify the workflow mutable state is deleted on the passive cluster via replication. + s.Eventually(func() bool { + _, err := s.clusters[1].HistoryClient().DescribeMutableState(ctx, &historyservice.DescribeMutableStateRequest{ + NamespaceId: nsID, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + ArchetypeId: chasm.WorkflowArchetypeID, + }) + if err == nil { + return false + } + var notFound *serviceerror.NotFound + return errors.As(err, ¬Found) + }, time.Second*30, replicationCheckInterval, "Workflow mutable state should be deleted on passive cluster via replication") +} + +func (s *deleteExecutionReplicationTestSuite) TestDeleteWorkflow_NotReplicatedWhenFeatureFlagDisabled() { + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + // Disable the feature flag on both clusters. + cleanup0 := s.clusters[0].OverrideDynamicConfig(s.T(), dynamicconfig.EnableDeleteWorkflowExecutionReplication, false) + defer cleanup0() + cleanup1 := s.clusters[1].OverrideDynamicConfig(s.T(), dynamicconfig.EnableDeleteWorkflowExecutionReplication, false) + defer cleanup1() + + ns := s.createGlobalNamespace() + + workflowID := "test-delete-no-repl-" + uuid.NewString() + taskQueue := "test-delete-no-repl-tq-" + uuid.NewString() + sourceClient := s.clusters[0].FrontendClient() + + // Start and complete a workflow. + startResp, err := sourceClient.StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: ns, + WorkflowId: workflowID, + WorkflowType: &commonpb.WorkflowType{Name: "test-wf-type"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + WorkflowRunTimeout: durationpb.New(time.Minute), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + }) + s.Require().NoError(err) + runID := startResp.GetRunId() + + wtHandler := func(task *workflowservice.PollWorkflowTaskQueueResponse) ([]*commandpb.Command, error) { + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_CompleteWorkflowExecutionCommandAttributes{ + CompleteWorkflowExecutionCommandAttributes: &commandpb.CompleteWorkflowExecutionCommandAttributes{ + Result: payloads.EncodeString("done"), + }, + }, + }}, nil + } + //nolint:staticcheck // TODO: replace with taskpoller.TaskPoller + poller := &testcore.TaskPoller{ + Client: sourceClient, + Namespace: ns, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue}, + Identity: "worker", + WorkflowTaskHandler: wtHandler, + Logger: s.logger, + T: s.T(), + } + _, err = poller.PollAndProcessWorkflowTask() + s.Require().NoError(err) + + // Wait for replication to passive. + targetClient := s.clusters[1].FrontendClient() + s.Eventually(func() bool { + resp, err := targetClient.DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + if err != nil { + return false + } + return resp.GetWorkflowExecutionInfo().GetStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, replicationWaitTime, replicationCheckInterval) + + // Delete on active cluster. + _, err = sourceClient.DeleteWorkflowExecution(ctx, &workflowservice.DeleteWorkflowExecutionRequest{ + Namespace: ns, + WorkflowExecution: &commonpb.WorkflowExecution{ + WorkflowId: workflowID, + RunId: runID, + }, + }) + s.Require().NoError(err) + + // Wait for deletion on active. + s.Eventually(func() bool { + _, err := sourceClient.DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + var notFound *serviceerror.NotFound + return errors.As(err, ¬Found) + }, time.Second*10, time.Second) + + // Workflow should still exist on the passive cluster (no replication of deletion). + //nolint:forbidigo // need to wait to confirm deletion did NOT replicate + time.Sleep(5 * time.Second) + _, err = targetClient.DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID, RunId: runID}, + }) + s.NoError(err, "Workflow should still exist on passive cluster when feature flag is disabled") +} + +func (s *deleteExecutionReplicationTestSuite) createGlobalNamespace() string { + ctx := testcore.NewContext() + ns := "test-delete-ns-" + common.GenerateRandomString(5) + _, err := s.clusters[0].FrontendClient().RegisterNamespace(ctx, &workflowservice.RegisterNamespaceRequest{ + Namespace: ns, + IsGlobalNamespace: true, + Clusters: s.clusterReplicationConfig(), + ActiveClusterName: s.clusters[0].ClusterName(), + WorkflowExecutionRetentionPeriod: durationpb.New(24 * time.Hour), + }) + s.Require().NoError(err) + + // Wait for namespace to be available on both clusters. + s.Eventually(func() bool { + _, err := s.clusters[0].FrontendClient().DescribeNamespace(ctx, &workflowservice.DescribeNamespaceRequest{ + Namespace: ns, + }) + return err == nil + }, namespaceCacheWaitTime, namespaceCacheCheckInterval) + + s.Eventually(func() bool { + _, err := s.clusters[1].FrontendClient().DescribeNamespace(ctx, &workflowservice.DescribeNamespaceRequest{ + Namespace: ns, + }) + return err == nil + }, namespaceCacheWaitTime, namespaceCacheCheckInterval) + + return ns +} diff --git a/tests/xdc/failover_test.go b/tests/xdc/failover_test.go index 05f5cf314d7..6af351a7d9c 100644 --- a/tests/xdc/failover_test.go +++ b/tests/xdc/failover_test.go @@ -154,7 +154,7 @@ func (s *FunctionalClustersTestSuite) TestSimpleWorkflowFailover() { if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -710,7 +710,7 @@ func (s *FunctionalClustersTestSuite) TestStartWorkflowExecution_Failover_Workfl s.logger.Info("PollAndProcessWorkflowTask 2", tag.Error(err)) s.NoError(err) s.Equal(1, workflowCompleteTimes) - s.Equal(2, len(executions)) + s.Len(executions, 2) s.Equal(executions[1].GetRunId(), we.GetRunId()) } @@ -748,7 +748,7 @@ func (s *FunctionalClustersTestSuite) TestTerminateFailover() { if activityCounter < activityCount { activityCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, activityCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, activityCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -1064,7 +1064,7 @@ func (s *FunctionalClustersTestSuite) TestContinueAsNewFailover() { previousRunID = task.WorkflowExecution.GetRunId() continueAsNewCounter++ buf := new(bytes.Buffer) - s.Nil(binary.Write(buf, binary.LittleEndian, continueAsNewCounter)) + s.NoError(binary.Write(buf, binary.LittleEndian, continueAsNewCounter)) return []*commandpb.Command{{ CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, @@ -1111,7 +1111,7 @@ func (s *FunctionalClustersTestSuite) TestContinueAsNewFailover() { } // make some progress in cluster0 and did some continueAsNew - for i := 0; i < 3; i++ { + for i := range 3 { _, err := poller0.PollAndProcessWorkflowTask() s.logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) s.NoError(err, strconv.Itoa(i)) @@ -1120,7 +1120,7 @@ func (s *FunctionalClustersTestSuite) TestContinueAsNewFailover() { s.failover(namespace, 0, s.clusters[1].ClusterName(), 2) // finish the rest in cluster1 - for i := 0; i < 2; i++ { + for i := range 2 { _, err := poller1.PollAndProcessWorkflowTask() s.logger.Info("PollAndProcessWorkflowTask", tag.Error(err)) s.NoError(err, strconv.Itoa(i)) @@ -1415,7 +1415,7 @@ func (s *FunctionalClustersTestSuite) TestUserTimerFailover() { T: s.T(), } - for i := 0; i < 2; i++ { + for range 2 { _, err = poller0.PollAndProcessWorkflowTask() if err != nil { timerCreated = false @@ -2068,7 +2068,7 @@ func (s *FunctionalClustersTestSuite) TestActivityHeartbeatFailover() { dweResponse, err := client1.DescribeWorkflowExecution(testcore.NewContext(), workflowID, "") s.NoError(err) pendingActivities := dweResponse.GetPendingActivities() - s.Equal(1, len(pendingActivities)) + s.Len(pendingActivities, 1) s.Equal(enumspb.PENDING_ACTIVITY_STATE_SCHEDULED, pendingActivities[0].GetState()) heartbeatPayload := pendingActivities[0].GetHeartbeatDetails() var heartbeatValue int @@ -2246,7 +2246,7 @@ func (s *FunctionalClustersTestSuite) TestLocalNamespaceMigration() { run6, err := client0.ExecuteWorkflow(testCtx, workflowOptions, wfWithBufferedEvents) s.NoError(err) s.NotNil(run6) - s.True(run6.GetRunID() != "") + s.NotEmpty(run6.GetRunID()) workflowOptions2 := sdkclient.StartWorkflowOptions{ ID: workflowID7, @@ -2259,7 +2259,7 @@ func (s *FunctionalClustersTestSuite) TestLocalNamespaceMigration() { run7, err := client0.ExecuteWorkflow(testCtx, workflowOptions2, wfWithBufferedEvents2) s.NoError(err) s.NotNil(run7) - s.True(run7.GetRunID() != "") + s.NotEmpty(run7.GetRunID()) // block until first workflow task started select { @@ -2335,6 +2335,9 @@ func (s *FunctionalClustersTestSuite) TestLocalNamespaceMigration() { err = run3.Get(testCtx, nil) s.NoError(err) + // Wait for all 6 workflow runs (wf1, wf2, wf3, wf6, wf7, wf8) to be indexed before force-replication. + s.waitForVisibilityCount(testCtx, namespace, 6) + // start force-replicate wf sysClient, err := sdkclient.Dial(sdkclient.Options{ HostPort: s.clusters[0].Host().FrontendGRPCAddress(), @@ -2347,8 +2350,10 @@ func (s *FunctionalClustersTestSuite) TestLocalNamespaceMigration() { TaskQueue: primitives.DefaultWorkerTaskQueue, WorkflowRunTimeout: time.Second * 30, }, "force-replication", migration.ForceReplicationParams{ - Namespace: namespace, - OverallRps: 10, + Namespace: namespace, + OverallRps: 10, + EnableVerification: true, + TargetClusterName: s.clusters[1].ClusterName(), }) s.NoError(err) @@ -2378,7 +2383,7 @@ func (s *FunctionalClustersTestSuite) TestLocalNamespaceMigration() { }) s.NoError(err) s.True(nsResp2.IsGlobalNamespace) - s.Equal(2, len(nsResp2.ReplicationConfig.Clusters)) + s.Len(nsResp2.ReplicationConfig.Clusters, 2) s.Equal(s.clusters[1].ClusterName(), nsResp2.ReplicationConfig.ActiveClusterName) // verify all wf in ns is now available in cluster2 @@ -2432,7 +2437,7 @@ func (s *FunctionalClustersTestSuite) TestLocalNamespaceMigration() { }, ) s.NoError(err) - s.True(len(listWorkflowResp.GetExecutions()) > 0) + s.NotEmpty(listWorkflowResp.GetExecutions()) } verify(workflowID, run1.GetRunID()) verify(workflowID2, run2.GetRunID()) @@ -2476,6 +2481,9 @@ func (s *FunctionalClustersTestSuite) TestForceMigration_ClosedWorkflow() { // Update ns to have 2 clusters s.updateNamespaceClusters(namespace, 0, s.clusters) + // Wait for wf1 to be indexed before force-replication. + s.waitForVisibilityCount(testCtx, namespace, 1) + // Start force-replicate wf sysClient, err := sdkclient.Dial(sdkclient.Options{ HostPort: s.clusters[0].Host().FrontendGRPCAddress(), @@ -2488,8 +2496,10 @@ func (s *FunctionalClustersTestSuite) TestForceMigration_ClosedWorkflow() { TaskQueue: primitives.DefaultWorkerTaskQueue, WorkflowRunTimeout: time.Second * 30, }, "force-replication", migration.ForceReplicationParams{ - Namespace: namespace, - OverallRps: 10, + Namespace: namespace, + OverallRps: 10, + EnableVerification: true, + TargetClusterName: s.clusters[1].ClusterName(), }) s.NoError(err) err = sysWfRun.Get(testCtx, nil) @@ -2498,10 +2508,14 @@ func (s *FunctionalClustersTestSuite) TestForceMigration_ClosedWorkflow() { // Verify all wf in ns is now available in cluster2 client1, worker1 := s.newClientAndWorker(s.clusters[1].Host().FrontendGRPCAddress(), namespace, taskqueue, "worker1") verify := func(wfID string, expectedRunID string) { - desc1, err := client1.DescribeWorkflowExecution(testCtx, wfID, "") - s.NoError(err) - s.Equal(expectedRunID, desc1.WorkflowExecutionInfo.Execution.RunId) - s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, desc1.WorkflowExecutionInfo.Status) + s.Eventually(func() bool { + desc1, err := client1.DescribeWorkflowExecution(testCtx, wfID, "") + if err != nil { + return false + } + return desc1.WorkflowExecutionInfo.Execution.RunId == expectedRunID && + desc1.WorkflowExecutionInfo.Status == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 15*time.Second, 200*time.Millisecond, "workflow %s should be replicated to cluster2", wfID) } verify(workflowID, run1.GetRunID()) @@ -2528,9 +2542,13 @@ func (s *FunctionalClustersTestSuite) TestForceMigration_ClosedWorkflow() { err = resetRun.Get(testCtx, nil) s.NoError(err) - descResp, err := client1.DescribeWorkflowExecution(testCtx, workflowID, resetResp.GetRunId()) - s.NoError(err) - s.Equal(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, descResp.GetWorkflowExecutionInfo().Status) + s.Eventually(func() bool { + descResp, err := client1.DescribeWorkflowExecution(testCtx, workflowID, resetResp.GetRunId()) + if err != nil { + return false + } + return descResp.GetWorkflowExecutionInfo().Status == enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 15*time.Second, 200*time.Millisecond, "reset workflow should be visible on cluster2") } func (s *FunctionalClustersTestSuite) TestForceMigration_ResetWorkflow() { @@ -2583,6 +2601,9 @@ func (s *FunctionalClustersTestSuite) TestForceMigration_ResetWorkflow() { // Update ns to have 2 clusters s.updateNamespaceClusters(namespace, 0, s.clusters) + // Wait for both workflow runs (original + reset) to be indexed before force-replication. + s.waitForVisibilityCount(testCtx, namespace, 2) + // Start force-replicate wf sysClient, err := sdkclient.Dial(sdkclient.Options{ HostPort: s.clusters[0].Host().FrontendGRPCAddress(), @@ -2595,13 +2616,49 @@ func (s *FunctionalClustersTestSuite) TestForceMigration_ResetWorkflow() { TaskQueue: primitives.DefaultWorkerTaskQueue, WorkflowRunTimeout: time.Second * 30, }, "force-replication", migration.ForceReplicationParams{ - Namespace: namespace, - OverallRps: 10, + Namespace: namespace, + OverallRps: 10, + EnableVerification: true, + TargetClusterName: s.clusters[1].ClusterName(), }) s.NoError(err) err = sysWfRun.Get(testCtx, nil) s.NoError(err) + // Verify the force-replication workflow actually ran VerifyReplicationTasks activities + // and that they verified exactly the expected number of workflow runs. + var totalVerifiedCount int64 + scheduledActivityTypes := make(map[int64]string) // scheduledEventId -> activity type name + histIter := sysClient.GetWorkflowHistory(testCtx, forceReplicationWorkflowID, sysWfRun.GetRunID(), + false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for histIter.HasNext() { + event, err := histIter.Next() + s.NoError(err) + switch event.GetEventType() { + case enumspb.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: + attrs := event.GetActivityTaskScheduledEventAttributes() + scheduledActivityTypes[event.GetEventId()] = attrs.GetActivityType().GetName() + case enumspb.EVENT_TYPE_ACTIVITY_TASK_COMPLETED: + attrs := event.GetActivityTaskCompletedEventAttributes() + activityType := scheduledActivityTypes[attrs.GetScheduledEventId()] + if activityType != "VerifyReplicationTasks" { + continue + } + result := attrs.GetResult() + if result != nil && len(result.GetPayloads()) > 0 { + var resp struct { + VerifiedWorkflowCount int64 + } + s.NoError(payloads.Decode(result, &resp)) + totalVerifiedCount += resp.VerifiedWorkflowCount + } + default: + } + } + // Expect exactly 2 verified workflow runs: original run + reset run + s.Equal(int64(2), totalVerifiedCount, + "force-replication should have verified exactly 2 workflow runs (original + reset run)") + s.waitForClusterSynced() // Verify all wf in ns is now available in cluster2 diff --git a/tests/xdc/history_replication_dlq_test.go b/tests/xdc/history_replication_dlq_test.go index 838bd14be8e..074619aaa97 100644 --- a/tests/xdc/history_replication_dlq_test.go +++ b/tests/xdc/history_replication_dlq_test.go @@ -546,7 +546,7 @@ func (s *historyReplicationDLQSuite) runTDBGCommand( s.T().Log("========================================") } -func (s *historyReplicationDLQSuite) getTaskExecutorDecorator() interface{} { +func (s *historyReplicationDLQSuite) getTaskExecutorDecorator() any { if s.enableReplicationStream { // The replication stream uses a different code path which converts tasks into executables using this interface, // so that's a good injection point for us. diff --git a/tests/xdc/history_replication_signals_and_updates_test.go b/tests/xdc/history_replication_signals_and_updates_test.go index a587383a083..3a7cde3006c 100644 --- a/tests/xdc/history_replication_signals_and_updates_test.go +++ b/tests/xdc/history_replication_signals_and_updates_test.go @@ -269,7 +269,7 @@ func (s *hrsuTestSuite) TestConflictResolutionReappliesSignals() { // Cluster2 sends the reapplied signal to cluster1, bringing the cluster histories into agreement. t.cluster1.executeHistoryReplicationTasksUntil(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED) - s.EqualValues(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) + s.Equal(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) } // TestConflictResolutionReappliesUpdates creates a split-brain scenario in which both clusters believe they are active. @@ -312,7 +312,7 @@ func (s *hrsuTestSuite) TestConflictResolutionReappliesUpdates() { // Cluster2 sends the reapplied update to cluster1, bringing the cluster histories into agreement. t.cluster1.executeHistoryReplicationTasksUntil(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ADMITTED) - s.EqualValues(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) + s.Equal(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) s.NoError(t.cluster2.pollAndCompleteUpdate(cluster2UpdateId)) s.EqualHistoryEvents(fmt.Sprintf(` @@ -1046,7 +1046,7 @@ func (c *hrsuTestCluster) getHistoryForRunId(ctx context.Context, runId string) } func (c *hrsuTestCluster) pollWorkflowResult(ctx context.Context, runId string) *historypb.HistoryEvent { - getHistoryWithLongPoll := func(token []byte) ([]*historypb.HistoryEvent, []byte) { + getHistoryWithLongPoll := func(token []byte) ([]*historypb.HistoryEvent, []byte, error) { responseInner, err := c.testCluster.FrontendClient().GetWorkflowExecutionHistory(ctx, &workflowservice.GetWorkflowExecutionHistoryRequest{ Namespace: c.t.tv.NamespaceName().String(), Execution: &commonpb.WorkflowExecution{ @@ -1058,25 +1058,33 @@ func (c *hrsuTestCluster) pollWorkflowResult(ctx context.Context, runId string) NextPageToken: token, HistoryEventFilterType: enumspb.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, }) - c.t.s.NoError(err) - return responseInner.History.Events, responseInner.NextPageToken + if err != nil { + return nil, nil, err + } + return responseInner.History.Events, responseInner.NextPageToken, nil } var token []byte var allEvents []*historypb.HistoryEvent - multiPoll := false for { - events, nextPageToken := getHistoryWithLongPoll(token) + if ctx.Err() != nil { + c.t.s.NoError(ctx.Err(), "context expired while waiting for workflow result") + return nil + } + events, nextPageToken, err := getHistoryWithLongPoll(token) + if err != nil { + // Transient error (e.g. CurrentBranchChanged after conflict resolution): retry from scratch. + token = nil + continue + } allEvents = append(allEvents, events...) if nextPageToken == nil { break } token = nextPageToken - multiPoll = true } c.t.s.Len(allEvents, 1) - c.t.s.True(multiPoll, "Expected to have multiple polls of history events") return allEvents[0] } @@ -1164,7 +1172,7 @@ func (s *hrsuTestSuite) TestConflictResolutionGetResult() { // Cluster2 sends the reapplied signal to cluster1, bringing the cluster histories into agreement. t.cluster1.executeHistoryReplicationTasksUntil(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED) - s.EqualValues(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) + s.Equal(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) // Complete the workflow in cluster2. This will cause the workflow result to be sent to cluste1. task, err := t.cluster2.testCluster.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{ @@ -1187,7 +1195,7 @@ func (s *hrsuTestSuite) TestConflictResolutionGetResult() { s.Require().NoError(err) t.cluster1.executeHistoryReplicationTasksUntil(enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED) - s.EqualValues(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) + s.Equal(t.cluster1.getHistory(ctx), t.cluster2.getHistory(ctx)) // Make sure we can get the workflow result after the conflict resolution (CurrentBranchChange). event := <-workflowResultCh diff --git a/tests/xdc/nexus_request_forwarding_test.go b/tests/xdc/nexus_request_forwarding_test.go index 610c4c62c6d..ab1be704714 100644 --- a/tests/xdc/nexus_request_forwarding_test.go +++ b/tests/xdc/nexus_request_forwarding_test.go @@ -23,7 +23,6 @@ import ( taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" - "go.temporal.io/sdk/converter" "go.temporal.io/server/api/matchingservice/v1" "go.temporal.io/server/common/authorization" "go.temporal.io/server/common/dynamicconfig" @@ -68,15 +67,11 @@ func TestNexusRequestForwardingTestSuite(t *testing.T) { } func (s *NexusRequestForwardingSuite) SetupSuite() { - re, err := dynamicconfig.ConvertWildcardStringListToRegexp([]string{"internal-test-*"}) - if err != nil { - panic(err) - } s.dynamicConfigOverrides = map[dynamicconfig.Key]any{ // Make sure we don't hit the rate limiter in tests dynamicconfig.FrontendGlobalNamespaceNamespaceReplicationInducingAPIsRPS.Key(): 1000, dynamicconfig.RefreshNexusEndpointsMinWait.Key(): 1 * time.Millisecond, - dynamicconfig.FrontendNexusRequestHeadersBlacklist.Key(): dynamicconfig.GetTypedPropertyFn(re), + dynamicconfig.FrontendNexusRequestHeadersBlacklist.Key(): []string{"internal-test-*"}, } s.setupSuite() } @@ -388,13 +383,13 @@ func (s *NexusRequestForwardingSuite) TestOperationCompletionForwardedFromStandb { name: "success", getCompletionFn: func() nexusrpc.CompleteOperationOptions { - return nexusrpc.CompleteOperationOptions{Result: s.mustToPayload("result")} + return nexusrpc.CompleteOperationOptions{Result: testcore.MustToPayload(s.T(), "result")} }, assertHistoryAndGetCompleteWF: func(t *testing.T, events []*historypb.HistoryEvent) *workflowservice.RespondWorkflowTaskCompletedRequest { completedEventIdx := slices.IndexFunc(events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCompletedEventAttributes() != nil }) - require.Greater(t, completedEventIdx, 0) + require.Positive(t, completedEventIdx) return &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", Commands: []*commandpb.Command{{CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, @@ -425,7 +420,7 @@ func (s *NexusRequestForwardingSuite) TestOperationCompletionForwardedFromStandb failedEventIdx := slices.IndexFunc(events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationFailedEventAttributes() != nil }) - require.Greater(t, failedEventIdx, 0) + require.Positive(t, failedEventIdx) return &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", Commands: []*commandpb.Command{{CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, @@ -453,7 +448,7 @@ func (s *NexusRequestForwardingSuite) TestOperationCompletionForwardedFromStandb canceledEventIdx := slices.IndexFunc(events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationCanceledEventAttributes() != nil }) - require.Greater(t, canceledEventIdx, 0) + require.Positive(t, canceledEventIdx) return &workflowservice.RespondWorkflowTaskCompletedRequest{ Identity: "test", Commands: []*commandpb.Command{{CommandType: enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, @@ -555,7 +550,7 @@ func (s *NexusRequestForwardingSuite) TestOperationCompletionForwardedFromStandb Endpoint: endpointName, Service: "service", Operation: "operation", - Input: s.mustToPayload("input"), + Input: testcore.MustToPayload(s.T(), "input"), }, }, }, @@ -582,7 +577,7 @@ func (s *NexusRequestForwardingSuite) TestOperationCompletionForwardedFromStandb startedEventIdx := slices.IndexFunc(pollResp.History.Events, func(e *historypb.HistoryEvent) bool { return e.GetNexusOperationStartedEventAttributes() != nil }) - s.Greater(startedEventIdx, 0) + s.Positive(startedEventIdx) // Wait for Nexus operation to be replicated s.Eventually(func() bool { @@ -601,7 +596,7 @@ func (s *NexusRequestForwardingSuite) TestOperationCompletionForwardedFromStandb completion.Header.Set(cnexus.CallbackTokenHeader, callbackToken) snap, err := s.sendNexusCompletionRequest(ctx, s.T(), s.clusters[1], publicCallbackUrl, completion) s.NoError(err) - s.Equal(1, len(snap["nexus_completion_requests"])) + s.Len(snap["nexus_completion_requests"], 1) s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": ns, "outcome": "request_forwarded"}) // Ensure that CompleteOperation request is tracked as part of normal service telemetry metrics @@ -619,7 +614,7 @@ func (s *NexusRequestForwardingSuite) TestOperationCompletionForwardedFromStandb var handlerErr *nexus.HandlerError s.ErrorAs(err, &handlerErr) s.Equal(nexus.HandlerErrorTypeNotFound, handlerErr.Type) - s.Equal(1, len(snap["nexus_completion_requests"])) + s.Len(snap["nexus_completion_requests"], 1) s.Subset(snap["nexus_completion_requests"][0].Tags, map[string]string{"namespace": ns, "outcome": "error_not_found"}) // Poll active cluster and verify the completion is recorded and triggers workflow progress. @@ -701,17 +696,10 @@ func (s *NexusRequestForwardingSuite) sendNexusCompletionRequest( } func requireExpectedMetricsCaptured(t *testing.T, snap map[string][]*metricstest.CapturedRecording, ns string, method string, expectedOutcome string) { - require.Equal(t, 1, len(snap["nexus_requests"])) + require.Len(t, snap["nexus_requests"], 1) require.Subset(t, snap["nexus_requests"][0].Tags, map[string]string{"namespace": ns, "method": method, "outcome": expectedOutcome}) require.Equal(t, int64(1), snap["nexus_requests"][0].Value) require.Equal(t, metrics.MetricUnit(""), snap["nexus_requests"][0].Unit) - require.Equal(t, 1, len(snap["nexus_latency"])) + require.Len(t, snap["nexus_latency"], 1) require.Subset(t, snap["nexus_latency"][0].Tags, map[string]string{"namespace": ns, "method": method, "outcome": expectedOutcome}) } - -func (s *NexusRequestForwardingSuite) mustToPayload(v any) *commonpb.Payload { - conv := converter.GetDefaultDataConverter() - payload, err := conv.ToPayload(v) - s.NoError(err) - return payload -} diff --git a/tests/xdc/nexus_state_replication_test.go b/tests/xdc/nexus_state_replication_test.go index aa52046bed9..3763e1fd203 100644 --- a/tests/xdc/nexus_state_replication_test.go +++ b/tests/xdc/nexus_state_replication_test.go @@ -13,6 +13,8 @@ import ( "github.com/google/uuid" "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" @@ -71,6 +73,13 @@ func (s *NexusStateReplicationSuite) SetupSuite() { callbacks.AllowedAddresses.Key(): []any{map[string]any{ "Pattern": "*", "AllowInsecure": true, }}, + // Cap callback retry backoff to avoid long waits after failover. + callbacks.RetryPolicyMaximumInterval.Key(): 1 * time.Second, + // Set a short circuit breaker timeout so it recovers quickly from bursts of failures. + dynamicconfig.OutboundQueueCircuitBreakerSettings.Key(): dynamicconfig.CircuitBreakerSettings{ + MaxRequests: 1, + Timeout: 1 * time.Second, + }, } s.setupSuite() } @@ -200,7 +209,7 @@ func (s *NexusStateReplicationSuite) TestNexusOperationEventsReplicated() { s.Eventually(func() bool { describeRes, err := sdkClient1.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) - s.Equal(1, len(describeRes.PendingNexusOperations)) + s.Len(describeRes.PendingNexusOperations, 1) op := describeRes.PendingNexusOperations[0] return op.State == enumspb.PENDING_NEXUS_OPERATION_STATE_STARTED }, time.Second*20, time.Millisecond*100) @@ -359,7 +368,7 @@ func (s *NexusStateReplicationSuite) TestNexusOperationCancelationReplicated() { s.Eventually(func() bool { describeRes, err := sdkClient0.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID()) s.NoError(err) - s.Equal(1, len(describeRes.PendingNexusOperations)) + s.Len(describeRes.PendingNexusOperations, 1) op := describeRes.PendingNexusOperations[0] fmt.Println(op.CancellationInfo) s.NotNil(op.CancellationInfo) @@ -590,7 +599,7 @@ func (s *NexusStateReplicationSuite) TestNexusOperationBufferedCompletionReplica break } } - s.Greater(scheduledEventID, int64(0)) + s.Positive(scheduledEventID) // Allow operation to complete synchronously during next retry attempt allowCompletion.Store(true) @@ -705,17 +714,17 @@ func (s *NexusStateReplicationSuite) waitCallback( execution *commonpb.WorkflowExecution, condition func(callback *workflowpb.CallbackInfo) bool, ) { - s.Eventually(func() bool { + s.EventuallyWithT(func(t *assert.CollectT) { descResp, err := sdkClient.DescribeWorkflowExecution(ctx, execution.WorkflowId, execution.RunId) - s.NoError(err) - s.Len(descResp.GetCallbacks(), 1) - return condition(descResp.GetCallbacks()[0]) + require.NoError(t, err) + require.Len(t, descResp.GetCallbacks(), 1) + require.True(t, condition(descResp.GetCallbacks()[0])) }, time.Second*20, time.Millisecond*100) } func (s *NexusStateReplicationSuite) completeNexusOperation(ctx context.Context, result any, callbackUrl, callbackToken string) { completion := nexusrpc.CompleteOperationOptions{ - Result: s.mustToPayload(result), + Result: testcore.MustToPayload(s.T(), result), Header: nexus.Header{commonnexus.CallbackTokenHeader: callbackToken}, } client := nexusrpc.NewCompletionHTTPClient(nexusrpc.CompletionHTTPClientOptions{ diff --git a/tests/xdc/replication_enable_test.go b/tests/xdc/replication_enable_test.go index 4aea135f82a..6ba98d8db9e 100644 --- a/tests/xdc/replication_enable_test.go +++ b/tests/xdc/replication_enable_test.go @@ -44,7 +44,7 @@ func (s *ReplicationEnableTestSuite) SetupSuite() { s.logger = log.NewTestLogger() // Minimal dynamic config overrides required for replication enable/disable testing - dynamicConfigOverrides := map[dynamicconfig.Key]interface{}{ + dynamicConfigOverrides := map[dynamicconfig.Key]any{ dynamicconfig.ClusterMetadataRefreshInterval.Key(): time.Second * 5, dynamicconfig.EnableReplicationStream.Key(): true, dynamicconfig.EnableSeparateReplicationEnableFlag.Key(): true, diff --git a/tests/xdc/stream_based_replication_test.go b/tests/xdc/stream_based_replication_test.go index d937cfd1aec..6cee35713e6 100644 --- a/tests/xdc/stream_based_replication_test.go +++ b/tests/xdc/stream_based_replication_test.go @@ -514,7 +514,7 @@ func (s *streamBasedReplicationTestSuite) TestForceReplicateResetWorkflow_BaseWo }) s.NoError(err) - for i := 0; i < 5; i++ { + for range 5 { wfExec, err := client1.DescribeWorkflowExecution(testcore.NewContext(), &workflowservice.DescribeWorkflowExecutionRequest{ Namespace: ns, Execution: &commonpb.WorkflowExecution{ diff --git a/tests/xdc/user_data_replication_test.go b/tests/xdc/user_data_replication_test.go index 61f3fc659d0..b49f6be6698 100644 --- a/tests/xdc/user_data_replication_test.go +++ b/tests/xdc/user_data_replication_test.go @@ -303,7 +303,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataIsReplicatedFromActiveToPassi if perType := response.GetUserData().GetData().GetPerType(); a.NotNil(perType) { for tqType := 1; tqType <= 3; tqType++ { data := perType[int32(tqType)].GetDeploymentData() - if a.Equal(1, len(data.GetVersions())) { + if a.Len(data.GetVersions(), 1) { //nolint:staticcheck a.True(data.GetVersions()[0].Equal(expectedVersionData)) } } @@ -335,7 +335,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataIsReplicatedFromActiveToPassi if perType := response.GetUserData().GetData().GetPerType(); a.NotNil(perType) { for tqType := 1; tqType <= 3; tqType++ { data := perType[int32(tqType)].GetDeploymentData() - if a.Equal(1, len(data.GetVersions())) { + if a.Len(data.GetVersions(), 1) { //nolint:staticcheck a.True(data.GetVersions()[0].Equal(expectedVersionData)) } } @@ -385,7 +385,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataEntriesAreReplicatedOnDemand( s.NoError(err) expectedReplicatedTaskQueues := make(map[string]struct{}, numTaskQueues) - for i := 0; i < numTaskQueues; i++ { + for i := range numTaskQueues { taskQueue := fmt.Sprintf("v1q%v", i) res, err := activeFrontendClient.UpdateWorkerBuildIdCompatibility(ctx, &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{ Namespace: namespace, @@ -434,7 +434,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataEntriesAreReplicatedOnDemand( }) s.NoError(err) lastMessageId = replicationResponse.GetMessages().GetLastRetrievedMessageId() - s.Equal(1, len(replicationResponse.GetMessages().ReplicationTasks)) + s.Len(replicationResponse.GetMessages().ReplicationTasks, 1) task := replicationResponse.GetMessages().ReplicationTasks[0] s.Equal(namespace, task.GetNamespaceTaskAttributes().GetInfo().GetName()) @@ -476,7 +476,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataEntriesAreReplicatedOnDemand( s.failover(namespace, 0, s.clusters[1].ClusterName(), 2) activeFrontendClient = s.clusters[1].FrontendClient() - for i := 0; i < numTaskQueues; i++ { + for i := range numTaskQueues { taskQueue := fmt.Sprintf("v1q%v", i) get, err := activeFrontendClient.GetWorkerBuildIdCompatibility(ctx, &workflowservice.GetWorkerBuildIdCompatibilityRequest{ @@ -527,7 +527,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataTombstonesAreReplicated() { description, err := activeFrontendClient.DescribeNamespace(testcore.NewContext(), &workflowservice.DescribeNamespaceRequest{Namespace: namespace}) s.NoError(err) - for i := 0; i < 3; i++ { + for i := range 3 { buildId := fmt.Sprintf("v%d", i) _, err = activeFrontendClient.UpdateWorkerBuildIdCompatibility(ctx, &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{ Namespace: namespace, @@ -570,7 +570,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataTombstonesAreReplicated() { attrs := task.GetTaskQueueUserDataAttributes() s.Equal(description.GetNamespaceInfo().Id, attrs.NamespaceId) s.Equal(taskQueue, attrs.TaskQueueName) - s.Equal(3, len(attrs.UserData.VersioningData.VersionSets)) + s.Len(attrs.UserData.VersioningData.VersionSets, 3) s.Equal("v0", attrs.UserData.VersioningData.VersionSets[0].BuildIds[0].Id) s.Equal(persistencespb.STATE_DELETED, attrs.UserData.VersioningData.VersionSets[0].BuildIds[0].State) s.Equal("v1", attrs.UserData.VersioningData.VersionSets[1].BuildIds[0].Id) @@ -601,7 +601,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataTombstonesAreReplicated() { attrs = task.GetTaskQueueUserDataAttributes() s.Equal(description.GetNamespaceInfo().Id, attrs.NamespaceId) s.Equal(taskQueue, attrs.TaskQueueName) - s.Equal(2, len(attrs.UserData.VersioningData.VersionSets)) + s.Len(attrs.UserData.VersioningData.VersionSets, 2) s.Equal("v2", attrs.UserData.VersioningData.VersionSets[0].BuildIds[0].Id) s.Equal(persistencespb.STATE_ACTIVE, attrs.UserData.VersioningData.VersionSets[0].BuildIds[0].State) s.Equal("v3", attrs.UserData.VersioningData.VersionSets[1].BuildIds[0].Id) @@ -644,7 +644,7 @@ func (s *UserDataReplicationTestSuite) TestUserDataTombstonesAreReplicated() { attrs = task.GetTaskQueueUserDataAttributes() s.Equal(description.GetNamespaceInfo().Id, attrs.NamespaceId) s.Equal(taskQueue, attrs.TaskQueueName) - s.Equal(3, len(attrs.UserData.VersioningData.VersionSets)) + s.Len(attrs.UserData.VersioningData.VersionSets, 3) s.Equal("v2", attrs.UserData.VersioningData.VersionSets[0].BuildIds[0].Id) s.Equal(persistencespb.STATE_ACTIVE, attrs.UserData.VersioningData.VersionSets[0].BuildIds[0].State) s.Equal("v3", attrs.UserData.VersioningData.VersionSets[1].BuildIds[0].Id) diff --git a/tests/xdc/visibility_test.go b/tests/xdc/visibility_test.go index 68fcf14f279..0f74875e545 100644 --- a/tests/xdc/visibility_test.go +++ b/tests/xdc/visibility_test.go @@ -12,7 +12,6 @@ import ( enumspb "go.temporal.io/api/enums/v1" filterpb "go.temporal.io/api/filter/v1" historypb "go.temporal.io/api/history/v1" - namespacepb "go.temporal.io/api/namespace/v1" taskqueuepb "go.temporal.io/api/taskqueue/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" @@ -67,23 +66,7 @@ func (s *VisibilityTestSuite) TearDownSuite() { func (s *VisibilityTestSuite) TestSearchAttributes() { ns := s.createGlobalNamespace() - if testcore.UseSQLVisibility() { - // When Elasticsearch is enabled, the search attribute aliases are not used. - updateNamespaceConfig(s.Assertions, ns, - func() *namespacepb.NamespaceConfig { - return &namespacepb.NamespaceConfig{ - CustomSearchAttributeAliases: map[string]string{ - "Bool01": "CustomBoolField", - "Datetime01": "CustomDatetimeField", - "Double01": "CustomDoubleField", - "Int01": "CustomIntField", - "Keyword01": "CustomKeywordField", - "Text01": "CustomTextField", - }, - } - }, - s.clusters, 0) - } + s.registerTestSearchAttributes(ns) client0 := s.clusters[0].FrontendClient() // active client1 := s.clusters[1].FrontendClient() // standby diff --git a/tests/xdc/workflow_task_reported_problems_test.go b/tests/xdc/workflow_task_reported_problems_test.go index 821fd922f21..15c1503899e 100644 --- a/tests/xdc/workflow_task_reported_problems_test.go +++ b/tests/xdc/workflow_task_reported_problems_test.go @@ -104,11 +104,11 @@ func (s *WorkflowTaskReportedProblemsReplicationSuite) getWFTFailure(admin admin }, Archetype: chasm.WorkflowArchetype, }) - require.NoError(s.T(), err) - require.NotNil(s.T(), resp) - require.NotNil(s.T(), resp.DatabaseMutableState) - require.NotNil(s.T(), resp.DatabaseMutableState.ExecutionInfo) - require.NotNil(s.T(), resp.DatabaseMutableState.ExecutionInfo.LastWorkflowTaskFailure) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().NotNil(resp.DatabaseMutableState) + s.Require().NotNil(resp.DatabaseMutableState.ExecutionInfo) + s.Require().NotNil(resp.DatabaseMutableState.ExecutionInfo.LastWorkflowTaskFailure) switch i := resp.DatabaseMutableState.ExecutionInfo.GetLastWorkflowTaskFailure().(type) { case *persistencespb.WorkflowExecutionInfo_LastWorkflowTaskFailureCause: return "WorkflowTaskFailed", fmt.Sprintf("WorkflowTaskFailedCause%s", i.LastWorkflowTaskFailureCause.String()), nil diff --git a/tools/cassandra/cqlclient.go b/tools/cassandra/cqlclient.go index aeb4040e447..221f9c061ad 100644 --- a/tools/cassandra/cqlclient.go +++ b/tools/cassandra/cqlclient.go @@ -200,7 +200,7 @@ func (client *cqlClient) WriteSchemaUpdateLog(oldVersion string, newVersion stri } // Exec executes a cql statement -func (client *cqlClient) Exec(stmt string, args ...interface{}) error { +func (client *cqlClient) Exec(stmt string, args ...any) error { if err := client.session.Query(stmt, args...).Exec(); err != nil { return err } diff --git a/tools/cassandra/setup_task_tests.go b/tools/cassandra/setup_task_tests.go index 042359f2483..a78ed543d00 100644 --- a/tools/cassandra/setup_task_tests.go +++ b/tools/cassandra/setup_task_tests.go @@ -18,7 +18,8 @@ func (s *SetupSchemaTestSuite) SetupSuite() { s.Logger.Fatal("Error creating CQLClient", tag.Error(err)) } s.client = client - s.SetupSuiteBase(client, "") + // No ConnectParams needed: Cassandra CLI defaults work without explicit flags. + s.SetupSuiteBase(client, "", test.ConnectParams{}) } func (s *SetupSchemaTestSuite) TearDownSuite() { diff --git a/tools/cassandra/update_task_tests.go b/tools/cassandra/update_task_tests.go index 2bd235a312c..38e13ee864b 100644 --- a/tools/cassandra/update_task_tests.go +++ b/tools/cassandra/update_task_tests.go @@ -15,7 +15,8 @@ func (s *UpdateSchemaTestSuite) SetupSuite() { if err != nil { s.Logger.Fatal("Error creating CQLClient", tag.Error(err)) } - s.SetupSuiteBase(client, "") + // No ConnectParams needed: Cassandra CLI defaults work without explicit flags. + s.SetupSuiteBase(client, "", test.ConnectParams{}) } func (s *UpdateSchemaTestSuite) TearDownSuite() { diff --git a/tools/ci-notify/app.go b/tools/ci-notify/app.go index edc41d57847..f83e0ffe05f 100644 --- a/tools/ci-notify/app.go +++ b/tools/ci-notify/app.go @@ -234,7 +234,7 @@ func runDigestCommand(c *cli.Context) error { return nil } -func marshalIndent(v interface{}) (string, error) { +func marshalIndent(v any) (string, error) { b, err := json.MarshalIndent(v, "", " ") if err != nil { return "", err diff --git a/tools/common/schema/mock_db.go b/tools/common/schema/mock_db.go index d70e187bf57..0ddb14a2038 100644 --- a/tools/common/schema/mock_db.go +++ b/tools/common/schema/mock_db.go @@ -10,7 +10,7 @@ type ( ) // Exec executes a cql statement -func (db *mockSQLDB) Exec(stmt string, args ...interface{}) error { +func (db *mockSQLDB) Exec(stmt string, args ...any) error { return fmt.Errorf("unimplemented") } diff --git a/tools/common/schema/test/dbtest.go b/tools/common/schema/test/dbtest.go index a021a4c5d93..e139352b7d0 100644 --- a/tools/common/schema/test/dbtest.go +++ b/tools/common/schema/test/dbtest.go @@ -22,6 +22,16 @@ type ( DropDatabase(name string) error ListTables() ([]string, error) } + + // ConnectParams holds connection parameters for CLI tool invocations in tests. + // Empty fields are omitted from the command line, allowing the CLI defaults to apply. + ConnectParams struct { + Host string + Port string + User string + Password string + } + // DBTestBase is the base for all test suites that test // the functionality of a DB implementation DBTestBase struct { @@ -33,6 +43,24 @@ type ( } ) +// CLIFlags returns the CLI flags for the connection parameters. +func (p ConnectParams) CLIFlags() []string { + var flags []string + if p.Host != "" { + flags = append(flags, "--endpoint", p.Host) + } + if p.Port != "" { + flags = append(flags, "--port", p.Port) + } + if p.User != "" { + flags = append(flags, "--user", p.User) + } + if p.Password != "" { + flags = append(flags, "--password", p.Password) + } + return flags +} + // SetupSuiteBase sets up the test suite func (tb *DBTestBase) SetupSuiteBase(db DB) { tb.Assertions = require.New(tb.T()) // Have to define our overridden assertions in the test setup. If we did it earlier, tb.T() will return nil diff --git a/tools/common/schema/test/setuptest.go b/tools/common/schema/test/setuptest.go index fe059536066..c2344bab68b 100644 --- a/tools/common/schema/test/setuptest.go +++ b/tools/common/schema/test/setuptest.go @@ -24,10 +24,12 @@ type SetupSchemaTestBase struct { DBName string db DB pluginName string + conn ConnectParams } // SetupSuiteBase sets up the test suite -func (tb *SetupSchemaTestBase) SetupSuiteBase(db DB, pluginName string) { +func (tb *SetupSchemaTestBase) SetupSuiteBase(db DB, pluginName string, conn ConnectParams) { + tb.conn = conn tb.Assertions = require.New(tb.T()) // Have to define our overridden assertions in the test setup. If we did it earlier, tb.T() will return nil tb.Logger = log.NewTestLogger() tb.rand = rand.New(rand.NewSource(time.Now().UnixNano())) @@ -78,7 +80,7 @@ func (tb *SetupSchemaTestBase) RunSetupTest( tb.Nil(err) tb.Equal(0, len(tables)) - for i := 0; i < 4; i++ { + for i := range 4 { ver := convert.Int32ToString(tb.rand.Int31()) versioningEnabled := (i%2 == 0) @@ -131,11 +133,9 @@ func (tb *SetupSchemaTestBase) RunSetupTest( func (tb *SetupSchemaTestBase) getCommandBase() []string { command := []string{"./tool"} if tb.pluginName != "" { - command = append(command, []string{ - "-pl", tb.pluginName, - }...) + command = append(command, "-pl", tb.pluginName) } - return command + return append(command, tb.conn.CLIFlags()...) } func getExpectedTables(versioningEnabled bool, wantTables []string) map[string]struct{} { diff --git a/tools/common/schema/test/updatetest.go b/tools/common/schema/test/updatetest.go index 981c8bd8934..ad2b3598a1d 100644 --- a/tools/common/schema/test/updatetest.go +++ b/tools/common/schema/test/updatetest.go @@ -24,10 +24,12 @@ type UpdateSchemaTestBase struct { DBName string db DB pluginName string + conn ConnectParams } // SetupSuiteBase sets up the test suite -func (tb *UpdateSchemaTestBase) SetupSuiteBase(db DB, pluginName string) { +func (tb *UpdateSchemaTestBase) SetupSuiteBase(db DB, pluginName string, conn ConnectParams) { + tb.conn = conn tb.Assertions = require.New(tb.T()) // Have to define our overridden assertions in the test setup. If we did it earlier, tb.T() will return nil tb.Logger = log.NewTestLogger() tb.rand = rand.New(rand.NewSource(time.Now().UnixNano())) @@ -153,9 +155,7 @@ func (tb *UpdateSchemaTestBase) makeSchemaVersionDirs(rootDir string, sqlFileCon func (tb *UpdateSchemaTestBase) getCommandBase() []string { command := []string{"./tool"} if tb.pluginName != "" { - command = append(command, []string{ - "-pl", tb.pluginName, - }...) + command = append(command, "-pl", tb.pluginName) } - return command + return append(command, tb.conn.CLIFlags()...) } diff --git a/tools/common/schema/types.go b/tools/common/schema/types.go index e22c2cbe7e1..f5f9a72d816 100644 --- a/tools/common/schema/types.go +++ b/tools/common/schema/types.go @@ -34,7 +34,7 @@ type ( // for the schema-tool to work DB interface { // Exec executes a cql statement - Exec(stmt string, args ...interface{}) error + Exec(stmt string, args ...any) error // DropAllTables drops all tables DropAllTables() error // CreateSchemaVersionTables sets up the schema version tables diff --git a/tools/elasticsearch/tasks.go b/tools/elasticsearch/tasks.go index 6d61e9c4088..c10fc21162b 100644 --- a/tools/elasticsearch/tasks.go +++ b/tools/elasticsearch/tasks.go @@ -163,7 +163,7 @@ func (task *SetupTask) updateIndexMappings() error { } // Parse the template to extract mappings - var template map[string]interface{} + var template map[string]any if err := json.Unmarshal([]byte(config.TemplateContent), &template); err != nil { return fmt.Errorf("failed to parse template content: %w", err) } diff --git a/tools/fairsim/README.md b/tools/fairsim/README.md new file mode 100644 index 00000000000..f762299ec5e --- /dev/null +++ b/tools/fairsim/README.md @@ -0,0 +1,153 @@ + +# fairsim + +This tool simulates the behavior of Temporal's fair task queues, for use in +evaluating different parameters. + +## Build +run `make fairsim` + +## Usage + +There are two main modes: + +- **Generation**: Generates tasks by random distribution and then dispatches + them. +- **Script**: Process a script with queue pushes and pops. This can be used to + test with actual data or a custom distribution, and with continuous task + creation/dispatch. + +### Generation + +By default, fairsim generates tasks with fairness keys following a Zipf +distribution. + +Examples: + +```bash +# Generate a million tasks with 20 keys +fairsim -- -tasks=1000000 -keys=20 + +# Generate a million tasks with a much more lopsided distribution +fairsim -- -tasks=1000000 -keys=50 -zipf-s=3 -zipf-v=1.1 + +# Try alternate counter paramters +for w in 1 10 100 1000 10000; do + fairsim -counter-params <(echo '{"CMS":{"W":'$w'}}') -- -tasks=1000000 -keys=1000 | grep p90s: | tail -1 +done + +# Disable fairness to compare to fifo +fairsim -fair=0 -- -tasks=500 + +# Use only one partition (default is 4) +fairsim -partitions=1 -- -tasks=500 +``` + +### Script + +Examples: + +```bash +# Priority order +{ + echo "task -pri=4 -payload four" + echo "task -pri=2 -payload two" + echo "task -pri=3 -payload three" + echo "task -pri=5 -payload five" + echo "task -pri=1 -payload one" +} | fairsim -script=/dev/stdin -partitions=1 +# should see one, two, three, four, five + +# Fairness +{ + echo "task -fkey a" + echo "task -fkey a" + echo "task -fkey a" + echo "task -fkey a" + echo "task -fkey b" +} | fairsim -script=/dev/stdin -partitions=1 +# should see a, b, a, a, a + +# Alternating +{ + echo "task -fkey a" + echo "task -fkey a" + echo "task -fkey a" + echo "task -fkey a" + echo "task -fkey b" + echo poll # gets a + echo poll # gets b + echo "task -fkey c" + echo "task -fkey c" +} | fairsim -script=/dev/stdin -partitions=1 +# should see a, b, c, a, c, a, a + +# Weight +{ + for i in {1..20}; do echo "task -fkey a"; done + for i in {1..20}; do echo "task -fkey b -fweight 5"; done +} | fairsim -script=/dev/stdin -partitions=1 +# should see five b's for each a until b's are done +``` + +## Interpreting output + +By default, only **Percentile of percentiles** are printed. Add `-verbose` for full output. + +### Task section + +First, fairsim will print one line for each task dispatched: + +``` +task idx: 33 dsp: 15 lat: -18 pri: 3 fkey: "key1" fweight: 1 part: 2 payload:"external-key-32" + +``` + +- `idx`: Creation index. Tasks are assigned an incrementing index as they are created. +- `dsp`: Dispatch index. Order this task was dispatched in. +- `lat`: "Latency": dispatch index minus creation index. For a FIFO queue this + would always be zero. Negative means the task was moved earlier compared to + FIFO, positive means it was penalized. +- `pri`: Task priority. +- `fkey`: Fairness key. +- `fweight`: Fairness weight. +- `part`: Partition task was assigned to. +- `payload`: User-defined payload (could be used for correlation). + +### Statistics + +Next are statistics on the "latency" values. In general, lower numbers are +better, since that means more tasks were moved ahead of where they would be in a +FIFO queue. + +**Raw Latency Statistics** + +Basic stats on the "latency" values. Mean must always be zero since it's a +permutation. + +**Normalized Latency Statistics** + +When looking at per-key latencies, we expect a "heavy" fairness key with more +tasks to have worse latency than a "light" key, since the light key's tasks get +pushed in front of the heavy one. This is desirable, though, and doesn't really +reflect how much the heavy one was "penalized". We can normalize the latency by +dividing by the number of tasks for that key, and futher normalize by the number +of keys. + +Both the raw and normalized can be useful to look at. + +**Per-task Statistics** + +The raw and normalized latency stats are printed for each key, along with the +count. + +**Percentile of percentiles** + +Raw and normalized percentile stats are printed to give a summary of latency +across different keys: + +Rows are percentiles of latency for each key, and columns are percentiles across +those percentiles (counting each key once). E.g. the "p90s" row describes the +90th percentile latency for each key. The "@50" column of that is the median of +those 90th percentile latencies. + diff --git a/tools/fairsim/sim.go b/tools/fairsim/sim.go new file mode 100644 index 00000000000..e45082a2f45 --- /dev/null +++ b/tools/fairsim/sim.go @@ -0,0 +1,611 @@ +//nolint:errcheck // don't need to check fmt.Fprintf +package fairsim + +import ( + "bufio" + "cmp" + "container/heap" + "encoding/json" + "flag" + "fmt" + "io" + "math/rand/v2" + "os" + "slices" + "strings" + + "go.temporal.io/server/service/matching/counter" +) + +// matches service/matching.strideFactor, but we don't want to export that +const defaultStrideFactor = 1000 + +type ( + task struct { + pri int + fkey string + fweight float32 + pass int64 + index int64 + payload string + } + + state struct { + rnd *rand.Rand + counterFactory func() counter.Counter + partitions []partitionState + strideFactor float32 + } + + simulator struct { + state *state + stats *latencyStats + w io.Writer + verbose bool + nextIndex int64 + dispatchIndex int64 + defaultPriority int + } + + latencyStats struct { + byKey map[string][]int64 // latencies by fairness key + byKeyNormalized map[string][]float64 // normalized latencies by fairness key + overall []int64 // all latencies + overallNormalized []float64 // all normalized latencies + } + + partitionState struct { + perPri map[int]perPriState + heap taskHeap + } + + taskHeap []*task + + perPriState struct { + c counter.Counter + } + + unfairCounter struct{} +) + +// parseFlags creates a FlagSet, calls setup to register flags, and parses args. +// Returns remaining (non-flag) arguments. +func parseFlags(name string, args []string, setup func(*flag.FlagSet)) ([]string, error) { + fs := flag.NewFlagSet(name, flag.ContinueOnError) + var flagErrors strings.Builder + fs.SetOutput(&flagErrors) + setup(fs) + if err := fs.Parse(args); err != nil { + if flagErrors.Len() > 0 { + return nil, fmt.Errorf("%s: %w\n%s", name, err, flagErrors.String()) + } + return nil, fmt.Errorf("%s: %w", name, err) + } + return fs.Args(), nil +} + +func RunTool(args []string) error { + var ( + seed *int64 + fair *bool + partitions *int + strideFactor *int + counterFile *string + scriptFile *string + verbose *bool + ) + remainingArgs, err := parseFlags("fairsim", args, func(fs *flag.FlagSet) { + seed = fs.Int64("seed", rand.Int64(), "Random seed") + fair = fs.Bool("fair", true, "Enable fairness (false for FIFO)") + partitions = fs.Int("partitions", 4, "Number of partitions") + strideFactor = fs.Int("strideFactor", defaultStrideFactor, "Stride factor") + counterFile = fs.String("counter-params", "", "JSON file with CounterParams") + scriptFile = fs.String("script", "", "Script file to execute instead of generating tasks") + verbose = fs.Bool("verbose", false, "verbose output") + }) + if err != nil { + return err + } + + // Load counter params + var params counter.CounterParams + if *counterFile == "" { + params = counter.DefaultCounterParams + } else { + data, err := os.ReadFile(*counterFile) + if err != nil { + return fmt.Errorf("failed to load counter params: %w", err) + } else if err = json.Unmarshal(data, ¶ms); err != nil { + return fmt.Errorf("failed to load counter params: %w", err) + } + fmt.Printf("Using counter params: %#v\n\n", params) + } + + src := rand.NewPCG(uint64(*seed), uint64(*seed+1)) + rnd := rand.New(src) + + counterFactory := func() counter.Counter { return unfairCounter{} } + if *fair { + counterFactory = func() counter.Counter { return counter.NewHybridCounter(params, src) } + } + + st := newState(rnd, counterFactory, *partitions, *strideFactor) + stats := newLatencyStats() + + const defaultPriority = 3 + sim := newSimulator(st, stats, defaultPriority, os.Stdout, *verbose) + + // Check if script mode + if *scriptFile != "" { + return sim.runScript(*scriptFile) + } + + // Default behavior: run gentasks command with remaining args from command line + if err := sim.executeGenTasksCommand(remainingArgs); err != nil { + return err + } + + sim.finish() + return nil +} + +func newLatencyStats() *latencyStats { + return &latencyStats{ + byKey: make(map[string][]int64), + byKeyNormalized: make(map[string][]float64), + } +} + +func newState(rnd *rand.Rand, counterFactory func() counter.Counter, partitions, strideFactor int) *state { + return &state{ + rnd: rnd, + counterFactory: counterFactory, + partitions: make([]partitionState, partitions), + strideFactor: float32(strideFactor), + } +} + +func newSimulator(state *state, stats *latencyStats, defaultPriority int, w io.Writer, verbose bool) *simulator { + return &simulator{ + state: state, + stats: stats, + w: w, + verbose: verbose, + defaultPriority: defaultPriority, + } +} + +// addTask adds a task to the simulator, assigning defaults and an index. +func (sim *simulator) addTask(t *task) { + t.pri = cmp.Or(t.pri, sim.defaultPriority) + t.fweight = cmp.Or(t.fweight, 1.0) + t.index = sim.nextIndex + sim.nextIndex++ + sim.state.addTask(t) +} + +// processTask records stats for a dispatched task and returns the latency. +func (sim *simulator) processTask(t *task) int64 { + latency := sim.dispatchIndex - t.index + sim.stats.byKey[t.fkey] = append(sim.stats.byKey[t.fkey], latency) + sim.stats.overall = append(sim.stats.overall, latency) + sim.dispatchIndex++ + return latency +} + +// printTask writes a single task's dispatch info to the writer. +func (sim *simulator) printTask(t *task, partition int, latency int64) { + if !sim.verbose { + return + } + fmt.Fprintf(sim.w, "task idx:%6d dsp:%6d lat:%6d pri:%2d fkey:%10q fweight:%3g part:%2d payload:%q\n", + t.index, sim.dispatchIndex-1, latency, t.pri, t.fkey, t.fweight, partition, t.payload) +} + +// drainTasks pops and processes all remaining tasks, printing each one. +func (sim *simulator) drainTasks() { + for t, partition := sim.state.popTask(); t != nil; t, partition = sim.state.popTask() { + latency := sim.processTask(t) + sim.printTask(t, partition, latency) + } +} + +// finish drains remaining tasks, calculates normalized stats, and prints stats. +func (sim *simulator) finish() { + sim.drainTasks() + sim.stats.calculateNormalized() + sim.stats.fprint(sim.w, sim.verbose) +} + +func (sim *simulator) runScript(scriptFile string) error { + file, err := os.Open(scriptFile) + if err != nil { + return fmt.Errorf("failed to open script file: %w", err) + } + defer file.Close() + + var commands []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + commands = append(commands, line) + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("error reading script file: %w", err) + } + + return sim.runCommands(commands) +} + +func (sim *simulator) runCommands(commands []string) error { + for _, cmd := range commands { + if err := sim.executeCommand(cmd); err != nil { + return fmt.Errorf("error executing command %q: %w", cmd, err) + } + } + sim.finish() + return nil +} + +func (sim *simulator) executeCommand(line string) error { + parts := strings.Fields(line) + if len(parts) == 0 { + return nil + } + + switch parts[0] { + case "task": + return sim.executeTaskCommand(parts[1:]) + case "poll": + return sim.executePollCommand() + case "stats": + return sim.executeStatsCommand() + case "clearstats": + return sim.executeClearStatsCommand() + case "gentasks": + return sim.executeGenTasksCommand(parts[1:]) + default: + return fmt.Errorf("unknown command: %q", parts[0]) + } +} + +func (sim *simulator) executeTaskCommand(args []string) error { + var ( + fkey *string + fweight *float64 + pri *int + payload *string + ) + if _, err := parseFlags("task", args, func(fs *flag.FlagSet) { + fkey = fs.String("fkey", "", "fairness key") + fweight = fs.Float64("fweight", 1.0, "fairness weight") + pri = fs.Int("pri", sim.defaultPriority, "priority") + payload = fs.String("payload", "", "payload") + }); err != nil { + return err + } + + sim.addTask(&task{ + fkey: *fkey, + fweight: float32(*fweight), + pri: *pri, + payload: *payload, + }) + return nil +} + +func (sim *simulator) executePollCommand() error { + t, partition := sim.state.popTask() + if t == nil { + fmt.Fprintln(sim.w, "No tasks in queue") + return nil + } + latency := sim.processTask(t) + sim.printTask(t, partition, latency) + return nil +} + +func (sim *simulator) executeStatsCommand() error { + sim.stats.calculateNormalized() + sim.stats.fprint(sim.w, sim.verbose) + return nil +} + +func (sim *simulator) executeClearStatsCommand() error { + sim.stats = newLatencyStats() + return nil +} + +func (sim *simulator) executeGenTasksCommand(args []string) error { + var ( + tasks *int + keys *int + keyprefix *string + zipfS *float64 + zipfV *float64 + ) + if _, err := parseFlags("gentasks", args, func(fs *flag.FlagSet) { + tasks = fs.Int("tasks", 100, "number of tasks to generate") + keys = fs.Int("keys", 10, "number of unique fairness keys") + keyprefix = fs.String("keyprefix", "key", "prefix for generated fairness keys") + zipfS = fs.Float64("zipf-s", 2.0, "zipf distribution s parameter") + zipfV = fs.Float64("zipf-v", 2.0, "zipf distribution v parameter") + }); err != nil { + return err + } + + if *tasks <= 0 { + return fmt.Errorf("tasks must be positive, got %d", *tasks) + } + if *keys <= 0 { + return fmt.Errorf("keys must be positive, got %d", *keys) + } + + zipf := rand.NewZipf(sim.state.rnd, *zipfS, *zipfV, uint64(*keys-1)) + for range *tasks { + sim.addTask(&task{ + fkey: fmt.Sprintf("%s%d", *keyprefix, zipf.Uint64()), + }) + } + return nil +} + +// --- state methods --- + +// addTask adds a task to a random partition, computing its pass via the counter. +func (s *state) addTask(t *task) { + partition := &s.partitions[s.rnd.IntN(len(s.partitions))] + + if partition.perPri == nil { + partition.perPri = make(map[int]perPriState) + } + + priState, exists := partition.perPri[t.pri] + if !exists { + priState = perPriState{c: s.counterFactory()} + partition.perPri[t.pri] = priState + } + + t.pass = priState.c.GetPass(t.fkey, 0, max(1, int64(s.strideFactor/t.fweight))) + heap.Push(&partition.heap, t) +} + +// popTask returns the task with minimum (pri, pass, index) from a random partition. +func (s *state) popTask() (*task, int) { + for _, idx := range s.rnd.Perm(len(s.partitions)) { + partition := &s.partitions[idx] + if partition.heap.Len() > 0 { + t := heap.Pop(&partition.heap).(*task) //nolint:revive + return t, idx + } + } + return nil, -1 +} + +// --- unfairCounter (FIFO mode) --- + +func (u unfairCounter) GetPass(key string, base int64, inc int64) int64 { return base } +func (u unfairCounter) EstimateDistinctKeys() int { return 0 } +func (u unfairCounter) TopK() []counter.TopKEntry { return nil } + +// --- taskHeap (heap.Interface) --- + +func (h taskHeap) Len() int { return len(h) } + +func (h taskHeap) Less(i, j int) bool { + if h[i].pri != h[j].pri { + return h[i].pri < h[j].pri + } + if h[i].pass != h[j].pass { + return h[i].pass < h[j].pass + } + return h[i].index < h[j].index +} + +func (h taskHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *taskHeap) Push(x any) { *h = append(*h, x.(*task)) } + +func (h *taskHeap) Pop() any { + old := *h + n := len(old) + item := old[n-1] + *h = old[:n-1] + return item +} + +// --- latencyStats --- + +func (stats *latencyStats) calculateNormalized() { + totalTasks := len(stats.overall) + totalKeys := len(stats.byKey) + if totalTasks == 0 || totalKeys == 0 { + return + } + // Normalized latency: raw_displacement / count_for_key * total_keys / total_tasks. + // Dividing by count_for_key adjusts for volume (high-volume keys naturally have larger + // displacements). The total_keys/total_tasks factor further scales by the key's share + // of total traffic, making values comparable across different workload sizes. + // Values center on 0: negative = expedited, positive = delayed. + fairShareFactor := float64(totalKeys) / float64(totalTasks) + stats.overallNormalized = stats.overallNormalized[:0] + for key, latencies := range stats.byKey { + taskCount := float64(len(latencies)) + normalizedLatencies := make([]float64, len(latencies)) + for i, rawLatency := range latencies { + normalizedLatencies[i] = float64(rawLatency) / taskCount * fairShareFactor + stats.overallNormalized = append(stats.overallNormalized, normalizedLatencies[i]) + } + stats.byKeyNormalized[key] = normalizedLatencies + } +} + +func (stats *latencyStats) fprint(w io.Writer, verbose bool) { + if verbose { + if len(stats.overall) > 0 { + slices.Sort(stats.overall) + mean := float64(sum(stats.overall)) / float64(len(stats.overall)) + median := stats.overall[len(stats.overall)/2] + p95 := stats.overall[int(float64(len(stats.overall))*0.95)] + + fmt.Fprint(w, "\n=== Raw Latency Statistics ===\n") + fmt.Fprintf(w, "Overall: mean=%.2f, median=%d, p95=%d, min=%d, max=%d\n", + mean, median, p95, stats.overall[0], stats.overall[len(stats.overall)-1]) + } + + if len(stats.overallNormalized) > 0 { + slices.Sort(stats.overallNormalized) + mean := sum(stats.overallNormalized) / float64(len(stats.overallNormalized)) + median := stats.overallNormalized[len(stats.overallNormalized)/2] + p95 := stats.overallNormalized[int(float64(len(stats.overallNormalized))*0.95)] + + fmt.Fprint(w, "\n=== Normalized Latency Statistics ===\n") + fmt.Fprintf(w, "Overall: mean=%.4f, median=%.4f, p95=%.4f, min=%.4f, max=%.4f\n", + mean, median, p95, stats.overallNormalized[0], stats.overallNormalized[len(stats.overallNormalized)-1]) + } + + type keyStats struct { + key string + meanRaw float64 + medianRaw int64 + meanNormalized float64 + medianNormalized float64 + count int + } + + var keyStatsList []keyStats + for key, latencies := range stats.byKey { + if len(latencies) == 0 { + continue + } + + slices.Sort(latencies) + meanRaw := float64(sum(latencies)) / float64(len(latencies)) + medianRaw := latencies[len(latencies)/2] + + normalizedLatencies := stats.byKeyNormalized[key] + slices.Sort(normalizedLatencies) + meanNormalized := sum(normalizedLatencies) / float64(len(normalizedLatencies)) + medianNormalized := normalizedLatencies[len(normalizedLatencies)/2] + + keyStatsList = append(keyStatsList, keyStats{ + key: key, + meanRaw: meanRaw, + medianRaw: medianRaw, + meanNormalized: meanNormalized, + medianNormalized: medianNormalized, + count: len(latencies), + }) + } + + slices.SortFunc(keyStatsList, func(a, b keyStats) int { return cmp.Compare(a.medianNormalized, b.medianNormalized) }) + + fmt.Fprint(w, "\nPer-key stats (sorted by median normalized latency):\n") + for _, ks := range keyStatsList { + fmt.Fprintf(w, " %s: raw(mean=%.2f, median=%d) norm(mean=%.4f, median=%.4f) count=%d\n", + ks.key, ks.meanRaw, ks.medianRaw, ks.meanNormalized, ks.medianNormalized, ks.count) + } + } + + ps := []float64{20, 50, 80, 90, 95} + + fmt.Fprint(w, "\nRaw fairness metrics (percentile of per-key percentiles):\n") + fmt.Fprintf(w, " @%2.0f @%2.0f @%2.0f @%2.0f @%2.0f\n", + ps[0], ps[1], ps[2], ps[3], ps[4]) + for _, p := range ps { + pofps := percentileOfPercentiles(stats.byKey, p, ps) + fmt.Fprintf(w, " p%2.0fs: %7.0f %7.0f %7.0f %7.0f %7.0f\n", + p, pofps[0], pofps[1], pofps[2], pofps[3], pofps[4]) + } + + fmt.Fprint(w, "\nNormalized fairness metrics (percentile of per-key percentiles):\n") + fmt.Fprintf(w, " @%2.0f @%2.0f @%2.0f @%2.0f @%2.0f\n", + ps[0], ps[1], ps[2], ps[3], ps[4]) + for _, p := range ps { + pofps := percentileOfPercentiles(stats.byKeyNormalized, p, ps) + fmt.Fprintf(w, " p%2.0fs: %7.3f %7.3f %7.3f %7.3f %7.3f\n", + p, pofps[0], pofps[1], pofps[2], pofps[3], pofps[4]) + } +} + +// --- latencyStats query methods (for testing) --- + +func (stats *latencyStats) meanNormalized(key string) float64 { + values := stats.byKeyNormalized[key] + if len(values) == 0 { + return 0 + } + return sum(values) / float64(len(values)) +} + +func (stats *latencyStats) percentile(key string, p float64) float64 { + values := stats.byKey[key] + if len(values) == 0 { + return 0 + } + sorted := slices.Clone(values) + slices.Sort(sorted) + idx := int(p / 100.0 * float64(len(sorted)-1)) + return float64(sorted[idx]) +} + +func (stats *latencyStats) overallPercentile(p float64) float64 { + if len(stats.overall) == 0 { + return 0 + } + sorted := slices.Clone(stats.overall) + slices.Sort(sorted) + idx := int(p / 100.0 * float64(len(sorted)-1)) + return float64(sorted[idx]) +} + +func (stats *latencyStats) keyCount() int { + return len(stats.byKey) +} + +func (stats *latencyStats) taskCount(key string) int { + return len(stats.byKey[key]) +} + +// --- generic helpers --- + +func sum[T int64 | float64](slice []T) T { + var total T + for _, v := range slice { + total += v + } + return total +} + +func percentileOfPercentiles[T int64 | float64](dataByKey map[string][]T, keyPercentile float64, crossPercentile []float64) []float64 { + var keyPercentiles []float64 + + for _, values := range dataByKey { + if len(values) == 0 { + continue + } + sorted := make([]float64, len(values)) + for i, v := range values { + sorted[i] = float64(v) + } + slices.Sort(sorted) + idx := int(keyPercentile / 100.0 * float64(len(sorted)-1)) + keyPercentiles = append(keyPercentiles, sorted[idx]) + } + + if len(keyPercentiles) == 0 { + return nil + } + + slices.Sort(keyPercentiles) + + out := make([]float64, len(crossPercentile)) + for i, p := range crossPercentile { + idx := int(p / 100.0 * float64(len(keyPercentiles)-1)) + out[i] = keyPercentiles[idx] + } + return out +} diff --git a/tools/fairsim/sim_test.go b/tools/fairsim/sim_test.go new file mode 100644 index 00000000000..fb9bfd86df4 --- /dev/null +++ b/tools/fairsim/sim_test.go @@ -0,0 +1,178 @@ +package fairsim + +import ( + "io" + "math/rand/v2" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "go.temporal.io/server/service/matching/counter" +) + +func newTestSimulator() *simulator { + src := rand.NewPCG(rand.Uint64(), rand.Uint64()) + rnd := rand.New(src) + + counterFactory := func() counter.Counter { + return counter.NewHybridCounter(counter.DefaultCounterParams, src) + } + + st := newState(rnd, counterFactory, 1, defaultStrideFactor) + stats := newLatencyStats() + return newSimulator(st, stats, 3, io.Discard, false) +} + +// pOfP returns the crossP-th percentile of per-key keyP-th percentiles. +func pOfP[T int64 | float64](data map[string][]T, keyP, crossP float64) float64 { + result := percentileOfPercentiles(data, keyP, []float64{crossP}) + if result == nil { + return 0 + } + return result[0] +} + +func addZipfTasks(sim *simulator, numTasks, numKeys int, zipfS, zipfV float64) { + zipf := rand.NewZipf(sim.state.rnd, zipfS, zipfV, uint64(numKeys-1)) + for range numTasks { + sim.addTask(&task{ + fkey: makeKey(int(zipf.Uint64())), + }) + } +} + +func makeKey(i int) string { + return "key" + strconv.Itoa(i) +} + +func drainAndCalc(sim *simulator) { + sim.drainTasks() + sim.stats.calculateNormalized() +} + +// TestFairness_MapCounter tests fairness with 90 keys (below MapLimit=100), +// ensuring the map-based counter provides fair scheduling under Zipf skew. +func TestFairness_MapCounter(t *testing.T) { + t.Parallel() + + sim := newTestSimulator() + addZipfTasks(sim, 100_000, 90, 2.0, 2.0) + drainAndCalc(sim) + + // Light keys (p90) should be expedited (negative median displacement). + // percentileOfPercentiles sorts by value, so p90 = 90th percentile of per-key + // medians sorted ascending. Since most keys are light under Zipf, even the p90 + // should still be a light key with negative displacement. + p90median := pOfP(sim.stats.byKey, 50, 90) + assert.Less(t, p90median, 0.0, + "p90 key median displacement should be negative (light keys expedited)") + + // Heaviest key is at p100 (max median, most delayed by fairness) + p100median := pOfP(sim.stats.byKey, 50, 100) + assert.Greater(t, p100median, 0.0, + "heaviest key median displacement should be positive (delayed)") +} + +// TestFairness_CMSCounter tests fairness with 500 keys (above MapLimit=100), +// ensuring the count-min sketch still provides fair scheduling. +func TestFairness_CMSCounter(t *testing.T) { + t.Parallel() + + sim := newTestSimulator() + addZipfTasks(sim, 100_000, 500, 2.0, 2.0) + drainAndCalc(sim) + + p90median := pOfP(sim.stats.byKey, 50, 90) + assert.Less(t, p90median, 0.0, + "p90 key median displacement should be negative (light keys expedited)") + + p100median := pOfP(sim.stats.byKey, 50, 100) + assert.Greater(t, p100median, 0.0, + "heaviest key median displacement should be positive (delayed)") +} + +// TestFairness_ExtremeSkew tests with 2 keys where one dominates (95% of tasks). +func TestFairness_ExtremeSkew(t *testing.T) { + t.Parallel() + + sim := newTestSimulator() + + // 9500 tasks for the heavy key, 500 for the light key + for range 9500 { + sim.addTask(&task{fkey: "heavy"}) + } + for range 500 { + sim.addTask(&task{fkey: "light"}) + } + drainAndCalc(sim) + + heavyMedian := sim.stats.percentile("heavy", 50) + lightMedian := sim.stats.percentile("light", 50) + + assert.Greater(t, heavyMedian, 0.0, + "heavy key should have positive median displacement (delayed)") + assert.Less(t, lightMedian, 0.0, + "light key should have negative median displacement (expedited)") +} + +// TestFairness_Weights tests that higher-weight keys get better (lower) displacement. +func TestFairness_Weights(t *testing.T) { + t.Parallel() + + sim := newTestSimulator() + + // Round-robin 3 keys with equal task counts but different weights. + // Higher weight = lower pass increment = dispatched sooner. + for range 1000 { + sim.addTask(&task{fkey: "normal", fweight: 1.0}) + sim.addTask(&task{fkey: "high", fweight: 2.0}) + sim.addTask(&task{fkey: "low", fweight: 0.5}) + } + drainAndCalc(sim) + + highMean := sim.stats.percentile("high", 50) + normalMean := sim.stats.percentile("normal", 50) + lowMean := sim.stats.percentile("low", 50) + + assert.Less(t, highMean, normalMean, + "high-weight key should have lower displacement than normal-weight") + assert.Greater(t, lowMean, normalMean, + "low-weight key should have higher displacement than normal-weight") +} + +// TestFairness_UniformDistribution tests that with equal traffic per key, +// fairness doesn't create significant disparity. +func TestFairness_UniformDistribution_50(t *testing.T) { + sim := newTestSimulator() + const numTasks = 100_000 + for i := range numTasks { + sim.addTask(&task{fkey: makeKey(i % 50)}) + } + drainAndCalc(sim) + + p90 := pOfP(sim.stats.byKey, 50, 90) + p10 := pOfP(sim.stats.byKey, 50, 10) + spread := p90 - p10 + // Map counter tracks exact counts, so uniform distribution should yield + // very tight spread. + assert.Less(t, spread, 1000.0, + "uniform distribution with map counter should have tight spread") +} + +func TestFairness_UniformDistribution_500(t *testing.T) { + sim := newTestSimulator() + const numTasks = 100_000 + for i := range numTasks { + sim.addTask(&task{fkey: makeKey(i % 500)}) + } + drainAndCalc(sim) + + p90 := pOfP(sim.stats.byKey, 50, 90) + p10 := pOfP(sim.stats.byKey, 50, 10) + spread := p90 - p10 + // CMS with 500 keys has hash collisions (initial width=100), so spread + // is wider than the map counter. Assert it stays within half the total + // task count — a basic sanity bound. + assert.Less(t, spread, float64(numTasks)/2, + "uniform distribution with CMS should have bounded spread") +} diff --git a/tools/flakereport/bisect.go b/tools/flakereport/bisect.go new file mode 100644 index 00000000000..2af605fc1a4 --- /dev/null +++ b/tools/flakereport/bisect.go @@ -0,0 +1,506 @@ +package flakereport + +import ( + "context" + "errors" + "fmt" + "math" + "sort" + "strings" + "time" + + "golang.org/x/sync/errgroup" +) + +const ( + minBisectFailures = 5 // skip test if fewer than this many failures in window + minBisectRuns = 30 // skip test if ran fewer than this many times total +) + +// logBeta returns log(Beta(a, b)) = lgamma(a) + lgamma(b) - lgamma(a+b). +func logBeta(a, b float64) float64 { + la, _ := math.Lgamma(a) + lb, _ := math.Lgamma(b) + lab, _ := math.Lgamma(a + b) + return la + lb - lab +} + +// logBetaBinomial returns the log marginal likelihood of observing k failures +// in n trials with a Beta(alpha, beta) prior on the failure probability. +// With Beta(1,1) prior this is the log Beta-Binomial marginal likelihood. +func logBetaBinomial(n, k int, alpha, beta float64) float64 { + return logBeta(float64(k)+alpha, float64(n-k)+beta) - logBeta(alpha, beta) +} + +// logSumExpNormalize converts log-weights to probabilities using the log-sum-exp trick +// to avoid numerical underflow. Returns a slice of probabilities summing to 1.0. +func logSumExpNormalize(logWeights []float64) []float64 { + if len(logWeights) == 0 { + return nil + } + // Find max for numerical stability + maxLog := logWeights[0] + for _, lw := range logWeights[1:] { + if lw > maxLog { + maxLog = lw + } + } + // Compute exp(lw - maxLog) and sum + probs := make([]float64, len(logWeights)) + sum := 0.0 + for i, lw := range logWeights { + probs[i] = math.Exp(lw - maxLog) + sum += probs[i] + } + // Normalize + for i := range probs { + probs[i] /= sum + } + return probs +} + +// buildObservations groups TestRun records for a single (normalized) test name by commit SHA +// and returns them sorted chronologically using the commit order slice. +// runToSHA maps workflow RunID → commit SHA. +// commitOrder lists SHAs from oldest to newest. +// Runs whose SHA is not in commitOrder are skipped (e.g. from force-pushes or unrelated branches). +func buildObservations(testName string, runs []TestRun, commitOrderSlice []string, runToSHA map[int64]string) []CommitObservation { + // Build commit index map: SHA → index + commitIdx := make(map[string]int, len(commitOrderSlice)) + for i, sha := range commitOrderSlice { + commitIdx[sha] = i + } + + // Aggregate pass/fail counts per commit SHA for this test + type counts struct{ passes, fails int } + bySHA := make(map[string]*counts) + + for _, run := range runs { + if run.Skipped { + continue + } + if normalizeTestName(run.Name) != testName { + continue + } + sha, ok := runToSHA[run.RunID] + if !ok || sha == "" { + continue + } + if _, inOrder := commitIdx[sha]; !inOrder { + continue + } + if bySHA[sha] == nil { + bySHA[sha] = &counts{} + } + if run.Failed { + bySHA[sha].fails++ + } else { + bySHA[sha].passes++ + } + } + + // Convert to slice and sort by commit index + obs := make([]CommitObservation, 0, len(bySHA)) + for sha, c := range bySHA { + obs = append(obs, CommitObservation{ + CommitSHA: sha, + CommitIdx: commitIdx[sha], + Prior: 1.0, // uniform prior; adjusted by heuristics if enabled + Passes: c.passes, + Fails: c.fails, + }) + } + sort.Slice(obs, func(i, j int) bool { + return obs[i].CommitIdx < obs[j].CommitIdx + }) + return obs +} + +// runBisect computes posterior probability for each candidate culprit commit. +// obs must be sorted by CommitIdx ascending (chronological). +// The model: there is one transition commit c; before c failure prob is p_before, +// at/after c failure prob is p_after. Both have Beta(1,1) priors. +// Returns results sorted by Probability descending. +// Returns nil if there are fewer than 2 commits with observations (can't localize). +func runBisect(obs []CommitObservation) []BisectResult { + if len(obs) < 2 { + return nil + } + + // Prefix sums of fails and passes in commit order + N := len(obs) + prefixFails := make([]int, N+1) + prefixPasses := make([]int, N+1) + for i, o := range obs { + prefixFails[i+1] = prefixFails[i] + o.Fails + prefixPasses[i+1] = prefixPasses[i] + o.Passes + } + totalFails := prefixFails[N] + totalPasses := prefixPasses[N] + totalObs := totalFails + totalPasses + + if totalObs == 0 { + return nil + } + + // Compute log-posterior for each candidate culprit commit + logWeights := make([]float64, N) + for i, o := range obs { + kBefore := prefixFails[i] + nBefore := prefixPasses[i] + prefixFails[i] + kAfter := totalFails - kBefore + nAfter := totalObs - nBefore + + logWeights[i] = math.Log(o.Prior) + + logBetaBinomial(nBefore, kBefore, 1, 1) + + logBetaBinomial(nAfter, kAfter, 1, 1) + } + + probs := logSumExpNormalize(logWeights) + + results := make([]BisectResult, N) + for i, o := range obs { + results[i] = BisectResult{ + CommitSHA: o.CommitSHA, + CommitIdx: o.CommitIdx, + Probability: probs[i], + FailsBefore: prefixFails[i], + PassesBefore: prefixPasses[i], + FailsAfter: totalFails - prefixFails[i], + PassesAfter: totalPasses - prefixPasses[i], + CommitTitle: o.CommitSHA, // placeholder; overwritten if heuristics fetch metadata + HeuristicNote: "", + } + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Probability > results[j].Probability + }) + return results +} + +// commitPriorWeight returns a prior weight multiplier [0.0, 2.0] for a commit, +// based on its changed files relative to the test name. For now this only reduces +// the prior for commits that only touch test files or docs/config files. +// Weight < 1.0 makes the commit less likely to be the culprit. +// Weight > 1.0 makes it more likely. +func commitPriorWeight(meta CommitMeta, testName string) (weight float64, note string) { + files := meta.Files + if len(files) == 0 { + return 1.0, "" + } + + // Commits only touching non-code paths cannot affect test behavior + if allFilesMatchPrefixes(files, []string{".github/", "docs/", "CODEOWNERS"}) || + allFilesMatchSuffixes(files, []string{".md", ".txt", ".yaml", ".yml"}) { + return 0.05, "only touches docs/.github/config — deprioritized" + } + + // Commits only touching test files for other packages → mild deprioritization + if allFilesMatchSuffixes(files, []string{"_test.go"}) { + return 0.3, "only touches test files" + } + + return 1.0, "" +} + +// allFilesMatchPrefixes returns true if every file in the list starts with one of the given prefixes. +func allFilesMatchPrefixes(files, prefixes []string) bool { + for _, f := range files { + matched := false + for _, p := range prefixes { + if strings.HasPrefix(f, p) { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + +// allFilesMatchSuffixes returns true if every file in the list ends with one of the given suffixes. +func allFilesMatchSuffixes(files, suffixes []string) bool { + for _, f := range files { + matched := false + for _, s := range suffixes { + if strings.HasSuffix(f, s) { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + +// runBisectForTest runs the full bisect pipeline for a single test name. +// commitMetas is a pre-populated, read-only cache of commit metadata (SHA → CommitMeta). +func runBisectForTest(cfg BisectConfig, testName string, allRuns []TestRun, commitOrderSlice []string, runToSHA map[int64]string, commitMetas map[string]CommitMeta) TestBisectReport { + obs := buildObservations(testName, allRuns, commitOrderSlice, runToSHA) + + totalPasses, totalFails := 0, 0 + for _, o := range obs { + totalPasses += o.Passes + totalFails += o.Fails + } + totalObs := totalPasses + totalFails + + // Check signal thresholds + if totalFails < cfg.MinFailures { + return TestBisectReport{ + TestName: testName, + Skipped: true, + } + } + if totalObs < cfg.MinRuns { + return TestBisectReport{ + TestName: testName, + Skipped: true, + } + } + + // Apply heuristics from the pre-fetched commit metadata cache. + for i := range obs { + if meta, ok := commitMetas[obs[i].CommitSHA]; ok { + obs[i].Prior, obs[i].HeuristicNote = commitPriorWeight(meta, testName) + } + } + + results := runBisect(obs) + if results == nil { + return TestBisectReport{ + TestName: testName, + Skipped: true, + } + } + + // Build a SHA → note lookup from the obs slice (already computed above). + obsNotes := make(map[string]string, len(obs)) + for _, o := range obs { + obsNotes[o.CommitSHA] = o.HeuristicNote + } + + // Annotate results with commit metadata + for i := range results { + results[i].HeuristicNote = obsNotes[results[i].CommitSHA] + if meta, ok := commitMetas[results[i].CommitSHA]; ok { + results[i].CommitTitle = meta.Title + results[i].CommitAuthor = meta.Author + if !meta.CommittedAt.IsZero() { + results[i].CommitDate = meta.CommittedAt.Format("2006-01-02") + } + } + } + + // Filter suspects below the minimum probability threshold. + if cfg.MinProbability > 0 { + filtered := results[:0] + for _, r := range results { + if r.Probability >= cfg.MinProbability { + filtered = append(filtered, r) + } + } + results = filtered + } + + // Filter suspects where the failure rate did not increase at the transition commit. + // A decreasing failure rate indicates the commit fixed a flake, not introduced one, + // and is not actionable as a culprit. + { + filtered := results[:0] + for _, r := range results { + nBefore := r.PassesBefore + r.FailsBefore + nAfter := r.PassesAfter + r.FailsAfter + rateBefore := 0.0 + if nBefore > 0 { + rateBefore = float64(r.FailsBefore) / float64(nBefore) + } + rateAfter := 0.0 + if nAfter > 0 { + rateAfter = float64(r.FailsAfter) / float64(nAfter) + } + if rateAfter > rateBefore { + filtered = append(filtered, r) + } + } + results = filtered + } + + if len(results) == 0 { + return TestBisectReport{ + TestName: testName, + TotalObs: totalObs, + Skipped: true, + } + } + + return TestBisectReport{ + TestName: testName, + TopSuspects: results, + TotalObs: totalObs, + } +} + +// selectTopFlakyTests selects the top-N flakiest tests that meet minimum signal thresholds. +// allRuns is the full set of test runs. Returns normalized test names sorted by failure rate desc. +func selectTopFlakyTests(allRuns []TestRun, cfg BisectConfig) []string { + type testStats struct { + fails int + total int + } + stats := make(map[string]*testStats) + for _, run := range allRuns { + if run.Skipped { + continue + } + name := normalizeTestName(run.Name) + if stats[name] == nil { + stats[name] = &testStats{} + } + stats[name].total++ + if run.Failed { + stats[name].fails++ + } + } + + type ranked struct { + name string + rate float64 + fails int + } + var candidates []ranked + for name, s := range stats { + if s.fails < cfg.MinFailures || s.total < cfg.MinRuns { + continue + } + candidates = append(candidates, ranked{ + name: name, + rate: float64(s.fails) / float64(s.total), + fails: s.fails, + }) + } + + sort.Slice(candidates, func(i, j int) bool { + if candidates[i].rate != candidates[j].rate { + return candidates[i].rate > candidates[j].rate + } + return candidates[i].fails > candidates[j].fails + }) + + n := len(candidates) + if cfg.TopN > 0 && cfg.TopN < n { + n = cfg.TopN + } + + names := make([]string, n) + for i := range names { + names[i] = candidates[i].name + } + return names +} + +// runBisectAnalysis is the top-level bisect orchestrator called from the generate command. +// It runs bisect for the top-N flakiest tests and returns their reports. +func runBisectAnalysis(ctx context.Context, cfg BisectConfig, allRuns []TestRun, workflowRuns []WorkflowRun) ([]TestBisectReport, error) { + // Build runID → SHA map from workflow runs + runToSHA := make(map[int64]string, len(workflowRuns)) + var oldestSHA string + var oldestTime time.Time + for _, wr := range workflowRuns { + if wr.HeadSHA == "" { + continue + } + runToSHA[wr.ID] = wr.HeadSHA + if oldestTime.IsZero() || wr.CreatedAt.Before(oldestTime) { + oldestTime = wr.CreatedAt + oldestSHA = wr.HeadSHA + } + } + + if oldestSHA == "" { + return nil, errors.New("no workflow runs with commit SHAs found") + } + + // Get commit ordering from git log + fmt.Printf("Building commit order from %s..HEAD\n", oldestSHA[:7]) + commitOrderSlice, err := commitOrder(ctx, oldestSHA) + if err != nil { + return nil, fmt.Errorf("failed to get commit order: %w", err) + } + if len(commitOrderSlice) == 0 { + return nil, fmt.Errorf("commit order is empty for anchor %s: SHA may not be an ancestor of HEAD (force-push or unrelated branch?)", oldestSHA[:7]) + } + fmt.Printf("Commit range: %d commits\n", len(commitOrderSlice)) + + // Select qualifying flaky tests + targetTests := selectTopFlakyTests(allRuns, cfg) + if cfg.TopN > 0 { + fmt.Printf("Selected %d tests for bisect analysis (top %d, min %d failures, min %d runs)\n", + len(targetTests), cfg.TopN, cfg.MinFailures, cfg.MinRuns) + } else { + fmt.Printf("Selected %d tests for bisect analysis (all qualifying, min %d failures, min %d runs)\n", + len(targetTests), cfg.MinFailures, cfg.MinRuns) + } + + // Pre-fetch commit metadata for all unique SHAs that appear in the run set. + // Fetching once here (deduplicated, in parallel) avoids O(tests × commits) serial + // subprocess calls — the dominant cost of the bisect pipeline. + uniqueSHAs := make([]string, 0, len(runToSHA)) + seenSHA := make(map[string]struct{}, len(runToSHA)) + for _, sha := range runToSHA { + if _, ok := seenSHA[sha]; !ok { + seenSHA[sha] = struct{}{} + uniqueSHAs = append(uniqueSHAs, sha) + } + } + commitMetas := fetchCommitMetasParallel(ctx, cfg.Repo, uniqueSHAs, defaultConcurrency) + fmt.Printf("Fetched metadata for %d/%d unique commits\n", len(commitMetas), len(uniqueSHAs)) + + // Run bisect for each test + reports := make([]TestBisectReport, 0, len(targetTests)) + for _, testName := range targetTests { + start := time.Now() + report := runBisectForTest(cfg, testName, allRuns, commitOrderSlice, runToSHA, commitMetas) + fmt.Printf(" Bisected %s in %s (skipped=%v)\n", testName, time.Since(start).Round(time.Millisecond), report.Skipped) + reports = append(reports, report) + } + + return reports, nil +} + +// fetchCommitMetasParallel fetches commit metadata for a list of SHAs concurrently, +// bounded by maxConcurrency. Returns an immutable map of SHA → CommitMeta; failed +// fetches are logged and omitted (heuristic priors fall back to 1.0). +func fetchCommitMetasParallel(ctx context.Context, repo string, shas []string, maxConcurrency int) map[string]CommitMeta { + metas := make([]CommitMeta, len(shas)) + ok := make([]bool, len(shas)) + + g, ctx := errgroup.WithContext(ctx) + g.SetLimit(maxConcurrency) + for i, sha := range shas { + g.Go(func() error { + meta, err := fetchCommitMeta(ctx, repo, sha) + if err != nil { + fmt.Printf("Warning: failed to fetch metadata for commit %s: %v\n", sha[:7], err) + return nil + } + metas[i] = meta + ok[i] = true + return nil + }) + } + _ = g.Wait() + + commitMetas := make(map[string]CommitMeta, len(shas)) + for i, sha := range shas { + if ok[i] { + commitMetas[sha] = metas[i] + } + } + return commitMetas +} diff --git a/tools/flakereport/bisect_test.go b/tools/flakereport/bisect_test.go new file mode 100644 index 00000000000..eb307fa8255 --- /dev/null +++ b/tools/flakereport/bisect_test.go @@ -0,0 +1,568 @@ +package flakereport + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLogBetaBinomial(t *testing.T) { + t.Run("symmetry: k=0 equals k=n", func(t *testing.T) { + // logBetaBinomial(n, 0) == logBetaBinomial(n, n) by symmetry of Beta(1,1) + v0 := logBetaBinomial(10, 0, 1, 1) + v10 := logBetaBinomial(10, 10, 1, 1) + assert.InDelta(t, v0, v10, 1e-10, "logBetaBinomial(10,0,1,1) should equal logBetaBinomial(10,10,1,1)") + }) + + t.Run("extreme k is more likely than middle k under Beta(1,1)", func(t *testing.T) { + // Under a Beta(1,1) (uniform) prior, the marginal likelihood favors extreme k. + // Observing all failures (k=n=10) or all passes (k=0) is MORE likely than k=5, + // because the posterior can concentrate near p=1 or p=0 to explain the data. + vMid := logBetaBinomial(10, 5, 1, 1) + v0 := logBetaBinomial(10, 0, 1, 1) + assert.Greater(t, v0, vMid, "logBetaBinomial(10,0,1,1) should exceed logBetaBinomial(10,5,1,1) under uniform prior") + }) + + t.Run("n=0 k=0 is valid", func(t *testing.T) { + // No observations: should return a finite value (0 with Beta-Binomial normalization) + v := logBetaBinomial(0, 0, 1, 1) + assert.False(t, math.IsNaN(v), "logBetaBinomial(0,0,1,1) should not be NaN") + assert.False(t, math.IsInf(v, 0), "logBetaBinomial(0,0,1,1) should not be Inf") + }) + + t.Run("single failure in 1 trial", func(t *testing.T) { + v := logBetaBinomial(1, 1, 1, 1) + assert.False(t, math.IsNaN(v), "logBetaBinomial(1,1,1,1) should not be NaN") + assert.False(t, math.IsInf(v, 0), "logBetaBinomial(1,1,1,1) should not be Inf") + }) +} + +func TestLogSumExpNormalize(t *testing.T) { + t.Run("empty input returns nil", func(t *testing.T) { + result := logSumExpNormalize(nil) + assert.Nil(t, result) + }) + + t.Run("single element returns 1.0", func(t *testing.T) { + result := logSumExpNormalize([]float64{-5.0}) + require.Len(t, result, 1) + assert.InDelta(t, 1.0, result[0], 1e-10) + }) + + t.Run("uniform log-weights produce equal probabilities", func(t *testing.T) { + logWeights := []float64{-3.0, -3.0, -3.0, -3.0} + result := logSumExpNormalize(logWeights) + require.Len(t, result, 4) + for _, p := range result { + assert.InDelta(t, 0.25, p, 1e-10) + } + }) + + t.Run("probabilities sum to 1.0", func(t *testing.T) { + logWeights := []float64{-10.0, -5.0, -1.0, -3.0, -7.0} + result := logSumExpNormalize(logWeights) + require.Len(t, result, 5) + sum := 0.0 + for _, p := range result { + sum += p + } + assert.InDelta(t, 1.0, sum, 1e-10) + }) + + t.Run("highest log-weight gets highest probability", func(t *testing.T) { + logWeights := []float64{-10.0, -5.0, -1.0, -3.0} + result := logSumExpNormalize(logWeights) + require.Len(t, result, 4) + // index 2 has the highest log-weight (-1.0) + maxIdx := 0 + for i, p := range result { + if p > result[maxIdx] { + maxIdx = i + } + } + assert.Equal(t, 2, maxIdx) + }) + + t.Run("handles very large negative values without underflow", func(t *testing.T) { + logWeights := []float64{-1000.0, -999.0, -998.0} + result := logSumExpNormalize(logWeights) + require.Len(t, result, 3) + sum := 0.0 + for _, p := range result { + assert.False(t, math.IsNaN(p), "probability should not be NaN") + assert.False(t, math.IsInf(p, 0), "probability should not be Inf") + assert.GreaterOrEqual(t, p, 0.0, "probability should be non-negative") + sum += p + } + assert.InDelta(t, 1.0, sum, 1e-10) + }) +} + +func TestRunBisect(t *testing.T) { + t.Run("returns nil with fewer than 2 observations", func(t *testing.T) { + obs := []CommitObservation{ + {CommitSHA: "abc", CommitIdx: 0, Prior: 1.0, Passes: 10, Fails: 2}, + } + result := runBisect(obs) + assert.Nil(t, result) + }) + + t.Run("returns nil with zero total observations", func(t *testing.T) { + obs := []CommitObservation{ + {CommitSHA: "abc", CommitIdx: 0, Prior: 1.0, Passes: 0, Fails: 0}, + {CommitSHA: "def", CommitIdx: 1, Prior: 1.0, Passes: 0, Fails: 0}, + } + result := runBisect(obs) + assert.Nil(t, result) + }) + + t.Run("sharp signal: transition at commit index 3", func(t *testing.T) { + // First 3 commits: 0 fails out of 30 each + // Last 3 commits: 10 fails out of 30 each (1/3 failure rate) + // The 4th commit (index 3) should have the highest posterior probability + obs := []CommitObservation{ + {CommitSHA: "sha0", CommitIdx: 0, Prior: 1.0, Passes: 30, Fails: 0}, + {CommitSHA: "sha1", CommitIdx: 1, Prior: 1.0, Passes: 30, Fails: 0}, + {CommitSHA: "sha2", CommitIdx: 2, Prior: 1.0, Passes: 30, Fails: 0}, + {CommitSHA: "sha3", CommitIdx: 3, Prior: 1.0, Passes: 20, Fails: 10}, + {CommitSHA: "sha4", CommitIdx: 4, Prior: 1.0, Passes: 20, Fails: 10}, + {CommitSHA: "sha5", CommitIdx: 5, Prior: 1.0, Passes: 20, Fails: 10}, + } + results := runBisect(obs) + require.NotNil(t, results) + require.Len(t, results, 6) + + // Probabilities should sum to 1.0 + sum := 0.0 + for _, r := range results { + sum += r.Probability + } + assert.InDelta(t, 1.0, sum, 1e-10) + + // The top result should be sha3 (index 3) — the transition point + require.Equal(t, "sha3", results[0].CommitSHA, "sha3 should be the top suspect") + require.Greater(t, results[0].Probability, 0.5, "top suspect should have >50% probability") + + // Results should be sorted by probability descending + for i := 1; i < len(results); i++ { + assert.LessOrEqual(t, results[i].Probability, results[i-1].Probability, + "results should be sorted by probability descending") + } + }) + + t.Run("counts are correctly propagated", func(t *testing.T) { + obs := []CommitObservation{ + {CommitSHA: "sha0", CommitIdx: 0, Prior: 1.0, Passes: 10, Fails: 0}, + {CommitSHA: "sha1", CommitIdx: 1, Prior: 1.0, Passes: 5, Fails: 5}, + } + results := runBisect(obs) + require.NotNil(t, results) + + // Find sha1 result (should be the top suspect since all failures are after it) + var sha1Result *BisectResult + for i := range results { + if results[i].CommitSHA == "sha1" { + sha1Result = &results[i] + break + } + } + require.NotNil(t, sha1Result) + // sha1 is the transition: before sha1 = 10 passes, 0 fails; after/at sha1 = 5 passes, 5 fails + assert.Equal(t, 10, sha1Result.PassesBefore) + assert.Equal(t, 0, sha1Result.FailsBefore) + assert.Equal(t, 5, sha1Result.PassesAfter) + assert.Equal(t, 5, sha1Result.FailsAfter) + }) + + t.Run("two commits uniform signal returns valid probabilities", func(t *testing.T) { + obs := []CommitObservation{ + {CommitSHA: "sha0", CommitIdx: 0, Prior: 1.0, Passes: 5, Fails: 5}, + {CommitSHA: "sha1", CommitIdx: 1, Prior: 1.0, Passes: 5, Fails: 5}, + } + results := runBisect(obs) + require.NotNil(t, results) + require.Len(t, results, 2) + sum := results[0].Probability + results[1].Probability + assert.InDelta(t, 1.0, sum, 1e-10) + }) +} + +func TestRunBisectInformationlessData(t *testing.T) { + t.Run("zero observations across many commits returns nil", func(t *testing.T) { + // With no observations at any commit, there is no data to localize a transition. + obs := []CommitObservation{ + {CommitSHA: "sha0", CommitIdx: 0, Prior: 1.0}, + {CommitSHA: "sha1", CommitIdx: 1, Prior: 1.0}, + {CommitSHA: "sha2", CommitIdx: 2, Prior: 1.0}, + {CommitSHA: "sha3", CommitIdx: 3, Prior: 1.0}, + {CommitSHA: "sha4", CommitIdx: 4, Prior: 1.0}, + } + result := runBisect(obs) + assert.Nil(t, result) + }) + + t.Run("balanced data (equal failures and successes per commit) yields symmetric posterior", func(t *testing.T) { + // Each commit has the same number of failures and successes (50% failure rate + // throughout), so there is no directional signal about where a transition occurred. + // + // The Beta-Binomial model does NOT produce posterior == prior in this case: placing + // all observations on one side of the transition is more parsimonious (one marginal + // distribution instead of two), so the model assigns slightly higher likelihood to + // extreme transition points. However, the posterior IS symmetric around the midpoint: + // prob[i] == prob[N-1-i] when priors are uniform and per-commit observations are + // identical. + obs := []CommitObservation{ + {CommitSHA: "sha0", CommitIdx: 0, Prior: 1.0, Passes: 5, Fails: 5}, + {CommitSHA: "sha1", CommitIdx: 1, Prior: 1.0, Passes: 5, Fails: 5}, + {CommitSHA: "sha2", CommitIdx: 2, Prior: 1.0, Passes: 5, Fails: 5}, + {CommitSHA: "sha3", CommitIdx: 3, Prior: 1.0, Passes: 5, Fails: 5}, + {CommitSHA: "sha4", CommitIdx: 4, Prior: 1.0, Passes: 5, Fails: 5}, + } + results := runBisect(obs) + require.NotNil(t, results) + require.Len(t, results, 5) + + // Probabilities must be non-negative and sum to 1.0. + sum := 0.0 + for _, r := range results { + assert.GreaterOrEqual(t, r.Probability, 0.0) + sum += r.Probability + } + assert.InDelta(t, 1.0, sum, 1e-10) + + // With uniform priors and identical per-commit observations the distribution has + // a specific symmetry: the weight for transition at index i depends only on the + // total observations before/after the split. Because the "before" segment for + // index i mirrors the "after" segment for index N-1-i+1, the pairs (sha1, sha4) + // and (sha2, sha3) must have equal probability. sha0 is unpaired: its "before" + // segment is empty while no index has an empty "after" segment. + // + // Note: sha0 will have a higher probability than the others because placing all + // observations on one side of the transition is more parsimonious for the + // Beta-Binomial model than splitting them between two equal-rate distributions. + // The balanced data does not produce a diffuse uniform posterior. + probBySHA := make(map[string]float64, len(results)) + for _, r := range results { + probBySHA[r.CommitSHA] = r.Probability + } + // No commit should dominate with balanced data (>50% would imply a spurious signal). + for sha, p := range probBySHA { + assert.Less(t, p, 0.5, "no single commit should dominate with balanced data (sha=%s)", sha) + } + // Paired commits must have equal probability. + assert.InDelta(t, probBySHA["sha1"], probBySHA["sha4"], 1e-10, + "symmetric pair: sha1 and sha4 should have equal probability") + assert.InDelta(t, probBySHA["sha2"], probBySHA["sha3"], 1e-10, + "symmetric pair: sha2 and sha3 should have equal probability") + }) +} + +func TestBuildObservations(t *testing.T) { + commitOrderSlice := []string{"sha-a", "sha-b", "sha-c", "sha-d"} + runToSHA := map[int64]string{ + 100: "sha-a", + 200: "sha-b", + 300: "sha-c", + 400: "sha-d", + 500: "sha-unknown", // not in commitOrder + } + + runs := []TestRun{ + // sha-a: 3 passes, 1 fail for "TestFoo" + {Name: "TestFoo", Failed: false, RunID: 100}, + {Name: "TestFoo", Failed: false, RunID: 100}, + {Name: "TestFoo", Failed: false, RunID: 100}, + {Name: "TestFoo", Failed: true, RunID: 100}, + // sha-b: 2 passes for "TestFoo" + {Name: "TestFoo", Failed: false, RunID: 200}, + {Name: "TestFoo", Failed: false, RunID: 200}, + // sha-c: 1 pass, 2 fails for "TestFoo" + {Name: "TestFoo", Failed: false, RunID: 300}, + {Name: "TestFoo", Failed: true, RunID: 300}, + {Name: "TestFoo", Failed: true, RunID: 300}, + // sha-d: 1 fail for "TestFoo" with retry suffix (should normalize to "TestFoo") + {Name: "TestFoo (retry 1)", Failed: true, RunID: 400}, + // sha-unknown: should be skipped (not in commitOrder) + {Name: "TestFoo", Failed: false, RunID: 500}, + // different test name: should be excluded + {Name: "TestBar", Failed: true, RunID: 100}, + // skipped run: should be excluded + {Name: "TestFoo", Failed: false, Skipped: true, RunID: 100}, + // no SHA mapping: should be excluded + {Name: "TestFoo", Failed: false, RunID: 999}, + } + + obs := buildObservations("TestFoo", runs, commitOrderSlice, runToSHA) + + require.Len(t, obs, 4, "should have one observation per commit with data") + + // Verify chronological ordering + for i := 1; i < len(obs); i++ { + assert.Less(t, obs[i-1].CommitIdx, obs[i].CommitIdx, + "observations should be sorted by commit index") + } + + // Find each commit's observation + byShA := make(map[string]CommitObservation) + for _, o := range obs { + byShA[o.CommitSHA] = o + } + + // sha-a: 3 passes, 1 fail + require.Contains(t, byShA, "sha-a") + assert.Equal(t, 3, byShA["sha-a"].Passes) + assert.Equal(t, 1, byShA["sha-a"].Fails) + assert.Equal(t, 0, byShA["sha-a"].CommitIdx) + + // sha-b: 2 passes, 0 fails + require.Contains(t, byShA, "sha-b") + assert.Equal(t, 2, byShA["sha-b"].Passes) + assert.Equal(t, 0, byShA["sha-b"].Fails) + assert.Equal(t, 1, byShA["sha-b"].CommitIdx) + + // sha-c: 1 pass, 2 fails + require.Contains(t, byShA, "sha-c") + assert.Equal(t, 1, byShA["sha-c"].Passes) + assert.Equal(t, 2, byShA["sha-c"].Fails) + assert.Equal(t, 2, byShA["sha-c"].CommitIdx) + + // sha-d: 0 passes, 1 fail (the retry suffix is normalized to "TestFoo") + require.Contains(t, byShA, "sha-d") + assert.Equal(t, 0, byShA["sha-d"].Passes) + assert.Equal(t, 1, byShA["sha-d"].Fails) + assert.Equal(t, 3, byShA["sha-d"].CommitIdx) + + // sha-unknown should not appear + assert.NotContains(t, byShA, "sha-unknown") + + // Prior should default to 1.0 + for _, o := range obs { + assert.InDelta(t, 1.0, o.Prior, 1e-10) + } +} + +func TestSelectTopFlakyTests(t *testing.T) { + t.Run("excludes tests below MinFailures threshold", func(t *testing.T) { + runs := makeRunsForTest("TestLowFails", 100, 2) // 2 failures: below minBisectFailures=5 + cfg := BisectConfig{TopN: 10, MinFailures: 5, MinRuns: 10} + result := selectTopFlakyTests(runs, cfg) + assert.NotContains(t, result, "TestLowFails") + }) + + t.Run("excludes tests below MinRuns threshold", func(t *testing.T) { + runs := makeRunsForTest("TestFewRuns", 10, 6) // only 10 total runs: below minBisectRuns=30 + cfg := BisectConfig{TopN: 10, MinFailures: 5, MinRuns: 30} + result := selectTopFlakyTests(runs, cfg) + assert.NotContains(t, result, "TestFewRuns") + }) + + t.Run("ranks by failure rate descending", func(t *testing.T) { + // TestHighRate: 20/40 = 50% failure rate + // TestLowRate: 10/40 = 25% failure rate + var runs []TestRun + runs = append(runs, makeRunsForTest("TestHighRate", 40, 20)...) + runs = append(runs, makeRunsForTest("TestLowRate", 40, 10)...) + cfg := BisectConfig{TopN: 10, MinFailures: 5, MinRuns: 30} + result := selectTopFlakyTests(runs, cfg) + require.GreaterOrEqual(t, len(result), 2) + assert.Equal(t, "TestHighRate", result[0]) + assert.Equal(t, "TestLowRate", result[1]) + }) + + t.Run("limits to TopN results", func(t *testing.T) { + var runs []TestRun + for i := range 5 { + runs = append(runs, makeRunsForTest("TestFlaky"+string(rune('A'+i)), 40, 10)...) + } + cfg := BisectConfig{TopN: 3, MinFailures: 5, MinRuns: 30} + result := selectTopFlakyTests(runs, cfg) + assert.Len(t, result, 3) + }) + + t.Run("skips skipped runs", func(t *testing.T) { + runs := []TestRun{ + {Name: "TestSkipped", Skipped: true, Failed: false}, + {Name: "TestSkipped", Skipped: true, Failed: true}, + } + cfg := BisectConfig{TopN: 10, MinFailures: 1, MinRuns: 1} + result := selectTopFlakyTests(runs, cfg) + assert.NotContains(t, result, "TestSkipped") + }) + + t.Run("normalizes test names", func(t *testing.T) { + // Retry suffix should be stripped; all runs collapse to one test + var runs []TestRun + for range 30 { + runs = append(runs, TestRun{Name: "TestFlaky", Failed: false}) + } + for range 8 { + runs = append(runs, TestRun{Name: "TestFlaky (retry 1)", Failed: true}) + } + cfg := BisectConfig{TopN: 10, MinFailures: 5, MinRuns: 30} + result := selectTopFlakyTests(runs, cfg) + require.Len(t, result, 1) + assert.Equal(t, "TestFlaky", result[0]) + }) +} + +func TestCommitPriorWeight(t *testing.T) { + t.Run("docs-only commit is deprioritized", func(t *testing.T) { + meta := CommitMeta{ + SHA: "abc", + Files: []string{".github/workflows/ci.yml", "docs/architecture.md"}, + } + weight, note := commitPriorWeight(meta, "TestWorkflowSuite/TestRun") + assert.InDelta(t, 0.05, weight, 1e-10) + assert.NotEmpty(t, note) + }) + + t.Run("markdown-only commit is deprioritized", func(t *testing.T) { + meta := CommitMeta{ + SHA: "abc", + Files: []string{"README.md", "CHANGELOG.md"}, + } + weight, note := commitPriorWeight(meta, "TestWorkflowSuite/TestRun") + assert.InDelta(t, 0.05, weight, 1e-10) + assert.NotEmpty(t, note) + }) + + t.Run("service/history/ commit is neutral weight", func(t *testing.T) { + meta := CommitMeta{ + SHA: "abc", + Files: []string{"service/history/workflow_executor.go", "service/history/mutable_state.go"}, + } + weight, note := commitPriorWeight(meta, "TestWorkflowSuite/TestContinueAsNew") + assert.InDelta(t, 1.0, weight, 1e-10) + assert.Empty(t, note) + }) + + t.Run("test-only commit is mildly deprioritized", func(t *testing.T) { + meta := CommitMeta{ + SHA: "abc", + Files: []string{"service/frontend/nexus_handler_test.go", "service/worker/deployment_test.go"}, + } + weight, note := commitPriorWeight(meta, "TestWorkflowSuite/TestRun") + assert.InDelta(t, 0.3, weight, 1e-10) + assert.NotEmpty(t, note) + }) + + t.Run("mixed code and doc files is neutral", func(t *testing.T) { + meta := CommitMeta{ + SHA: "abc", + Files: []string{"service/frontend/handler.go", "README.md"}, + } + weight, note := commitPriorWeight(meta, "TestWorkflowSuite/TestRun") + // Not all files are docs/config (handler.go is not), not all are test files + // and "service/history/" is not a prefix of service/frontend/ + assert.InDelta(t, 1.0, weight, 1e-10) + assert.Empty(t, note) + }) + + t.Run("empty files list returns neutral weight", func(t *testing.T) { + meta := CommitMeta{SHA: "abc", Files: nil} + weight, note := commitPriorWeight(meta, "TestWorkflowSuite/TestRun") + assert.InDelta(t, 1.0, weight, 1e-10) + assert.Empty(t, note) + }) + + t.Run("service/frontend/ commit is neutral weight", func(t *testing.T) { + meta := CommitMeta{ + SHA: "abc", + Files: []string{"service/frontend/nexus_handler.go"}, + } + weight, note := commitPriorWeight(meta, "TestNexusSuite/TestOperation") + assert.InDelta(t, 1.0, weight, 1e-10) + assert.Empty(t, note) + }) +} + +func TestRunBisectForTestDirectionFilter(t *testing.T) { + // Build a scenario where the failure rate DECREASES at a commit (the flake was fixed, + // not introduced). The direction filter must suppress this suspect so it does not appear + // as a culprit in the report. + commitOrderSlice := []string{"sha0", "sha1", "sha2", "sha3", "sha4", "sha5"} + runToSHA := map[int64]string{0: "sha0", 1: "sha1", 2: "sha2", 3: "sha3", 4: "sha4", 5: "sha5"} + + var allRuns []TestRun + // sha0-sha4: 8 failures + 2 passes each (80% failure rate) + for shaIdx := range 5 { + runID := int64(shaIdx) + for range 8 { + allRuns = append(allRuns, TestRun{Name: "TestFoo", Failed: true, RunID: runID}) + } + for range 2 { + allRuns = append(allRuns, TestRun{Name: "TestFoo", Failed: false, RunID: runID}) + } + } + // sha5: 10 passes, 0 fails — the flake disappeared here + for range 10 { + allRuns = append(allRuns, TestRun{Name: "TestFoo", Failed: false, RunID: 5}) + } + + cfg := BisectConfig{ + MinFailures: 5, + MinRuns: 30, + MinProbability: 0.5, + } + + report := runBisectForTest(cfg, "TestFoo", allRuns, commitOrderSlice, runToSHA, nil) + + // The only high-probability transition point (sha5, where rate dropped 80%→0%) should be + // filtered out by the direction filter, leaving no actionable suspects. + assert.True(t, report.Skipped, "report should be skipped: the only transition is a fix, not an introduction") + assert.Empty(t, report.TopSuspects) +} + +func TestRunBisectForTestDirectionFilterKeepsIntroduction(t *testing.T) { + // Complementary: failure rate INCREASES at sha3 (flake introduced). The direction filter + // must retain this suspect. + commitOrderSlice := []string{"sha0", "sha1", "sha2", "sha3", "sha4", "sha5"} + runToSHA := map[int64]string{0: "sha0", 1: "sha1", 2: "sha2", 3: "sha3", 4: "sha4", 5: "sha5"} + + var allRuns []TestRun + // sha0-sha2: clean (0% failure rate) + for shaIdx := range 3 { + runID := int64(shaIdx) + for range 10 { + allRuns = append(allRuns, TestRun{Name: "TestFoo", Failed: false, RunID: runID}) + } + } + // sha3-sha5: 8 failures + 2 passes each (80% failure rate — flake introduced at sha3) + for shaIdx := 3; shaIdx < 6; shaIdx++ { + runID := int64(shaIdx) + for range 8 { + allRuns = append(allRuns, TestRun{Name: "TestFoo", Failed: true, RunID: runID}) + } + for range 2 { + allRuns = append(allRuns, TestRun{Name: "TestFoo", Failed: false, RunID: runID}) + } + } + + cfg := BisectConfig{ + MinFailures: 5, + MinRuns: 30, + MinProbability: 0.5, + } + + report := runBisectForTest(cfg, "TestFoo", allRuns, commitOrderSlice, runToSHA, nil) + + require.False(t, report.Skipped, "report should not be skipped: a real introduction exists") + require.NotEmpty(t, report.TopSuspects) + assert.Equal(t, "sha3", report.TopSuspects[0].CommitSHA, "sha3 should be the top suspect") + assert.Greater(t, report.TopSuspects[0].Probability, 0.5) +} + +// makeRunsForTest creates a slice of TestRun with the given total count and failure count. +// The first `failures` runs are marked as failed, the rest as passed. +func makeRunsForTest(name string, total, failures int) []TestRun { + runs := make([]TestRun, total) + for i := range runs { + runs[i] = TestRun{ + Name: name, + Failed: i < failures, + } + } + return runs +} diff --git a/tools/flakereport/doc.go b/tools/flakereport/doc.go new file mode 100644 index 00000000000..0c9de1b7135 --- /dev/null +++ b/tools/flakereport/doc.go @@ -0,0 +1,76 @@ +// Package flakereport implements Bayesian commit bisection for identifying which commit +// most likely introduced a flaky test in a CI system. +// +// # Problem +// +// A flaky test is one whose failure probability changes at some point in the commit +// history — typically because a code change introduced a race condition, timing +// sensitivity, or environmental dependency. Given a rolling window of CI runs (each +// associated with the HEAD commit at the time), we want to rank candidate commits by +// their posterior probability of being the "transition commit" that caused the flakiness. +// +// # Data +// +// The raw input is a set of test runs downloaded from GitHub Actions artifacts. Each run +// records whether a specific test passed or failed, and is tagged with the workflow RunID. +// RunIDs are mapped to commit SHAs via the GitHub Actions API (WorkflowRun.HeadSHA). +// Runs are then grouped into CommitObservation records: for each (test, commit SHA) pair +// we count the number of passing and failing runs. +// +// # Model +// +// The inference model assumes exactly one transition commit c in the history: +// +// - Before commit c: the test has a background failure probability p_before. +// - At commit c and all later commits: the test has an elevated failure probability p_after. +// +// Neither p_before nor p_after is known, so both are assigned independent uniform (Beta(1,1)) +// priors. The model treats them as nuisance parameters and marginalizes them out, yielding +// the Beta-Binomial marginal likelihood: +// +// P(data | transition at c) = BetaBinomial(n_before, k_before; 1, 1) +// × BetaBinomial(n_after, k_after; 1, 1) +// +// where n_before / k_before are the total runs / failures before commit c, and n_after / +// k_after are the total runs / failures at and after commit c. The closed form is: +// +// BetaBinomial(n, k; α, β) = Beta(k+α, n-k+β) / Beta(α, β) +// +// All arithmetic is performed in log-space to avoid floating-point underflow, with the +// log-sum-exp trick applied during normalization. +// +// # Commit Priors +// +// The prior probability that a given commit is the transition commit is not uniform across +// all commits. Commits that only touch documentation, CI configuration, or test files +// cannot plausibly affect production code behaviour, so they receive a reduced prior weight +// (down to 0.05×). Commits that touch source code receive the default weight of 1.0. +// These heuristic weights are fetched from the GitHub API in parallel and applied before +// running the inference. +// +// # Inference Algorithm +// +// For N commits with observations, the algorithm runs in O(N) time using prefix sums: +// +// 1. Compute prefix sums of failures and passes across the commit sequence. +// 2. For each candidate transition commit i, use the prefix sums to split the data into +// "before" and "after" segments and evaluate the log marginal likelihood. +// 3. Add the log prior weight to each log-likelihood. +// 4. Normalize via log-sum-exp to obtain posterior probabilities that sum to 1. +// +// # Output +// +// Commits are ranked by posterior probability. Only commits above a configurable minimum +// probability threshold (default 0.50) are reported. Results are surfaced in the GitHub +// Actions step summary, a markdown report artifact, and optionally a Slack message +// listing the "hottest" commits (those with the highest aggregate posterior probability +// across all analyzed tests). +// +// # Limitations +// +// The model assumes a single transition in the observed window. If multiple commits each +// contributed independently to flakiness, or if the failure rate oscillates, the model +// may identify the wrong commit or produce a diffuse posterior with no strong suspect. +// The model also requires a minimum of 5 failures and 30 total runs before it will +// attempt bisection on a test, to avoid over-fitting noisy data. +package flakereport diff --git a/tools/flakereport/flakereport.go b/tools/flakereport/flakereport.go new file mode 100644 index 00000000000..4259d589534 --- /dev/null +++ b/tools/flakereport/flakereport.go @@ -0,0 +1,398 @@ +package flakereport + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/urfave/cli/v2" + "go.temporal.io/server/common/headers" +) + +const ( + minFlakyFailures = 3 + defaultMaxLinks = 3 + defaultLookbackDays = 7 + defaultWorkflowID = 80591745 + defaultRepository = "temporalio/temporal" + defaultBranch = "main" + defaultConcurrency = 20 + slackMaxListItems = 10 +) + +// NewCliApp instantiates a new instance of the CLI application +func NewCliApp() *cli.App { + app := cli.NewApp() + app.Name = "flakesreport" + app.Usage = "Generate flaky test reports" + app.Version = headers.ServerVersion + + app.Commands = []*cli.Command{ + { + Name: "generate", + Usage: "Generate flaky test reports from GitHub Actions", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repository", + Value: defaultRepository, + Usage: "GitHub repository (owner/repo)", + }, + &cli.StringFlag{ + Name: "branch", + Value: defaultBranch, + Usage: "Git branch to analyze", + }, + &cli.IntFlag{ + Name: "days", + Value: defaultLookbackDays, + Usage: "Number of days to look back", + }, + &cli.Int64Flag{ + Name: "workflow-id", + Value: defaultWorkflowID, + Usage: "GitHub Actions workflow ID", + }, + &cli.IntFlag{ + Name: "max-links", + Value: defaultMaxLinks, + Usage: "Maximum failure links per test", + }, + &cli.IntFlag{ + Name: "concurrency", + Value: defaultConcurrency, + Usage: "Number of parallel workers for artifact processing", + }, + &cli.StringFlag{ + Name: "output-dir", + Value: "out", + Usage: "Output directory for report files", + }, + &cli.StringFlag{ + Name: "slack-webhook", + Usage: "Slack webhook URL for notifications", + EnvVars: []string{"SLACK_WEBHOOK"}, + }, + &cli.StringFlag{ + Name: "run-id", + Usage: "GitHub Actions run ID (for links)", + }, + &cli.StringFlag{ + Name: "ref-name", + Usage: "Git ref name (for failure messages)", + }, + &cli.StringFlag{ + Name: "sha", + Usage: "Git commit SHA (for failure messages)", + }, + &cli.BoolFlag{ + Name: "bisect", + Usage: "Run Bayesian commit bisect on all qualifying flaky tests after generating the report", + }, + &cli.IntFlag{ + Name: "bisect-top-n", + Value: 0, + Usage: "Limit bisect to the N flakiest qualifying tests; 0 = all qualifying tests", + }, + &cli.IntFlag{ + Name: "bisect-days", + Value: 28, + Usage: "Lookback window in days for bisect artifact data (independent of --days)", + }, + &cli.Float64Flag{ + Name: "bisect-min-probability", + Value: 0.50, + Usage: "Only report tests where the top suspect commit has at least this probability (0–1)", + }, + }, + Action: runGenerateCommand, + }, + } + + return app +} + +// fetchAndAnalyzeWorkflowRuns fetches workflow runs between since and until and counts successes. +// until is zero for an open-ended (up to now) window. +func fetchAndAnalyzeWorkflowRuns(ctx context.Context, repo string, workflowID int64, branch string, since, until time.Time) ([]WorkflowRun, int, error) { + fmt.Println("\n=== Fetching workflow runs ===") + runs, err := fetchWorkflowRuns(ctx, repo, workflowID, branch, since, until) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch workflow runs: %w", err) + } + + if len(runs) == 0 { + fmt.Println("No workflow runs found in the specified time range") + return nil, 0, nil + } + + // Count successful runs + successfulRuns := 0 + for _, run := range runs { + if run.Conclusion == "success" { + successfulRuns++ + } + } + fmt.Printf("Workflow runs: %d total, %d successful (%.1f%% success rate)\n", + len(runs), successfulRuns, float64(successfulRuns)/float64(len(runs))*100.0) + + return runs, successfulRuns, nil +} + +// collectArtifactJobs collects all artifact jobs from workflow runs +func collectArtifactJobs(ctx context.Context, repo string, runs []WorkflowRun, tempDir string) ([]ArtifactJob, error) { + fmt.Println("\n=== Collecting artifacts ===") + var jobs []ArtifactJob + totalArtifacts := 0 + + for i, run := range runs { + artifacts, err := fetchRunArtifacts(ctx, repo, run.ID) + if err != nil { + fmt.Printf("Warning: Failed to fetch artifacts for run %d: %v\n", run.ID, err) + continue + } + + if len(artifacts) == 0 { + fmt.Printf("Run %d/%d (ID: %d, CreatedAt: %s): No test artifacts found\n", + i+1, len(runs), run.ID, run.CreatedAt.Format(time.RFC3339Nano)) + continue + } + + fmt.Printf("Run %d/%d (ID: %d, CreatedAt: %s): Found %d artifacts\n", + i+1, len(runs), run.ID, run.CreatedAt.Format(time.RFC3339Nano), len(artifacts)) + + for _, artifact := range artifacts { + totalArtifacts++ + jobs = append(jobs, ArtifactJob{ + Repo: repo, + RunID: run.ID, + RunCreatedAt: run.CreatedAt, + Artifact: artifact, + TempDir: tempDir, + RunNumber: i + 1, + TotalRuns: len(runs), + ArtifactNum: totalArtifacts, + }) + } + } + + fmt.Printf("\nTotal artifacts to process: %d\n", totalArtifacts) + return jobs, nil +} + +// buildReportSummary builds the complete report summary from processed data +func buildReportSummary(flakyReports, timeoutReports, crashReports, ciBreakerReports []TestReport, + suiteReports []SuiteReport, + allFailures []TestFailure, allTestRuns []TestRun, runs []WorkflowRun, successfulRuns int) *ReportSummary { + + // Calculate overall failure rate + overallFailureRate := 0.0 + totalTestRuns := len(allTestRuns) + if totalTestRuns > 0 { + overallFailureRate = (float64(len(allFailures)) / float64(totalTestRuns)) * 1000.0 + } + fmt.Printf("Overall failure rate: %.2f per 1000 test runs\n", overallFailureRate) + + return &ReportSummary{ + FlakyTests: flakyReports, + Timeouts: timeoutReports, + Crashes: crashReports, + CIBreakers: ciBreakerReports, + Suites: suiteReports, + TotalFailures: len(allFailures), + TotalTestRuns: totalTestRuns, + OverallFailureRate: overallFailureRate, + TotalFlakyCount: len(flakyReports), + TotalWorkflowRuns: len(runs), + SuccessfulRuns: successfulRuns, + } +} + +func runGenerateCommand(c *cli.Context) (err error) { + // Extract parameters + repo := c.String("repository") + branch := c.String("branch") + days := c.Int("days") + workflowID := c.Int64("workflow-id") + maxLinks := c.Int("max-links") + concurrency := c.Int("concurrency") + outputDir := c.String("output-dir") + slackWebhook := c.String("slack-webhook") + runID := c.String("run-id") + refName := c.String("ref-name") + sha := c.String("sha") + runBisect := c.Bool("bisect") + bisectTopN := c.Int("bisect-top-n") + bisectDays := c.Int("bisect-days") + bisectMinProbability := c.Float64("bisect-min-probability") + // Send failure notification on error + defer func() { + if err != nil { + sendFailureNotification(slackWebhook, runID, refName, sha, repo, err) + } + }() + + fmt.Println("Starting flaky test report generation...") + fmt.Printf("Repository: %s\n", repo) + fmt.Printf("Branch: %s\n", branch) + fmt.Printf("Lookback days: %d\n", days) + fmt.Printf("Workflow ID: %d\n", workflowID) + fmt.Printf("Parallel workers: %d\n", concurrency) + + // Create context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + // Fetch and analyze workflow runs + now := time.Now() + reportSince := now.AddDate(0, 0, -days) + runs, successfulRuns, err := fetchAndAnalyzeWorkflowRuns(ctx, repo, workflowID, branch, reportSince, time.Time{}) + if err != nil { + return err + } + if len(runs) == 0 { + return nil + } + + // Create temp directory for downloads + tempDir, err := os.MkdirTemp("", "flakereport-*") + if err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer func() { + if err := os.RemoveAll(tempDir); err != nil { + fmt.Printf("Warning: Failed to remove temp directory: %v\n", err) + } + }() + + // Collect all artifacts + jobs, err := collectArtifactJobs(ctx, repo, runs, tempDir) + if err != nil { + return err + } + + // Process artifacts in parallel + fmt.Println("\n=== Processing artifacts in parallel ===") + allFailures, allTestRuns, processedArtifacts := processArtifactsParallel(ctx, jobs, concurrency) + + fmt.Println("\n=== Processing Results ===") + fmt.Printf("Total test runs: %d\n", len(allTestRuns)) + fmt.Printf("Total test failures found: %d\n", len(allFailures)) + fmt.Printf("Processed artifacts: %d\n", processedArtifacts) + + // Count test runs by name for failure rate calculation + testRunCounts := countTestRuns(allTestRuns) + + // Group failures by test name, then remove parent entries whose subtests were observed. + grouped := groupFailuresByTest(allFailures) + filterParentTests(grouped, testRunCounts) + fmt.Printf("Unique tests with failures: %d\n", len(grouped)) + + // Classify failures + flakyMap, timeoutMap, crashMap := classifyFailures(grouped) + fmt.Printf("Flaky tests: %d\n", len(flakyMap)) + fmt.Printf("Timeout tests: %d\n", len(timeoutMap)) + fmt.Printf("Crash tests: %d\n", len(crashMap)) + + // Identify CI breakers (tests that failed all retries in a single job) + ciBreakerMap, ciBreakCounts := identifyCIBreakers(allFailures) + filterParentTests(ciBreakerMap, testRunCounts) + fmt.Printf("CI breaker tests (failed all retries): %d\n", len(ciBreakerMap)) + + // Convert to reports with failure rates and sort + flakyReports := convertToReports(flakyMap, testRunCounts, repo, maxLinks) + timeoutReports := convertToReports(timeoutMap, testRunCounts, repo, maxLinks) + crashReports := convertCrashesToReports(crashMap, jobs, repo, maxLinks) + ciBreakerReports := convertCIBreakersToReports(ciBreakerMap, ciBreakCounts, len(runs), repo, maxLinks) + + // Compute suite-level breakdown + suiteReports := generateSuiteReports(allFailures, allTestRuns) + fmt.Printf("Suites: %d\n", len(suiteReports)) + + // Build summary + summary := buildReportSummary( + flakyReports, timeoutReports, crashReports, ciBreakerReports, + suiteReports, + allFailures, allTestRuns, runs, successfulRuns, + ) + + // Optionally run Bayesian bisect analysis + var bisectReports []TestBisectReport + if runBisect { + fmt.Println("\n=== Running Bayesian bisect analysis ===") + // Extend the run set with the incremental window (days→bisectDays) to avoid + // re-fetching runs we already have from the report window. + bisectRuns := runs + if bisectDays > days { + bisectSince := now.AddDate(0, 0, -bisectDays) + fmt.Printf("Fetching incremental runs for bisect (days %d–%d)...\n", days, bisectDays) + extraRuns, _, extraErr := fetchAndAnalyzeWorkflowRuns(ctx, repo, workflowID, branch, bisectSince, reportSince) + if extraErr != nil { + fmt.Printf("Warning: Failed to fetch bisect runs: %v\n", extraErr) + } else { + bisectRuns = append(extraRuns, runs...) + } + } + bisectCfg := BisectConfig{ + Repo: repo, + TopN: bisectTopN, + MinFailures: minBisectFailures, + MinRuns: minBisectRuns, + MinProbability: bisectMinProbability, + } + bisectReports, err = runBisectAnalysis(ctx, bisectCfg, allTestRuns, bisectRuns) + if err != nil { + fmt.Printf("Warning: Bisect analysis failed: %v\n", err) + } + } + + fmt.Println("\n=== Writing report files ===") + if err = os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Write failure JSON data + if err = writeFailuresJSON(outputDir, allFailures, repo); err != nil { + return fmt.Errorf("failed to write failures.json: %w", err) + } + + // Write GitHub summary (to GITHUB_STEP_SUMMARY if set, otherwise to output dir) + fmt.Println("\n=== Writing GitHub summary ===") + summaryContent := generateGitHubSummary(summary, runID, maxLinks) + if len(bisectReports) > 0 { + summaryContent += generateBisectSummary(bisectReports, repo, bisectMinProbability) + } + if err := writeGitHubSummary(summaryContent, outputDir); err != nil { + fmt.Printf("Warning: Failed to write GitHub summary: %v\n", err) + } + + // Build and send Slack message + message := buildSuccessMessage(summary, runID, repo, days) + if slackWebhook != "" { + fmt.Println("\n=== Sending Slack notification ===") + if err := message.send(slackWebhook); err != nil { + fmt.Printf("Warning: Failed to send Slack notification: %v\n", err) + } + } else { + md := message.renderMarkdown() + if writeErr := os.WriteFile(filepath.Join(outputDir, "slack-report.md"), []byte(md), 0644); writeErr != nil { + fmt.Printf("Warning: Failed to write slack-report.md: %v\n", writeErr) + } + } + + fmt.Println("\n=== Report generation complete! ===") + return nil +} + +func sendFailureNotification(webhookURL, runID, refName, sha, repo string, err error) { + if webhookURL == "" { + return + } + + fmt.Println("Sending failure notification to Slack...") + message := buildFailureMessage(runID, refName, sha, repo) + if sendErr := message.send(webhookURL); sendErr != nil { + fmt.Printf("Warning: Failed to send failure notification: %v\n", sendErr) + } +} diff --git a/tools/flakereport/git.go b/tools/flakereport/git.go new file mode 100644 index 00000000000..30215bd5b8c --- /dev/null +++ b/tools/flakereport/git.go @@ -0,0 +1,40 @@ +package flakereport + +import ( + "context" + "fmt" + "os/exec" + "strings" + "time" +) + +// commitOrder returns commit SHAs between oldSHA (exclusive) and HEAD (inclusive), +// in chronological order (oldest first). +// Shells out to: git log --reverse --format=%H ..HEAD +func commitOrder(ctx context.Context, oldSHA string) ([]string, error) { + ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctxTimeout, "git", "log", + "--reverse", + "--format=%H", + fmt.Sprintf("%s..HEAD", oldSHA), + ) + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("git log failed: %w\nstderr: %s", err, string(exitErr.Stderr)) + } + return nil, fmt.Errorf("failed to run git log: %w", err) + } + + var commits []string + for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") { + line = strings.TrimSpace(line) + if line != "" { + commits = append(commits, line) + } + } + return commits, nil +} diff --git a/tools/flakereport/github.go b/tools/flakereport/github.go new file mode 100644 index 00000000000..435423915e4 --- /dev/null +++ b/tools/flakereport/github.go @@ -0,0 +1,292 @@ +package flakereport + +import ( + "archive/zip" + "context" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// fetchWorkflowRuns retrieves all completed workflow runs within a date range. +// since is the oldest bound (inclusive); until is the newest bound (zero means open-ended). +// Implements proper pagination to fix the 100-run limit bug. +func fetchWorkflowRuns(ctx context.Context, repo string, workflowID int64, branch string, since, until time.Time) ([]WorkflowRun, error) { + var allRuns []WorkflowRun + + createdFilter := ">=" + since.Format("2006-01-02") + if !until.IsZero() { + createdFilter = since.Format("2006-01-02") + ".." + until.Format("2006-01-02") + } + fmt.Printf("Fetching workflow runs created %s...\n", createdFilter) + + page := 1 + for { + ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) + + cmd := exec.CommandContext(ctxTimeout, "gh", "api", + fmt.Sprintf("/repos/%s/actions/workflows/%d/runs?branch=%s&created=%s&per_page=100&page=%d", + repo, workflowID, branch, createdFilter, page), + ) + + output, err := cmd.Output() + cancel() // Cancel context immediately after command completes + + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("gh api failed (page %d): %w\nstderr: %s", page, err, string(exitErr.Stderr)) + } + return nil, fmt.Errorf("failed to execute gh command (page %d): %w", page, err) + } + + var response struct { + WorkflowRuns []WorkflowRun `json:"workflow_runs"` + } + + if err := json.Unmarshal(output, &response); err != nil { + return nil, fmt.Errorf("failed to parse workflow runs response (page %d): %w", page, err) + } + + if len(response.WorkflowRuns) == 0 { + break + } + + allRuns = append(allRuns, response.WorkflowRuns...) + fmt.Printf("Fetched page %d: %d runs (total: %d)\n", page, len(response.WorkflowRuns), len(allRuns)) + + // If we got fewer than 100 results, this is the last page + if len(response.WorkflowRuns) < 100 { + break + } + + page++ + } + + fmt.Printf("Total workflow runs fetched: %d\n", len(allRuns)) + return allRuns, nil +} + +// fetchRunArtifacts retrieves all artifacts for a specific workflow run +func fetchRunArtifacts(ctx context.Context, repo string, runID int64) ([]WorkflowArtifact, error) { + ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctxTimeout, "gh", "api", + fmt.Sprintf("/repos/%s/actions/runs/%d/artifacts?per_page=100", repo, runID), + ) + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("gh api failed for run %d: %w\nstderr: %s", runID, err, string(exitErr.Stderr)) + } + return nil, fmt.Errorf("failed to execute gh command for run %d: %w", runID, err) + } + + var response ArtifactsResponse + if err := json.Unmarshal(output, &response); err != nil { + return nil, fmt.Errorf("failed to parse artifacts response for run %d: %w", runID, err) + } + + // Filter for JUnit/test artifacts + var testArtifacts []WorkflowArtifact + for _, artifact := range response.Artifacts { + if artifact.Expired { + continue + } + name := strings.ToLower(artifact.Name) + if strings.Contains(name, "junit") { + testArtifacts = append(testArtifacts, artifact) + } + } + + return testArtifacts, nil +} + +// downloadArtifact downloads a single artifact zip file +func downloadArtifact(ctx context.Context, repo string, artifactID int64, outputDir string) (string, error) { + ctxTimeout, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + zipPath := filepath.Join(outputDir, fmt.Sprintf("artifact-%d.zip", artifactID)) + + cmd := exec.CommandContext(ctxTimeout, "gh", "api", + fmt.Sprintf("/repos/%s/actions/artifacts/%d/zip", repo, artifactID), + ) + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("failed to download artifact %d: %w\nstderr: %s", artifactID, err, string(exitErr.Stderr)) + } + return "", fmt.Errorf("failed to download artifact %d: %w", artifactID, err) + } + + if err := os.WriteFile(zipPath, output, 0644); err != nil { + return "", fmt.Errorf("failed to write artifact zip %d: %w", artifactID, err) + } + + return zipPath, nil +} + +// extractArtifactZip extracts zip file and returns paths to JUnit XML files +func extractArtifactZip(zipPath, outputDir string) ([]string, error) { + r, err := zip.OpenReader(zipPath) + if err != nil { + return nil, fmt.Errorf("failed to open zip file %s: %w", zipPath, err) + } + defer func() { + if err := r.Close(); err != nil { + fmt.Printf("Warning: Failed to close zip reader: %v\n", err) + } + }() + + var xmlFiles []string + + for _, f := range r.File { + // Skip directories + if f.FileInfo().IsDir() { + continue + } + + // Only extract XML files + if !strings.HasSuffix(strings.ToLower(f.Name), ".xml") { + continue + } + + // Create extraction path + extractPath := filepath.Join(outputDir, filepath.Base(f.Name)) + + // Open file from zip + rc, err := f.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file %s in zip: %w", f.Name, err) + } + + // Create output file + outFile, err := os.Create(extractPath) + if err != nil { + _ = rc.Close() + return nil, fmt.Errorf("failed to create output file %s: %w", extractPath, err) + } + + // Copy content + _, err = io.Copy(outFile, rc) + if closeErr := rc.Close(); closeErr != nil { + _ = outFile.Close() + return nil, fmt.Errorf("failed to close zip file reader: %w", closeErr) + } + if closeErr := outFile.Close(); closeErr != nil { + return nil, fmt.Errorf("failed to close output file: %w", closeErr) + } + + if err != nil { + return nil, fmt.Errorf("failed to extract file %s: %w", f.Name, err) + } + + xmlFiles = append(xmlFiles, extractPath) + } + + return xmlFiles, nil +} + +// parseArtifactName extracts run_id, job_id, and matrix_name from artifact name. +// Functional tests: junit-xml--{run_id}--{job_id}--{run_attempt}--{matrix_name}--{display_name}--functional-test +// Unit/integration: junit-xml--{run_id}--{job_id}--{run_attempt}--unit-test +// Returns: runID, jobID, matrixName ("unknown" for fields that are absent or unparseable) +func parseArtifactName(artifactName string) (runID string, jobID string, matrixName string) { + parts := strings.Split(artifactName, "--") + if len(parts) < 3 { + return "unknown", "unknown", "unknown" + } + + runID = parts[1] + + jobID = parts[2] + if jobID == "" { + jobID = "unknown" + } + + // Functional test artifacts carry a matrix name (DB config) at parts[4]. + // Unit/integration artifacts have only 5 parts where parts[4] is the test type + // (e.g. "unit-test"), not a matrix name. Functional artifacts have >=7 parts. + if len(parts) >= 6 { + matrixName = parts[4] + } else { + matrixName = "unknown" + } + + return runID, jobID, matrixName +} + +// buildGitHubURL constructs GitHub Actions URL from run/job IDs +// If jobID == "unknown": https://github.com/{repo}/actions/runs/{runID} +// Otherwise: https://github.com/{repo}/actions/runs/{runID}/job/{jobID} +func buildGitHubURL(repo, runID, jobID string) string { + baseURL := fmt.Sprintf("https://github.com/%s/actions/runs/%s", repo, runID) + if jobID != "unknown" && jobID != "" { + return fmt.Sprintf("%s/job/%s", baseURL, jobID) + } + return baseURL +} + +// fetchCommitMeta fetches commit title, author, and changed file list for a single commit SHA. +// Uses: GET /repos/{owner}/{repo}/commits/{sha} +func fetchCommitMeta(ctx context.Context, repo, sha string) (CommitMeta, error) { + ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctxTimeout, "gh", "api", + fmt.Sprintf("/repos/%s/commits/%s", repo, sha), + ) + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return CommitMeta{SHA: sha}, fmt.Errorf("gh api failed for commit %s: %w\nstderr: %s", sha, err, string(exitErr.Stderr)) + } + return CommitMeta{SHA: sha}, fmt.Errorf("failed to execute gh command for commit %s: %w", sha, err) + } + + var response struct { + SHA string `json:"sha"` + Commit struct { + Message string `json:"message"` + Author struct { + Name string `json:"name"` + Date time.Time `json:"date"` + } `json:"author"` + } `json:"commit"` + Files []struct { + Filename string `json:"filename"` + } `json:"files"` + } + + if err := json.Unmarshal(output, &response); err != nil { + return CommitMeta{SHA: sha}, fmt.Errorf("failed to parse commit response for %s: %w", sha, err) + } + + // Extract just the first line of the commit message as the title + title := response.Commit.Message + if idx := strings.IndexByte(title, '\n'); idx >= 0 { + title = title[:idx] + } + + files := make([]string, 0, len(response.Files)) + for _, f := range response.Files { + files = append(files, f.Filename) + } + + return CommitMeta{ + SHA: sha, + Title: title, + Author: response.Commit.Author.Name, + CommittedAt: response.Commit.Author.Date, + Files: files, + }, nil +} diff --git a/tools/flakereport/parallel.go b/tools/flakereport/parallel.go new file mode 100644 index 00000000000..3f992d6fc7e --- /dev/null +++ b/tools/flakereport/parallel.go @@ -0,0 +1,151 @@ +package flakereport + +import ( + "context" + "fmt" + "path/filepath" + "sync" + "time" +) + +// ArtifactJob represents a job to download and process an artifact +type ArtifactJob struct { + Repo string + RunID int64 + RunCreatedAt time.Time + Artifact WorkflowArtifact + TempDir string + RunNumber int + TotalRuns int + ArtifactNum int +} + +// ArtifactResult represents the result of processing an artifact +type ArtifactResult struct { + Failures []TestFailure + AllRuns []TestRun + Error error +} + +// processArtifactsParallel downloads and processes artifacts in parallel with a worker pool +// Returns: all failures, all test runs, and count of successfully processed artifacts +func processArtifactsParallel(ctx context.Context, jobs []ArtifactJob, concurrency int) ([]TestFailure, []TestRun, int) { + if len(jobs) == 0 { + return nil, nil, 0 + } + + totalArtifacts := len(jobs) + + // Create channels + jobChan := make(chan ArtifactJob, totalArtifacts) + resultChan := make(chan ArtifactResult, totalArtifacts) + + // Start worker pool + var wg sync.WaitGroup + for range concurrency { + wg.Add(1) + go worker(ctx, jobChan, resultChan, totalArtifacts, &wg) + } + + // Send jobs to workers + go func() { + for _, job := range jobs { + jobChan <- job + } + close(jobChan) + }() + + // Wait for all workers to finish + go func() { + wg.Wait() + close(resultChan) + }() + + // Collect results + var allFailures []TestFailure + var allTestRuns []TestRun + processedArtifacts := 0 + errorCount := 0 + + for result := range resultChan { + if result.Error != nil { + errorCount++ + // Error already logged by worker + continue + } + allFailures = append(allFailures, result.Failures...) + allTestRuns = append(allTestRuns, result.AllRuns...) + processedArtifacts++ + } + + if errorCount > 0 { + fmt.Printf("Warning: %d artifacts failed to process\n", errorCount) + } + + return allFailures, allTestRuns, processedArtifacts +} + +// worker processes jobs from the job channel +func worker(ctx context.Context, jobs <-chan ArtifactJob, results chan<- ArtifactResult, totalArtifacts int, wg *sync.WaitGroup) { + defer wg.Done() + + for job := range jobs { + result := processArtifactJob(ctx, job, totalArtifacts) + results <- result + } +} + +// processArtifactJob downloads and processes a single artifact +func processArtifactJob(ctx context.Context, job ArtifactJob, totalArtifacts int) ArtifactResult { + var result ArtifactResult + + fmt.Printf(" [%d/%d] Run %d/%d: Downloading artifact %s (ID: %d)...\n", + job.ArtifactNum, totalArtifacts, job.RunNumber, job.TotalRuns, + job.Artifact.Name, job.Artifact.ID) + + // Download artifact + zipPath, err := downloadArtifact(ctx, job.Repo, job.Artifact.ID, job.TempDir) + if err != nil { + result.Error = fmt.Errorf("failed to download artifact %d: %w", job.Artifact.ID, err) + fmt.Printf(" Warning: %v\n", result.Error) + return result + } + + // Extract XML files + xmlFiles, err := extractArtifactZip(zipPath, job.TempDir) + if err != nil { + result.Error = fmt.Errorf("failed to extract artifact %d: %w", job.Artifact.ID, err) + fmt.Printf(" Warning: %v\n", result.Error) + return result + } + + fmt.Printf(" [%d/%d] Extracted %d XML files from %s\n", + job.ArtifactNum, totalArtifacts, len(xmlFiles), job.Artifact.Name) + + // Parse JUnit XML files + for _, xmlFile := range xmlFiles { + suites, err := parseJUnitFile(xmlFile) + if err != nil { + fmt.Printf(" Warning: Failed to parse %s: %v\n", filepath.Base(xmlFile), err) + continue + } + + // Extract failures + failures := extractFailures(suites, job.Artifact.Name, job.RunID, job.RunCreatedAt) + result.Failures = append(result.Failures, failures...) + + // Extract all test runs for failure rate calculation + _, jobID, matrixName := parseArtifactName(job.Artifact.Name) + testRuns := extractAllTestRuns(suites, job.RunID, jobID, matrixName) + result.AllRuns = append(result.AllRuns, testRuns...) + } + + fmt.Printf(" [%d/%d] Found %d failures from %d test runs in %s\n", + job.ArtifactNum, totalArtifacts, len(result.Failures), len(result.AllRuns), job.Artifact.Name) + + for i := 0; i < len(result.Failures); i++ { + fmt.Printf(" Sample failure %d: %s\n", i+1, result.Failures[i].Name) + } + + return result +} diff --git a/tools/flakereport/parser.go b/tools/flakereport/parser.go new file mode 100644 index 00000000000..e3d50f2928c --- /dev/null +++ b/tools/flakereport/parser.go @@ -0,0 +1,444 @@ +package flakereport + +import ( + "encoding/xml" + "fmt" + "os" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/jstemmer/go-junit-report/v2/junit" +) + +var finalRegex = regexp.MustCompile(`\s*\(final\)$`) +var trailingSuffixRegex = regexp.MustCompile(`\s*\([^)]+\)$`) + +// parseJUnitFile reads and parses a single JUnit XML file +func parseJUnitFile(filePath string) (*junit.Testsuites, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open file %s: %w", filePath, err) + } + defer func() { + if err := file.Close(); err != nil { + fmt.Printf("Warning: Failed to close file %s: %v\n", filePath, err) + } + }() + + var testsuites junit.Testsuites + decoder := xml.NewDecoder(file) + if err := decoder.Decode(&testsuites); err != nil { + // Try parsing as a single testsuite + if _, seekErr := file.Seek(0, 0); seekErr != nil { + return nil, fmt.Errorf("failed to seek file %s: %w", filePath, seekErr) + } + var testsuite junit.Testsuite + decoder = xml.NewDecoder(file) + if err := decoder.Decode(&testsuite); err != nil { + return nil, fmt.Errorf("failed to parse JUnit XML %s: %w", filePath, err) + } + testsuites.Suites = []junit.Testsuite{testsuite} + } + + return &testsuites, nil +} + +// topLevelTestName extracts the suite/top-level test name from a test name. +// For "TestSuiteV0/TestMethod" returns "TestSuiteV0". +// For "TestFoo" (no slash) returns "TestFoo". +func topLevelTestName(name string) string { + if idx := strings.IndexByte(name, '/'); idx >= 0 { + return name[:idx] + } + return name +} + +// isGoTestSuite returns true if the name looks like a Go testify suite name +// (starts with "Test" and contains "Suite"). +// e.g. TestDeploymentVersionSuiteV0, TestFunctionalSuite +func isGoTestSuite(name string) bool { + return strings.HasPrefix(name, "Test") && strings.Contains(name, "Suite") +} + +// extractFailures extracts all test failures from parsed JUnit data +// Filters for: passed = false AND skipped = false (matching tringa SQL query) +func extractFailures(suites *junit.Testsuites, artifactName string, runID int64, timestamp time.Time) []TestFailure { + var failures []TestFailure + + // Parse artifact name for run_id, job_id, and matrix_name + _, jobID, matrixName := parseArtifactName(artifactName) + + for _, suite := range suites.Suites { + for _, testcase := range suite.Testcases { + // Filter: failure present AND not skipped + if testcase.Failure != nil && testcase.Skipped == nil { + failure := TestFailure{ + ClassName: testcase.Classname, + Name: testcase.Name, + SuiteName: topLevelTestName(testcase.Name), + ArtifactID: artifactName, + RunID: runID, + JobID: jobID, + MatrixName: matrixName, + Timestamp: timestamp, + } + failures = append(failures, failure) + } + } + } + + return failures +} + +// extractAllTestRuns extracts all test runs (including successes) from parsed JUnit data +// Used for calculating failure rates +func extractAllTestRuns(suites *junit.Testsuites, runID int64, jobID, matrixName string) []TestRun { + var runs []TestRun + + for _, suite := range suites.Suites { + for _, testcase := range suite.Testcases { + run := TestRun{ + SuiteName: topLevelTestName(testcase.Name), + Name: testcase.Name, + Failed: testcase.Failure != nil, + Skipped: testcase.Skipped != nil, + RunID: runID, + JobID: jobID, + MatrixName: matrixName, + } + runs = append(runs, run) + } + } + + return runs +} + +// normalizeTestName strips all trailing parenthesized suffixes from test names, +// e.g. "(retry 1)", "(final)", "(timeout)". +func normalizeTestName(name string) string { + for { + stripped := trailingSuffixRegex.ReplaceAllString(name, "") + if stripped == name { + return name + } + name = stripped + } +} + +// groupFailuresByTest groups failures by normalized test name +func groupFailuresByTest(failures []TestFailure) map[string][]TestFailure { + grouped := make(map[string][]TestFailure) + + for _, failure := range failures { + normalizedName := normalizeTestName(failure.Name) + grouped[normalizedName] = append(grouped[normalizedName], failure) + } + + return grouped +} + +// countTestRuns counts total runs (including successes) by normalized test name +func countTestRuns(allRuns []TestRun) map[string]int { + counts := make(map[string]int) + + for _, run := range allRuns { + // Only count non-skipped tests + if !run.Skipped { + normalizedName := normalizeTestName(run.Name) + counts[normalizedName]++ + } + } + + return counts +} + +// classifyFailure returns "timeout", "crash", or "flaky" based on the test name. +// Uses Contains (not HasSuffix) so it works on both raw and normalized names. +func classifyFailure(name string) string { + lower := strings.ToLower(name) + if strings.Contains(lower, "(timeout)") { + return "timeout" + } + if strings.Contains(lower, "(crash)") { + return "crash" + } + return "flaky" +} + +// classifyFailures separates failures into categories. +// Classifies using the raw failure name (not the normalized key) since +// normalizeTestName strips suffixes like "(timeout)". +func classifyFailures(grouped map[string][]TestFailure) (flaky, timeout, crash map[string][]TestFailure) { + flaky = make(map[string][]TestFailure) + timeout = make(map[string][]TestFailure) + crash = make(map[string][]TestFailure) + + for testName, failures := range grouped { + switch classifyFailure(failures[0].Name) { + case "timeout": + timeout[testName] = failures + case "crash": + crash[testName] = failures + case "flaky": + flaky[testName] = failures + default: + panic("unknown failure classification: " + classifyFailure(failures[0].Name)) //nolint:forbidigo + } + } + + return flaky, timeout, crash +} + +// buildReports builds TestReport slice from grouped failures, sorts by FailureCount/TotalRuns descending. +// numerator and denominator return the rate components for each test. +func buildReports(grouped map[string][]TestFailure, numerator func(string, []TestFailure) int, denominator func(string, []TestFailure) int, repo string, maxLinks int) []TestReport { + var reports []TestReport + + for testName, failures := range grouped { + // Find most recent failure + var lastFailure time.Time + for _, f := range failures { + if f.Timestamp.After(lastFailure) { + lastFailure = f.Timestamp + } + } + + report := TestReport{ + TestName: testName, + FailureCount: numerator(testName, failures), + TotalRuns: denominator(testName, failures), + LastFailure: lastFailure, + GitHubURLs: make([]string, 0, maxLinks), + } + + // Add up to maxLinks URLs + for i := 0; i < len(failures) && i < maxLinks; i++ { + failure := failures[i] + runIDStr := strconv.FormatInt(failure.RunID, 10) + url := buildGitHubURL(repo, runIDStr, failure.JobID) + report.GitHubURLs = append(report.GitHubURLs, url) + } + + reports = append(reports, report) + } + + // Sort by rate descending (most problematic tests first) + sort.Slice(reports, func(i, j int) bool { + ri := float64(reports[i].FailureCount) / float64(max(reports[i].TotalRuns, 1)) + rj := float64(reports[j].FailureCount) / float64(max(reports[j].TotalRuns, 1)) + if ri != rj { + return ri > rj + } + return reports[i].FailureCount > reports[j].FailureCount + }) + + return reports +} + +// convertToReports converts grouped failures to TestReport slice +// testRunCounts maps test name to total number of runs (including successes) +func convertToReports(grouped map[string][]TestFailure, testRunCounts map[string]int, repo string, maxLinks int) []TestReport { + return buildReports(grouped, + func(_ string, failures []TestFailure) int { return len(failures) }, + func(name string, failures []TestFailure) int { + if n := testRunCounts[name]; n > 0 { + return n + } + return len(failures) // Fallback if we don't have run counts + }, + repo, maxLinks) +} + +// filterParentTests removes top-level test names from grouped when subtests of +// that parent were observed in testRunCounts. A top-level failure whose subtests +// ran in other CI jobs is already captured (with a correct denominator) in the +// Flaky Suites section, so including it in the per-test table produces a +// misleading 1/1 entry. +func filterParentTests(grouped map[string][]TestFailure, testRunCounts map[string]int) { + suitePrefix := make(map[string]bool, len(testRunCounts)) + for name := range testRunCounts { + if idx := strings.IndexByte(name, '/'); idx >= 0 { + suitePrefix[name[:idx]] = true + } + } + for testName := range grouped { + if !strings.Contains(testName, "/") && suitePrefix[testName] { + delete(grouped, testName) + } + } +} + +// isFinalRetry returns true if the test name has the "(final)" suffix, +// indicating the test runner exhausted all retries. +func isFinalRetry(testName string) bool { + return finalRegex.MatchString(testName) +} + +// analyzeArtifactForCIBreakers analyzes a single artifact for CI breakers. +// A test is a CI breaker if it has a failure with the "(final)" suffix, +// meaning the test runner exhausted all retries. +func analyzeArtifactForCIBreakers(artifactID string, artifactFailures []TestFailure) map[string][]TestFailure { + ciBreakers := make(map[string][]TestFailure) + + for _, failure := range artifactFailures { + if !isFinalRetry(failure.Name) { + continue + } + normalized := normalizeTestName(failure.Name) + ciBreakers[normalized] = append(ciBreakers[normalized], failure) + } + + for testName := range ciBreakers { + fmt.Printf(" CI BREAKER: %s (artifact %s)\n", testName, artifactID) + } + + return ciBreakers +} + +// convertCrashesToReports converts crash failures to TestReport slice. +// Crashes are job-level events. The rate is unique-jobs-with-crash / total jobs of that type. +// The crash name (e.g. "functional-test") matches the artifact name suffix, so we count +// artifacts ending with "--" as the denominator. +func convertCrashesToReports(grouped map[string][]TestFailure, jobs []ArtifactJob, repo string, maxLinks int) []TestReport { + // Count artifacts per type suffix (e.g. "functional-test", "unit-test") + artifactsByType := make(map[string]int) + for _, job := range jobs { + name := job.Artifact.Name + if idx := strings.LastIndex(name, "--"); idx >= 0 { + artifactsByType[name[idx+2:]]++ + } + } + + return buildReports(grouped, + func(_ string, failures []TestFailure) int { + jobIDs := make(map[string]bool, len(failures)) + for _, f := range failures { + jobIDs[f.JobID] = true + } + return len(jobIDs) + }, + func(name string, _ []TestFailure) int { + if n := artifactsByType[name]; n > 0 { + return n + } + return 1 // Fallback to avoid division by zero + }, + repo, maxLinks) +} + +// convertCIBreakersToReports converts CI breaker failures to TestReport slice. +// totalWorkflowRuns is the total number of CI runs analyzed (denominator for break rate). +func convertCIBreakersToReports(grouped map[string][]TestFailure, ciBreakCounts map[string]int, totalWorkflowRuns int, repo string, maxLinks int) []TestReport { + return buildReports(grouped, + func(name string, _ []TestFailure) int { return ciBreakCounts[name] }, + func(_ string, _ []TestFailure) int { return totalWorkflowRuns }, + repo, maxLinks) +} + +// identifyCIBreakers finds tests that failed their final retry in a CI job. +// A test breaks CI if it has a failure with the "(final)" suffix in an artifact. +// Returns: ciBreakers map and count of how many artifacts each test broke. +func identifyCIBreakers(failures []TestFailure) (map[string][]TestFailure, map[string]int) { + // Group failures by artifact ID first + byArtifact := make(map[string][]TestFailure) + for _, failure := range failures { + byArtifact[failure.ArtifactID] = append(byArtifact[failure.ArtifactID], failure) + } + + fmt.Println("\n=== CI Breaker Analysis ===") + fmt.Printf("Total failures to analyze: %d\n", len(failures)) + fmt.Printf("Grouped into %d artifacts\n", len(byArtifact)) + + // Track tests that broke CI + ciBreakers := make(map[string][]TestFailure) + ciBreakCount := make(map[string]int) + + // Analyze each artifact for CI breakers + for artifactID, artifactFailures := range byArtifact { + breakersInArtifact := analyzeArtifactForCIBreakers(artifactID, artifactFailures) + + // Aggregate results + for testName, failures := range breakersInArtifact { + ciBreakers[testName] = append(ciBreakers[testName], failures...) + ciBreakCount[testName]++ + } + } + + fmt.Printf("Unique tests that broke CI: %d\n", len(ciBreakers)) + + return ciBreakers, ciBreakCount +} + +// suiteRunKey returns a string that uniquely identifies a single (CI run × DB config) pair. +// Each workflow run may spawn multiple matrix jobs sharing the same RunID but with distinct +// MatrixNames (DB configs). Keying by (RunID, MatrixName) ensures shards belonging to the +// same run+config are counted once, regardless of how many shard JobIDs they produce. +func suiteRunKey(runID int64, matrixName string) string { + return fmt.Sprintf("%d:%s", runID, matrixName) +} + +// generateSuiteReports creates per-suite flake breakdown from all failures and test runs. +// Suite flake rate = % of (CI run × DB config) pairs where the suite had at least one +// non-retry failure. +func generateSuiteReports(allFailures []TestFailure, allTestRuns []TestRun) []SuiteReport { + // Track unique (CI run × DB config) pairs per suite (denominator). + // Using MatrixName avoids the inflation caused by per-shard JobIDs: suites whose + // test methods are spread across N shards would otherwise be counted N times per + // (run × DB config). + suiteRuns := make(map[string]map[string]bool) + for _, run := range allTestRuns { + if run.Skipped || !isGoTestSuite(run.SuiteName) { + continue + } + if suiteRuns[run.SuiteName] == nil { + suiteRuns[run.SuiteName] = make(map[string]bool) + } + suiteRuns[run.SuiteName][suiteRunKey(run.RunID, run.MatrixName)] = true + } + + // Track (CI run × DB config) pairs with non-retry failures per suite (numerator) + suiteFailedRuns := make(map[string]map[string]bool) + suiteLastFailure := make(map[string]time.Time) + for _, failure := range allFailures { + if !isGoTestSuite(failure.SuiteName) { + continue + } + // Only report the original, complete run + if normalizeTestName(failure.Name) != failure.Name { + continue + } + if suiteFailedRuns[failure.SuiteName] == nil { + suiteFailedRuns[failure.SuiteName] = make(map[string]bool) + } + suiteFailedRuns[failure.SuiteName][suiteRunKey(failure.RunID, failure.MatrixName)] = true + if failure.Timestamp.After(suiteLastFailure[failure.SuiteName]) { + suiteLastFailure[failure.SuiteName] = failure.Timestamp + } + } + + var reports []SuiteReport + for suiteName, runIDs := range suiteRuns { + failedRuns := len(suiteFailedRuns[suiteName]) + if failedRuns == 0 { + continue + } + totalRuns := len(runIDs) + flakeRate := float64(failedRuns) / float64(totalRuns) * 100.0 + reports = append(reports, SuiteReport{ + SuiteName: suiteName, + FlakeRate: flakeRate, + FailedRuns: failedRuns, + TotalRuns: totalRuns, + LastFailure: suiteLastFailure[suiteName], + }) + } + + sort.Slice(reports, func(i, j int) bool { + return reports[i].SuiteName < reports[j].SuiteName + }) + + return reports +} diff --git a/tools/flakereport/parser_test.go b/tools/flakereport/parser_test.go new file mode 100644 index 00000000000..6bf90f21c69 --- /dev/null +++ b/tools/flakereport/parser_test.go @@ -0,0 +1,313 @@ +package flakereport + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNormalizeTestName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "test with retry suffix", + input: "TestSomething (retry 1)", + expected: "TestSomething", + }, + { + name: "test with retry suffix and extra spaces", + input: "TestSomething (retry 5)", + expected: "TestSomething", + }, + { + name: "test without retry suffix", + input: "TestSomething", + expected: "TestSomething", + }, + { + name: "test with retry in name but not suffix", + input: "TestRetry", + expected: "TestRetry", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := normalizeTestName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestParseArtifactName(t *testing.T) { + tests := []struct { + name string + artifactName string + expectedRunID string + expectedJobID string + expectedMatrixName string + }{ + { + name: "functional test artifact", + artifactName: "junit-xml--22373551837--64609560060--1--integration-0--Integration--functional-test", + expectedRunID: "22373551837", + expectedJobID: "64609560060", + expectedMatrixName: "integration-0", + }, + { + name: "unit test artifact", + artifactName: "junit-xml--22373551837--64609560061--1--unit-test", + expectedRunID: "22373551837", + expectedJobID: "64609560061", + expectedMatrixName: "unknown", + }, + { + name: "integration test artifact", + artifactName: "junit-xml--22373551837--64609560062--1--integration-test", + expectedRunID: "22373551837", + expectedJobID: "64609560062", + expectedMatrixName: "unknown", + }, + { + name: "artifact name with empty job id", + artifactName: "junit-xml--22373551837----1--unit-test", + expectedRunID: "22373551837", + expectedJobID: "unknown", + expectedMatrixName: "unknown", + }, + { + name: "invalid artifact name", + artifactName: "test-results", + expectedRunID: "unknown", + expectedJobID: "unknown", + expectedMatrixName: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runID, jobID, matrixName := parseArtifactName(tt.artifactName) + assert.Equal(t, tt.expectedRunID, runID) + assert.Equal(t, tt.expectedJobID, jobID) + assert.Equal(t, tt.expectedMatrixName, matrixName) + }) + } +} + +func TestBuildGitHubURL(t *testing.T) { + tests := []struct { + name string + repo string + runID string + jobID string + expectedURL string + }{ + { + name: "with job ID", + repo: "temporalio/temporal", + runID: "12345678", + jobID: "87654321", + expectedURL: "https://github.com/temporalio/temporal/actions/runs/12345678/job/87654321", + }, + { + name: "without job ID", + repo: "temporalio/temporal", + runID: "12345678", + jobID: "unknown", + expectedURL: "https://github.com/temporalio/temporal/actions/runs/12345678", + }, + { + name: "with empty job ID", + repo: "temporalio/temporal", + runID: "12345678", + jobID: "", + expectedURL: "https://github.com/temporalio/temporal/actions/runs/12345678", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + url := buildGitHubURL(tt.repo, tt.runID, tt.jobID) + assert.Equal(t, tt.expectedURL, url) + }) + } +} + +func TestClassifyFailures(t *testing.T) { + // Simulate the real flow: raw failures → groupFailuresByTest → classifyFailures. + // groupFailuresByTest normalizes names, stripping suffixes like (timeout), (retry N). + failures := []TestFailure{ + {Name: "TestNormal"}, + {Name: "TestNormal"}, + {Name: "TestFlaky"}, + {Name: "TestFlaky"}, + {Name: "TestFlaky"}, + {Name: "TestFlaky"}, + {Name: "TestFlaky"}, + {Name: "TestTimeout (timeout)"}, + {Name: "TestTimeout (timeout)"}, + {Name: "TestTimeout (timeout) (retry 1)"}, + {Name: "TestFoo (crash)"}, + {Name: "TestFoo (crash)"}, + } + + grouped := groupFailuresByTest(failures) + flaky, timeout, crash := classifyFailures(grouped) + + // TestFlaky should be in flaky + assert.Contains(t, flaky, "TestFlaky") + assert.Len(t, flaky["TestFlaky"], 5) + + // TestTimeout should be in timeout (normalized key has no suffix) + assert.Contains(t, timeout, "TestTimeout") + assert.Len(t, timeout["TestTimeout"], 3) + + // TestFoo (crash) should be in crash (normalized key has no suffix) + assert.Contains(t, crash, "TestFoo") + assert.Len(t, crash["TestFoo"], 2) + + // TestNormal should be in flaky + assert.Contains(t, flaky, "TestNormal") + assert.Len(t, flaky["TestNormal"], 2) +} + +func TestFilterParentTests(t *testing.T) { + makeFailures := func(names ...string) map[string][]TestFailure { + m := make(map[string][]TestFailure, len(names)) + for _, n := range names { + m[n] = []TestFailure{{Name: n}} + } + return m + } + + t.Run("removes parent when subtests observed", func(t *testing.T) { + grouped := makeFailures("TestFooSuite", "TestFooSuite/TestBar") + counts := map[string]int{ + "TestFooSuite/TestBar": 10, + "TestFooSuite/TestBaz": 20, + } + filterParentTests(grouped, counts) + require.NotContains(t, grouped, "TestFooSuite") + require.Contains(t, grouped, "TestFooSuite/TestBar") + }) + + t.Run("keeps parent when no subtests observed", func(t *testing.T) { + grouped := makeFailures("TestStandalone") + counts := map[string]int{"TestStandalone": 5} + filterParentTests(grouped, counts) + require.Contains(t, grouped, "TestStandalone") + }) + + t.Run("keeps subtest entry regardless", func(t *testing.T) { + grouped := makeFailures("TestFooSuite/TestBar") + counts := map[string]int{"TestFooSuite/TestBar": 10} + filterParentTests(grouped, counts) + require.Contains(t, grouped, "TestFooSuite/TestBar") + }) +} + +func TestGenerateSuiteReports(t *testing.T) { + now := time.Now() + twoDaysAgo := now.Add(-48 * time.Hour) + oneDayAgo := now.Add(-24 * time.Hour) + + // Each CI run has 2 DB configs ("db-a" and "db-b"), and within each config + // the suite is spread across 2 shards (distinct JobIDs). The denominator + // should count unique (RunID × MatrixName) pairs, not raw JobIDs, so shards + // within the same run+config collapse into one entry. + failures := []TestFailure{ + // SuiteA: failure in run 1, db-a (shard job 101) + {Name: "TestFoo", SuiteName: "TestFunctionalSuiteA", RunID: 1, JobID: "101", MatrixName: "db-a", Timestamp: twoDaysAgo}, + // SuiteA: retry failure — should be excluded from suite flake rate + {Name: "TestFoo (retry 1)", SuiteName: "TestFunctionalSuiteA", RunID: 1, JobID: "101", MatrixName: "db-a", Timestamp: twoDaysAgo}, + // SuiteA: failure in run 2, db-b (shard job 204) + {Name: "TestBar", SuiteName: "TestFunctionalSuiteA", RunID: 2, JobID: "204", MatrixName: "db-b", Timestamp: oneDayAgo}, + // SuiteB: failure in run 1, db-a + {Name: "TestBaz", SuiteName: "TestFunctionalSuiteB", RunID: 1, JobID: "101", MatrixName: "db-a", Timestamp: twoDaysAgo}, + } + + allRuns := []TestRun{ + // SuiteA: runs 1–3, each with db-a (shards 101,102) and db-b (shards 103,104 / 203,204 / 303,304) + // → 3 runs × 2 DB configs = 6 unique (run, config) pairs + {SuiteName: "TestFunctionalSuiteA", Name: "TestFoo", RunID: 1, JobID: "101", MatrixName: "db-a"}, + {SuiteName: "TestFunctionalSuiteA", Name: "TestFoo", RunID: 1, JobID: "102", MatrixName: "db-a"}, // same (run=1, db-a) pair + {SuiteName: "TestFunctionalSuiteA", Name: "TestFoo (retry 1)", RunID: 1, JobID: "101", MatrixName: "db-a"}, + {SuiteName: "TestFunctionalSuiteA", Name: "TestFoo", RunID: 1, JobID: "103", MatrixName: "db-b"}, + {SuiteName: "TestFunctionalSuiteA", Name: "TestFoo", RunID: 1, JobID: "104", MatrixName: "db-b"}, // same (run=1, db-b) pair + {SuiteName: "TestFunctionalSuiteA", Name: "TestBar", RunID: 2, JobID: "203", MatrixName: "db-a"}, + {SuiteName: "TestFunctionalSuiteA", Name: "TestBar", RunID: 2, JobID: "204", MatrixName: "db-b"}, + {SuiteName: "TestFunctionalSuiteA", Name: "TestFoo", RunID: 3, JobID: "301", MatrixName: "db-a"}, + {SuiteName: "TestFunctionalSuiteA", Name: "TestFoo", RunID: 3, JobID: "303", MatrixName: "db-b"}, + // SuiteB present in runs 1, 2, single DB config each + {SuiteName: "TestFunctionalSuiteB", Name: "TestBaz", RunID: 1, JobID: "101", MatrixName: "db-a"}, + {SuiteName: "TestFunctionalSuiteB", Name: "TestBaz", RunID: 2, JobID: "203", MatrixName: "db-a"}, + // Skipped test should not count + {SuiteName: "TestFunctionalSuiteC", Name: "TestSkipped", RunID: 1, Skipped: true}, + // Non-suite names should be filtered out + {SuiteName: "DATA RACE", Name: "DATA RACE: detected", RunID: 1}, + {SuiteName: "TestStandalone", Name: "TestStandalone", RunID: 1}, + // Suite with no failures should be excluded + {SuiteName: "TestHealthySuite", Name: "TestOk", RunID: 1, MatrixName: "db-a"}, + {SuiteName: "TestHealthySuite", Name: "TestOk", RunID: 2, MatrixName: "db-a"}, + } + + reports := generateSuiteReports(failures, allRuns) + + // Should have SuiteA and SuiteB (SuiteC is all skipped) + require.Len(t, reports, 2) + + // Sorted by suite name + require.Equal(t, "TestFunctionalSuiteA", reports[0].SuiteName) + require.Equal(t, "TestFunctionalSuiteB", reports[1].SuiteName) + + // SuiteA: 2 failed (run,config) pairs out of 6 total + // Failed: (run=1, db-a) and (run=2, db-b); retry on (run=1, db-a) doesn't add a new key + require.Equal(t, 2, reports[0].FailedRuns) + require.Equal(t, 6, reports[0].TotalRuns) + require.InDelta(t, 33.3, reports[0].FlakeRate, 0.1) + require.Equal(t, oneDayAgo, reports[0].LastFailure) + + // SuiteB: 1 failed (run,config) pair out of 2 total + require.Equal(t, 1, reports[1].FailedRuns) + require.Equal(t, 2, reports[1].TotalRuns) + require.InDelta(t, 50.0, reports[1].FlakeRate, 0.1) + require.Equal(t, twoDaysAgo, reports[1].LastFailure) +} + +func TestNormalizeTestNameFinal(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "retry with final suffix", + input: "TestFoo (retry 2) (final)", + expected: "TestFoo", + }, + { + name: "no suffix", + input: "TestFoo", + expected: "TestFoo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, normalizeTestName(tt.input)) + }) + } +} + +func TestIsFinalRetry(t *testing.T) { + require.True(t, isFinalRetry("TestFoo (retry 2) (final)")) + require.True(t, isFinalRetry("TestFoo/SubTest (retry 1) (final)")) + require.False(t, isFinalRetry("TestFoo")) + require.False(t, isFinalRetry("TestFoo (retry 1)")) + require.False(t, isFinalRetry("TestFinal")) +} diff --git a/tools/flakereport/report.go b/tools/flakereport/report.go new file mode 100644 index 00000000000..c68292610eb --- /dev/null +++ b/tools/flakereport/report.go @@ -0,0 +1,102 @@ +package flakereport + +import ( + "fmt" + "math" + "strings" + "time" +) + +const boldFlakeRateThreshold = 5.0 + +// hoursAgo formats a timestamp as "Xh ago" relative to now. +func hoursAgo(t time.Time) string { + h := math.Round(time.Since(t).Hours()) + if h < 1 { + h = 1 + } + return fmt.Sprintf("%dh ago", int(h)) +} + +// formatReportLines returns a plain-text bullet line per report. +func formatReportLines(reports []TestReport) []string { + var lines []string + for _, r := range reports { + pct := 0.0 + if r.TotalRuns > 0 { + pct = float64(r.FailureCount) / float64(r.TotalRuns) * 100.0 + } + lines = append(lines, fmt.Sprintf("• %.1f%% (%d/%d): `%s`", + pct, r.FailureCount, r.TotalRuns, r.TestName)) + } + return lines +} + +// formatLinks formats GitHub URLs as numbered markdown links +func formatLinks(urls []string, maxLinks int) string { + linkCount := len(urls) + if linkCount > maxLinks { + linkCount = maxLinks + } + var parts []string + for i := 0; i < linkCount; i++ { + parts = append(parts, fmt.Sprintf("[%d](%s)", i+1, urls[i])) + } + return strings.Join(parts, " ") +} + +// generateSuiteBreakdownTable creates a markdown table of per-suite flake data +func generateSuiteBreakdownTable(suiteReports []SuiteReport) string { + if len(suiteReports) == 0 { + return "" + } + + var sb strings.Builder + sb.WriteString("| Suite | Flake Rate | Last Failure |\n") + sb.WriteString("|-------|------------|-------------|\n") + + for _, sr := range suiteReports { + lastFailure := "-" + if sr.FailedRuns > 0 && !sr.LastFailure.IsZero() { + lastFailure = hoursAgo(sr.LastFailure) + } + rate := fmt.Sprintf("%.1f%% (%d/%d)", sr.FlakeRate, sr.FailedRuns, sr.TotalRuns) + if sr.FlakeRate > boldFlakeRateThreshold { + rate = "**" + rate + "**" + } + sb.WriteString(fmt.Sprintf("| `%s` | %s | %s |\n", sr.SuiteName, rate, lastFailure)) + } + + return sb.String() +} + +// generateTestReportTable creates a markdown table of test reports with rate column. +func generateTestReportTable(reports []TestReport, rateHeader string, maxLinks int) string { + if len(reports) == 0 { + return "" + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("| Test | %s | Last Failure | Links |\n", rateHeader)) + sb.WriteString("|------|------------|-------------|-------|\n") + + for _, report := range reports { + pct := 0.0 + if report.TotalRuns > 0 { + pct = float64(report.FailureCount) / float64(report.TotalRuns) * 100.0 + } + links := formatLinks(report.GitHubURLs, maxLinks) + lastFailure := "N/A" + if !report.LastFailure.IsZero() { + lastFailure = hoursAgo(report.LastFailure) + } + rate := fmt.Sprintf("%.1f%% (%d/%d)", pct, report.FailureCount, report.TotalRuns) + if pct > boldFlakeRateThreshold { + rate = "**" + rate + "**" + } + sb.WriteString(fmt.Sprintf("| `%s` | %s | %s | %s |\n", + report.TestName, rate, lastFailure, links)) + } + + return sb.String() +} diff --git a/tools/flakereport/slack.go b/tools/flakereport/slack.go new file mode 100644 index 00000000000..99c2bfe31b4 --- /dev/null +++ b/tools/flakereport/slack.go @@ -0,0 +1,318 @@ +package flakereport + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strings" + "time" +) + +// SlackMessage represents Slack Block Kit message +type SlackMessage struct { + Text string `json:"text"` + Blocks []SlackBlock `json:"blocks"` +} + +type SlackBlock struct { + Type string `json:"type"` + Text *SlackText `json:"text,omitempty"` + Fields []SlackText `json:"fields,omitempty"` +} + +type SlackText struct { + Type string `json:"type"` + Text string `json:"text"` +} + +// truncateToSlackLimit truncates text to stay within Slack's block text limit +// Slack blocks have a 3000 character limit per text field +func truncateToSlackLimit(text string, limit int) string { + if len(text) <= limit { + return text + } + return text[:limit-50] + "\n\n...(truncated due to length)" +} + +// buildSuccessMessage creates success notification with report summary +func buildSuccessMessage(summary *ReportSummary, runID, repo string, days int) *SlackMessage { + // Calculate CI success rate + ciSuccessRate := 0.0 + if summary.TotalWorkflowRuns > 0 { + ciSuccessRate = (float64(summary.SuccessfulRuns) / float64(summary.TotalWorkflowRuns)) * 100.0 + } + + // Summary stats + summaryText := fmt.Sprintf("*CI Success Rate:* %d/%d (%.2f%%)\n*Total Test Runs:* %d\n*Total Failures:* %d\n*Failure Rate:* %.2f per 1000 tests\n\n*CI Breakers:* %d\n*Crashes:* %d\n*Flaky Tests:* %d\n*Timeouts:* %d", + summary.SuccessfulRuns, + summary.TotalWorkflowRuns, + ciSuccessRate, + summary.TotalTestRuns, + summary.TotalFailures, + summary.OverallFailureRate, + len(summary.CIBreakers), + len(summary.Crashes), + summary.TotalFlakyCount, + len(summary.Timeouts)) + + // Build message + msg := &SlackMessage{ + Text: "Flaky Tests Report Generated", + Blocks: []SlackBlock{ + { + Type: "header", + Text: &SlackText{ + Type: "plain_text", + Text: fmt.Sprintf("Flaky Tests Report - Last %d Days", days), + }, + }, + { + Type: "section", + Text: &SlackText{ + Type: "mrkdwn", + Text: truncateToSlackLimit(summaryText, 2900), // Keep under 3000 char limit + }, + }, + }, + } + + // Add CI breakers details + if lines := formatReportLines(summary.CIBreakers); len(lines) > 0 { + if len(lines) > slackMaxListItems { + lines = lines[:slackMaxListItems] + } + text := fmt.Sprintf("*CI Breakers (top %d):*\n%s", slackMaxListItems, strings.Join(lines, "\n")) + msg.Blocks = append(msg.Blocks, SlackBlock{ + Type: "section", + Text: &SlackText{ + Type: "mrkdwn", + Text: truncateToSlackLimit(text, 2900), + }, + }) + } + + // Add flaky tests details (already sorted by failure rate) + var flakyFiltered []TestReport + for _, r := range summary.FlakyTests { + if r.FailureCount >= minFlakyFailures { + flakyFiltered = append(flakyFiltered, r) + } + } + if lines := formatReportLines(flakyFiltered); len(lines) > 0 { + if len(lines) > slackMaxListItems { + lines = lines[:slackMaxListItems] + } + text := fmt.Sprintf("*Flaky Tests (top %d):*\n%s", slackMaxListItems, strings.Join(lines, "\n")) + msg.Blocks = append(msg.Blocks, SlackBlock{ + Type: "section", + Text: &SlackText{ + Type: "mrkdwn", + Text: truncateToSlackLimit(text, 2900), + }, + }) + } + + // Add link to report + if runID != "" { + linkURL := fmt.Sprintf("https://github.com/%s/actions/runs/%s", repo, runID) + msg.Blocks = append(msg.Blocks, SlackBlock{ + Type: "section", + Text: &SlackText{ + Type: "mrkdwn", + Text: fmt.Sprintf("<%s|Report & Artifacts>", linkURL), + }, + }) + } + + return msg +} + +// addBisectSection appends a Bayesian bisect section to an existing Slack message. +// TODO seankane: after validating this methodology can identify problematic commits +// add the section to the Slack message. +func (msg *SlackMessage) addBisectSection(reports []TestBisectReport, repo string) { + // Count qualifying + qualifying := 0 + for _, r := range reports { + if !r.Skipped { + qualifying++ + } + } + if qualifying == 0 { + return + } + + // Find hot commits (top suspect in 2+ tests) + type commitCount struct { + title string + count int + } + hotCommits := make(map[string]*commitCount) + for _, r := range reports { + if r.Skipped || len(r.TopSuspects) == 0 { + continue + } + top := r.TopSuspects[0] + if _, ok := hotCommits[top.CommitSHA]; !ok { + hotCommits[top.CommitSHA] = &commitCount{title: top.CommitTitle} + } + hotCommits[top.CommitSHA].count++ + } + + var lines []string + for sha, cc := range hotCommits { + if cc.count >= 2 { + shortSHA := sha + if len(shortSHA) > 7 { + shortSHA = shortSHA[:7] + } + commitURL := fmt.Sprintf("https://github.com/%s/commit/%s", repo, sha) + title := cc.title + if title == sha || title == "" { + title = shortSHA + } + lines = append(lines, fmt.Sprintf("• <%s|%s> — %d tests — %s", commitURL, shortSHA, cc.count, title)) + } + } + + if len(lines) == 0 { + // No multi-test suspects; just report the count + msg.Blocks = append(msg.Blocks, SlackBlock{ + Type: "section", + Text: &SlackText{ + Type: "mrkdwn", + Text: fmt.Sprintf("*🔍 Commit Suspects (Bayesian)*\n%d tests analyzed. See GitHub summary for details.", qualifying), + }, + }) + return + } + + // Sort lines for deterministic output + sort.Strings(lines) + if len(lines) > slackMaxListItems { + lines = lines[:slackMaxListItems] + } + + text := fmt.Sprintf("*🔍 Hot Commits (Bayesian, implicated in 2+ tests):*\n%s\n\nSee GitHub summary for full details.", + strings.Join(lines, "\n")) + msg.Blocks = append(msg.Blocks, SlackBlock{ + Type: "section", + Text: &SlackText{ + Type: "mrkdwn", + Text: truncateToSlackLimit(text, 2900), + }, + }) +} + +// buildFailureMessage creates failure notification +func buildFailureMessage(runID, refName, sha, repo string) *SlackMessage { + msg := &SlackMessage{ + Text: "Flaky Tests Report Generation Failed", + Blocks: []SlackBlock{ + { + Type: "header", + Text: &SlackText{ + Type: "plain_text", + Text: "Flaky Tests Report Generation Failed", + }, + }, + { + Type: "section", + Fields: []SlackText{ + { + Type: "mrkdwn", + Text: fmt.Sprintf("*Run ID:*\n%s", runID), + }, + { + Type: "mrkdwn", + Text: fmt.Sprintf("*Branch:*\n%s", refName), + }, + { + Type: "mrkdwn", + Text: fmt.Sprintf("*Commit:*\n%.7s", sha), + }, + { + Type: "mrkdwn", + Text: "*Status:*\nFailed", + }, + }, + }, + }, + } + + // Add link to workflow run + if runID != "" { + linkURL := fmt.Sprintf("https://github.com/%s/actions/runs/%s", repo, runID) + msg.Blocks = append(msg.Blocks, SlackBlock{ + Type: "section", + Text: &SlackText{ + Type: "mrkdwn", + Text: fmt.Sprintf("<%s|View Workflow Run>", linkURL), + }, + }) + } + + return msg +} + +// renderMarkdown renders a SlackMessage as markdown, treating each block's text as markdown. +func (msg *SlackMessage) renderMarkdown() string { + var sb strings.Builder + for _, block := range msg.Blocks { + if block.Text != nil { + sb.WriteString(block.Text.Text) + sb.WriteString("\n\n") + } + for _, field := range block.Fields { + sb.WriteString(field.Text) + sb.WriteString("\n") + } + if len(block.Fields) > 0 { + sb.WriteString("\n") + } + } + return sb.String() +} + +// send sends message to webhook URL +func (msg *SlackMessage) send(webhookURL string) error { + if webhookURL == "" { + return errors.New("webhook URL is empty") + } + + jsonData, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("failed to marshal message: %w", err) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + } + + req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer func() { + if err := resp.Body.Close(); err != nil { + fmt.Printf("Warning: Failed to close response body: %v\n", err) + } + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + fmt.Println("Slack notification sent successfully") + return nil +} diff --git a/tools/flakereport/types.go b/tools/flakereport/types.go new file mode 100644 index 00000000000..d0d51a19bb1 --- /dev/null +++ b/tools/flakereport/types.go @@ -0,0 +1,145 @@ +package flakereport + +import "time" + +// TestFailure represents a single test failure extracted from JUnit XML +type TestFailure struct { + ClassName string // Test class/module name + Name string // Test function name + SuiteName string // Top-level test suite name + ArtifactID string // Artifact identifier from GitHub + RunID int64 // GitHub Actions run ID + JobID string // GitHub Actions job ID (or "unknown") + MatrixName string // DB config name from artifact name (e.g. "sqlite", "cassandra") + Timestamp time.Time // When the workflow run was created +} + +// TestRun represents a test execution (success or failure) +type TestRun struct { + SuiteName string // Top-level test suite name + Name string // Test name + Failed bool // Whether the test failed + Skipped bool // Whether the test was skipped + RunID int64 // Workflow run ID + JobID string // GitHub Actions job ID (unique per matrix job/shard) + MatrixName string // DB config name from artifact name (e.g. "sqlite", "cassandra") +} + +// TestReport represents aggregated failures for a single test +type TestReport struct { + TestName string // Normalized test name (retry suffix stripped) + FailureCount int // Total number of failures + TotalRuns int // Total number of times this test ran (including successes) + GitHubURLs []string // Up to max_links failure URLs + LastFailure time.Time // Timestamp of the most recent failure +} + +// SuiteReport represents aggregated flake data for a test suite +type SuiteReport struct { + SuiteName string // Test suite name from JUnit XML + FlakeRate float64 // Percentage of job executions with at least one non-retry failure + FailedRuns int // Number of job executions with at least one non-retry failure + TotalRuns int // Total number of job executions where this suite appeared + LastFailure time.Time // Timestamp of the most recent failure +} + +// ReportSummary contains all processed report data +type ReportSummary struct { + FlakyTests []TestReport + Timeouts []TestReport // Tests ending with "(timeout)" + Crashes []TestReport // Tests containing "crash" + CIBreakers []TestReport // Tests that failed all retries (3x) in a single job + Suites []SuiteReport // Per-suite flake breakdown + TotalFailures int // Total raw failure count + TotalTestRuns int // Total test executions (all tests, all runs) + OverallFailureRate float64 // Overall failures per 1000 test runs + TotalFlakyCount int // Total flaky tests (not just top 10) + TotalWorkflowRuns int // Total workflow runs analyzed + SuccessfulRuns int // Workflow runs that succeeded +} + +// FailedTestRecord represents a single test failure for the failures.json analytics export +type FailedTestRecord struct { + SuiteName string `json:"suite_name"` + TestName string `json:"test_name"` + FailureDate string `json:"failure_date"` + Link string `json:"link"` + FailureType string `json:"failure_type"` +} + +// WorkflowRun represents a GitHub Actions workflow run +type WorkflowRun struct { + ID int64 `json:"id"` + Number int `json:"run_number"` + CreatedAt time.Time `json:"created_at"` + Status string `json:"status"` + Conclusion string `json:"conclusion"` + HeadBranch string `json:"head_branch"` + HeadSHA string `json:"head_sha"` +} + +// WorkflowArtifact represents a downloadable artifact +type WorkflowArtifact struct { + ID int64 `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + Expired bool `json:"expired"` +} + +// ArtifactsResponse represents the GitHub API response for artifacts +type ArtifactsResponse struct { + TotalCount int `json:"total_count"` + Artifacts []WorkflowArtifact `json:"artifacts"` +} + +// CommitObservation holds aggregated pass/fail data for a single (test, commit) pair. +type CommitObservation struct { + CommitSHA string + CommitIdx int // chronological index (0 = oldest) + Prior float64 // prior weight (1.0 = uniform; adjusted by heuristics) + HeuristicNote string // reason for prior adjustment, if any + Passes int + Fails int +} + +// BisectResult is one candidate culprit commit with its posterior probability. +type BisectResult struct { + CommitSHA string + CommitIdx int + Probability float64 // posterior P(this commit introduced the flakiness) + PassesBefore int + FailsBefore int + PassesAfter int + FailsAfter int + CommitTitle string + CommitAuthor string + CommitDate string // formatted date of the commit, e.g. "2024-01-15" + HeuristicNote string // e.g. "only touches .github/ — deprioritized" +} + +// TestBisectReport is the full bisect output for a single test. +type TestBisectReport struct { + TestName string + TopSuspects []BisectResult // sorted by Probability descending + TotalObs int // total observations (pass + fail) used + Skipped bool // true if below signal or confidence threshold +} + +// CommitMeta holds changed-file info fetched from the GitHub API. +// GET /repos/{owner}/{repo}/commits/{sha} +type CommitMeta struct { + SHA string + Title string + Author string + CommittedAt time.Time + Files []string // relative paths of changed files +} + +// BisectConfig holds configuration for a bisect analysis run. +type BisectConfig struct { + Repo string + TopN int // max tests to analyze; 0 = all qualifying tests + MinFailures int + MinRuns int + MinProbability float64 // only report tests whose top suspect exceeds this (0–1); 0 = report all +} diff --git a/tools/flakereport/writer.go b/tools/flakereport/writer.go new file mode 100644 index 00000000000..70ebdc4659f --- /dev/null +++ b/tools/flakereport/writer.go @@ -0,0 +1,233 @@ +package flakereport + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" +) + +// writeFailuresJSON writes failures.json containing every individual test failure (for analytics). +func writeFailuresJSON(outputDir string, failures []TestFailure, repo string) error { + records := make([]FailedTestRecord, 0, len(failures)) + for _, f := range failures { + runIDStr := strconv.FormatInt(f.RunID, 10) + records = append(records, FailedTestRecord{ + SuiteName: f.SuiteName, + TestName: f.Name, + FailureDate: f.Timestamp.Format(time.RFC3339), + Link: buildGitHubURL(repo, runIDStr, f.JobID), + FailureType: classifyFailure(f.Name), + }) + } + + data, err := json.MarshalIndent(records, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal failures.json: %w", err) + } + if err := os.WriteFile(filepath.Join(outputDir, "failures.json"), data, 0644); err != nil { + return fmt.Errorf("failed to write failures.json: %w", err) + } + return nil +} + +// generateGitHubSummary creates markdown summary for GitHub Actions +func generateGitHubSummary(summary *ReportSummary, runID string, maxLinks int) string { + timestamp := time.Now().Format("2006-01-02 15:04:05") + + var content string + content += fmt.Sprintf("## Flaky Tests Report - %s\n\n", timestamp) + + // Overall statistics + content += "### Overall Statistics\n\n" + + // CI success rate + ciSuccessRate := 0.0 + if summary.TotalWorkflowRuns > 0 { + ciSuccessRate = (float64(summary.SuccessfulRuns) / float64(summary.TotalWorkflowRuns)) * 100.0 + } + content += fmt.Sprintf("* **CI Success Rate**: %d/%d (%.2f%%)\n", summary.SuccessfulRuns, summary.TotalWorkflowRuns, ciSuccessRate) + content += fmt.Sprintf("* **Total Test Runs**: %d\n", summary.TotalTestRuns) + content += fmt.Sprintf("* **Total Failures**: %d\n", summary.TotalFailures) + content += fmt.Sprintf("* **Overall Failure Rate**: %.1f per 1000 tests\n\n", summary.OverallFailureRate) + + // Summary table + content += "### Failure Categories Summary\n\n" + content += "| Category | Unique Tests |\n" + content += "|----------|--------------|\n" + content += fmt.Sprintf("| CI Breakers | %d |\n", len(summary.CIBreakers)) + content += fmt.Sprintf("| Crashes | %d |\n", len(summary.Crashes)) + content += fmt.Sprintf("| Timeouts | %d |\n", len(summary.Timeouts)) + content += fmt.Sprintf("| Flaky Tests | %d |\n\n", summary.TotalFlakyCount) + + // CI Breakers section (tests that failed all retries) + if len(summary.CIBreakers) > 0 { + content += "### CI Breakers (Failed All Retries)\n\n" + content += generateTestReportTable(summary.CIBreakers, "CI Break Rate", maxLinks) + "\n" + } + + // Crashes section + if len(summary.Crashes) > 0 { + content += "### Crashes\n\n" + content += generateTestReportTable(summary.Crashes, "Crash Rate", maxLinks) + "\n" + } + + // Timeouts section + if len(summary.Timeouts) > 0 { + content += "### Timeouts\n\n" + content += generateTestReportTable(summary.Timeouts, "Flake Rate", maxLinks) + "\n" + } + + // Flaky tests section (show ALL tests) + if len(summary.FlakyTests) > 0 { + content += "### Flaky Tests\n\n" + content += generateTestReportTable(summary.FlakyTests, "Flake Rate", maxLinks) + "\n" + } + + // Flaky suites + if len(summary.Suites) > 0 { + content += "### Flaky Suites\n\n" + content += generateSuiteBreakdownTable(summary.Suites) + "\n" + } + + // Link to run + if runID != "" { + content += fmt.Sprintf("\n[View Full Report & Artifacts](https://github.com/%s/actions/runs/%s)\n", defaultRepository, runID) + } + + return content +} + +// countQualifyingBisectReports returns the number of non-skipped reports. +func countQualifyingBisectReports(reports []TestBisectReport) int { + n := 0 + for _, r := range reports { + if !r.Skipped { + n++ + } + } + return n +} + +// escapeTableCell replaces pipe characters so they don't corrupt GFM table rows. +func escapeTableCell(s string) string { + return strings.ReplaceAll(s, "|", "|") +} + +// writeBisectTable writes all suspect (test, commit) pairs into a single flat table. +func writeBisectTable(sb *strings.Builder, reports []TestBisectReport, repo string) { + sb.WriteString("| Test | Prob | Commit | Date | Author | Before | After | Note |\n") + sb.WriteString("|------|------|--------|------|--------|--------|-------|------|\n") + for _, r := range reports { + if r.Skipped || len(r.TopSuspects) == 0 { + continue + } + for _, s := range r.TopSuspects { + shortSHA := s.CommitSHA + if len(shortSHA) > 7 { + shortSHA = shortSHA[:7] + } + commitURL := fmt.Sprintf("https://github.com/%s/commit/%s", repo, s.CommitSHA) + title := s.CommitTitle + if title == s.CommitSHA || title == "" { + title = shortSHA + } + beforeStr := fmt.Sprintf("%d/%d (%.0f%%)", s.FailsBefore, s.PassesBefore+s.FailsBefore, + pct(s.FailsBefore, s.PassesBefore+s.FailsBefore)) + afterStr := fmt.Sprintf("%d/%d (%.0f%%)", s.FailsAfter, s.PassesAfter+s.FailsAfter, + pct(s.FailsAfter, s.PassesAfter+s.FailsAfter)) + fmt.Fprintf(sb, "| `%s` | %.1f%% | [%s](%s) %s | %s | %s | %s | %s | %s |\n", + escapeTableCell(r.TestName), s.Probability*100, shortSHA, commitURL, escapeTableCell(title), + s.CommitDate, escapeTableCell(s.CommitAuthor), beforeStr, afterStr, escapeTableCell(s.HeuristicNote)) + } + } + sb.WriteString("\n") +} + +// generateBisectSummary creates the markdown section for bisect results to append to the GitHub summary. +func generateBisectSummary(reports []TestBisectReport, repo string, minProb float64) string { + qualifying := countQualifyingBisectReports(reports) + + skipped := len(reports) - qualifying + threshold := fmt.Sprintf("%.0f%%", minProb*100) + + var sb strings.Builder + sb.WriteString("\n## Bayesian Commit Suspects\n\n") + + if qualifying == 0 { + sb.WriteString("No actionable commit suspects found") + if skipped > 0 { + sb.WriteString(fmt.Sprintf(" — %d tests analyzed but none above %s confidence", skipped, threshold)) + } + sb.WriteString("\n") + return sb.String() + } + + sb.WriteString(fmt.Sprintf("%d tests with actionable suspects (≥%s confidence)", qualifying, threshold)) + if skipped > 0 { + sb.WriteString(fmt.Sprintf(", %d below confidence threshold", skipped)) + } + sb.WriteString("\n\n") + + // Sort by top suspect probability descending so the most actionable rows appear first. + sort.Slice(reports, func(i, j int) bool { + pi := 0.0 + if len(reports[i].TopSuspects) > 0 { + pi = reports[i].TopSuspects[0].Probability + } + pj := 0.0 + if len(reports[j].TopSuspects) > 0 { + pj = reports[j].TopSuspects[0].Probability + } + return pi > pj + }) + + writeBisectTable(&sb, reports, repo) + return sb.String() +} + +// pct returns percentage of num/denom, returning 0 if denom is 0. +func pct(num, denom int) float64 { + if denom == 0 { + return 0 + } + return float64(num) / float64(denom) * 100.0 +} + +// writeGitHubSummary writes markdown summary to GITHUB_STEP_SUMMARY (if set) +// and always writes to outputDir/github-report.md. +func writeGitHubSummary(content string, outputDir string) error { + // Always write to output dir + outPath := filepath.Join(outputDir, "github-report.md") + if err := os.WriteFile(outPath, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write github-report.md: %w", err) + } + fmt.Printf("GitHub report written to %s\n", outPath) + + // Also write to GITHUB_STEP_SUMMARY if available + summaryFile := os.Getenv("GITHUB_STEP_SUMMARY") + if summaryFile == "" { + return nil + } + + file, err := os.OpenFile(summaryFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open summary file: %w", err) + } + defer func() { + if err := file.Close(); err != nil { + fmt.Printf("Warning: Failed to close summary file: %v\n", err) + } + }() + + if _, err := file.WriteString(content); err != nil { + return fmt.Errorf("failed to write summary: %w", err) + } + + fmt.Println("GitHub Actions step summary written") + return nil +} diff --git a/tools/flakes/.python-version b/tools/flakes/.python-version deleted file mode 100644 index 24ee5b1be99..00000000000 --- a/tools/flakes/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/tools/flakes/README.md b/tools/flakes/README.md deleted file mode 100644 index dc53221740f..00000000000 --- a/tools/flakes/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Flaky Tests Analysis Tool - -This tool processes test failure data and generates reports, with optional GitHub Actions summaries and Slack notifications. - -## Usage - -### Basic Processing -```bash -# Process test data and generate reports -uv run main.py --file out.json -``` - -### With GitHub Actions Summary -```bash -# Process data and generate GitHub Actions summary -uv run main.py \ - --file out.json \ - --github-summary \ - --run-id "123456789" -``` - -### With Slack Notifications -```bash -# Process data, generate summary, and send Slack notification -uv run main.py \ - --file out.json \ - --github-summary \ - --slack-webhook "https://hooks.slack.com/services/..." \ - --run-id "123456789" \ - --ref-name "main" \ - --sha "abc123def456" -``` - -## Command Line Options - -- `--file`, `-f`: Input JSON file to process (default: out.json) -- `--github-summary`: Generate GitHub Actions summary (writes to GITHUB_STEP_SUMMARY) -- `--slack-webhook`: Slack webhook URL for notifications -- `--run-id`: GitHub Actions run ID -- `--ref-name`: Git branch name (required for failure messages) -- `--sha`: Git commit SHA (required for failure messages) - -## Output Files - -The tool generates several report files: -- `flaky.txt`: Markdown table of flaky tests (includes all retry attempts grouped by test name) -- `flaky_count.txt`: Total count of flaky tests (for summary table) -- `flaky_slack.txt`: Plain text version for Slack -- `crash.txt`: Tests that crashed -- `timeout.txt`: Tests that timed out - -## Features - -### Retry Grouping -Tests with retry suffixes (e.g., "TestName (retry 1)", "TestName (retry 2)") are automatically grouped together under the base test name. This ensures that: -- All retry attempts are combined into a single entry in the flaky tests report -- Failure counts include all attempts (base + all retries) -- All failure links are preserved and displayed -- No separate "Retry Failures" section is needed since retries are grouped with their base tests - -## GitHub Actions Integration - -### Automatic Error Handling -- **Success Case**: Processes data, generates GitHub Actions summary, and sends success Slack notification -- **Failure Case**: Detects failures and sends Slack notification with workflow failure details - -### GitHub Actions Summary -When `--github-summary` is specified, the tool: -- Creates a comprehensive summary with failure counts by category -- Includes detailed tables for each failure type -- Writes directly to the GitHub Actions step summary -- Provides links back to the workflow run - -### Workflow Integration -The tool is typically called once in the workflow with all necessary parameters: -```yaml -- name: Run Python script to process flaky tests - run: | - cd tools/flakes && uv run main.py \ - --file ../../out.json \ - --github-summary \ - --slack-webhook "${{ secrets.SLACK_WEBHOOK }}" \ - --run-id "${{ github.run_id }}" \ - --ref-name "${{ github.ref_name }}" \ - --sha "${{ github.sha }}" -``` diff --git a/tools/flakes/main.py b/tools/flakes/main.py deleted file mode 100644 index 4e8af6a6d4f..00000000000 --- a/tools/flakes/main.py +++ /dev/null @@ -1,604 +0,0 @@ -import argparse -import json -import os -import re -import sys -from datetime import datetime -from typing import Any, Dict - -import requests - - -# Minimum number of failures for a test to be considered flaky -MIN_FLAKY_FAILURES = 3 - - -def normalize_test_name(name: str) -> str: - """ - Strip retry suffix from test name. - Examples: - - "TestSomething (retry 1)" -> "TestSomething" - - "TestSomething (retry 2)" -> "TestSomething" - - "TestSomething" -> "TestSomething" - """ - return re.sub(r'\s*\(retry \d+\)$', '', name) - - -def process_tests(data, pattern, output_file: str, max_links: int = 3): - # Group data by test name and collect artifacts for tests matching pattern - test_groups = {} - for item in data: - name_parts = item["name"].split("/") - if len(name_parts) < 2: - continue - if not item["name"].endswith(pattern): - continue - - test_name = normalize_test_name(item["name"]) - if test_name not in test_groups: - test_groups[test_name] = [] - - parts = item["artifact"].split("--") - if len(parts) > 0 and len(parts[1]) > 0 and len(parts[2]) > 0: - p2 = parts[2] - if p2 == "unknown": - job_url = ( - f"https://github.com/temporalio/temporal/actions/runs/{parts[1]}" - ) - else: - job_url = f"https://github.com/temporalio/temporal/actions/runs/{parts[1]}/job/{p2}" - else: - job_url = item["artifact"] - - test_groups[test_name].append(job_url) - - # Transform into list with counts and multiple links - transformed = [] - for test_name, artifacts in test_groups.items(): - failure_count = len(artifacts) - # Get up to max_links most recent artifacts (already sorted desc from SQL) - recent_artifacts = artifacts[:max_links] - - transformed.append({ - "name": test_name, - "count": failure_count, - "artifacts": recent_artifacts, - }) - - # Sort by failure count descending - transformed.sort(key=lambda x: x["count"], reverse=True) - - # Write bullet point format - lines = [] - for item in transformed: - # Format: * XXX failures: `TestName` [1](url1) [2](url2) [3](url3) - links = " ".join([f"[{i+1}]({url})" for i, url in enumerate(item["artifacts"])]) - lines.append(f"* {item['count']} failures: `{item['name']}` {links}\n") - - with open(output_file, "w") as outfile: - outfile.writelines(lines) - - -def process_crash(data, pattern, output_file: str, max_links: int = 3): - # Group data by test name and collect artifacts for crash tests - test_groups = {} - for item in data: - if "crash" not in item["name"]: - continue - - test_name = normalize_test_name(item["name"]) - if test_name not in test_groups: - test_groups[test_name] = [] - - parts = item["artifact"].split("--") - if len(parts) > 0 and len(parts[1]) > 0: - job_url = ( - f"https://github.com/temporalio/temporal/actions/runs/{parts[1]}" - ) - else: - job_url = item["artifact"] - - test_groups[test_name].append(job_url) - - # Transform into list with counts and multiple links - transformed = [] - for test_name, artifacts in test_groups.items(): - failure_count = len(artifacts) - recent_artifacts = artifacts[:max_links] - transformed.append({ - "name": test_name, - "count": failure_count, - "artifacts": recent_artifacts, - }) - - # Sort by failure count descending - transformed.sort(key=lambda x: x["count"], reverse=True) - - # Write bullet point format - lines = [] - for item in transformed: - links = " ".join([f"[{i+1}]({url})" for i, url in enumerate(item["artifacts"])]) - lines.append(f"* {item['count']} failures: `{item['name']}` {links}\n") - - with open(output_file, "w") as outfile: - outfile.writelines(lines) - - -def process_flaky(data, output_file: str, max_links: int = 3): - """ - Process flaky tests and generate reports. - - Args: - data: Test failure data - output_file: Path to output file - max_links: Maximum number of failure links to include per test - - Returns: - Total count of tests with > MIN_FLAKY_FAILURES - """ - # Group data by test name and collect artifacts - test_groups = {} - for item in data: - name_parts = item["name"].split("/") - if len(name_parts) < 2: - continue - - test_name = normalize_test_name(item["name"]) - if test_name not in test_groups: - test_groups[test_name] = [] - - parts = item["artifact"].split("--") - if len(parts) > 0 and len(parts[1]) > 0 and len(parts[2]) > 0: - p2 = parts[2] - if p2 == "unknown": - job_url = ( - f"https://github.com/temporalio/temporal/actions/runs/{parts[1]}" - ) - else: - job_url = f"https://github.com/temporalio/temporal/actions/runs/{parts[1]}/job/{p2}" - else: - job_url = item["artifact"] - - test_groups[test_name].append(job_url) - - # Transform into list with counts and multiple links - transformed = [] - for test_name, artifacts in test_groups.items(): - failure_count = len(artifacts) - - # Only include tests that meet the minimum threshold - if failure_count <= MIN_FLAKY_FAILURES: - continue - - # Get up to max_links most recent artifacts (already sorted desc from SQL) - recent_artifacts = artifacts[:max_links] - - transformed.append({ - "name": test_name, - "count": failure_count, - "artifacts": recent_artifacts, - }) - - # Sort by failure count descending - transformed.sort(key=lambda x: x["count"], reverse=True) - - # Remember total count before limiting for display - total_count = len(transformed) - - # Limit to top 10 flaky tests for display - display_tests = transformed[:10] - - # Write bullet point format (for GitHub) - lines = [] - for item in display_tests: - # Format: * XXX failures: `TestName` [1](url1) [2](url2) [3](url3) - links = " ".join([f"[{i+1}]({url})" for i, url in enumerate(item["artifacts"])]) - lines.append(f"* {item['count']} failures: `{item['name']}` {links}\n") - - with open(output_file, "w") as outfile: - outfile.writelines(lines) - - # Also create a plain text version for Slack (without links for cleaner viewing) - slack_file = output_file.replace(".txt", "_slack.txt") - slack_lines = [] - for item in display_tests: - # Format for Slack: • XXX failures: `TestName` - slack_lines.append(f"• {item['count']} failures: `{item['name']}`\n") - - with open(slack_file, "w") as outfile: - outfile.writelines(slack_lines) - - # Write count metadata for summary - count_file = output_file.replace(".txt", "_count.txt") - with open(count_file, "w") as f: - f.write(str(total_count)) - - return total_count - - -def create_success_message( - crash_count: int, - flaky_count: int, - timeout_count: int, - flaky_content: str, - run_id: str, - total_failures: int, -) -> Dict[str, Any]: - """Create a success Slack message with flaky tests report.""" - - blocks = [ - { - "type": "section", - "text": {"type": "mrkdwn", "text": "Flaky Tests Report - Last 7 Days"}, - }, - { - "type": "section", - "text": {"type": "mrkdwn", "text": f"Total Failures: {total_failures}"}, - }, - { - "type": "section", - "fields": [ - {"type": "mrkdwn", "text": f"Crashes: {crash_count}"}, - {"type": "mrkdwn", "text": f"Flaky Tests: {flaky_count}"}, - {"type": "mrkdwn", "text": f"Timeouts: {timeout_count}"}, - ], - }, - ] - - # Add flaky tests details if there are any - if flaky_count > 0 and flaky_content: - blocks.append( - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"Flaky Tests Details:\n{flaky_content}", - }, - } - ) - - # Add link to full report - blocks.append( - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"", - }, - } - ) - - return {"text": "Flaky Tests Report - Last 7 Days", "blocks": blocks} - - -def create_failure_message(run_id: str, ref_name: str, sha: str) -> Dict[str, Any]: - """Create a failure Slack message.""" - - blocks = [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Flaky Tests Report Generation Failed*", - }, - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "The flaky tests report workflow failed to generate the report.", - }, - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": f"*Run ID:* ", - }, - {"type": "mrkdwn", "text": f"*Branch:* `{ref_name}`"}, - { - "type": "mrkdwn", - "text": f"*Commit:* ", - }, - {"type": "mrkdwn", "text": "*Status:* Failed"}, - ], - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "Please check the workflow logs for more details.", - }, - }, - ] - - return {"text": "Flaky Tests Report Generation Failed", "blocks": blocks} - - -def send_slack_message(webhook_url: str, message: Dict[str, Any]) -> bool: - """Send message to Slack webhook.""" - try: - response = requests.post( - webhook_url, - json=message, - headers={"Content-Type": "application/json"}, - timeout=30, - ) - response.raise_for_status() - print(f"Slack message sent successfully (status: {response.status_code})") - return True - except requests.exceptions.RequestException as e: - print(f"Failed to send Slack message: {e}", file=sys.stderr) - return False - - -def read_flaky_content( - file_path: str, max_items: int = 10, max_length: int = 1000 -) -> str: - """Read and format flaky tests content for Slack.""" - try: - if not os.path.exists(file_path): - return "" - - with open(file_path, "r") as f: - lines = f.readlines() - - # Take first max_items lines and format them - content_lines = [] - for line in lines[:max_items]: - line = line.strip() - if line: - content_lines.append(line) - - # Join with newlines for proper formatting in Slack - content = "\n".join(content_lines) - if len(content) > max_length: - content = content[:max_length] + "..." - - return content - except Exception as e: - print( - f"Warning: Could not read flaky content from {file_path}: {e}", - file=sys.stderr, - ) - return "" - - -def count_failures_in_file(file_path: str) -> int: - """Count the number of failure entries in a report file.""" - try: - if not os.path.exists(file_path): - return 0 - with open(file_path, "r") as f: - content = f.read() - return content.count("* ") - except Exception: - return 0 - - -def create_github_actions_summary( - crash_count: int, - flaky_count: int, - timeout_count: int, - run_id: str, -) -> str: - """Create GitHub Actions summary content.""" - summary_lines = [] - - # Header - summary_lines.append(f"## Flaky Tests Report - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - summary_lines.append("") - - # Summary table - summary_lines.append("### Failure Categories Summary") - summary_lines.append("") - summary_lines.append("| Category | Unique Tests |") - summary_lines.append("|----------|--------------|") - summary_lines.append(f"| Crashes | {crash_count} |") - summary_lines.append(f"| Timeouts | {timeout_count} |") - summary_lines.append(f"| Flaky Tests | {flaky_count} |") - summary_lines.append("") - - # Add detailed tables for each category - if crash_count > 0 and os.path.exists("out/crash.txt"): - summary_lines.append("### Crashes") - summary_lines.append("") - with open("out/crash.txt", "r") as f: - summary_lines.append(f.read()) - summary_lines.append("") - - if timeout_count > 0 and os.path.exists("out/timeout.txt"): - summary_lines.append("### Timeouts") - summary_lines.append("") - with open("out/timeout.txt", "r") as f: - summary_lines.append(f.read()) - summary_lines.append("") - - if flaky_count > 0 and os.path.exists("out/flaky.txt"): - summary_lines.append("### Flaky Tests") - summary_lines.append("") - with open("out/flaky.txt", "r") as f: - summary_lines.append(f.read()) - summary_lines.append("") - - if crash_count == 0 and flaky_count == 0 and timeout_count == 0: - summary_lines.append("**No test failures found in the last 7 days!**") - - return "\n".join(summary_lines) - - -def write_github_actions_summary(summary_content: str) -> None: - """Write GitHub Actions summary to the step summary file.""" - try: - summary_file = os.environ.get("GITHUB_STEP_SUMMARY") - if summary_file: - with open(summary_file, "w") as f: - f.write(summary_content) - print(f"GitHub Actions summary written to {summary_file}") - else: - print("GITHUB_STEP_SUMMARY environment variable not set, skipping summary creation") - except Exception as e: - print(f"Warning: Could not write GitHub Actions summary: {e}", file=sys.stderr) - - -def process_json_file(input_filename: str, max_links: int = 3): - with open(input_filename, "r") as file: - # Load the file content as JSON - data = json.load(file) - - # Create output directory if it doesn't exist - os.makedirs("out", exist_ok=True) - - process_flaky(data, "out/flaky.txt", max_links) - process_tests(data, "(timeout)", "out/timeout.txt", max_links) - process_crash(data, "(crash)", "out/crash.txt", max_links) - - # Return total number of failures in the original data - return len(data) - - -def create_argument_parser() -> argparse.ArgumentParser: - """Create and configure the argument parser.""" - parser = argparse.ArgumentParser( - description="Process flaky test data, generate GitHub Actions summary, and optionally send Slack notifications" - ) - parser.add_argument( - "--file", - "-f", - default="out.json", - help="Input JSON file to process (default: out.json)", - ) - - # GitHub Actions summary options - parser.add_argument( - "--github-summary", - action="store_true", - help="Generate GitHub Actions summary", - ) - - # Slack notification options - parser.add_argument("--slack-webhook", help="Slack webhook URL for notifications") - parser.add_argument("--run-id", help="GitHub Actions run ID") - parser.add_argument("--ref-name", help="Git branch name") - parser.add_argument("--sha", help="Git commit SHA") - - # Display options - parser.add_argument( - "--max-links", - type=int, - default=3, - help="Maximum number of failure links to show per test (default: 3)", - ) - - return parser - - -def get_failure_counts() -> tuple[int, int, int]: - """Count failures from generated report files.""" - crash_count = count_failures_in_file("out/crash.txt") - - # For flaky tests, read from metadata file which contains total count (not just top 10) - try: - with open("out/flaky_count.txt", "r") as f: - flaky_count = int(f.read().strip()) - except (FileNotFoundError, ValueError): - # Fallback to counting lines if metadata doesn't exist - flaky_count = count_failures_in_file("out/flaky.txt") - - timeout_count = count_failures_in_file("out/timeout.txt") - - print(f"Failure counts - Crashes: {crash_count}, Flaky: {flaky_count}, Timeout: {timeout_count}") - - return crash_count, flaky_count, timeout_count - - -def handle_success_case(args, total_failures: int) -> None: - """Handle the successful processing case.""" - # Count failures from generated files - crash_count, flaky_count, timeout_count = get_failure_counts() - - # Generate GitHub Actions summary if requested - if args.github_summary: - print("Generating GitHub Actions summary...") - summary_content = create_github_actions_summary( - crash_count, flaky_count, timeout_count, args.run_id or "unknown" - ) - write_github_actions_summary(summary_content) - - if args.slack_webhook: - send_success_slack_notification(args, crash_count, flaky_count, timeout_count, total_failures) - - -def send_success_slack_notification(args, crash_count: int, flaky_count: int, timeout_count: int, total_failures: int) -> None: - """Send success Slack notification.""" - print("Sending success Slack notification...") - if not args.run_id: - print( - "Error: --run-id is required for success messages", - file=sys.stderr, - ) - sys.exit(1) - - # Read flaky content - flaky_content = read_flaky_content("out/flaky_slack.txt") - - message = create_success_message( - crash_count, - flaky_count, - timeout_count, - flaky_content, - args.run_id, - total_failures, - ) - - # Send the message - if not send_slack_message(args.slack_webhook, message): - sys.exit(1) - - -def send_failure_slack_notification(args) -> None: - """Send failure Slack notification.""" - print("Sending failure Slack notification...") - if not all([args.run_id, args.ref_name, args.sha]): - print( - "Error: --run-id, --ref-name, and --sha are required for failure messages", - file=sys.stderr, - ) - sys.exit(1) - message = create_failure_message(args.run_id, args.ref_name, args.sha) - if not send_slack_message(args.slack_webhook, message): - sys.exit(1) - - -def handle_failure_case(args, error_msg: str) -> None: - """Handle the failure case with appropriate error reporting and notifications.""" - print(error_msg, file=sys.stderr) - - if args.slack_webhook: - send_failure_slack_notification(args) - - sys.exit(1) - - -def main(): - """Main entry point for the flaky tests processing script.""" - parser = create_argument_parser() - args = parser.parse_args() - - # Try to process the JSON file and handle both success and failure cases - try: - total_failures = process_json_file(args.file, args.max_links) - print(f"Successfully processed {args.file}") - handle_success_case(args, total_failures) - - except FileNotFoundError: - handle_failure_case(args, f"Error: File {args.file} not found") - except json.JSONDecodeError as e: - handle_failure_case(args, f"Error: Invalid JSON in {args.file}: {e}") - except Exception as e: - handle_failure_case(args, f"Error processing {args.file}: {e}") - - -if __name__ == "__main__": - main() diff --git a/tools/flakes/pyproject.toml b/tools/flakes/pyproject.toml deleted file mode 100644 index 229c5c873e9..00000000000 --- a/tools/flakes/pyproject.toml +++ /dev/null @@ -1,9 +0,0 @@ -[project] -name = "flakes" -version = "0.1.0" -description = "A tool for analyzing flaky tests in the Temporal server repo" -readme = "README.md" -requires-python = ">=3.13" -dependencies = [ - "requests>=2.31.0", -] diff --git a/tools/flakes/uv.lock b/tools/flakes/uv.lock deleted file mode 100644 index 2ed7d8534df..00000000000 --- a/tools/flakes/uv.lock +++ /dev/null @@ -1,87 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13" - -[[package]] -name = "certifi" -version = "2025.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, -] - -[[package]] -name = "flakes" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "requests" }, -] - -[package.metadata] -requires-dist = [{ name = "requests", specifier = ">=2.31.0" }] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] diff --git a/tools/parallelize/parallelize.go b/tools/parallelize/parallelize.go new file mode 100644 index 00000000000..8663eb94c96 --- /dev/null +++ b/tools/parallelize/parallelize.go @@ -0,0 +1,198 @@ +package parallelize + +import ( + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strings" +) + +func Main() error { + if len(os.Args) < 2 { + return errors.New("usage: parallelize [...]") + } + + var failed bool + for _, dir := range os.Args[1:] { + if err := processDir(dir); err != nil { + fmt.Fprintf(os.Stderr, "error processing %s: %v\n", dir, err) + failed = true + } + } + if failed { + return errors.New("some files failed to process") + } + return nil +} + +func processDir(dir string) error { + return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, "_test.go") { + return nil + } + return processFile(path) + }) +} + +func processFile(path string) error { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + if err != nil { + return fmt.Errorf("parse %s: %w", path, err) + } + + // Collect line numbers where we need to insert t.Parallel(). + // Each entry is the line of the opening '{' of the test function body. + type insertion struct { + line int // line number of the '{' opening the function body + paramName string // name of the *testing.T parameter + } + var insertions []insertion + + for _, decl := range f.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + if !isTestFunc(fn) { + continue + } + paramName := testingTParamName(fn) + if paramName == "" { + continue + } + if hasParallelCall(fn.Body, paramName) { + continue + } + if hasNoLintComment(fn) { + continue + } + bodyLine := fset.Position(fn.Body.Lbrace).Line + insertions = append(insertions, insertion{line: bodyLine, paramName: paramName}) + } + + if len(insertions) == 0 { + return nil + } + + // Sort by line descending so insertions don't shift line numbers of subsequent insertions. + sort.Slice(insertions, func(i, j int) bool { + return insertions[i].line > insertions[j].line + }) + + fi, err := os.Stat(path) + if err != nil { + return fmt.Errorf("stat %s: %w", path, err) + } + + src, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + + lines := strings.Split(string(src), "\n") + for _, ins := range insertions { + // ins.line is 1-indexed, so it conveniently equals the 0-based index + // of the line right after '{', which is where we want to insert. + idx := ins.line + newLine := "\t" + ins.paramName + ".Parallel()" + lines = append(lines[:idx+1], lines[idx:]...) + lines[idx] = newLine + } + + if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), fi.Mode()); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + + fmt.Printf("parallelize: %s\n", path) + return nil +} + +// isTestFunc returns true for func TestXxx(t *testing.T). +func isTestFunc(fn *ast.FuncDecl) bool { + if fn.Recv != nil { + return false // method, not a function + } + if !strings.HasPrefix(fn.Name.Name, "Test") { + return false + } + if fn.Body == nil { + return false + } + return testingTParamName(fn) != "" +} + +// testingTParamName returns the name of the *testing.T parameter, or "" if not found. +func testingTParamName(fn *ast.FuncDecl) string { + if fn.Type.Params == nil || len(fn.Type.Params.List) == 0 { + return "" + } + for _, field := range fn.Type.Params.List { + starExpr, ok := field.Type.(*ast.StarExpr) + if !ok { + continue + } + selExpr, ok := starExpr.X.(*ast.SelectorExpr) + if !ok { + continue + } + pkg, ok := selExpr.X.(*ast.Ident) + if !ok { + continue + } + if pkg.Name == "testing" && selExpr.Sel.Name == "T" { + if len(field.Names) > 0 { + return field.Names[0].Name + } + } + } + return "" +} + +// hasNoLintComment checks for //parallelize:ignore in the function's doc comment. +func hasNoLintComment(fn *ast.FuncDecl) bool { + if fn.Doc == nil { + return false + } + for _, c := range fn.Doc.List { + if strings.Contains(c.Text, "parallelize:ignore") { + return true + } + } + return false +} + +// hasParallelCall checks if the function body already contains .Parallel(). +func hasParallelCall(body *ast.BlockStmt, paramName string) bool { + found := false + ast.Inspect(body, func(n ast.Node) bool { + if found { + return false + } + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + ident, ok := sel.X.(*ast.Ident) + if !ok { + return true + } + if ident.Name == paramName && sel.Sel.Name == "Parallel" { + found = true + } + return true + }) + return found +} diff --git a/tools/parallelize/parallelize_test.go b/tools/parallelize/parallelize_test.go new file mode 100644 index 00000000000..8efe5c5b94f --- /dev/null +++ b/tools/parallelize/parallelize_test.go @@ -0,0 +1,175 @@ +package parallelize + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestProcessFile(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + }{ + { + name: "adds t.Parallel to multiple tests", + input: `package foo_test + +import "testing" + +func TestFoo(t *testing.T) { + t.Log("foo") +} + +func TestBar(t *testing.T) { + t.Log("bar") +} +`, + expected: `package foo_test + +import "testing" + +func TestFoo(t *testing.T) { + t.Parallel() + t.Log("foo") +} + +func TestBar(t *testing.T) { + t.Parallel() + t.Log("bar") +} +`, + }, + { + name: "skips test that already has t.Parallel", + input: `package foo_test + +import "testing" + +func TestFoo(t *testing.T) { + t.Parallel() + t.Log("hello") +} +`, + expected: `package foo_test + +import "testing" + +func TestFoo(t *testing.T) { + t.Parallel() + t.Log("hello") +} +`, + }, + { + name: "skips test with parallelize:ignore", + input: `package foo_test + +import "testing" + +//parallelize:ignore +func TestFoo(t *testing.T) { + t.Log("hello") +} +`, + expected: `package foo_test + +import "testing" + +//parallelize:ignore +func TestFoo(t *testing.T) { + t.Log("hello") +} +`, + }, + { + name: "skips non-test functions", + input: `package foo_test + +import "testing" + +func helperFunc(t *testing.T) { + t.Log("helper") +} +`, + expected: `package foo_test + +import "testing" + +func helperFunc(t *testing.T) { + t.Log("helper") +} +`, + }, + { + name: "skips test suite methods", + input: `package foo_test + +import "testing" + +func (s *MySuite) TestFoo(t *testing.T) { + t.Log("suite test") +} +`, + expected: `package foo_test + +import "testing" + +func (s *MySuite) TestFoo(t *testing.T) { + t.Log("suite test") +} +`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + path := filepath.Join(t.TempDir(), "example_test.go") + require.NoError(t, os.WriteFile(path, []byte(tc.input), 0644)) + + require.NoError(t, processFile(path)) + + got, err := os.ReadFile(path) + require.NoError(t, err) + require.Equal(t, tc.expected, string(got)) + }) + } +} + +func TestProcessDir(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + + testFile := `package foo_test + +import "testing" + +func TestFoo(t *testing.T) { + t.Log("hello") +} +` + nonTestFile := `package foo + +func Foo() {} +` + + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo_test.go"), []byte(testFile), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "foo.go"), []byte(nonTestFile), 0644)) + + require.NoError(t, processDir(dir)) + + got, err := os.ReadFile(filepath.Join(dir, "foo_test.go")) + require.NoError(t, err) + require.Contains(t, string(got), "t.Parallel()") + + got, err = os.ReadFile(filepath.Join(dir, "foo.go")) + require.NoError(t, err) + require.Equal(t, nonTestFile, string(got)) +} diff --git a/tools/sql/clitest/setup_task_tests.go b/tools/sql/clitest/setup_task_tests.go index ec0a843d955..c8ab9b2f3b4 100644 --- a/tools/sql/clitest/setup_task_tests.go +++ b/tools/sql/clitest/setup_task_tests.go @@ -46,7 +46,12 @@ func (s *SetupSchemaTestSuite) SetupSuite() { s.Fail("error creating sql connection:%v", err) } s.conn = conn - s.SetupSuiteBase(conn, s.pluginName) + s.SetupSuiteBase(conn, s.pluginName, test.ConnectParams{ + Host: s.host, + Port: s.port, + User: testUser, + Password: testPassword, + }) } // TearDownSuite tear down test suite diff --git a/tools/sql/clitest/update_task_tests.go b/tools/sql/clitest/update_task_tests.go index 5c01aec3215..48df6260ff9 100644 --- a/tools/sql/clitest/update_task_tests.go +++ b/tools/sql/clitest/update_task_tests.go @@ -50,7 +50,12 @@ func (s *UpdateSchemaTestSuite) SetupSuite() { if err != nil { s.Logger.Fatal("Error creating CQLClient", tag.Error(err)) } - s.SetupSuiteBase(conn, s.pluginName) + s.SetupSuiteBase(conn, s.pluginName, test.ConnectParams{ + Host: s.host, + Port: s.port, + User: testUser, + Password: testPassword, + }) } // TearDownSuite tear down test suite diff --git a/tools/sql/conn.go b/tools/sql/conn.go index 47bd70aa385..6326436cc09 100644 --- a/tools/sql/conn.go +++ b/tools/sql/conn.go @@ -56,7 +56,7 @@ func (c *Connection) WriteSchemaUpdateLog(oldVersion string, newVersion string, } // Exec executes a sql statement -func (c *Connection) Exec(stmt string, args ...interface{}) error { +func (c *Connection) Exec(stmt string, args ...any) error { return c.adminDb.Exec(stmt, args...) } diff --git a/tools/tdbg/chasm_registry.go b/tools/tdbg/chasm_registry.go index 00ac039aab1..8430c1e3c9e 100644 --- a/tools/tdbg/chasm_registry.go +++ b/tools/tdbg/chasm_registry.go @@ -19,7 +19,7 @@ func newChasmRegistry(logger log.Logger) (*chasm.Registry, error) { return nil, err } - if err := registry.Register(chasmscheduler.NewLibrary(nil, nil, nil, nil, nil, nil)); err != nil { + if err := registry.Register(chasmscheduler.NewNilLibrary()); err != nil { return nil, err } diff --git a/tools/tdbg/commands.go b/tools/tdbg/commands.go index 6ff4f0289f5..a72c96ce911 100644 --- a/tools/tdbg/commands.go +++ b/tools/tdbg/commands.go @@ -8,6 +8,7 @@ import ( "time" "github.com/fatih/color" + "github.com/google/uuid" "github.com/urfave/cli/v2" commonpb "go.temporal.io/api/common/v1" historypb "go.temporal.io/api/history/v1" @@ -357,7 +358,8 @@ func describeMutableState(c *cli.Context, clientFactory ClientFactory) (*adminse WorkflowId: bid, RunId: rid, }, - Archetype: getArchetypeWithDefault(c, chasm.WorkflowArchetype), + Archetype: getArchetype(c), + ArchetypeId: chasm.ArchetypeID(c.Uint(FlagArchetypeID)), }) if err != nil { return nil, fmt.Errorf("unable to get Mutable State: %s", err) @@ -395,7 +397,8 @@ func AdminDeleteWorkflow(c *cli.Context, clientFactory ClientFactory, prompter * WorkflowId: wid, RunId: rid, }, - Archetype: getArchetypeWithDefault(c, chasm.WorkflowArchetype), + Archetype: getArchetype(c), + ArchetypeId: chasm.ArchetypeID(c.Uint(FlagArchetypeID)), }) if err != nil { return fmt.Errorf("unable to delete workflow execution: %s", err) @@ -725,7 +728,8 @@ func AdminRefreshWorkflowTasks(c *cli.Context, clientFactory ClientFactory) erro WorkflowId: wid, RunId: rid, }, - Archetype: getArchetypeWithDefault(c, chasm.WorkflowArchetype), + Archetype: getArchetype(c), + ArchetypeId: chasm.ArchetypeID(c.Uint(FlagArchetypeID)), }) if err != nil { return fmt.Errorf("unable to refresh Workflow Task: %s", err) @@ -856,7 +860,8 @@ func AdminReplicateWorkflow( WorkflowId: wid, RunId: rid, }, - Archetype: getArchetypeWithDefault(c, chasm.WorkflowArchetype), + Archetype: getArchetype(c), + ArchetypeId: chasm.ArchetypeID(c.Uint(FlagArchetypeID)), }) if err != nil { return fmt.Errorf("unable to replicate workflow: %w", err) @@ -866,3 +871,46 @@ func AdminReplicateWorkflow( fmt.Fprintln(c.App.Writer, "Replication tasks generated successfully.") return nil } + +// AdminMigrateSchedule migrates a schedule between V1 (workflow-backed) and V2 (CHASM). +func AdminMigrateSchedule(c *cli.Context, clientFactory ClientFactory) error { + ns, err := getRequiredOption(c, FlagNamespace) + if err != nil { + return err + } + scheduleID, err := getRequiredOption(c, FlagScheduleID) + if err != nil { + return err + } + targetStr, err := getRequiredOption(c, FlagTarget) + if err != nil { + return err + } + var target adminservice.MigrateScheduleRequest_SchedulerTarget + switch strings.ToLower(targetStr) { + case "chasm": + target = adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_CHASM + case "workflow": + target = adminservice.MigrateScheduleRequest_SCHEDULER_TARGET_WORKFLOW + default: + return fmt.Errorf("invalid target %q, valid values are: chasm, workflow", targetStr) + } + + adminClient := clientFactory.AdminClient(c) + ctx, cancel := newContext(c) + defer cancel() + + _, err = adminClient.MigrateSchedule(ctx, &adminservice.MigrateScheduleRequest{ + Namespace: ns, + ScheduleId: scheduleID, + Target: target, + Identity: getCurrentUserFromEnv(), + RequestId: uuid.NewString(), + }) + if err != nil { + return fmt.Errorf("unable to migrate schedule: %w", err) + } + + _, _ = fmt.Fprintf(c.App.Writer, "Successfully initiated migration of schedule %q in namespace %q to %s.\n", scheduleID, ns, targetStr) + return nil +} diff --git a/tools/tdbg/dlq_v1_service.go b/tools/tdbg/dlq_v1_service.go index c003c84cac1..0ee2de43791 100644 --- a/tools/tdbg/dlq_v1_service.go +++ b/tools/tdbg/dlq_v1_service.go @@ -57,7 +57,7 @@ func (ac *DLQV1Service) ReadMessages(c *cli.Context) (err error) { lastMessageID = common.EndMessageID } - paginationFunc := func(paginationToken []byte) ([]interface{}, []byte, error) { + paginationFunc := func(paginationToken []byte) ([]any, []byte, error) { t, err := toQueueType(dlqType) if err != nil { return nil, nil, err @@ -73,7 +73,7 @@ func (ac *DLQV1Service) ReadMessages(c *cli.Context) (err error) { if err != nil { return nil, nil, err } - var paginateItems []interface{} + var paginateItems []any for _, item := range resp.GetReplicationTasks() { paginateItems = append(paginateItems, item) } diff --git a/tools/tdbg/dlq_v2_service.go b/tools/tdbg/dlq_v2_service.go index d558d350917..2ff63d33056 100644 --- a/tools/tdbg/dlq_v2_service.go +++ b/tools/tdbg/dlq_v2_service.go @@ -455,7 +455,7 @@ func (ac *DLQV2Service) ListQueues(c *cli.Context) (err error) { return queues[i].MessageCount > queues[j].MessageCount }) - items := make([]interface{}, len(queues)) + items := make([]any, len(queues)) for i, queue := range queues { items[i] = queue } diff --git a/tools/tdbg/flags.go b/tools/tdbg/flags.go index 6ac347f079b..ee9e558cd3b 100644 --- a/tools/tdbg/flags.go +++ b/tools/tdbg/flags.go @@ -15,6 +15,7 @@ var ( FlagBusinessID = "business-id" FlagBusinessIDAlias = []string{"bid", FlagWorkflowID, FlagWorkflowIDAlias[0]} FlagArchetype = "archetype" + FlagArchetypeID = "archetype-id" FlagNumberOfShards = "number-of-shards" FlagMinEventID = "min-event-id" FlagMaxEventID = "max-event-id" @@ -76,4 +77,7 @@ var ( FlagVisibilityQuery = "query" FlagJobID = "job-id" FlagDecode = "decode" + FlagScheduleID = "schedule-id" + FlagScheduleIDAlias = []string{"sid"} + FlagTarget = "target" ) diff --git a/tools/tdbg/task_queue_commands.go b/tools/tdbg/task_queue_commands.go index f896ff6145c..dc010376840 100644 --- a/tools/tdbg/task_queue_commands.go +++ b/tools/tdbg/task_queue_commands.go @@ -51,7 +51,7 @@ func AdminListTaskQueueTasks(c *cli.Context, clientFactory ClientFactory) error MinPass: minPass, } - paginationFunc := func(paginationToken []byte) ([]interface{}, []byte, error) { + paginationFunc := func(paginationToken []byte) ([]any, []byte, error) { ctx, cancel := newContext(c) defer cancel() @@ -78,7 +78,7 @@ func AdminListTaskQueueTasks(c *cli.Context, clientFactory ClientFactory) error tasks = filteredTasks } - var items []interface{} + var items []any for _, task := range tasks { items = append(items, task) } diff --git a/tools/tdbg/tdbg_commands.go b/tools/tdbg/tdbg_commands.go index b851f401610..4a59901698c 100644 --- a/tools/tdbg/tdbg_commands.go +++ b/tools/tdbg/tdbg_commands.go @@ -64,6 +64,12 @@ func getCommands( }, }, }, + { + Name: "schedule", + Aliases: []string{"sch"}, + Usage: "Run admin operation on a schedule", + Subcommands: newAdminScheduleCommands(clientFactory), + }, { Name: "decode", Usage: "Decode payload", @@ -160,6 +166,10 @@ func newAdminExecutionCommands(clientFactory ClientFactory, prompterFactory Prom Usage: "Fully qualified archetype name of the execution", DefaultText: chasm.WorkflowArchetype, }, + &cli.UintFlag{ + Name: FlagArchetypeID, + Usage: "Archetype ID (optional, overrides --archetype if specified)", + }, }, Action: func(c *cli.Context) error { return AdminDescribeExecution(c, clientFactory) @@ -185,6 +195,10 @@ func newAdminExecutionCommands(clientFactory ClientFactory, prompterFactory Prom Usage: "Fully qualified archetype name of the execution", DefaultText: chasm.WorkflowArchetype, }, + &cli.UintFlag{ + Name: FlagArchetypeID, + Usage: "Archetype ID (optional, overrides --archetype if specified)", + }, &cli.StringFlag{ Name: FlagVisibilityQuery, Usage: "Visibility query to select workflows", @@ -242,6 +256,10 @@ func newAdminExecutionCommands(clientFactory ClientFactory, prompterFactory Prom Usage: "Fully qualified archetype name of the execution", DefaultText: chasm.WorkflowArchetype, }, + &cli.UintFlag{ + Name: FlagArchetypeID, + Usage: "Archetype ID (optional, overrides --archetype if specified)", + }, }, Action: func(c *cli.Context) error { return AdminReplicateWorkflow(c, clientFactory) @@ -267,6 +285,10 @@ func newAdminExecutionCommands(clientFactory ClientFactory, prompterFactory Prom Usage: "Fully qualified archetype name of the execution", DefaultText: chasm.WorkflowArchetype, }, + &cli.UintFlag{ + Name: FlagArchetypeID, + Usage: "Archetype ID (optional, overrides --archetype if specified)", + }, }, Action: func(c *cli.Context) error { return AdminDeleteWorkflow(c, clientFactory, prompterFactory(c)) @@ -275,6 +297,31 @@ func newAdminExecutionCommands(clientFactory ClientFactory, prompterFactory Prom } } +func newAdminScheduleCommands(clientFactory ClientFactory) []*cli.Command { + return []*cli.Command{ + { + Name: "migrate", + Usage: "Migrate a schedule between V1 (workflow-backed) and V2 (CHASM)", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: FlagScheduleID, + Aliases: FlagScheduleIDAlias, + Usage: "Schedule ID", + Required: true, + }, + &cli.StringFlag{ + Name: FlagTarget, + Usage: "Target scheduler implementation: chasm, workflow", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + return AdminMigrateSchedule(c, clientFactory) + }, + }, + } +} + func newAdminShardManagementCommands(clientFactory ClientFactory, taskCategoryRegistry tasks.TaskCategoryRegistry) []*cli.Command { // There are two different categories for the task type, and they have slightly // different semantics. The first is the task category for the list-tasks command, diff --git a/tools/tdbg/util.go b/tools/tdbg/util.go index 548eaa125c3..83c92763b44 100644 --- a/tools/tdbg/util.go +++ b/tools/tdbg/util.go @@ -38,7 +38,7 @@ func getCurrentUserFromEnv() string { return "unknown" } -func prettyPrintJSONObject(c *cli.Context, o interface{}) { +func prettyPrintJSONObject(c *cli.Context, o any) { var b []byte var err error if pb, ok := o.(proto.Message); ok { @@ -213,7 +213,7 @@ func paginate[V any](c *cli.Context, paginationFn collection.PaginationFn[V], pa isTableView := !c.Bool(FlagPrintJSON) iter := collection.NewPagingIterator(paginationFn) - var pageItems []interface{} + var pageItems []any for iter.HasNext() { item, err := iter.Next() if err != nil { @@ -242,7 +242,7 @@ func paginate[V any](c *cli.Context, paginationFn collection.PaginationFn[V], pa var exportRgx = regexp.MustCompile("^[A-Z]") -func printTable(items []interface{}, writer io.Writer) error { +func printTable(items []any, writer io.Writer) error { if len(items) == 0 { return nil } @@ -267,7 +267,7 @@ func printTable(items []interface{}, writer io.Writer) error { table.SetColumnSeparator("|") table.SetHeader(fields) table.SetHeaderLine(false) - for i := 0; i < len(items); i++ { + for i := range items { item := reflect.ValueOf(items[i]) for item.Type().Kind() == reflect.Ptr { item = item.Elem() @@ -309,13 +309,14 @@ func getNamespaceID(c *cli.Context, clientFactory ClientFactory, nsName namespac return namespace.ID(nsResponse.NamespaceInfo.GetId()), nil } -func getArchetypeWithDefault( - c *cli.Context, - defaultAchetype chasm.Archetype, -) chasm.Archetype { +func getArchetype(c *cli.Context) chasm.Archetype { + if c.IsSet(FlagArchetypeID) { + return "" + } + archetype := c.String(FlagArchetype) if archetype != "" { return archetype } - return defaultAchetype + return chasm.WorkflowArchetype } diff --git a/tools/testrunner/junit.go b/tools/testrunner/junit.go index 081ce870199..446f75c9801 100644 --- a/tools/testrunner/junit.go +++ b/tools/testrunner/junit.go @@ -183,6 +183,9 @@ func mergeReports(reports []*junitReport) (*junitReport, error) { var suffix string if i > 0 { suffix = fmt.Sprintf(" (retry %d)", i) + if i == len(reports)-1 { + suffix += " (final)" + } prevFailures := reports[i-1].collectTestCaseFailures() currCases := report.collectTestCases() diff --git a/tools/testrunner/junit_test.go b/tools/testrunner/junit_test.go index f9f09aa5774..be77f41d12a 100644 --- a/tools/testrunner/junit_test.go +++ b/tools/testrunner/junit_test.go @@ -89,14 +89,14 @@ func TestMergeReports_MultipleReports(t *testing.T) { require.Len(t, suites, 2) require.Equal(t, 4, report.Testsuites.Failures) require.Equal(t, "go.temporal.io/server/tests", suites[0].Name) - require.Equal(t, "go.temporal.io/server/tests (retry 1)", suites[1].Name) + require.Equal(t, "go.temporal.io/server/tests (retry 1) (final)", suites[1].Name) testNames := collectTestNames(suites) require.Len(t, testNames, 6) require.NotContains(t, testNames, "TestCallbacksSuite") require.NotContains(t, testNames, "TestCallbacksSuite/TestWorkflowNexusCallbacks_CarriedOver") require.Contains(t, testNames, "TestCallbacksSuite/TestWorkflowCallbacks_InvalidArgument") - require.Contains(t, testNames, "TestCallbacksSuite/TestWorkflowCallbacks_InvalidArgument (retry 1)") + require.Contains(t, testNames, "TestCallbacksSuite/TestWorkflowCallbacks_InvalidArgument (retry 1) (final)") } func TestMergeReports_IterationSuffixPreserved(t *testing.T) { diff --git a/tools/testrunner/log.go b/tools/testrunner/log.go index 05592d91812..94017ec1c4f 100644 --- a/tools/testrunner/log.go +++ b/tools/testrunner/log.go @@ -333,3 +333,21 @@ func isTestResultBoundary(line string) bool { func shouldStopOnTestBoundary(line string, _ int, _ int) bool { return isTestResultBoundary(line) } + +// parseFailedTestsFromOutput extracts failing test names from gotestsum stdout. +// It looks for "--- FAIL: TestName" lines produced as tests complete, and is +// used when the test binary was killed externally before producing a JUnit XML. +func parseFailedTestsFromOutput(stdout string) []string { + var failed []string + seen := make(map[string]struct{}) + for _, line := range strings.Split(strings.ReplaceAll(stdout, "\r\n", "\n"), "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "--- FAIL: ") { + continue + } + if name, ok := parseTripleDashTestName(line); ok { + addUniqueTest(&failed, seen, name) + } + } + return failed +} diff --git a/tools/testrunner/log_test.go b/tools/testrunner/log_test.go index 9285190dfa6..2cde8bcf5e7 100644 --- a/tools/testrunner/log_test.go +++ b/tools/testrunner/log_test.go @@ -22,6 +22,50 @@ func TestParseTestTimeouts(t *testing.T) { require.Equal(t, string(logOutput), stacktrace) } +func TestParseFailedTestsFromOutput(t *testing.T) { + t.Run("ExtractsFailedTestNames", func(t *testing.T) { + stdout := ` +=== RUN TestFoo +=== RUN TestFoo/SubTest1 + foo_test.go:42: assertion failed +--- FAIL: TestFoo/SubTest1 (1.23s) +--- FAIL: TestFoo (1.23s) +=== RUN TestBar +--- PASS: TestBar (0.00s) +=== RUN TestBaz + baz_test.go:10: something wrong +--- FAIL: TestBaz (0.50s) +FAIL +` + got := parseFailedTestsFromOutput(stdout) + require.Equal(t, []string{"TestFoo/SubTest1", "TestFoo", "TestBaz"}, got) + }) + + t.Run("DeduplicatesDuplicateLines", func(t *testing.T) { + stdout := ` +--- FAIL: TestDupe (0.10s) +--- FAIL: TestDupe (0.10s) +--- FAIL: TestOther (0.20s) +` + got := parseFailedTestsFromOutput(stdout) + require.Equal(t, []string{"TestDupe", "TestOther"}, got) + }) + + t.Run("ReturnsEmptyWhenNoFailures", func(t *testing.T) { + stdout := ` +=== RUN TestPass +--- PASS: TestPass (0.01s) +ok go.temporal.io/server/tests +` + got := parseFailedTestsFromOutput(stdout) + require.Empty(t, got) + }) + + t.Run("ReturnsEmptyOnEmptyInput", func(t *testing.T) { + require.Empty(t, parseFailedTestsFromOutput("")) + }) +} + func TestParseAlerts_DataRaceAndPanic(t *testing.T) { input, err := os.ReadFile("testdata/alerts-input.log") require.NoError(t, err) diff --git a/tools/testrunner/testrunner.go b/tools/testrunner/testrunner.go index 357f5d5dfb3..f0565d5bc11 100644 --- a/tools/testrunner/testrunner.go +++ b/tools/testrunner/testrunner.go @@ -13,6 +13,7 @@ import ( "slices" "strconv" "strings" + "time" "github.com/google/uuid" ) @@ -25,6 +26,11 @@ const ( crashReportNameFlag = "--crashreportname=" gotestsumPathFlag = "--gotestsum-path=" + // goTestTimeoutFlag is the go test flag whose value is also used as the + // testrunner's total-run deadline (so results are flushed before an external + // kill such as a GitHub Actions timeout). + goTestTimeoutFlagEq = "-timeout=" + // fullRerunThreshold is the number of test failures above which we do a full // rerun instead of retrying only the failed tests. fullRerunThreshold = 20 @@ -70,6 +76,7 @@ type runner struct { maxAttempts int crashName string alerts []alert + totalTimeout time.Duration // derived from the -timeout go test flag } func newRunner() *runner { @@ -81,6 +88,17 @@ func newRunner() *runner { // nolint:revive,cognitive-complexity func (r *runner) sanitizeAndParseArgs(command string, args []string) ([]string, error) { + // Pre-pass: read the go test -timeout value and use it as the testrunner's + // total deadline so results are flushed before an external kill (e.g. GitHub + // Actions timeout). The flag is NOT consumed — it still passes through to gotestsum. + for _, arg := range args { + if strings.HasPrefix(arg, goTestTimeoutFlagEq) { + if d, err := time.ParseDuration(strings.TrimPrefix(arg, goTestTimeoutFlagEq)); err == nil { + r.totalTimeout = d + } + } + } + var sanitizedArgs []string for _, arg := range args { if strings.HasPrefix(arg, maxAttemptsFlag) { @@ -189,6 +207,12 @@ func Main() { log.Fatalf("failed to parse command line options: %v", err) } + if r.totalTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, r.totalTimeout) + defer cancel() + } + switch command { case testCommand: r.runTests(ctx, args) @@ -208,6 +232,30 @@ func (r *runner) reportCrash() { } } +// writeCurrentReport writes the merged report from all completed attempts to the +// final output path. It is called after each attempt so that partial results +// survive if the process is killed externally between attempts. +// Reporting errors (e.g. unexpected missing reruns) are intentionally ignored +// here; they are only checked for the final write at the end of runTests. +func (r *runner) writeCurrentReport() { + reports := r.allReports() + if len(reports) == 0 { + return + } + merged, err := mergeReports(reports) + if err != nil { + log.Printf("warning: failed to merge reports for intermediate write: %v", err) + return + } + if len(r.alerts) > 0 { + merged.appendAlertsSuite(r.alerts) + } + merged.path = r.junitOutputPath + if err := merged.write(); err != nil { + log.Printf("warning: failed to write intermediate report: %v", err) + } +} + func (r *runner) runTests(ctx context.Context, args []string) { var currentAttempt *attempt for a := 1; a <= r.maxAttempts; a++ { @@ -217,6 +265,27 @@ func (r *runner) runTests(ctx context.Context, args []string) { stdout, err := currentAttempt.run(ctx, args) // Extract prominent alerts from this attempt's output. r.alerts = append(r.alerts, parseAlerts(stdout)...) + + // Check whether our total timeout fired (context deadline exceeded). + // This happens when the go test binary hangs and never produces its own + // "test timed out" panic. We collect whatever results are available from + // completed attempts and from the partially-executed current attempt, then + // flush the XML before the external kill arrives. + if ctx.Err() != nil { + log.Printf("total timeout reached, collecting partial results from %d completed attempt(s)", a-1) + // Try to read whatever gotestsum managed to write before it was killed. + if readErr := currentAttempt.junitReport.read(); readErr != nil { + // gotestsum didn't finish writing a JUnit XML. Fall back to parsing + // stdout for any "--- FAIL:" lines that completed before the kill. + if failedTests := parseFailedTestsFromOutput(stdout); len(failedTests) > 0 { + currentAttempt.junitReport = generateStatic(failedTests, "total timeout", "Timeout") + } + // If no failed tests are found either, the current attempt's report + // remains empty and mergeReports will include only prior attempts. + } + break + } + if err != nil && !errors.As(err, ¤tAttempt.exitErr) { log.Fatalf("test run failed with an unexpected error: %v", err) } @@ -237,6 +306,11 @@ func (r *runner) runTests(ctx context.Context, args []string) { log.Fatal(err) } + // Write intermediate results so they survive if we are killed externally + // between attempts (e.g. a GitHub Actions job timeout fires after this + // attempt but before the next one completes). + r.writeCurrentReport() + // If the run completely successfull, no need to retry. if currentAttempt.exitErr == nil { break @@ -282,7 +356,9 @@ func (r *runner) runTests(ctx context.Context, args []string) { if err = mergedReport.write(); err != nil { log.Fatal(err) } - if len(mergedReport.reportingErrs) > 0 { + // Skip the strict rerun-coverage check when the total timeout fired: the + // in-progress attempt was killed before it could execute all expected tests. + if len(mergedReport.reportingErrs) > 0 && ctx.Err() == nil { log.Fatal(mergedReport.reportingErrs) } diff --git a/tools/testrunner/testrunner_test.go b/tools/testrunner/testrunner_test.go index 726b69ae6df..1eccfcb5b44 100644 --- a/tools/testrunner/testrunner_test.go +++ b/tools/testrunner/testrunner_test.go @@ -3,6 +3,7 @@ package testrunner import ( "os" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -35,6 +36,35 @@ func TestRunnerSanitizeAndParseArgs(t *testing.T) { require.Equal(t, "test.cover.out", r.coverProfilePath) }) + t.Run("TotalTimeoutDerivedFromGoTestTimeout", func(t *testing.T) { + r := newRunner() + args, err := r.sanitizeAndParseArgs(testCommand, []string{ + "--gotestsum-path=/bin/gotestsum", + "--junitfile=test.xml", + "--", + "-timeout=35m", + "-coverprofile=test.cover.out", + }) + require.NoError(t, err) + // The testrunner should derive its total deadline from the go test -timeout flag. + require.Equal(t, 35*time.Minute, r.totalTimeout) + // The flag must still be present in the passthrough args so gotestsum/go test + // also honour it. + require.Contains(t, args, "-timeout=35m") + }) + + t.Run("TotalTimeoutNotSetWhenNoGoTestTimeout", func(t *testing.T) { + r := newRunner() + _, err := r.sanitizeAndParseArgs(testCommand, []string{ + "--gotestsum-path=/bin/gotestsum", + "--junitfile=test.xml", + "--", + "-coverprofile=test.cover.out", + }) + require.NoError(t, err) + require.Zero(t, r.totalTimeout) + }) + t.Run("GoTestSumPathMissing", func(t *testing.T) { r := newRunner() _, err := r.sanitizeAndParseArgs(testCommand, []string{ @@ -124,6 +154,43 @@ func TestStripRunFromArgs(t *testing.T) { }) } +func TestWriteCurrentReport(t *testing.T) { + out, err := os.CreateTemp("", "junit-report-*.xml") + require.NoError(t, err) + defer func() { _ = os.Remove(out.Name()) }() + + r := newRunner() + r.junitOutputPath = out.Name() + + // Simulate attempt 1 completing with failures. + j1 := &junitReport{path: "testdata/junit-attempt-1.xml"} + require.NoError(t, j1.read()) + a1 := r.newAttempt() + a1.junitReport = j1 + + r.writeCurrentReport() + + result := &junitReport{path: out.Name()} + require.NoError(t, result.read()) + require.Equal(t, 2, result.Failures) + require.Len(t, result.Suites, 1) + + // Simulate attempt 2 also completing. The intermediate write should now + // contain failures from both attempts, so that if the process is killed + // before attempt 3 the file on disk already has the full picture. + j2 := &junitReport{path: "testdata/junit-attempt-2.xml"} + require.NoError(t, j2.read()) + a2 := r.newAttempt() + a2.junitReport = j2 + + r.writeCurrentReport() + + result2 := &junitReport{path: out.Name()} + require.NoError(t, result2.read()) + require.Equal(t, 4, result2.Failures) // 2 from attempt 1 + 2 from attempt 2 + require.Len(t, result2.Suites, 2) +} + func TestRunnerReportCrash(t *testing.T) { out, err := os.CreateTemp("", "junit-report-*.xml") require.NoError(t, err) diff --git a/tools/tests/cql_cli_test.go b/tools/tests/cql_cli_test.go index 818c24f134a..d0985442228 100644 --- a/tools/tests/cql_cli_test.go +++ b/tools/tests/cql_cli_test.go @@ -8,17 +8,21 @@ import ( ) func TestCQLClientTestSuite(t *testing.T) { + t.Parallel() suite.Run(t, new(cassandra.CQLClientTestSuite)) } func TestSetupCQLSchemaTestSuite(t *testing.T) { + t.Parallel() suite.Run(t, new(cassandra.SetupSchemaTestSuite)) } func TestUpdateCQLSchemaTestSuite(t *testing.T) { + t.Parallel() suite.Run(t, new(cassandra.UpdateSchemaTestSuite)) } func TestVersionTestSuite(t *testing.T) { + t.Parallel() suite.Run(t, new(cassandra.VersionTestSuite)) } diff --git a/tools/tests/mysql_cli_test.go b/tools/tests/mysql_cli_test.go index 8a29477a095..5dc990d8ace 100644 --- a/tools/tests/mysql_cli_test.go +++ b/tools/tests/mysql_cli_test.go @@ -12,6 +12,7 @@ import ( ) func TestMySQLConnTestSuite(t *testing.T) { + t.Parallel() suite.Run(t, clitest.NewSQLConnTestSuite( environment.GetMySQLAddress(), strconv.Itoa(environment.GetMySQLPort()), @@ -21,6 +22,7 @@ func TestMySQLConnTestSuite(t *testing.T) { } func TestMySQLHandlerTestSuite(t *testing.T) { + t.Parallel() suite.Run(t, clitest.NewHandlerTestSuite( environment.GetMySQLAddress(), strconv.Itoa(environment.GetMySQLPort()), @@ -29,10 +31,7 @@ func TestMySQLHandlerTestSuite(t *testing.T) { } func TestMySQLSetupSchemaTestSuite(t *testing.T) { - t.Setenv("SQL_HOST", environment.GetMySQLAddress()) - t.Setenv("SQL_PORT", strconv.Itoa(environment.GetMySQLPort())) - t.Setenv("SQL_USER", testUser) - t.Setenv("SQL_PASSWORD", testPassword) + t.Parallel() suite.Run(t, clitest.NewSetupSchemaTestSuite( environment.GetMySQLAddress(), strconv.Itoa(environment.GetMySQLPort()), @@ -42,10 +41,7 @@ func TestMySQLSetupSchemaTestSuite(t *testing.T) { } func TestMySQLUpdateSchemaTestSuite(t *testing.T) { - t.Setenv("SQL_HOST", environment.GetMySQLAddress()) - t.Setenv("SQL_PORT", strconv.Itoa(environment.GetMySQLPort())) - t.Setenv("SQL_USER", testUser) - t.Setenv("SQL_PASSWORD", testPassword) + t.Parallel() suite.Run(t, clitest.NewUpdateSchemaTestSuite( environment.GetMySQLAddress(), strconv.Itoa(environment.GetMySQLPort()), @@ -59,8 +55,7 @@ func TestMySQLUpdateSchemaTestSuite(t *testing.T) { } func TestMySQLVersionTestSuite(t *testing.T) { - t.Setenv("SQL_USER", testUser) - t.Setenv("SQL_PASSWORD", testPassword) + t.Parallel() suite.Run(t, clitest.NewVersionTestSuite( environment.GetMySQLAddress(), strconv.Itoa(environment.GetMySQLPort()), diff --git a/tools/tests/postgresql_cli_test.go b/tools/tests/postgresql_cli_test.go index 89d7bae9ebc..f2c88bc6c0c 100644 --- a/tools/tests/postgresql_cli_test.go +++ b/tools/tests/postgresql_cli_test.go @@ -34,10 +34,6 @@ func (p *PostgresqlSuite) TestPostgreSQLHandlerTestSuite() { } func (p *PostgresqlSuite) TestPostgreSQLSetupSchemaTestSuite() { - p.T().Setenv("SQL_HOST", environment.GetPostgreSQLAddress()) - p.T().Setenv("SQL_PORT", strconv.Itoa(environment.GetPostgreSQLPort())) - p.T().Setenv("SQL_USER", testUser) - p.T().Setenv("SQL_PASSWORD", testPassword) suite.Run(p.T(), clitest.NewSetupSchemaTestSuite( environment.GetPostgreSQLAddress(), strconv.Itoa(environment.GetPostgreSQLPort()), @@ -47,10 +43,6 @@ func (p *PostgresqlSuite) TestPostgreSQLSetupSchemaTestSuite() { } func (p *PostgresqlSuite) TestPostgreSQLUpdateSchemaTestSuite() { - p.T().Setenv("SQL_HOST", environment.GetPostgreSQLAddress()) - p.T().Setenv("SQL_PORT", strconv.Itoa(environment.GetPostgreSQLPort())) - p.T().Setenv("SQL_USER", testUser) - p.T().Setenv("SQL_PASSWORD", testPassword) suite.Run(p.T(), clitest.NewUpdateSchemaTestSuite( environment.GetPostgreSQLAddress(), strconv.Itoa(environment.GetPostgreSQLPort()), @@ -64,8 +56,6 @@ func (p *PostgresqlSuite) TestPostgreSQLUpdateSchemaTestSuite() { } func (p *PostgresqlSuite) TestPostgreSQLVersionTestSuite() { - p.T().Setenv("SQL_USER", testUser) - p.T().Setenv("SQL_PASSWORD", testPassword) suite.Run(p.T(), clitest.NewVersionTestSuite( environment.GetPostgreSQLAddress(), strconv.Itoa(environment.GetPostgreSQLPort()), @@ -76,11 +66,13 @@ func (p *PostgresqlSuite) TestPostgreSQLVersionTestSuite() { } func TestPostgres(t *testing.T) { + t.Parallel() s := &PostgresqlSuite{pluginName: postgresql.PluginName} suite.Run(t, s) } func TestPostgresPGX(t *testing.T) { + t.Parallel() s := &PostgresqlSuite{pluginName: postgresql.PluginNamePGX} suite.Run(t, s) } diff --git a/tools/tests/test_data.go b/tools/tests/test_data.go index f3cbf4ae022..b702c413b37 100644 --- a/tools/tests/test_data.go +++ b/tools/tests/test_data.go @@ -1,9 +1,6 @@ package tests const ( - testUser = "temporal" - testPassword = "temporal" - testMySQLExecutionSchemaFile = "../../schema/mysql/v8/temporal/schema.sql" testMySQLVisibilitySchemaFile = "../../schema/mysql/v8/visibility/schema.sql" testMySQLExecutionSchemaVersionDir = "../../schema/mysql/v8/temporal/versioned" From ddce32bbd02979b0f3ddd258ea5e21e5c925cf96 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 10 Apr 2026 18:19:57 -0700 Subject: [PATCH 36/40] Update go.temporal.io/api to released v1.62.8 The pre-release version was missing ContinueAsNewInitialVersioningBehavior fields that landed in the released tag. The released v1.62.8 includes both our WorkerCommand proto and the versioning fields from main. Co-Authored-By: Claude Opus 4.6 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 82469f91fc5..a8031c011ee 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/sdk/metric v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 - go.temporal.io/api v1.62.8-0.20260407190616-8574d6aa8b01 + go.temporal.io/api v1.62.8 go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 go.temporal.io/sdk v1.41.1 go.uber.org/fx v1.24.0 diff --git a/go.sum b/go.sum index d1ea3ed8d09..fa7633c649a 100644 --- a/go.sum +++ b/go.sum @@ -440,8 +440,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= -go.temporal.io/api v1.62.8-0.20260407190616-8574d6aa8b01 h1:YZf5B/BjOlNm4h2TtYVXvDrZURRxMIAPmXzAqKKBK34= -go.temporal.io/api v1.62.8-0.20260407190616-8574d6aa8b01/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.8 h1:g8RAZmdebYODoNa2GLA4M4TsXNe1096WV3n26C4+fdw= +go.temporal.io/api v1.62.8/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2/go.mod h1:T8dnzVPeO+gaUTj9eDgm/lT2lZH4+JXNvrGaQGyVi50= go.temporal.io/sdk v1.41.1 h1:yOpvsHyDD1lNuwlGBv/SUodCPhjv9nDeC9lLHW/fJUA= From 1873d673d799c88da030ecb2c984ec083fe3cdcb Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 10 Apr 2026 18:52:49 -0700 Subject: [PATCH 37/40] Remove unrelated files from PR diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reset matching_engine_test.go, task_queue_partition_manager.go, and protogen to match main — these were pulled in by duplicate merge commits and are not part of this PR's changes. Co-Authored-By: Claude Opus 4.6 --- .claude/worktrees/agent-a02c129e | 1 + protogen | Bin 3392386 -> 0 bytes service/matching/matching_engine_test.go | 294 ++++++++++++++---- .../matching/task_queue_partition_manager.go | 110 +++++-- 4 files changed, 307 insertions(+), 98 deletions(-) create mode 160000 .claude/worktrees/agent-a02c129e delete mode 100755 protogen diff --git a/.claude/worktrees/agent-a02c129e b/.claude/worktrees/agent-a02c129e new file mode 160000 index 00000000000..3133f82c85e --- /dev/null +++ b/.claude/worktrees/agent-a02c129e @@ -0,0 +1 @@ +Subproject commit 3133f82c85ee13fd1a06e04cbd5363f522a38f4f diff --git a/protogen b/protogen deleted file mode 100755 index ee31d559bfeda9d8d720d8c2b4a2baf74a1f3f2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3392386 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%0Aht@D0h#KIEbDwydoT?@1N%oo7Vf%Q<)0>M6)J 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 t1%_}8$ev9L+j(cczgs}pD^ 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

@8|+1mYL|Hl#=TKFxvW`UREHn( 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

*qg@o{FNfKCKU|a3C zh(qSfuFsEhd*=DQI_3)WM2;_yIE*!!M>I>V95&CKlL&wb+OeZBPh6)b=3pjDkbcA` zAYAi|$fA?V83S>Twm8E|ogGjShjX^U^$x?=r8h&-xc@5@^+tCoD7w$8I?RD+qUg)& zzo7bMiw(g+hN4pfin3u8c^71VA^Fa5FvxvUbv`JXL&|y_Zov5}SE|6y%T;b|)ha2@ zLXILt;gIBh#gR>S-6M+3T{jf%sDm-La}EnsuN=ht3t73 zeI*J-=yA9U+XNO0OoshTb&4@sn{jpcj#fLGZMaHYE%UhrH!VOk7@Y^F5)TaC1u7}` zE{>O@K}L_;n;v$;G2J*6^fl1~5ZDU!$@}Ckk)xZucPAAk`k9*iu%_R~ z2T>~4ApEN=2y`|%c5kBn21^aYLU&o_dILWMX4@Qg9b+*lim$G>l6<#UV>`U5ZDo;s zQ}j4_7cSY=uvY1Fu&-WfF~*-$!HtOL@AJY#8wmTen0gTQ#_v}_=TX$gnk-|3wN5(S*f{Fr%M7GOpqK7Lv}Xg~gkYvIKFNLxvO^u=`bZG* z7}7Kl`5QbJOZ2*?&(e_>XHTM@Wny^)pT!izGO1d1g;JKRSurb6CXl^c$^Ket)djVg zEX#;>1YM9(NT9a1nBtZmDt5?$Bh1oBCWOXjSvy-te(T|k%52jlLu9`(x@b1mC!abwr8XaLZT*!tAq5W z!a3RCoUXtY>slH;Levj*LJzTVIIj#aBT;7MFR%hSj_@DjwfbEW<+zQSQvB&k>1>9T zL)purhNH1C_SU9tQs1T9Nm!ZE+k{p-uU2B!wOe$vON&$jgAhbC=~TxmJ7}!Hyn#&C zDHB*xy=klIg+|&FeZP6$sox>$+9P@yj>nQOCjW5qnoEJC37Xd7Q}X)f5pA_B6UiS6 zgvp&2X-4GjoIEEZ&v8I$lu?Zyyv~YLvUSwn^>pK+!vb{@%e=<*y`t%?^9*rBzbD0P z`^Y?eJh>kb$J9Ero!?$5XH z7cAs3$8DvXxti9RpmegxK)DT86?YQ9V-&q*rBfWcZ{Eb38d3t$rU+&l0t~R&C{`Hn z%+bc8(qx%Lj|mmP@VOsN`mD$GBA(0+?()#GP9fTc+2czVLNbA6#I;0g#Cs7|0<{QR)gSao@pa@arm>PG|f*36m0h@u>$q7d4hx@jCb;gNNbOeLH z#bFI1+xBY?+|rB#M>FUm@*!a{;H8W$N04LKaJW@2v1%AN5->HC!IXwL8<4Gnu%U*y zd-GlycyODc|H{I|IcOhoZjIA>Eax)DX4-=AM-0NpvaUBk_+wFYT!64&E^_*20Jt4* zWqU*!ow~puCP_Lmi<4>5%TO%$%TRK&9Eyo&#BFT=j0xG^XmvDb9W|I*lgJIBfF10O zF%V1y1XYfn{xrJB+@gMJP?F4|^aZq27j-<7h(4)2WDA4bZA?Dk;7+^){0yLAyvV=V zvaP+i9jFEvi;S7?drZ_M9!qk|m9Fz#tE?zm2_2mq^#|I8!pa=PU@U46+#JjPG+pL* zTjyk%KP;RJ;XF5-s~g;bJKBw$C3OzS!Iu>8+O9+iaio^)V#KSYOcHzF*8uORkJ z^s$U(q9&D!J4g;rQui*|KZ(;Fl1^6MnGw(992l~od|L$Afrzj^m}yRd2MVMG_t(93 zuA3^Iq3ThnBXCFB_8QdK0Uu3Q)^3y)wMLZPCDGqCD;El78%s{%eXcp8P`@$Bq&;vO zq|+_QjTswEl9}<0@Sfy1MPOrDayT)6Uzx#zAS(*7)$b&Q07IyyH5F!cf;Krz6!I<) zYtE}J3cCg5vl2{dn^iw+!)D>2_>>PbBH3SI*d&+x)>tBaD4ZMNTo32c8ftU1b0oUU zN@q8KsF~IP*+Qu%Q~);~rNwA6svG(;3K|flb!@yOn=A9UuDaPWr?Ppu0PGCRUQSdp z;bZ*%6c-_yKW^|_JF0ulC|Nsdw{Si*oYQdDaPAG~t~I05$Wp(E8B>ykSgkU}T6GXd zHTsmtrX6wyuuWthRl1sX6in4pT?2)h#^A8Xb2`m`g6}yBUosO_NK;!1qcJKsDf_89 zrI1+Fhq-K7oiJ{O=VyW*9)8J~zbJ#$a>TCJSip!D1;#jRB-GxuxmTN94i(-?ca#xd z3tP6RjnZ2ljiQ-4TaVNQwsOnN@s4+fveMQ!&tkteH+7@NkX>=ndzNtvjdKY%{EUPtuCkPSpZqPIv zD{s4~`d~t)0T?=V8?_W>rIdJEFEI-+-JqW`wLg}g=CiOQ#f%AZ7L;9Sub-0C=bAjU z7EttMsF*r9ZIJ)Wo#cy%h#1m$#CYxUCt4Xs%7^riPKlxm^k>J)PmZ;XDc0)8^JxsO zVdsnNn-X1;@>o8Nu|iJYSJ@OKwe`VFha*PrRRImYNEiU6cQ{G8Dq>=7b+(Eyo0C2n zFvaM3^2K3EH=3-%`ozE{eSSy>WgG?&O9OLO5f7_G*UJN^iF^*gTBL1@Rn(JYJ4e4v zkp%gqF_mFJ7VS|9WAdCa5i2s?9;9C;NFxeLjd``f!#Zg9AdMy@xZXsng~|q!W^Z-uNuOHEKe#h--XymGLW}h4iSX{f9x>gix<8gccUjE=!Qzq{I?e zj}UjKWCFO}g%qOKgi?ou;EFy&VF6X`87V!I23UL;`WHA-bGWT$_|nvW=}XR58(8$> zWv6_4VUE$i2F1oxWGTL(@=eYz|32k7kPc7bu~QX)F*`unGX7>0wlu|>U>U2`6B%-T zMN@K+9?tlGraDf@H*aIhphwFJIIxAZ6;Napy@S$;L}vi6eqMLZSLcbIKuwBS^q1)B z7@<-!BLJa}v=2fJ)-VcT7X#yeV;`o@Q7C66YPzYjf=e3?FD1B<+~MIGL~3R6EKt_Y zVo%nc+TK9@jR~;34crz*4;1=qX63O|+CHo2OQx~f?Q_fe$Ap~7gjhV(7EY%aDQ)0` z<{hhUa5wgZSW6sl=NQYeDMw{-De9{S%E>8iL6hQ)`ud@`B^<+)Rf--268qD$olLbLLDmNRIVo+|P4B7YR>^WATqJnP)` zw2AsW9pu~c!xp*8hsa&n%BQnI99%-wb(!iqgQkt)Wkcd0fqNjY_poI1$>!6L&QqdK zihn}%X)E*q{7G{QGIiw41NsqUB+`iH-ecRX-Q?Zr>r4KLNMhmd6{w3%e6*KifvwxX zt=01pH)lXuNFQ4@&xGHEvbbxqK1l*)72iNvrd)G6bTuM8NNo`tRzve9X4Nnf6yWs1uoJRpHn^<8^}P(D4@fqf8sgObDSxS0$sXLFmX50DUu~ zwKitvW2GVpbym*7+_sp(EEg{LR3{U)IsNEj)YlQQ&c#!b`W8rpL)bYCHxpE$6QT%A zH2n^tnvs4QMypnN))hVIk}o)avvJ3J^)NYzE@O9K2*Db$b(qvGV@Yxcf`u`;iHl){ z=gWxBTU=@;3yCo(a+P6Lr&)`TBd-|!y#Fu0(-RFPqtvQ_tJJ{GN11OXTj}&<5^JN% zto{8DITSUtANmejG%nM@EAI>Nae&bmsHXzccEbY&cL2Zv{+>YeJpg$YQg2`ia!FfT2y ze=D;{2Z@_Aj(aYeN8$l7;eA~cb=BV6UzuR$Wj|30&v_e%c%gg5dCnc3qq$|t00`Q6QW~T zc?uEG1Eg#*1!Q&!%Q9dK>xIY|3o$KNo0`SP8Y{?4r>!$tvS~QS;f!1L)P5>DJ5GTRRL&=?zBKyqDH=0hE2Xx-@)orWFDm=6U^yyTSYS$U|Y(m3G-@(uuIy*OJw}*-LkD?PE zwje0+oJXYVzeQJSw}mnpUQTRoxjBae$0B`Fj)b_zSX{wnY#w3G1O2Duvgov$zySD5 z44Z<-aL1-Z7$9I9?+=F|)pmj~v7?nHnoePQ48m6}x^cpJLsa6EF_*PTye82#gR*u| z&MUGV%)ICxwhqAK$$*qq;fFJ^h;E5wUgYQ5D2IZKB;t{Eq-4=-ja+`T6WrbmMc`s; zA8}OY%GU8#vM^pi5O5O{hFF1^M8L9jSaJC>9Sjsw31`BTP>u^{zrpoB;X0?K{L>B3 z`)=^$hWGR9WF4L$HOp2+h>)BXZOQIFs624fU_FpUrHoz7bEtUICC%%-$a5kAuwTd* zpllAakf{MWh&dCcC+LYG3eIkKSTA6yy`p`1B6%fZ!Niz_XL;Ty76YE(M-%-LSzwEE zn=P4Zoh942g`p+lVVDIBw7sDNct$D%STXNJlqN)G4>ZMWZTj`C#X75rXO`C?W}|4F ze+vS74uxN11I!87jU_w!7+%#^nJKiCNU*wE1d?KmRPVmxAoKLQSQguhE4CR3m}5D8nVudY~K;?nWHv@ntS>nd4g?e zOzG@kI%le#Tz)D?$(a(JNAN|qL)fyDh7mJNk#fd-({1f8LgB511yb3={Gb@rnQ2LO zHo+#@#{#K5Y1XS82xHFdX@Pth+b%^4g$^hwZ(l~M8{eZpAlz%th}vbGQGXz z++UD#Sw5j+*l^EgY2jbZ((IYiflD~0N_I^2&!h|K)ZDeGK#E>@QtOKbNG=;dlc*(v&jZrZ-L;d^W0)T+6e2^AT)Z;iG&(Z>w zgXO)=u`Y4F46qIM;Kd}>EQ>woM93q+-=YB)rdC<_S-34C-xy5}E7pJzRAJO*hn@55 zVR+>432%hI*6W)-)%=cUG6Vbe$B_lH{M>1!u6S(e7$5nh&p!tHH~(%;N|$E9ezso5 z77XcZG-nR({S<`m#<;@j!fb($4z5R6r=b0{Gkp*EWliO(1E_qI!~nvk+!66uo2*8@;Td zEYO0OKJ8}NbE!q+lMJ+ElMO-^DjTB9Wm4X_%_40ULz`2g&tPUWY4Bu~GeH`ycKTwq z(lX;Gn;G9htHDBJ$2?kH*h9Ls1fAR})1u``(uP$=$e&acs>IYQD^iI>@!HAXFf1l2 z3ZMHdbij>SIOpA{B6xt=(jljzDS((=zGStsr# z%97-=B4yI<$qMD(!>xDPq|25+3rIoQqdzc3m^c%v*{mkpNl)_8>9R2Tjm6zezt}3e zqMHW3%}7)KRzPteFl{qa3n61$nH|TFiJnB!pzr634K{j1{(hM(wl=GPYiZBEvfn6H zfovaLY;49-VH^sx%VV;KRBn?L?HOmlRA#$ZqI#$8Y^zvd^{#;JkQq0zZBG;>| znJfUiLJM}s80^MiS=MB(Z*DSG*WE2N)5E|=F_9`T+?>gxx_z{?9DN6IjT2-8N7+Me zHgHqVYh^r?y;gd{d8cr`%!qxhj0$Hm6!~>IS~~qMk)8Du8jq5lWcrNxjV+5@>Gbje zB1QO?3Qwv4Pl&75RoN6UYiCuxY{iajw00M^~b#yv&- zX%Q!awYs)LJA_DXC0L#;{AqaDVZ@y)Rox&fXmAQQO6;!-=-}2=`^237MR|p^4 znpFC;uZ8#DEy(SLyU+-Cep-@iC_@!ltT%vY7P)mwAD%CN=F~xa>Q9spKw?z5&IPp} zDm<*6a1Cn%*!F+lGRkV-hX-J{YB1xzDoMrHmRmZInC8?KJ`? z@de3{qyl1>J_Hnlq;Gk=X?ASV5F%=^%8}-J~%>kHllrflBlmh-cBM(TqWo3;TWDWKst~MW1q?R0XABzQf78?o2H>AK+M^!b!wxg_;uyUxdk&hpf@M4w-LkY~%ZrJq*C6+DAoNmn ze@tOPqBq6zXo&b)EJi%c;i_>Z#6tl-)zERdxg0lh`=!!Frd z$<}V5Z+>md7@m*8U);oB{SBWN4s^}WY5_>DzW|1_0al10L+a6;mh51(M5~C#!~2(0 zWek)OV5@TDadOpha`kas-uof>!H3-UkK*#5iGmuU;3jh^#47#?ksEV;!Px)uO#hg; z-z5;sYV#AGP2{(Bq@Gj+NJr{rCDgId@%tic6wDgFobql*XU7#_vkxyViKs z$N)V5-8>mYN%os#<+s+&a+?#><PM>p&PcG-U*by@yDa&?EkJodP zpQI^!a-xQk#S$&eqE}e<0~?tdVgfkXriRRzU8b`^cqmumJTYA7UjK}6T@2?Nj4GDt zB3p64bf?!TqcjlFARMS1!qFb?WZ_&npmbnoOF`vIYBLnn5Gt%Ex`d5uS?~+tQ6@Xf zBIRr@Fji)M|Xlks#W#Ib3EPf)f@%<)q4}1A3 zmBj3UW4L?_&CmDPT+9|Jd5UDc+?JXYxU!&E6y)lHTxDlqwO#@IB7F7Af<>g{sqp5; zLJ-=4R%ra|ENYWGD7i9Mh$hYLH9DY>-_+PWKE1<2ulQCQsV)kO??ez8zJfrd2FP^h zMVZ`?$@Q6BlgZtw=`mtw1)KnkOjDc~FhPr1FMK!GYVsp`gXT|1p2ed*QSE=ulct`3 z3*`_IZRY6#w&la1i8ewXT_|>o7>@@QZ1b2cA&^_t$(5aR2gJK~vz6`V;f0+H48f{n zDVddLVfkvMACDB@-Ikznv{M(Qof)e;l*PQI(VbL;l>5_hU8J6igRzDBo8f+)xEY$P zFr`@R8`l_QwR*I=Q22^LU0%*|4Ry>4D#(^jZW34T0fhrQH_}wK=rw|NK~s4Y!bF*e zrg|%o^EwmU5Hu%RbXLI-|4LE*R+PUNeL@Js#FAxV?2(%MDF&P`5R7W7w6B&Ru5(IC zmG7s#e$uDsdADy%`yZ#Mhqj>dQ$jGPHH&o+DMFBzlS2r^fZ0GAu9x*fooIcMa?N!n zHCQrOY`P^BZFXGrsBPHztAadTkdGH+vAj@_-={{zx1x;juWC4-yjZvbOk2x{Hi{=< zVN_)StT4u;eU-U5d z$3kWN*pBJ59WBiLh*VA%Jw@Uxq~l7FE5uzX<*Nmb*KjW6cla(l!iID)JCoJ@lhOf~ z-z1Jgd7@-=z#`8ti?VKvaC5@RQ9nPaW%BH560xe2KvI^g-`MREd>zWYI{+X#%3yKu zYdd640i`V2mC1Vsn1%sr7~rTYM6Z;}6(Tqb(Pxy3AKUX%@3!FYjnZ)&S$YPFAtYCe zUPV|1pXdE@B#AJSw{J6WY;R)$2>Mob-nzvDbBGOj)ZzG@3yR_eYeVDJx_=0#C#&!n z9+9J>2XG6SEF{TB9E9pimL3jY^WXOr|^p+MDZ+P zAP&M@au0g)f_p`;V6CayFB8pqD%dGIiQ2*JG{NcHs+hk>^JQzdmE-bMspdCI(Wfl9 z#h&WsEjUYWQ`{(zn-X=0%PHaRGLa+P``yDTKc+&G3L8Wi>n8pX9@@`0T2|MV!RjJd z6KpPV_;^9+nQFDnt>p_!Bo8SBl1deo>e@qku5B^Zq0Ff z5pvKMh_kKAE)ajA^hbxFwm>EIaBRQUkWVz^oQ7ku7ilI@D}_6>&(&dlbx)W5uh0n? zgB<**zyaVVV3%Ed&2KLHr^uhh+@%hb4_Mi68T)>wu~Ih4hY){K+^2+f=w<-2{>P3V zNE>eL4}x<$d@c|(HrkL|_i9%C>N68WL zAM!rzO#5{!1_-e@ei#ViVV`a1xxz4IKuyWx85}X=w!F+J?6;na>UjZJv4t5QlbLTA zlMi>Q(aTaoUYkKu@C5`T45f0P$i2-S3@lpyIn*a|>XyLpo5cMLsfKXnr*=$s%?$7f z!=Ug<;vn6+7sqtlzCpLe=sG9^(%@GZfRtG$v)yj)Ab2r{J6jea#c9DjixAUfibyD9 zi#{V62S-@Zoy{CgLFJT-J zq~+<>vd<|pB_O*!Kac}B2-grM@>6}jB%2PQswA!iPsGM+dwS5@0Nc{ASzE{ex=o19 zK`x+#fe%`xreCtcvt8{#I9FE?Y{*thw7+c=i=hP`Eqle;ciV()jblxy0OfOnB`Hv^ z_Sf>*G*b=DUVw>VMzB?!^_dpqD$}j46x{UDMG|E_(burRDWl9GYrinWNGV|@Zu+|W zI6Kixv>Hg?PRK$Ao&Ho-q_Q$SgO_5IJ*Ux=mIwiU? z5HQ}ypDZ$&0?ukYl(7ndWl=np`6nUMOZ1%Qbh{s}!Ko_B@-}(49n2rZOko3jt1hA^ z2A#T<2Ki2NTWz8omJpM)m|0GZ4dKx@?fEAZ(peyL_%6^`jzAlS1g`=vxA z$x%eeScbzH!;~{?^*Hwn>32VxeUa5~B|9<#0#sbjkS#dRrPcsKlea+G3uIf)+gafv zWo6?ykC&;OH<8Ua&z2$1lVmsKluveZ?`Tp|Ov>6wsT1QN1jYT&$lb;(_0do>npMaV zKC01~Rv7ComvK7Ic{};K|I=T^SGHqgJx#Z7V9y0>3B=1U1N;lbavVO?zkf9N4jC{w zgFLSNs2Fx2u^uxwDB(8aCx~zaxte&)=0#8Q42VJgS(cPStzf`=s&&~+Vz_1jd)8Q` zNI)XZbvU_~nahXJ-09l*0)8Biwu;-zB1|YOloPh~i;B_Nj6k0)%Wuo>NeqPIugqq^ zZDi7{Zl*`uqcYq{sbbD_uz-&Nm2iHt?Ei>nLc|)Maf|;K1_TuFeyjZWV9CABB=E-` z+`Vwx1o`tgTzm{ZD^p~tiaP~1OY{fnpfKOfS1Pc+B@T<1 za{Q{a-kA*?^8|Ny?lH6dPJ1r3t8iC$W+l$cqTiBW)Q3;~2ItEBI6Rp}Gckb7JvBSs z9F|FJ2(oF|e`A2S?X9u0^4pRM2V0ox*D)^fX#07Zi>zFEiQX8qWvH8L`@f!_$YJ<7 z+mN=i!XemFx0mgZw7V7h0@he;j|k2;C$i%8sr0oag8r@;KPQwuZKFHt7I~j68)esi z1Osc&*-tbGLz%}CLqq=-iet~pjUD;6sR}#pisO?Leq&@h`Y9s76L}5`hL|KZqP9N@ zN;umFu>>8N2wRizzSk4H!p9%u#-Te|(pMV-=aY#1BL|r99J-nszTv1cvZ%s5zQtZv zoQMB*$aa-oNYV4_dH~X@N)kM#gY_VywAPnxF=ZgL>~uR0meul?$b=mwENcEkz;Hb} zN8I;fawn8UGH(2vPDVAFX@DbkS%Fvb2v+nNard*?0`6Dy!4e_aUMnD3rgPR=SR}Dp zn-^rtJN%yV4#Lknxm(CB4kSZvJZ#rj(3ItuEY!XP4U#V6q$JY!53f%KQ`kF#WS2=i zvE{pLW7@zr#wt-f$6X6VE?~jTYga1inz06H6fre-*hFZsWg@nVIn{|A=w}L3mm3Y9xPj@lPt9-gP6PN^rK-TySartD{^y>O>^PZXmR{-73K} z939ST{@V+(G`azhA-~M6?Sgiwz+o6H;l|dY6B8Y_Q7xTPMCQUGjsoLwWjw?2x<1`@ zh}oJIHW%x~g@$_zAvWyYVNrC*m7gaIi9NL`(XYaSm~IzF&xOKlngM2?*NE9i*(bLv;x2C4S4`Xx(Xh_pE03w^ghjd4YYDqwla{RN7K2 zZea9ylA7;KkmKyQO<0J4^|HoilNXY=tSFT$Kc7M6?`nVdj(L) zO$_pizF&^f(RfynAyYN6XJHTM;v~cf2q=nt_;vRYnnPv>!F)i*O#nB{1Q}%1K_Q8s z1sNCusdxp#QM5ZAtsYGIWhsw8v{TU>MSqe@KTplG*Y97hdVj3Xr^<%KxBgo6)W^`l zl*m1_LitrGeIPkhv{TyAkqe_2 zO)gjNlyW#1z9r>5$xS{2O5_n&d5F4A3t~xISnzYRt9${!nuYnQSG)2R&PC9iUl>Ke zo^T$SPS&Mhu%d*P0Lhgs?!e@IQ4&h)?vps2**dugXXb~}cb~X#iQXxtun9vFuGRqbeW#-`D zR;1YWqRfW3hZ(whu@p9eXt--t0M)o7*@*(a^XAEU?Q)szWo!b$=+?3Oiv3QH8HB_Y z*r-!ku|g1N9+}G*nfHT6r?!X?WyL;VEQdqjuu_ZVw%vXJsf{&@IJxB}X@bW0UkFju&xdm&Utkr4JY&RMgp!y?OnKw@ z*lGy6&F?{Kkt@m*JtfzHgqULP)PS+4@l@b$s&FuRI-<5(m=mEo>+G}Ytqznprm_kF z-jSO~#@otWi&YQ{1sBFd<{%-(;xERWYq2VX2aigzOxmM*gl~YaNnb>3Ng0HZ3ya-R7 z$M)W8#qGa?f+-&-HyUR1Dxnj$xeVJ1+U$N8_L|2XBjjX&*;W={nHw_u(XO_2!KP+{ z1fgPZxl$cV8sLEM8|j*C;pY}8MF}89#oi(At3tYvq;?G}fJb;n^Jw00VtpdE{o(?H zk3xq#KC|74Z8s58Y4H>@lbjXU8XdD)I!{K?o)%bvGkTB^(tmi49X<=n;(SptYHjS2@%5pewGD|M!ON1t8|-9#N@UKf?<}4 zgtqjrN7iu?D|^heb;9luYz|D1CbP*elq_)eU@pVB$)wmI$ylnF z#zWechmGgrehZmUqYJC{EryCFN7o<}N16W4zlbI@tzJRjONAFWa*zhq&mUA)NSUwRe#X%098Lp9@!24citLHCG3d?vE;VC=yHC5FsLbP zYs9WCNwi{VP(vox1&QeI<0%XB!b%Qnr=*Sy*8{HPFn515SaUqkj|zfI=uQ;GX!B0s0@zWuSp zclw$i%?@e9kU7dMfqYlfafC4_IL*Z*G9X!E7A&rx)k6g3+!c>!jA+6~+90IMe)Eg^?6tzcE~wsd}>KM!7K<=;4~mh;}UFNhIXg zj|sTV;y1V&Ru!K3Ufqp^NA&IodP?U}eirR)&0`Xjan!9&TK^wn~B5R`s{vs;Dk zYn!qGSc@BgwI_O#^>Ec{kp66^6r|>sFzzV%DfLOgpmXxI7$Iiwo51}6F#}2zr=uso zk~uO}*RoG+`;A%L88_ltgwdmDJms93j)y9FlQ7ali8@77n$Waepe5;wt&V@C92}t8 zxdL)+V@dse{H%Sg2~Mh804M%0;4F^~;A9O46E>HD6^~j9U^jsF1Kd(-hQ0y44)Oy~ zTP$#*u>e0Lpq)8&_pzlM`P%ob?yjw59W7Q(4%B)Js6ncQy$VRRLYwvIQ}!!?_=rue zB1C53lUWJK0f5H#xi^0knsQv}dq=_h4r!1^xJdp1<_P2~R_pBtrXpA4j zHrqRmJ~lI2DG6BS+Y>-fqVvP`bcn>vm+cCZ`Pz6iC@%&!E0)BhLC%oa$lGzlZOw_k zLo@oe(83P`w@{}^BY zxOsESOktd1S3ng*5qWFfcsCK*d8))8ZJ~9ViG(2*jB|&S8T=1PK(T}ga!eZtVnA@V zPK#zHw9{a>I-GYF+s6G2vXAfDr2UYAdQ(7M;IPcZ*}At?kD{B?rpTmHbN@S!l!p!pUG4p7Q@^mv9z({4Le>{PW=i4Mmf zs~^~eV|U8mSlFJ_*?}}y*N7zmow;RCNqLfdE_zD4G zzGs-lGoebd=nv)9)?-_0;4*Bzem<|G9LTj|%L4%B1cXbvKfZ4?9z@t0gxw7$jX-`e z!yAb1q)~h%TF!`%0dq=5nI{*d(4d$+_TFjpiBGbxOo)Eg`U{W&z6K;KrXX=dt+Se%gxd5y|Tkn?Ow2?S-)EjGM$BBvk`7>p@*%doAM zIYC64rL{T-WoENVrw%3Z3zitgSG7>T}>GAm`MT*MJZN_JX=JW9wmz;X)C4|NlZ+_ z0t2hnean2Qe!?n8#=ByToF?vMc%^w~TY@TOB2H`$IbD`JJ>PK#6_aG4{ahjm8zMut z9^y68zu`;Bi}+_Tqa$RTmF;KlG(J_x!R%?yhx>^8aw=aUzpeUm!L7;4XsM#RCY8^p zqzaq&FcIgliNOY}aV_*1MWa0%r_UVTfT^90+G_5(VBdW-_z|oyfPeYfEL(dCcj3~L zL4cjgNokf9d-C%GEMjORR$jseM_hNeMQ3B`V1mg#*b*k;m&u++gtJdFG}Z2uI-$Wk z&6yni&V*fiWS@S+nEr{F$ejMxuPOyt;#V;XzMLb!KIEqJF)Y?BY_ zhx$G!AM(5o@Y)e#m}8<_t!f(gAfMRdVi}L)U47O0JDl5s-6MujQeE;D98|BX zEC+svmtWhuRhdO#>^=H!`<>_Kygp-}J?&*nLanwe#hs=yFwhuXs`9dxR%fePqwrrD$HY(!zK{%|G4+MjVDi;?Wvg5XH zk#LOL)KCAQ)duoYo4CFF;Y+YU6e5|9c1`%f3)|<4=ngD;Ij*$(oO72Wn4SBMlfBKb z^ZOEA6&ciVC_XY{fikWmgJ1Ng7u_=?ef)fCQhQ0%73^rwkJNW#?+xoB(1)m>Lv8D1 zga)^FL?W0}7{~NK7~I+mbGP=I{AIlPwTD!ef+V_)vS)4Yf^LEK@lf(9lDb7y! zt#PN#K0)WaTlOt(D~mSgR?4webYF?xu>g&GD%Qti`E{&M#|YukQbu;M3J>8bCx_DW z%ZoUXo+aOpIys8K*a=nuB%iDxb}wh|F{#Id*Np)zP@V9rvO#-LlgDR(2bu;)s8_Z~ zk_tSC5C&fDRJ2qGoV5G%`K69x{CH7*ZD3cu*vuLGG9|$TGi3q{iFJj)d>% zDgC^aui`hdqt?x=yhU(X4dFKNLO;moRi)T8{o##M>mg8B zDr`onPCHRH9<)_rkE1a!k>?R#zo=DTy+FPTiZS*X1$C)}E8tG*m9q3Oj1JpysRm=T z*?s&~Gb%(j2VyDAu@$7hQlfCqIE*@&7UQ z9&mD2_5T0!J?CjNvr~3ww(n+3dLf|%K`Ehxs-Oa1@ft($f>`kWuwcKJ5JIRzKzfl# zk2r<@*&_OhL#qPc(!)s#v?R96(qCd*umAEkH%$%QD+6z+z(= z0`}PuwB9MYf>M2o|5j;t#le6|b=RYcA^`n2x!oOzDK(L1V&UTItqv^gg-C@@DtHxO z;_VyafUOiB`$Tg*ss94=Z_@Nf^QRRhMs{bW5*1_bpVN+OsP4ax6OI zRboHh*)7I1N>4sKzTK8hM)vhY_tw)>#XZg_+UdO86-9SXD}zEka0~J4xEm^NB^{95 z{T271ygd(9?9qxnQn4pUq}6i(2gp&OzBoer#^l-9gw4C?{j9o&XlZRwtRI0NM zKP0!sx!o7Li%ROkwPk?s?QM2Po8JevD~=9%php2k^K36aj7&d{f`99ae9N_nE-;&H znIzPJIjY4S(_%lwqy^Hb%NDNjN%_xIFiI%q4W~ZDct51=NwidFcN*^n_uXC6o+uq- zLM-L7a{q*8ce+n0)QQeYNu?E6y*#Iw;@L8*ev{DTBx@CRt|W@+CWA0!GG@MSA~XRB z6H(rX%+YONX0z9wg3U&zU|h7MBfXz-y9Zn7HU>mrXmN+ilcp(H)NrTQ?8mj_%$hs9 zhWw0Kvh2<%+i^8}lb)&NTU);AAt0V)SXER!qZZp{h@nljq9uJS_kmqZ_h! z7bFBt&jw6ozwH@*iES67mkCqU;Cr(V6Rq-=)a+2~s!Y*h-og~^4FmChDl%qsl@Kh* z4DE(e*l^omH>10_R?1xeeFeeaW|znAzQjEuPPERlftitDEV+ufm!zc?0X!@fO2BKy zR6vm&%W0X8PaBQQmC6mSWW9vBDoaIr-=gs>m@F)s_siQpUR)J=vXJz*D-5Yj%;0$#>M;Go2$d_#_|P+jaL&J^M$U#T6FRTQ#F1^Q(1D z=ih6|=~Si~o6Zx8?uV-4@P5*Dc+tJxX-wz6BzuO*1O$FQz%Th#6?>q9ywOyb0J>jb zw^r?HS$KD`XXHmbzG}x+ZA$hM70u~qQ^AOKRF7NQ6R6xrMkZ2D8PyLdA|&6?>Q=Ve zO|4-HZ{$r=nB8BE9~X_#Qkyk4kyPnmBL9n}HiQ|6CWg(4{1ie8Zsj|}Vw0f!0os>^ z#m2qyiF{zO1u?pekX+)0=0f~;9%8@=+piCFsM<)@PRpQHnUj{;F+^1s*PT>c`CAb3 z^RWhtaA<6*g4nkG2;1UUqerKvl_?!LhIV(o=`%d7d#=@<<=x(Fb;qfUsGho@8h`xIg~4% zP)}}$8;`80;{x9q!q_|PHpxWan3E+4V()=o(uL5c!E+CZ(iJmYqiE)^0ad3(4HS`W zLx0phTvXIsp+rQ8h3rMct6{_6XVYj+AJX6gH+YpnK+`A<;>w6MVTDq^o%Y$&76jVOZh;19gX$&0!HtqD1y- z1i901L3Y;;ei#lborZ{VfyikU_h4(4OhD3;Wjzsc5T8S{gdRU+aXh-JJaKo-z&p0e80dg`mF zX?o;ju7iR+6#sY#ho$r94aGkl;;{g*7V_;;tK*P3$jE=K#}R(Q2?POT8nxG<<;FW#)TT`(IQVefcO`op4ztw z*4^V>{+4=U_OLu#$pc^(KWFn(RSS>_<`)biy`J$B2PkCzFcgKbOO`ei5sP4M6O}BW z)a{(=i(Irl3?K~`ZH*p;wPP5is1(sm$kz-AM`P3A41`F*14)C_W2(f&1T~`)GbSsV zm(l6GPXTACr#JU%XG=;{f4r;gC^x7D23gj^B5WHE5m%M#0rM1OD*Or1z}7;svlOzE z^Gf&f(Y&@f9;cDpYB*Y|;3JW>LlyZ7QxHXyT@%fcax*;O2@245n0d{PZnq=alPsG| z;IDX2cMQ+gZKrUYXc5O?ImW2S;)Wz3IonnpShmtRE}EB&+_bLADY~0<@s%o1i$DFB zy8P;f{k}moj~e5cCS}G3VAeqjQ<;9rk9)?#S#0SOn=Yq}S)&`4(iYQTvr?ifxZ58j3ej zvZFclxt@^$WPG(`0_#plw9sbD%DcWcAyFjNCq~q^v+V?X+bL&c4p6u9p78#$=vpc7 zawhfRCw9$z5JzYRsjjo!hvHOC*mKzxJk-Kbzfz4cpJ=m(+T6o!b}Jyqvh!OV^RTzw zZBu_kII5&&XbVGAY+__%KJj$FYLmf$LOOA>yFdp4<6ALK+OsK`ablyMPc(s2N%|5u z+{cB`6ncahK;NOw$Te!q5``O-8YsiS> zq9ajDVH_*HT`VK{iwFdWcVI{Af5^1ko0{H~@Ra1d*zTn@6#d9!Pd}-LNcA8QX)ESH!CsA|heG zzf2%`5_cReB%Vr=H2^DdPY})l*d9xg$1$SwI3&n=TbuX~ivUs!PJBWIiS+C^EqQjV zFS6T>KD+FRWztJZtU2fs8nrD{^L%X?>f&~%w ztI<4s0y9J_(~M`Owyp}VBLEKC*ly7SYyx8hLfniHD&qJbYPP&yItuo!s6gV+x zL&VUJcM?JCj-D>~%kU=H#W}$oJi`m>&e4h(h~w(csy$!!FO=CDTg(pC^eJ$w^9Ew&;2V$DrSTfy$w9QDP<6G_VUVBUy zQ&3Uq7KAW~o7)1ZLNPA|K+KyLT}jVyE)D$w$uv+rP}fuikc(gwW?c+_YgS88)VsYe>?azz}?R<&aX%j3f4c zCM7Ip@QH91M8oLyx~Pjx6Pw$gNlC7f-RJCwVo;KAU>mvu(KU_^dL9)Z` zHUHBZOqa^h@-fX^J5ss`L>I6D98u@eDv+v?FnuMd63W8Oq>q0=>iE2NSOE+ zvJ!Sg`UDl!lVs<>Yv&z=@KS(U^+T=b%mS-`oAWsi-yNSV>~o@K(eWL)b+Grlx9C~c z3K4SdP_@8=Oz~iLY--1A(J&5@ks6vgyQmRg+OSLbV#}F0{3|o;x`tiYunTm+@&?m- zW+PtKu(KNO#|=B39FT1P~^sH8y5)o%&?sb`3~z9=?Z`}MgWiifH=J3@f$=h zcK~Dx7}4+RhIUr%o$Yo{yRA_*IRc7UVb8SL8rd=Ew+j+Gdposv1QOvV-x~2d4f{vK zz0d$}wTEhU4O`m?INWN_G;B=+my(X;?7y->RNcgKbg8Bocc3Wf6h$u~HTUZA0e0YT zvjD(gGrvg-7B&tzRQw3`J4WRtcwleHHUQTa+Mq>$v8cJxlCs6>p`FX<*xL`+8M z*U@z}Rr*JbUy0Fc+62tucbWD=D5pF47*Pp>-2-rqzgu)|SBeWaJRGSqG}g|M8};VFiXK4*lm@!MH})qxe~Pm?3=v-uluuDq1h7DM-0yOGyg7Qw#su$bV5z-Uw3iW? z{mpu==3fB<@2&(EOse+$N=_!CwmxE?1h!la8t^*XD7wSqSt#)=+&7`g5+0#4UIAnz z=Vl2)qarBqZeg_?k4!QJ{bDAL$P!)rsJcT}x&p3FdA=}?WYM18G?!C&9+O8+gxer1 z2)r$T%ly^D@nyqy?XbIM*sUCPHw~kAb|XlYn>BeUZ>xdvA1e*l+p34?nmr2}Vn67} zhukoxZGB?IO!yaN0yqu!>sM1{oxVc~-|$DWD0!9^EF6F>tW;(QL0u|0)|V#PI~p@2 z%37zQ5ck}8vYRq_s4_e^x&>H6zLaheQ^{={(&G@=-UWU#Fu-^@mE~bXT!_(NKH-w& z_o2Ho&*!?znanmy8zpS%0g=*?L=+S*3lGTRjpGtpJ07-?g&`(1Ly>ByDCl?p9JGZ8qYXK%!3W61o}I7TkLM7G-zC; z+C(1Dt;`{e8t+Q7fz7keQN)u39vxR^Y3y7na@NT9&W{L{anAT&5gHx zpYgP#M%05J`7QgvZ{44^seYQ9TA7B&?bB}0y+@8qK5(4QuYw?e;+@R?f%uF#iGE6S zJyH1g0OvsSA7HKfL;i))^+j@0{pSh`-1datY{bd+AciSVPNW!zAt?SY&wdCTCj>#V z57rfWc<#hgeG1xhu2s&_s@=)vx@}npUy8rX)##<h@n}vRIfNl3#~I-)n5tZ|p_@hib1TE#2p3~$M8>T{;=sQi6&ybPON;EKN70& zkkJZ@g7G=26_noCO0g{DY!6w?x;}7Be|MXD$+=pkN_!b8btruv2Ja^1;~Nyg@x0um z;>5NoesXOF*8v@DYBKW!*V#z@0Odjn0j9}b30Swe%11UyOv+7&>9vmJF_%5*-0d#A zmzr`crw;!~0)0mb{%q0vLHId)x{(Uu^FBlf*TB8H4OzS#WOFM#n2{f8ts}+Q82CA1+YbY_8$iP^?1mKsL3HTmg*cI zO-S?fP8q0daylg``|aFH^v6}u_u>Wq);-+8RB(qd zCGl-v4xh_bcdoyx(^hxhMHCJb88W>qM)lpAU3=Z0;i`BpV6(`1UYW&9W)>a!-s_pO-!;4k>g~CRnE{t>ma0cLQCk5`U){EEEyTf@>L+6R1L0Q zM;qr&1z048t+r3Bl7+Bo3G}M~=!CgEyw*ua^l+@;I&^8~h;T~LMpy(QD>K2~hVp<( z>{b(?DVQ2)%II;Tf+P#Qy-RgI+AD1kWVVk}`JYAFK6AJ{P;;4PE+9)dGc+7# z0^>9DvEMqM2W~=qnN9;{dGaI#_*{Yc^mu~o&cg6Wq9IueB9v;SUl zq9G&YZ1uIb1(A0`bS?8k*f6)oyYO)0pGxd$si3@HAG@pQZG=zB9F^A|mG9||KBR`l zO8ZGE=l^0K_Lr4?c2i%xu-6~bX}6gDBC)?CF0;y+@cP?5SgJ4(|2_BTC$^j`BPd=V zSKMp5u0Pw72dQq#&prNvUhaip+f=`B0un32a&K{;MXuEQ zE_7*|)0FD2h9I6~YyBj78gzmvO)&DyHn1*?5E{8`)mpl!^1)#7Kgomd$=+l-X$6$` zk|*!{Knv?&VwjWfjm|?BO)8Gg0L(56ZKNDcec%)rYgE4}`+n7_`sMGjO8C~Q9nBqeS| zyq<{HjQs^}PWR1&o_JOLCI~Yf0U!lQ&_hNHfTa?`w(L@ROd5+4OX=IFYucUxx=KQ1 z<%oY?K@zy#la*u!haB|6P27A^9w2AmPk_sTK&g#aTq{+xTD*S~QuXMeD4;~*$#Lg9K==*uPG!$s&XPP9GC2DSd0%+<>k19a*u*`5$ zJAC~c=vYv9W6XLtkk#m37KC?=cM6gFe9Ry>G&_!dZA(atJOzpEC{Z-(0{~qCRl%ry z5t_!E%7B}cAY(NaGMv4ncMV@gP^*;6~0G_y6TlJ;`{1XS*V+R;p4vl|zfV~k8z7cK2qw0+Cfv-!Nt;KWQ&?s-mRDl|Y&auPqd0bLJEaMRZeS3(}@1`emV z>?y10N@8ml3#%9V%E8`lw&XU&Fspi5C}cy5F2pVg^^WO9R^U{{TJSW`#(Q1*2p2aB z0z6`Pm_#}e52Tro9m5@~^xHdyXM0&Jj`3i0P?cvc!d2?RY#el80sF8YIPt25Ru}3` z0QF9DbECr%5%m~Mk6QC3y|DXGA4oPXvX@+2Gy2pa61r?*u^(Id!s0QN@D{&Yh@&Yh zD$)P4Jn}lKvQ_<*nBZ|5?7p$jJK8N{ zpZ~+I9Q(YF{dDZ}*NI^pJ$E7M+SnJ$&;1?V-n=T6Oqn*8jGroD#)119B_GP()FOU9mwKMtbn}^-z z?3ZI-o{%Op`pXMo+}M}D6399BWvREnX@&#%kW^k8`vTE?a`emW2gs8h{nECYF#WMF zFMIkMj$B~7Wt2FEWjC*Oc}%Lp=$9K~yL;@*-7$q>M}K)dw#&!9tc~sLu`h4NG#4B_ zY;j^s#=hVR`>QB(81=Fwet|D2X{<5>3((-682fcaV)u@Hxic{`zedh`G>NZR_q_3c zO~}g`{dGxdi^sm4kOm)Z^L}Tiwl+xQ!Z^qw)ze0BufTLEd1aovL~VXh{-fy1%IjV3=sj57-$po%`H}ZPDBX6(M-mu=2ch`|#>f`eM`d~g#AI^vBDB<-<`NaB^ zd~$tSKDAES<@(Hgz54q3tonxe2KCwbM)eQn8`n3@H>rOl|M13}=bP2H%(tj-mCxCJ zn|$l~+m_W5@89rGRPJLfyqKbC*AzH7cq{S*1e>$~Nj?AW8Wd;LH1Pu2I#KVARN z{4*W%YM-4jKmS~vP>%ZN^S$f))|O~mbpgU@=V!x~GFGKY(cN&xT&V!ai&yd)2G0wQsWg#6Xp<(4jwrBxh!2nr zt~-%c{^1jXe#u_R5=6FKLj;Md9zu0EiqTsm(8BjW&DaE(wYvN$)y;kC$W2aEQ~XEAzts$Di~ z7_ujDEb`c7PTs0}P8Lnecwl%Y4_u6%hE&>e4cDDb@UB&Fw^%vn1?)Jy1!V?Np-Q+B zRJ(&%wQ#!c)J1$JrHgcdtp%^!0(9*7@a$JoSOmT!dY(|@o^cehp0FpVwD@-PWKx%x z6|Qn;L5!yr!v)K1vmL{wyWpe9K5b+Bedi8HT^(>)jFw%4jK2UNx;tCDO3`9ZRdQ#0 zYanZw9`fmK^%+41e1AF24h9N|;?Fw1`~R?gF_%)E$Sr0dESw{hXX=ke2iIKEf0X%q zSzO6UJk%eqmqS=>G}A>CC17MvB*b3jdJxn}l`6UJ5Tjy(Q+n(h$h@SHVpAl@`7lrG zhxjk9IB4I;REL$C^CTc_Y_tdGx_#DGts?hkwFbC@q{9F-CS)D+{~Pz)(Z2N@A5X=O zB8Y~c`1NXzQ&a$TrsBzNTel&C=G0PLDV7;VMBvzc3eh6F2uz|LF7PG-j>!~#evfp3)K6l?4}lTS$M#C>{^Drn;hu4}#@Sop zGd}!K6lP_p|8k|XM}ke98Zl=$JD-K+ta&)L^1Ia=)n37sMVzy+L-E%rZ04VC0~5pTE{I`_K8?|$UK#(uQE|#FX*df?t#4A($G#Ed`&(^X!aRSC)a+7JiiuDmuOI^N3xA?<# z@H6&u_i<1ZB^$5|WV2=LF{qS<*c-K+rsoE+HhBH%r58gjsTtErJ)}-l{ca*oEvNei zAqxNIRl?^*!?)Om;$iMF;0K8=!4AYzpxLAYb&O|t>MUa?EDKhVUL))#c;M41iK zdI`^}L|5iyUoyAZX+8eL9tQlvqL7$k(xw;{U{pPL&PdEAkIgWh_PCD~ULw21pQ$WfekNoup0a zpp3Kkz+k}Xnt1((u9GrK-j8?;bHweqFbUprkA%Ms7^9StcE;X<11j{eQh4(uTuCdC ztRKW>&S?;PX!H2Jl4wST<)MAIgj%0#K{4B$;$vJH@F^7M*P?U}JO5tet_h+bC0vEj z3nj+1k7#)dHiXd6*gT@xFH(4oKQTW3s)7%r8xO8>yBdW|v6AW3B)XK3fU69++UIC2 z*jPwC58Dz3pnCK&CQQ35wku-yGi^a5;hIca&wGztm$q~U5~m@L*Vr;Q)Et|zz8Om? z>-tvpa1#~!FscKOz89hyipZ<->flw@D_TO)5r1&*7s()~x){$F{Y!*HL(0{X^7Py< z!w-Sze_raBQydsgWn26uQdr_$^|UGu;_2k+`OT}9O13(lgKckhd_dT~YT<4CDpGf5 z6u;$3^mYdpzC{Mwx6%W<_-xL(Lvc&(5X`jxz(9o#TyQWyZFaVWxxY~4Z}u}Gp{k-KReJ*@Z-4;SK1_6Q7Rtl z{L(;qM0*N{&`;diQQ{0F@ezze023v2{id|$c1tMu!7m`;A`<^fMSH%G(I05jq(LaP z%znofv*U|)TroSXm>gEbopejV|Ds?|7wsHYgqDL{CjpeZ91a!yr&NX0u=HEDnZ1!G zbR_r)2=MEIyAxxg@-M1YQMkjP7vQ!uz|B^MG~nb=KDo8m1}I{%UT#RYq-l>EI^xRn_7 z!pREN8RgDS<{+re&eVjS#%dXd0EYKv{baV{whQrZf<&KRc_(2f?+#i4XUZG3$S*PWP5V;-if9I9Kt zGRrnhHb`fC<)k{>*sJx!rln1aA3?yO;(4~2S7rfJZW9*QGV52K?TjhW+6t96VXgAU zBSJKV4JedWU<<4Z5AXOp!7Qa7YWcoBgRJ`E!e>UL&pu0t#U+7);-)B#WvP zy%rGHl-zB5ur`_=wx3i2*!XU1M8!!O{~Z`4o1JXPM9j%JBDOH%Yoq8vTSl3U5-c`p zr&a?+DNy8kyT=v#hX<2^*5PD`XuM+Z$Az+r3vD9IXGR`~L_9_+XujQ=|FG4LZ;g-B z)`(iwBb|c9l8Mq>=kc%@CyyuF8=I1ET49EC!5PsqZlpB>Nly@4g$22LK5d|Aa6Rpwp zZRw`=V%1#>Kt;U(H28?6P_@Cxx;?FoAi5p`yMaBk;G+GCM+~+-28-a! zup0`a%#?+*(EjTn7NAe%RjJ!qizkl-M_-K1TyTBsFg& z8}0g#4T=@e<(gBslW|3HXkp@;Q`D%^$ z4Xo6x;n#@=Z)vgf+x>;@c0s$pyxlHq_gA)qwYgo^ke+)*N6>74NA`sG7k5D4i*|CW zUE?S^vp%^!&IT*}w8Z#8Knih=e{8r{2>EnpHS9EEAXyXV)7B8;(K-#5O>O2_RT*B+ zOZVq^P2G>Fvd4mFl(e&j#oLnw5KQ`cSRh0ZwYYy2+&cyTM!{3K=#4^R-H>5r3KM{p z*D?i$s;UgLOv;fx)md263ACQ=^iOsoI%@qD9KHPrm!fh2sOZvmG;pF3QYloYHvGj6 zcTvMHZ;&Mo?)$>}Pzx3YU+geENle_~U3NtC(;D<5Xn! zeivSk;U2}QHcU-+PRiqj(7Kn?mwx7ZM7(8@B7c|F$=E4Q&*O$v?mGkGFxMDgn8bE+F7ZMu4`m4o#wW)okCV!6kj%D?5~4n) ze5CZ@;mrp&bDNW4!fU@Rg{*#SH`i~w-S&Pv$IAcI&N0FC0=M5K{e-TQe%wBhecX0q z+CSdA2YWt6X&dK1Q=aEPo6jd1Z2`Euw|zd_+i&hRQ!1ZGVH{oGsfb;cnhY8PV|0sr z1w|=AM8#;P&p|Pn9o^P0RxcC;uWI<+#iz~gX9*S`paTZ|V->%KGAf0ULdDU=?HI05 zvc;|z+OX*F?1Q*gOH&d*Fb?n8fsK*xweX%Fm_^6;q%G+mY93DnJr~n`>NjX>ay161s{dO1lv5Chzg@I_c9`;f45cRt*fJ@3YLZnvG&ZOgmesTeVd zp!f-5Y@}u6X~QFi+eyrMAEdWEZj&2Y9@7<|#{{7t02KBaPEi3*5|}F#KuSos{*?@Dhn4hp46BXL4&Fr&l_izhyd+|W~ z^MPDKNX&`Jaa6t7!%plL*;&(PZxsC^5Zlj6x_R;vPAwIqoDAS0!K&)b*!USjE3OS) zgcJh@3UiUisYnHnm)f@noqhm$%<(GCK|U(IPd+aOsk;y@q<2a3cHaj=%kJ3&Uvup5 z9g%sT#DoDQ16_FXSqT1U3R5LzLiV8YKp}*b6@vg9>hX4AtMAy0g|=yQ26rI;oY`>- zF;xxP0Y}|kz)p~yH4PxW?;Bhp3~pR>#mK34%?D%OPTr$%%E;hOpcC8JmA4I7rZV2T z<0^!4X`UCl`R|%7By8YaECn-K2zq+jqC2?_@Vu)xeV{kR>uD`5wQPI09kJSS8Gz1| zRdfC$U<_6}c?$eTjS3(nlHge7Ma8hJkbQhJM~|ZSV)vhitt$zpoyRV@23Cv789tbd zyaE|1UIv4umUzz)M4i4Q>a>YAD)=~~6mTu2hEG#`8WW|OIEYplhfmFI9zHkE>3mPP z%Bs=gL9~(FR`m-DpB?}qAL+y9_d=hY+3T0}LTG=|>(A}A^LqU99{W+B@bHNN`^PxH zXaM%}b~x`Hgl9W1{PyxdHpMO)r>k8$sC7-a_3y$jXAj03@f~G2uq_2YJ=^m^>Esk; zpYIgVpove*!78ax|HabX2(*ynSY@hFxD{n50hO)HZiO~EQCQXlbvO*Cqyu8I6xAo1 z$oJ+U9H%tQ=kc9zJKmdoZgPkh-2`0oLdjwiuwmQBRm;)hAqvM;?Ah>8YX?&m_ZdI(7eV{LZCkR3V1AWk0gCk?qD4fzv>Jo{+YIb?SY zXb$chLYU)NbXqiQCHFIbcMsW9;nx@5|Ldva^y}&&zXOZ62xb;-YpF3I*8y;8ed}71 zY&``5*($gM&9W4>{WSVs&c)+x zZEniD@$R?H=Qb!fjO4PnI{;hR-u89U6u#uXX#3fhfy~3s_R2r+9&-Ls7ypU+TFgfs zhLFk%&k9(&cBXqxKR@SAu-)vTmh*to9f{NcMFty{_xlVI!~3{xD{~1NW?w$J zBs)0KROTM$F_qMUamLyGX2hpFYq;6@c8(RdQ}7xgY_IG4Wmde}2vy7Nmel_!HGK2O zrO64Y`%&sP^7A27vjN`T2;!6R;7xbSQaek9CFK-RBni2d;F_9J|Hdj0SbS3IXacm5 z#{`*K}q4O$2t4JveRjI)gNL*YG z?v`J%^9zn7ii zsca+^-_mauk4sb#gd;%Xk4Zi1aWfe|`%>ZpXQ}#96UD%G%GWaZFl5455%Dk=Ak=g9 zzyWv?>h#3bYVzLZv&>)Cf9cMBKh@ zmBl`Z~M#euoA+5fc-zpq7?IoD;m|0RVEzV<{672^Kk3z?K{rrXIYdwUDTXaEb8M=AO$HD;oAR zTVnxOJYSNdcZ$Ddlj$m76aAoit3RH3KGv4b`ymriXQI^@ngonwT%d3wzWoqiARzT_ ztRd^Dk~gy@tcfjX29lvL!WXa9^P+HLFlTA$4}zrZgo3YyXd>7Hj6l`A(qz^c!p#-F z_0htl1ji=VQbIjXMCV@NRVEy$9#oEI)>sam(Md_lMUhP~x^as`5_?CUXllIRCLUnD zH?g=^G6!{tL;-5xZzmU9ZGEeZaDGPKL`veCF3I zx{m@D_7lhEmXT0d-5IiO4M8K#O$H^KCW@bPT{SG7hDI<&+HFSpE7GVD zMlZKJitgzx-2L0xUq8%@Zi7(F11O~!W>dyl*gz(bv7&YP8FMYio3X)>*^V{2YZQH-k-TjRLD0@EgTgwN2D~{|h{fqq{ zJia|zo6$WcRpTuJkQ^~56;D_cZnGZ{s(nso+u+W7A|cWqGz6CyQG(Lw6|K(;K?Jf- zk!R!IXa4aF;|IZ9io&7l4~2QN;w>urFx#J#VL^H#^$!uUnb~)tp>7`pEJQQZ4ardW zI1rQmr7eH9`(E=bip67c5w?SPav+=>d!m@C?p~~HBxczgDR;Uw_2;K{Z%Ps7L2_2c zxdCpXqp$jRH%SX;Q#?E8{D(9Dn~X^151jpb#`!OYc&Xvjwu5#1>l6R;#HdvB!Q0;q z>Hn7(_-gqZ*d~P6F|0*o_kThDNrYfg&?d?AehRY{iv3_Ahvo4AnTM&r9`xTBQ4?;4 zRUTr7Zo)X<7xuI?Ulp8vLl{%lm??ZmrhFT_lz_LBPxIVwF=V#h_ zr42vu+a|@$`R&8XjBVNS1HWzC4%v;~fhRFhtjw8R(cy#-j(D`;3I{i{-@W|iGu!Wl zI)XLsmrlYo5v`Urlo`irXbZpju!ZOMq~TLbUZGLk(h`UpJJK+-QtwzA}JD%p)Cw<@zWQb%qr*)L1j zSD+5i?w=PcS1SWB_Fo0cw3DJcn13FOl_gYD zaR4g+j!=Jc@E{%-4WRSTvMt2@TC~Xz8oheGG#ukvu+XvH^X@iJy2&2`s)B5jb0x$i zH^-1i*J=!hQB%kK(iS{1JdwS;KR3DOKzHAP_L~Fk!2|8L2l_`3v_}qfj~|#^^&S69 zio|aIz24pU}UB{k?j)2UA z?Qc3)QU|{K3Na_Dan56up7chki`L{gu7-%_QWvh z|**Wy|k&i{WvuT}l6IKJDD!(BPZ=OK(V z*~<8vH>tka(aLXI8Go798E6$jOlu0ygA?#$>y`UH;>;xJ_QhKyTiW=3a>O#S7_%V# zj_2D(ubBIj*(>J%Wd0Sa{>fr;D#9LZC&c3$#N0UETkr|Q;k^EEuc7Gp3egkPy+Ry3 zrqujkr<6z{MB$KAk^9pF9(2HJfXsfWbQpKFQX$>~Ml)PH#CsZ{f9W!IiS@(ECpf?&vJR}}ZRK@|O za|o0fdfzl7SjLk$dUF3X?lE0@-^!5`=VlshL#eYfsU!GAO)1=pj)?b=p9PNLsH3ei zdsMfC_3NEwN;aZH&=5sb)aD@gC|f2PB-zWvi~3#tct9wjPt;h8q>3Ow&OXG%$R0gM zo`9yr|Gy3zM0`00!Uk&00M2QCqZqRPdNh-=Etn>1ygawntt-U$KG5dxs-}-u?c_@B zn2PK~~3`>XaiEvxK^YUN188dQ~t`Unv`Uv^gtn1pMf9BGxxG`gA?`k+^QY!!=o z=7eq=PP4+Uwo3`#SK~e1wYZC4A2E3SbkUL2zyBQqQ7G17_U5G`f z*KD~NL5DabHzu}T_O>&tXn*O`HO-NY_Y-Uzd$wxNRSDSsZsD>v#JfjZTNDoTTGigH zVa;`iQ&GlN$?6RBDpGrLAln|u!=rOgQ@ z-sZGF!T_RZ_xLJVcKdBT*jAvdz5Ysc4HR6ru=vRlPbU_6)gdaifINj7pHk#=6{?1u zV7;A-5vZTN?Tt#kfFxb+;<)x1>=A!oh+Zw8l!+&G1#!R3#QAk5w*xGj$;5Uuu`SWl z_}Y=*I9>5@`OO55n0?wWY$$fce$i@Ix1#9(k6oqW8k(XNt^TR9UsJYYTi=_aC(7$g z(aH7njJg&J(9Y8J0L=^#?SK5vIHc!}&KU6iOQZ5PtS*8O*eZB}x&A90|3zy{&Tn%k zwXV~!5BnNRXKGAEfUKDVn$SN}?Q5^G`%+5ybm@)&ugtS}flOW`$q}UvoZ!AX6To!? zu$E|M1rc*YUPyJFX(c}@v!$R*_*1FBoHg!Wl2s^)Gh4ae_?fES7gGk$Q_3=4uQ=1Kw_dci zGo1@!ogT_}VIYyspCvtHGwAu%MrrNoeBU2QY6EPYtXsx(UTU`&DX)uGPNd6KSjTKB zeBVFYC#x_yX&bL|(mt4H#@XqqJ0q+EVh6@9=+uUO^y0hW{{CF@S#oEo*mSPZ8r{bT++iKIxjoQs_qPK>XSc-vTwRbD>3_8Q!#gNPdMhQ4 z?fM?o96Z~(+j{KQ9%{MUt*L)lHs`z20*zW$dr$4F6*YI3B<^LExEYjudc~aq4JwxX zEHaU(1JSiT7KTGRZAmAtm6M5#Lf)# zFWq((2p>gXCV^;f(^A~+c5>rkNb0Rl6|IZZj$*BwBj*v1C$|w44~R$~ZWcjgb<$FY ziun94leD4;h#N`r*=0>o7m{Pv`Iug6R%LT*P#=vs~&l z<$@gZqWB5HZQ~${E^B4sf&YBkFS?~Cy`dVD4<3?!R?;`5D)!3y{&AJ+zMPue)aFi1 z-NRku4r{2`;-iE?mLBV}C%Z7qo*Z_q?E+l%r;o=&_=nL(Rnrz$k)}N$A9Boq=$0Gg ziS_MMfT@YO*tsFMipQGHgAd1D$wq0R*&;ml4+LpeSR>0gk066tLFPnPXoX`|@I1Kk zevz}S{vWQBYBUo;YX)PV(-HZ?US=VU ze^PfUi)}Pkxmuxex90jN#N69`Ufv}1z|oSI_ClvU-|3#K7M9Q!JZYu;n zoOI((JGcw^i}(9G?T+eunECfsw~9a!zq%U#zG_ca>3rp$sPdRBMy(b`g4FwVh8t~d zCzc>22m7BW`kKQp<6}O3eXWZ0KwRbA1-|%az^Q;)!5pXXJpd56MoU3$1t|ohdEi$i z6&V?fU4|7a;MYV6;PC_-Z;17s6PW+1L$VQxq{MF=&72)Q*GtcJ9W4X67K!AsD%mu> zW48G995dI8p&}2l(-c%vOnfPtKOtHJ*5PC_eV$S=`rXs z3)2Km`&(!vKHWVy)$X6#gce(r2)H7CJyx1Bb2t)goFb`tBEV$N)YpHiwN0QCaFZ_y z0LY-ty%+3?XJ`B1-BCQld#@tJk>(fZm*;r3FecH-Azl?eca{s4q}-_Vk(5x4j+cet z#)Q~d!1IU9{Y^%ZcL_7pq+}10i{Y^qk~?RAlK8^SQ8fa7ASg`O+9ix2U`Uu7Ej+Vb14k{*)A8_xE zV|{a^{~(dU?_euPeBUyuJ_GX8e!=bQzi9hG0-gT?%3XC39FDCGIBJ_3~;&3rVrV7<6#r z7bkMWEJ11rB10`MrT59+hA)K3PKu`63qUw-53yYz;!dEaNRpnPmHW*aoQ9cP;*7_qDbq#5T}C zmO@CWTVRV*JW~VQx;q}$}_Hv<65whKTgjsT&5 za%IZ6=p>dis@hqpKRdObq;_r~0Rp^eHqRgXa|M<2xRpSW;Hq|{X`s5o!QFYnlyfcK zY@M*TU1T=iwF#x57L7oO{ya&FdO;ExYP~N=cv!GASWyt+Y5_z7(u^h4jUi~XbBAdJ8XYJw!GV^l0gDMCWF`O zse2s!V=2*R;r=AKBDbFrh|TxA6ViJrsDWZ@w>sH}V>=8pqWAX}{1CxHQq?EKg9s!P z-4`-;`}I{70AI}r{cIwNkOj$P-Q|&BHqoMyqDyD&O#OjbqnQw^8%I(%rY`6Eo2a#( z#9;*o&dG$ntFeq?8S;M#@`&An5Bg3c3^J~wFUwj ztoe%T4M4yC}BT(?*id`a7)To#nvsl6dkwH8VhfgWUT9{w{ zTyu`o=%rFRLAFR9b2XI?OKvfR?73WKhXq^iy`_*0c1{s1+`Vc77ifsPzeu|l=Wf9^ z!Ql)M$h=U(vDGAX2}Wonh$4-b)WnXxO;;w@B50F`D3GBBpDJL`#CEPEq_EPzqv;5V zJp~ew=I1Y1O;Vczx>;3!n$a*-EJ-pXB-7E&DuNPyR@z^Lt>g#a+oPkh%u z@?CrIyW$MsiI~H&-?ihv>t8(}U;H)jq2S&=fV&?3HTy|Php_9#eUT!Te9aF18uMGQ zTGsF{9AJlvlkjupt$mHGW)k^3;-~lJn(yvw&+Us1lM+4V-Z+3c*)yK1_6C~&0l&9A z2M61Io4chG{hw-lQGf!gAAyXs@%+QK)Gfmmu?M!^4&YxX7ChEAR}JO^SRtlOQy6Dz{ z_%RW64~eLsM0f~FHpSLIiB#xbT;{N8K(zxE8P#$LNN4<0nKLb@mA2bW3c~YhKQ$li zl!+4JnA;}0u$o>Xc_TD;IG1KC3BZl7OKnAJzew#y@DfBMJb@dudM^dg#sz1mRuiKQ zwhn+qMb&mc9bDnE+;>lnw(e#jFZgDYBd2}_d!8M1(_m?ni9o~HM#$ohYNnGWHxa@z zp8$d&6n$T5pafIsKsLEQn7C5fP}u7wd#$8SHv~;&wj^V|>xeRViluO3hyx(tAqVp` zw5?VVTB@~?JvI6~3Kpa0|JJsR9zW0qBqlwLL~AV7?Q{XzBs>q7qMeQS*w*-}+6h`` zw@Rs<0GT-6p3dAcQh_L4rRzX3G}iA&Pea6+=d!jujm+@eyxG0NDYI_o3-<1Mtc1T( zti3g_p$!EEisMKB3VBXl%z+?Kcg3pkY#G?`qi9iM?D^>fxQTbl%V)a7B^FjB7kAo;N(g zO+SdyKt`1unPm{QR0-=_?R2Y*q4h;8zGSrHVI=?mw-aA9H+no*Xntm!yl6gb^OtPv zXo+HG(Ck=Lhb8$Po_Rw9h#l*c#fKPmZ^B0rGDZUN2 zTo2yw7CCn?CG!9-3Q%Ek#h*iMkP$j`1qw+N&1coNyJ?gafx+$Oz8LJ2ql;|xJ)lv5 z@>m->mT%~(f;>h?No$=b9tSCDl^ugu(gO#=+oUp}TqgDXTQeWa?7z>#d2+VPUbIpG zY`o5vsfk}S_5fah+(eI=;d3bfyg6}s^mv9aMkU{eynooIui&lA+)7$PQMEuj9d<^7 zS00v$?Saf5E5_6(YO&Kjg=8H?11Zur9UPj#3ZGkOU|~tgrIGsj7XZI)nDpDM3f5a%w-0a>A3g31sRt0+Y0)Is#OH zB5)epCZnk81DX#6Sa!c?wU=xtz;E)w7B4bmtK{I-kmf52S%-fou^&R0@WeSET0kU& z?jNyR6Z_1tBuf!s3&xg^p~O}s3asUqPy0Q^@uWh7ioZ{cxmi}Q zeeDMpcRZ8$hkW5-A3w*H(T)5I$pq)GVzB~;D+@OA2|)mP^Q5A`NREJ}Q1Vou*G2bf zHx*j7k*f8Cj`TqcRlyXrTBOeml!pilX5zj9HKBXe0h!#ix7^((m}`+L3(D1pNc@}} zcs3(;ekkbPJ;>LsBl@>un)RAF_I(6#S@})WOh=KDJ(DK2O&pG1SMm2%1@&or$lJ>W z@!=n~*^wz6_LUODc(sHp;nzh^=Gz~`R`+hfGm=9?klcKR$*Z5z?asfALXX(%K&)@r zD~y>0NUU(7=oCF`IWHwro-o`+@pSsL!>cT-7tE7-RrSiz92`_D{+< zur>>1=QC#+aYn>1m3Xis55~jzx$$!ABB^D8ztr%AL4&K8*!!aQhyITOo>jJGWupQQ z6>Se~0j{@{JSc@APR2VEz&zOPivMC7uH(7>C3N2**cB;fh}hKbrUMkg)F!`=PD_B$ zvXMxU5^as7BP#6_T#QNBl`~|W_@8RkgyMs9g6T+rE{w9b%1Z{OwS+AUuNFPZ90C7> z%caF-aH_II_vqT5UiLM>ziw$0ZLE)PX#^vF}1F93%KGA1bw zM%UK}E%Fy9DlL8=svszGf5{#!VPf-_)$GEW(IcM9DN@8TV)H0CC1ML*r*IAxDYT!TF2LYp>bB$*V%N5lfzPRM?oc~wsMiMkZ?w; zli`cAY{39$cwW7n!(svmpdgEhwatU~L@O0P{Ya~OT%`<(ERlO!EBEm^uEtN%Tad$#G}`w?!0b-RCS^1Wrz8Tzy=~UV)ID}iZCAk9&eHk=zT3)yeWN~WGtfutR%(erMW1VZX)$Sbg3?Vdz-tn z4SfAsTjfe!xK*8I?{2Gc;V}j@TMTH3QmKLU^f+pD2SJ2e!OBbtoeOOacGk09dUNPR z<8QGh6>2inf2RJ7LD&;L*_qy6!#L`v_^TW6GUu;rhYbC#;a_Xmn~LHTLcCi4H}w2k zA_eS^9sU)02&aNaVq>wInajaZBZjUFf?@)K8nNOE!O-ueTT?jbeZ+fE#cAU=pK zqdTQ1Q}lv3qnH)-RG4lp9ZrAKmnn0D@e#W?r6F7)_up8jjn6)UNyB{-F(jxbMZcR& znS-pUEp;cgy$p9EH+%=`*!QM=<>(F~9hrxdLt@Bjp=6EyN4h%^;tEVGU_QhxCsN8p zA%#<0P?s9VD%-z#8NV@wKY7Kyp-6t8llmHNkVh{xFMeIP_=;5R1a9-l|8^%PIv(oM zZ*bLLear6rmcQ&9?$*@qNbQj30jX~$l}PIv)PrG^VljsYGe0ysqCj~evqY*3Myl#Q zk`$@XP3fQnBbTra~MMVy0!Hh(b=Vx z{$;@gF3YX9QSEi93~(68HHwEDt&6pTE)uPl+uAY?AG%4jvP9;gcq9vs*#l*O-3#3< zr$kNkpqO@&62*(kGR)+c%4cKmc19TOsXkS#+Moo2TGKgVwIh} zSG1vbuLsroDGX<>9&~H%or7}d?UN7M`>zU{M2f@~AxIKbFz;_1x-)|2gaAq)8*o1t zVhp%yc#aj$bZAq+9cdNNJB9@eaP=dS^FEEM;oAgyNDGGb{B=MP&M+k$^)uW|sub(` zn2Bf`8je=NGc$rr-G$dK9)Z)gLQ;rB{USjk9nidN7?ku)5J^;2c_Jj~CfPlf> zz+0IHVHIi};~Htb%67Jvgs!i(4)Pl4jX4E3D8^|#gK2--=f=7*>G*V9Hj$%~Dr@mP zmED9Y$*$-|Uzs-K?ml$y=-o<+aS^^1yDGw_^d3qv620kxo_B3&XE6135m^u9Q(1=k z5``qqF101wG6`}*atWKR%v6Ce4D$qJQ#_1#S!O~cbt3mpSxK6Mw$l=+jb&+Xy4u~EEX%vl>nA60=)N?aiEQud>Q$Mku9!EVEZ zZc~VpB9Oy~m}Tfh;2;c4C*};X04d-H6+6FT=T+Rfa7uq(#V#R6&d#aW)qHkilCcz6 zBYicywqiGM1iNMp(OH?dv?PkpDaKdgpkMR^e!!a#c*fEo5qHg9?q&XzII5lXuS zYCW0CiJx~Pb6*dI#--cO`9e zSD`a?uF8vKMiwLZu_Yt)_TVhskVjW=8d`7rXwktLVP-QEu8QArJ$VdDU1htXZr9YUo~SCx zlQ^NP_I_3Q+r1!;D%%&btB~2ydX?#2Z~*i+jAmfLjWxTbrq_j200j#tL2B2r^!E z4EDv?X-|t+oX9EEp3tTN(=aw<3Uh_uX=z55+NF7^XQXSBUvnU@%k96%3PQFf}O~i=&kyqR(*H(*Q!z96-x)|>oB7yhFZlZs27t<8B{xvPjAYi zok&08jk34{yRZ=4PlT!1=74P?2Is#IZ>V#3V(eorEQ>gk#T7Dy_cp@|jn{+OF}lJb zpBmiPmR9Q&AL_HiYX3Wb< zd2Iu}!REm<6}!)p3Vfc~2I0LEIjeo&V)Rfeq*aoibSLL`b-NMuwwLb;^}`7591;R& zNP-w0x8_*g=|)LzVmkcqXBP8Xhc0*M2GLtwQL)?MieGhy8*!wBb;=~6>-7v@q*WJ@ ztv^tj#WOqjX^$!z>Fw$&cUDWX<8_i-%VIjrpd{*GG3mybq87$Xf|lm1KTTwXROi* zx0xHg$ZY~9O0d7xp(m};z5FTWC#mYYI{v;;>0^$bIyJ#?o@`2q64Y)FS>?~UsWzfj5 z;;LhFNz~zVqvkOwA%lc;uo=FP$}M) z5zJglV+F~d2F9xBN6i=02+$%t^g8!zie6XG&=TupYrCQRKZo~|PbE|PhtNPwD( z$w+Y_AmzmaR}i&TFk{%Ds%5TccM>D@%9&t z2F_0Pwus{Y0MLO>eS(E*ltAowD^(JzpMnac`H{gR>n1F!E!=;9Oo{VaCfhcCRDs@j zz2Ti|6lZ(_Yjr$Y8mAlBy41S(8^KlPKVWkp>3`q~j|l;QHimV84oo41!OvzdQ` zid0yJ+yi*U!IeRsZoQBU{DXH1Z}($#Ip!ZAQrgb=w#|KD<_}VMjRuYyoAhc4MZ`!v zNa)$3DmA`Mx+!*L+bkY>By*Bz-UiVPxrL?BKxl=HXTgFpLz{sg1WgcH`?X~X85+F| z+tR#BSm@g1Π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

NUcSHb=EY|{||KdCg7DqigRYc=?&W<>f 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%0DczvuBpdj-q^ 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 z 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\VpgF(*`J2>{vt9oKOofF^*c8kd0iFHYhEj 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%0D&y+$eRPzRkhGCC%DW5lu? zrSmqZQdi19$oCvSi0r$zGbk%W^dKy;@ZelA@E|Oi)=t#R79A!)2YknTA#&u4g|89e z{RF}aoM!edsMly~J40RZe17|1Y3RoeQCqbJNv%|T!Noeh;DVC5?Uj-*a2Ge)URtMd zl47<%c!HQYOHJaPrAm8lc8r6WmnFHm{Xh*T!jqyHw zM=AIYD|l8Ee1{eM(>Vq;mHsJPw_{Evqlp2Lct}Tr*i9s=(_eixFCE5 zQ7=>oc%V0!V&~zl8;MYF4A2&wXT%i)4l~KO;hkKnkK4JmC!)XeY}3E8|4lXALX)TL z4Ic0Hg*2msL6kcGm`X84Qf3501;zA^cIUrLmJzVgA0!aWm=aQh{Jz(p>WW4zqW=lAJ9N%qyES zuW*>h3DY!zVE(Q-bGwUdHR^@Q*r9R&Gk>tDDfsVkm zvpMr&hq=p6m3=>fVBXuD`MQg3ou>%X9D!gy+?;u$!>sL6k~<0n^Qq>{>m25=FqH`e zGw!U5^G4bqZ8d6ysZk)9TVdqVl8wr^CEfm<|gB^MlQqr#s9!`&9OM0>S)PbLRaH^DJSi5(ws}nloRWZtL7(zmnWT zAebjNXMWUS9xqJO1%mmN=FHU&bAvE>8&nQpezQ6A1s%~f#S5tV4L393<3n*VT+q>= zbl(Y$mV%RCRflbH2b#oIG#9(eVa^k#Qi0In^XAM~bh2GhB}{t+f_Xu6=KI0S3e%fP zVH|>wQ+P!uQTWnJZL#eSDMh;rgxGb>#opvFmkLw4Krq)dXP)dZ?-3@?VU+`z@e%Vl zeYQBvgM?|cKrruaF1d4tt@Bc0+Aa{x2bwdNILz5cRQ9<7!F;?qbEU)Fa8xmSr7s2M z4%k81)XJq~HtUq;7mo;+#H?(m!&WEVFBf;*KGIxW9|&g?eWYC$_7G(im&EWeBgaE` zLXy+p?jJAmmI2 zv2ue((*$V~Awi9vjX$5zXqJ%c1tRSW$iPBJHm-M_AF=!~c(%}Srngb$tDO3-%f*`# zriiPj3xve?SyybYnkm?Nfi{(QRSoQHQ0HSCv4f$hM4iG^pR99oEpL{41=gLyL=`Sv zx;m<}L0Mc3RKY`;shu&Nz=&FXVpI+MyNHM$0-?;u7*XLmzb`u*YW^Wxs5Y1FK_5P# zSf`2;uDnM0Mhk?r*}vt=f?()&%OBvWZrp|$b!=a*l4X@ZWIY#IH$_sVkZcxM@vBIx7pCI^AqTxtj-)+;w26?QBB`mcA|;`w z^?%2lG`E{K3#&j`hWhH4b?kFfInVOq+A&DLEE%J4@DZ^s>O%edma znPpH$VVG8M32R-kG=+*)FoeG4x^@jvxo=^LjW+TAsLL)lNPXvTnj5t-|8j#oG@XZh zHO%n}Fx9{mP%YQvdZb}(qGPF^mHBhFfe*|bW^GIP0cve=-MK1-x^zQ*CRGl7rThj7 zg#Yjb(71BwCnTE%Ui?)#OcJJP0wHG=GC_?l6QoUq1XT_k3$uhgUm()1M%s}ZG^!A! zO+;GcE}u6lf7{Lc_=YRzwO#ld1cE;!Rnqgccdb*ULX>- z#%oM9cgAZcH1fCnq<;ua(7IQpQ2t*~8$1<2$Eb|2LU;@~ zF1CKs4&(}iXFtOl4~p$wX4`y1qdXzc6bQC2!Pa;}qYJMzC|x_L(REiE#5eBH3^!#F z(NugJ3wJaP0SoSEdK5o!8r#F5^qD6$a$g0t zk>g|$F-0ImHG&^Gb^@FE&zFeGMAT`J#=S%oaY&UdNeizsR0*#TnNVQW zfpAM3WW9QqMu(8#hg}+-do3~mf3ENi76|@!Hvd&%YkDolw-mgA2A`!<*_9g#s`lb# z&DF`GsTx`WWAL>Gbt;f2?_@l2EuG3WvO^URT#cu4JxkjUL~)F!YG^Go+y334PEIv` zjT2?x{C9(l!KXADY^Z#P3WWL>L;d?tY4q^l4e~vHN~5>& zm1W%=i@c`FL0Psn0hVp!w#i7p&LHEHQySGs?%M@Ii=BvYR-3A)+3xtOBAQpZGm=|8-bx;geN8(48;Gb3rhIv)VnbzSl!bfbd5OiF#l# zuoI6sutU@X)7xfF{QPo!{;YZ7)HD)QJ0U=Q)9E?4R_kyj}&BS@9pjk*?c z`1DSZkj4o_(mCif;`9!YQFltCYN2fx2p&HF)~ZpXJcw!vg~ELGK}ScMhGtr=ak(<` zX`QRv7!N&ylv+e?Cb1KCEi4`t@rSQ)5@x5`X1V?=cQ=PC9LMd%XSlei=*EaYKNc(w z`HSuo^-X40s;B^%i&J3WQUZxDgJ8wHi$l>=c2Zu3{>dtQ!Q|C=k@| znTjRuY|;H1fuMek2It~s8a)LCno6xOe*oXEZQEOYZoJ*Kuv_e+TDHETm)fqeRmXOX z%X{OgIAGB;iQTrQ&{KF;r=(s>#3?EF4F>NBZu*d(iaSAlv6OkvRU2qKcL|l9$X3wx)qD)BJZPN818T}eHn(tNaSuYT-)$mc?0B)C2 zJRRF8o*aST!MAned3p;iPat?YG|Ay(JC&lw#R)_P&Wpu_d7Qu&$9AR*`mjLAyaYWp z9NP)pXpo6*=ne0ygEmR14>&LO_Kk)*GPVLN*v_6dt7)Hy4_nxOf&I#DxwBMq=bTQf zKB!JcxKX1HZI$6&1VVwN1dMnPv!%0CS!4jiahldn_yvOhZ%n;sqei_2+fN{<_?9r4 zlN&Xv5NUm2;HpNAy7e)r^IBC3*io@ml4#U^l<_qtrXJW?(h4EyN^ID+&x;@ zY<7sn78^b*)uFGU%E0Q&e%{}xQK=-DBoOi+VZWVsTBFH=tr7_8WQ5k`r!n1Q^f;|i zwa~T;1ka0*z?-4!#JBGz$oDHIAa8D8y&tGTtYBsoL{1!r3*vofjDlF**P!%mjT#+5 za*mMk=cvM%A`tmjvMAKMu!vd&U!h92ze%lZ})u6jN6^7{mw}i&IHyhMxNXd{P zMHU^J zwLFbgEwmbe;K6CE&S{5`6Tc#DT73&1bta2GY;h)Q!Oey`leH6Gl$Jd0eC4of1j3eN zoD36ti%x4aP-vwB!P6Ez51+>WXYgMIXPIzJ7YNSFSYK>nohjHk0zu7&1T;aa{jphQ z3kw3T_q3_{tEeAPqRN%HMbW|p`pPv|A9J%BCGp@GV`!NB-D_QsXZiW z`UwQ}2~guJfE{aG2f90vR(7G1tg2PLA$6eM;5yLdAD9g(9z}aO1oQtPowz(vbr^DC z<;&tQz9o$sJ@yYnwKnhK2U?q|e@JVyA9np$-Tv(8lygzMZr?&TJLbPNa9YJ^+s`0R z<%ifrO4SB>8#nbsBOF)po7sJZBeSfT>@Of2t;ZM2?ovqkO(E*gMJ{{sFOC$97SW$| zO~byrO0TO^#PUarL-dXJvF#~jeBKYE*AT7tZu>tEc{lb`)3=Mf=ts5bAKxlub}sbs zJ$++_Shtsi__OeieD5FQJu=O7WnrFwnh7y|``ew~Y3hWpRN}5DW$S$J^bfI8s~^3x=$yV|iVg(p|;tSEj2gPA_3B zL$0rs-5JG2f!m9VV>cHMiPCZt9nQ1;4ayjK6JPT9jfo9Tf4#+^j9@cVHBcdIl#_Nd zr+xZX%RyFtC_FSUW>nLi% z%Bz^#`;10?1=~*`sNEfEK(Ipvf|?C#lVb|G+77fLBZ476hO3T0Dza6^s{AXHU0JBM zRGEx-;&ZoQ;E9(ak35$pj4I3l!St#Zz&!{PcX|BKt+q_+my-z58Z($HM}~QjN#Q5APTT! zvmdrU*L%j^@1eATzZ!ED+Odu};7-A&6fSFwzy^%6!ZE6o!mmIXmc)WZ^tK1(|Ik2# zGQtsSBvp7|!(2#@;&%5Lp0iacM5#rcmDjporeH>pKIWvI??3||rH?t`#5)ioxu0Ni zo8~gZkHRpQ8UA`F;>A-2j)MEB)`Nlc_&c#?Wy#YFehSfMPOuVB`S(#BbMECQOP)I2 zh4TO*`iVLJeHZE!y2o{&s6gzg6WLv244NAyUW{~Q1mdPQ;>N_g)%&SRbpvCL*K3p` zm_E3FuNA)C&spH2k5VB;bRr(O)ridP0$U8+jE*YHK?0F=32ywu!0oNOWZ+iusTz4< z;MN(}Q^em*u^n6(Ow@haV(gYQ2u|hxwRg6X;EdfmGtC%vMxzOm_hf<4p%gB9=?phM za6*O9<_iSR?RqWx(J4pXmJjYlBAJg|D|j z@K->GrUl&|Lb6$~M26+HW?KV>3e#wTkn<@5pmKvoMS`@6kRXd};@*Uf#QP;Z>MPwEn(kMJz74*08Qq!$3x7{tT_KMkV zzRLxhzKQe57TH4uHoNI%8uCD93-JS)CI1TmFG0}0nHlGu)o7Nam?IEgU5}9CzW1O^ z<*)GqK{XSJI>-0D&8yF9R3|!j2KCmnh^~L0)hO#<26fKIWBQ1*8s#Hn1G9#?(v%Gr z2+4oLrFNJ>oaM0DKD2azr8=HyoPYc+kGEn%stc!TUrt4T^6q=FAI9jKN5ICet@~V+Oug;etb9lqy;&pri!x9X2nk^Gy|!7ST(GeO?;IuJPf0Q-8wH6a z^plhW1wsPYSi+0UR=8iI(Lx?45N!Ot|A+Q#G?&@X$txA|B!OV-irW;7i%F+)!BzW(*?qi0^BUH ze5XdmNXda;E(uFOLPIG9H^PjK!qWe5oi)1;U_zLZcm| zQ>g`yHIPo#LS8QrYUTo+n z5X`qAC`N3+1Pyk>br6!xg4b)a7xXBXPMN|qP9Wsmin~|dbm^2W$e2J(g$=)u$hXX; z(`m~f<42cH-G$miAh<#oV^6H=+>{EwOdztzMHc9#j~Ap(gp$$+fKMh1xm+O9-jPJM zE8+`$z|i87qQHRsp;n-zi06;2h*Q*+SH&0k*;c$o`Ye}mT7f;wc9z>BHAkK^SJnMT zpeSxE^N55F0x|#Ha*q5$p=AmL59Y|#?6{5X-jZs*eOROLTsQMiMn4%hmh~zy)a>~1 z0yOvWruMW7gCR89j$P$f$J_T*Ygb0V$`6h52V?YfYs{BD4Iy#w#v4Kb7GNjzUK1~1 zN^4hFETNNYN^7*wlo1SxxR+VnvI0Z-sa*Vo&mb#5{Y?q#AP_$Im7j!`DG)qyeiCeV zfew}ZRNc}~-a_$HkHVII!t39$tnNCcn5y;DTVtNdA5rs4G`%(6QU5ALZJ&odwzxIp zeKz%hLW44fVdhaizkzH#+29J(qSkofEk(LKU<&g`(YLK}ms$EyXp`FaT9xZKff-@i z%V{1SYEWvCU>#~c?Q5Nq>Y=weH7}~4&&V>S@7_QZbYl6H& zvRP2On>9hF(0&M0Z-J1Lj7%{3+Cz{w5yRNc7#Z!T#T*IoE2*zA4HO6oDF&%m(qC9% zyt$?cd8RIv*9b&f9M|vsgpv>6?h#s}K=9z?F*ocDg0zW{53fpl>$zUaMIh34fIp#| zSCBRlX+<~WdySC$2}Ihiu)XQEk|QLW1rm^ZvzeLE!Zbl3rw z&pv=s?h98)jSsRp@nLU#3xIm=C%#4~W$&{?}1cK^J z$Q%pGgv=j?8T?j@YesQ0K89unCmk%OYE|@7MWCwKBCjjMAF)FIC{=lP-tD1lhZ*Fl zL$&e;X|=b@zdZCZP~I=xge$wg_2SB|`J5dW%L>tJy+F7GOKx$+QY9pt1y21{u`~!1 ziGGm7i%1BK!!3xAAjN{|g0Q3+ClF}|vO^qy;FhCExVX3|>Ubc0L%b*UdtDjPLOSdH z`g96y8HTz2LT+jm#yuRH&}dSdCr_kMCw?djhK38G?{F-Nz(rv?>b-733Jo8Qu~zw3 zjmp1Juj^5$^>eps^yYAbIxpF(p;92Q`}S6P3=#so5C}Ps z+JQY^kTww##DmbGO2{<=k@kt^I#dhEW`TrX>2Qtsw4Xr8dCt}$N02rV5|j>2)D{_PCa^(DXBJ8aGlA_!7{rr-3q}}f8u0K46eXq&v*)X#oNF`ZhJ^VP zeqiRXTd|}Xh#w=*YV>L`BGC!N%+7`Ry>jo6ktWwoUsoQH_USb+;txfGyaZ)iUEbHW z9)p>HE31eo@bq{ORTdkRW$}t_pI$@(ybzPa@J8_{q5%2~1H%TH&$)F9&sRe)i->&l z-8zi|1=GFdLdnARFU-0w3LDNzu-{R-AQ%cphc?sN*0%Jfc&*2>)?=adc1Odp(6H-$ zxC?B)n=;U)yCWRHcflU!FGh^M&mgsBlfG2iKa3X6#6m%iU>=1%n7@C8nU}#-Aw3S# zKDSOg?lUO;s9UGYBL*4n1f9I1T(&?s_6pd4Wr9w(M^IY7V|j^E)V8wp=6GwTyV%_6 zF4$WVu}fq+TeUMHMN%$$z70K5BxckgE|O}YI7Kox+EgTLBMweL>5q^WHoh7a8(YQ1 z#^YjJny3bL$Hs24|8E=n73UAP0*-~$Ki<+$m8GAur2}KOrJu5;ug8pLr8=9jyMY}Q z3GME7vpy@{*{pi zd9oL%*|2ICX3Q1{3UwKU*1?|D`JScSiUwOk zu(VQzAWjvkpsWzgmnZ{F??=GS%UG4-^Q8je`*~nPq%L4KOgXF-a-%@7Ed<-bof?^= z4dS_nJ4V~ppiXkcy%WR|y#G+8(L*34EQ170T8!p|n8@=kQp#78%oV-DBuwPx3h5`L z!hFVp(K4T5jzOypJ@fjhT&4&_F6+RLT)KemcWR42Kfh!|NfD~H9X}P#;%I`KYs>^U zYRs);49Xf&67$E3BV4vLf%kMrfG^CCN4+q{pp2oGKM*M{9zjpI@dlU!T>emhsL-Nk zIpL-;2ASxumx;0GqpqWmeg?|(F4#&Tb-+T}?oAq9G8U^>n>1=z)U<$VUvkDt(iSVP zX4px3+eva4HzncyvV~*+Kk8e=idwN={;)qk7#rg_$a|}DzKfgxh00M2*-;b68kEJ2 zz1sA?mY>6s;`}JlLN_)L9D_=D0hEfeIWQfq&Gr8$txbhcoYv;a|BGvF;I=y9=#R8; z+lT*$ki^bb?-J#Mezp(3gIKsNXGxO}nz=13Np7{19JZ5`$0cF6oi`3{;})-4Qr!j} z(BfS^PFlS26bc?RHf}63oi03)!pxc)%d_73YQQ#SorVaI=@^K7bB+%s(PbmF%<{ zARJGxVy3eX7;4e8PE_755UMYRA5Ba<*NM$bU<`csrw360u=%hg=#r~6MZ)D^gUvTQ zC^ioTKl|FdO!;~hc;M?N!2@4^_@MauJ1`rY6LcCRLZ=9XaJ~n$y{6OQ2MyJz4;E@X z7k|wI&Fy)ms1n9SnohHXZ;wDotA!eOXgXb8D$**18qdYoszKVnl{7T+$AvHZwkE^Z zLmK!>!N(?a7jjr2GTi_+nDCaJX%C^sb2%n_1z~X!<1Z@e6CDuphoeJ_V*`pKF)MIm zaimW#Ct4~7D3x5(&Lg6}vNx^o^^bW7ef@6a>WdydVWU+2&eK2BlBXbh`dw#ECQJD9q1QpE$sD0CpTk9j~U{8SmW@E>G@P_|mCA z<`2`D#aSK-JPbpD4E6TM{3`WWIQa30(Tw#RFhJoR1nxb~*tlbxMr*~KdVw%&47woU zZ5n+8KJ!IQC+~7)1b+qR3r(knhYd29{j5=rQ20vq+MhLQ_Xw=wRAHe#h>i$SUHOQl zDi?~A>Ytp-SfS}uIZzo}B@im%o-%bTZ_piz8W0HTJCI*ntI@beaB#I&qt_vh=l{xt zZ<;{x<07-T;8ttn@DG26!tr)pGp~Q@a%Bc-0iO~iPxW$pl6N-Wl(6^_dy-c@r;f+# z7a>6_l`&`3isI zt?>q#*wvdS8S#-s?Bx9l6sS0dp-L@7E4` zH=`RFodQM_^p@G;;iPP)J1DAyNKsl~jes6gRJezQztP zK7Je#=@i!#H;&|Z>w!i5eB#CmM~6R7z}DHQ73o8-a=JT#z%kJQ`Th{S#)nLwnqa6F zxLiVUmO!X{F;qrmFP$K^Ry4Eq*GBZ@dv9#rbMA?^VWr!ZH6)v8r5negPfb7{(s{i2 z34^lav(${l_jj%b5*E=)HYtGT=um$o01VpN$DgocecsEc1;P)5;D^H-G`e{rT3_d#V%O&zBbL=?K+K;% zJnGn7dT+e#yWFmfm_JxV2e~9hOf)FtK04#ZYW2#A_EpWh-AQ~^a~*0xVVDjv^Ta0+ z@0g(+EdDGK2;KN%xu)s=JR#XEm|y(W4CNGInkf)+id?eoYnmW!A|$98O5>bb%|Xu+ zHl!VarqkH2=~OS+Jpw^}0Ny(bA`CHMhsLYeL;`~Fj!Ed47t+)OG}NQ<6lWF(;wtwiIgr44PqD4aP8im4Hu$_@BW|@d`DHsuFS@ z_s}fx@tay@gOvt0HUjlox9RqHu7T9bXylDU&P1*9+C= zLMfr-eYj+-R~hP?S$agH2FZ~>c7V^cj4d5$f4)UCbqLuHw9!M?>97Qa5l|(8GCUWzwTF*tbi4%- zBg21Tg$pO8ZK5VAn@%c{F6pIeO^HS8wWeeB-7!@6ese@ zQl2pcwm8@k5cF(;5PKiQ;$X+H7vx|^Wwo0V;$Vjl2RmwBFyha4oCPE1*cz(cY?gC4 z{7)|$YUb>Q7g3>jhBxgyCC$WqThY%N4P_c1p3W{%>Ruxd`aF+oBiOp!@03gNr31EZ zu&@OLwyYW;uKs9c0NYM(!aO@i9hqV^wbwd+KmdVwv}?jp_MIDt^RldbkOQ$%fA z)1-Esx94U+2-@aJT1VR@O#3hGH22`Ia9U|^P&r+_K+b5cOAEtvRzqWL z(4|hBd6rv3@Bb9q%@G!ECiD@FhXv!6(4A8Z=K%b8?NJE?cAGeKS{FY|5<^%ocaE zjkte!MKQ?2k;I4dUH;+t(7ozCM4iycDN+`zST0L+WftLZ<#UNVVBx_@m58ko2=Q%j z6GGGAq*_Qe3%X)Csg%=dJi%EZ352}J4N={l`u3*OqGQG3y%qd;WQK0(GU9Rz6; zA^g{UyLCznUtyO* z7eU%Y7_LgeXp^W@KOq+hMA|En(UplfI@gQ;-+})suTtSF69|4>;KRN9@q)C8O?kyf z^o>O&(WW^wyRfT^;|qOhqARYETgq+Cy|4auW3@UFjnxaFqFv;MjK1Y0b6>?UY@M1S zU&BG8y@rlvC2*?YLqFN>bvLl646qmli8`$igQ^9>+8aVlPA&WJV3x#Q*K*+fnA&Xw&KO95G33O@hHXFh7G$o#I6uQbd4irI5NRXO8UDHGb-WwU zkWCZvEP-H)Aq&h~lnc@(q99ZiY6@#aUKA5fP9Rs5s1jB!^n@h!5}%NSmMiCVgVgwR zLK5E1qh7aX%_bz_wTg>e>CGfe%Qe1^S9`uc6tV&XaCzZOMqg3RBidO>*gZ16oG-@V zNra?R6EKxrLaUOrr*LIi=?|FvrL{@eB)GgB(O(Btm=3a}Umyu<%~oWjKNcKm(ZM7% zw5{I2ZL>p$(b1#_CV8kE9wT%zY5kKP8iS$9fDsVPLj!DlkK6 zB9p&-1HQ(@?O|~zg_VQg>nBhonAog)LnbzJe^jv|mv11UCJ8(Ok??6d;dyV$&Q(LG zHdiK*2r=g+>QwrpYE5xFly5_#PPZT-MvJo~;T(ZJBKl4v^@+y1A62JR?y`Y8ONFmW zAo#avq$Z;ug;Xp(tPrZr6{pU&M4i0r-AxI-NjlAc6R!pH{3M-n)+<%3VB_DDblUQ! zLB2joI-UEL?aa#iloMA7gip%A4`*KWmiT0;P;IUzC5(YdIt`2{A%g@$2r?yeT#`;< zQKJFvh51pEPUV6*ii~~&%UcGeA5PNgc_?O*p;NW+oCVLNhEAWoWsoo1&}jqnTxIA) z8d=Q2PaB2iw1! zF1F7Us?8N=J4!K4s%aO25HisgV!SOvIttb1Y7)}C2`IqD<>+Kv5vKvD9;MV&4Z#P7 zD~q{^J}~w^<)Q1|Mu(m&FMVJvf67B6fkDTeJ}}TaeDF4IYKdMS9TUQuDLykvpYWAe z+ZCm^HKe4lm%pfpK4tXf@2IL9rOig_(;gc8jzOuFBb6H41!nM7sEfIB8Z*!=kcGn6uPz(CWniXRIK z=%9h!K>P4G#2>Zj5OcSgiSTc7YGM8@*60}K&#++W__A`eGDi7yhACad873xtWd(kQ zx(vsYvk}v<#Y5;lrgldTo-mNSmHH#b-Zj%+&{WS)fr~3}jTB$KKm^D~nB-xqjXnG; zDlYgtF4!KTF4De-3$x^2KQs{62(7O`@VxKHA1K&C0zp-|f3#7ffMBNy1odN7CamXI z%rw-+2y0+Vyi0rIY@Xx8oCo54`J*Y5>pz-^y`wV?$|@-G7e3ZsJ9EH}L#dqo~uyDe78*p#DooZxz~hzh6;%3k3Bj&P`$> z)h6Z$u}Rxs#oR4Uu;o6aB&rJBkSsyhkc^;P^dXwWe7}kmYDh+?{P82ZkEEW}syYuQ ztN?RMQI{hzWrFD zCF>K6D^cPV!t)q-P~y3tNQu{NZc*YNB9UsFW<98UJVzjOF5@yqgxlDqzoMewJg!uw zoh=Y)aYapBnWBKy*KO>=EPq%fQiZxaMJ+EcXV-uCi7YRxN8{6`vYoG{$ti7eDxv*N<1RyzO`hQ58R2Fxwzc+7@1!Tg{l;fyo1g7x1dNbf*DX z2IMeWmx7hTHNaE@6Q=c?@S^`1l$s`n^b?qYe(+|d5B(29DC`f><`gV7J&EVyA;Z*e zF2(cVe-JlVD=pclk+)72a5W0=@qHS7$7E+|34ZKV9BS%++iol$AMegXa3|3r*24G< z-n5^t!;`(1PKVf;BY|UQ9%5&HfM@Au9AbBF{|wcmj-B>`2i4<(&vBU`FeCj%^giSM z{<&@Md=Kn>0?)y~Xj;bPU3i3dR`KJ-b0Fa-RJmX*N>e;o483CxyTgDpwu?vW1tP9G z;4%U@W;7Vht4y65eo{WW5u^fBr;p|s3~LQTh*HB-%^Nt?;g;o z^E^YTJxfH|qOr32mlPB+k@?JCjdHgtg>QhuAMVwt|2%_ypY7G?eaJYrUZc?>W1K*! zw-V}Mx$)>c)b)jXH7eceZX)g6tI@zO4D#*UtI;Q47?gf=uSV;>V23tE&`^KCa>6D1 zQRN0z3xD*uvW5`-$i?HCkM=Qt6s`B5g7unjP-=}tnIXxzG&eH)Sg>=fZ{)In70*tr zE1t^v(8W=uFk+1<9x}|zk2!kOO^DZLm&cWbc%faK#kTndWpV#c5LsUul!gAGJdeUN z>p;6$iA6xb2HM3&{BZ#i*cDJx-M;`=-f;}>@r<%ORK5URh^=l?tNrv?DwhA3fGzAV zv~CKFrpHoI2X@-1{{HB2dMp(IY%WCM%sZe_&NfxdMJVQ<4rtV6p+UZ%4`?(IKaL;J z=!1m@nLGAtRJKhu%{#DNFQq}F`zj&krUs2Fg|ri-P=iL3D`ic3sFXsHKpf`ggAfRk zgAibhYS0{VtR>s)4I1SxLXjAV=qW-k7YK3n$Q02XT_n*xO{g|kTyz`jO`X;Xy-^@4 zEv^Id9Wix!agjm3)22>`7a5dpChK(GVuMUn$cAkyl&2udQ34Ty4X;f zZWq~m1VRU=9=-)GG#~XsjpuUep>|NCwC$>ZN>M=R2Q~U?u|Z^{C+pPyF%^eB1j33( zz}72Sr=OV()zl_m6UX)#*iZtVB`Csv$vWjqMohAjbsDzBAm4~&ou0yvG08er;K$?1 zI;~k^Q2L9>IvrhNs4vg-72*8^!rG%)DZ`g%&R>f1Gv9y+$>n7zuq0WhYnI|t*<_uj zE;T59U9wKwnY*b1=Jzfc5x!MbLA&bZPEw|;CZRmll~v>)ViggUr{aUkNy`jUm9m`c zz)k#IPyjmWMCmfT1L%!ZyZ}DuhvC6+*b2~xshBF-&nQezeZ>5ozQSpy?byP?U?@J@bLcCB__IBomm9>N?diWq;kytw}OwX{j2C3;E)HhXdP{2Da^FRtvJs;ECq23ID7;XW}#1JN-1su6bTELg_11;cu{6Gu1WtFsm z?kcIo<3xC=K=|5e0XtQpp2at6m;o9X42|LaS5c>uq&=fbLRFT7sjk$aQhhX`WQ6HR zD&{8oR-sYa<)(~$I+}`0LxxvLZ|GzydP8qk8I-zIk{=hSx<8Fv_cnmv>1i}_-D*^t zHmRD~8J(4zLPfz4pKRzEj75Fw3q`iF6{4DJ8p`oLS5|Pu5Tg5LJ?)`Ceq&Hpevv;K zCA!bQErogl5wfhnSSwOYbYI)b6tccCemBEdWX$#X_mu=AmXlp+ne8iN`|}II(P@roI$t0XK9A_UIYp=Up@umiMW?iS6}Y%fGyU-tomPKiQ2Mhe zIvsF$a)qZH`@F%^ezoxYbu|jxd4Xki9uW=uL%yyfibq;aevCT$=R6y4$5NLoD@wG~ zg?H24s}0HuL}Pef_j3x}zZ%m{BZkn|Y-G7SBABAI()G$S9$K;*ON90OQd-3yc-#q0 zB`|29qg2IM^IK@i_0;>bG8f-+KA_Pi@He?R78%pqDzG90 zi?P*q05+i8j?JDN?_eB+4dcKodID#@br!C81NbG?cGygCBQ*Isj;Gz|Tfb9{6)Ndl zKgX%Rwo&{ddftt`^&a4O0Pr&NwEoV(PVA{}L}e)+MxZ198Z*E3ok8Zb6rFOU2=KLZ z-+xkc`szD_(!Wg6>F4jHfeA>M3>An523s_^i{<&AOSAMBjlv>|PvE}vi$<4&#^qEg zNmmGjWB-87(MeK|Y^Hrx({4l>T>*PIK4bb@>mEPIbb}M^1-$ zbgBXKFpo|btTib8L61&!d34Yt7(+6Pwz@yV1Fn{LJsr3&Ar7!mAbj1%X%tn*LpWyX% z4VT0mQ|n%gH~ECym31G{3HOsvd+3fIFiD9g82tJVwgXP6ir~rng;!HvO2EA0Zsccv z>(Qx9w0s&S9q{Os`lCU<6DUdiaHZ<>=8v#HB~_{OkW{b*2nk5rvnuQ$lV zAsO1EtX_mjVobJRz5>SRnW|HnSTRc=VrvOR`crkfVZA}=!Bm}w;zuM^rzau%@Ij68 zL{cqwHZi75Fq?rPWl^Qz@TGUQ=)?75(Q*8MMZGrscX#tefsUi=UW#{kRRTM_Dgh2J z*r4XTs@S8iZool5Yg8|Pm>L+fRxo`ne}E1rggSWW*ap1w_qZt|LTeb)b0d5^E>)+r zy~=Ux;E4IDI#~`WS4jAzIg=)U6kki?!UYW}ucz3m*G-8x;jA*@ESs=qqd{2_qO+{a zQ9On4gb+D@lN~{46Qn7nM>J5b*@UY=X*@sNi-$;v9_6QpHyPw9-OF#U#~JhXCK!m) zs}w5|P*#j-5R3+l@pYeRgkUCj!7 z`%My*2}HtVB-~M}(OqEsFYb!QEPs*XqlTB`ef7G=#r?X5;vQRLP}Y4{kUTD{vEB2! z_T70NLXnnhDCGJYO#bdeAzwl7GwSYId=DC^D1FH6@6;j!xqejdQw5&}FEGZtUtul- z#z<(T(?HQ{nn0L$4b1D;N~f=D4f5UDN+;S3U*pn&9FcIXo2cXNP1rSnt*qhwYS+N0 zM9z%^|HUs6s~}iZS1$<3dKh>zOq~uvCYHC( zt%JsKZ?Cv|8L*GVyVLEJd!2xTrB5H!x`{bZ=d+5eL?&ZWZA)HqiPrm1nBE+t(l5Z+hW=>2=U|G6n_=kdLh{?*x%G_qZ!^a zHgS(IL(YSU1+GE4lC{F%Q>LYYc^H1{U9ZtzSkSLtqaIsPYbLhRsX}-j1J75jbb16N z0~@l!Qd*?~k=+y6kc$mjF+tiyD6Hdc7gxDL)+p}S=JxO96n*8Z@gcBOcV*@K!$eDU zw6gDPwJXw69nA_Fn6LO(?N;1yjUzj2bzGy?ew*w&Z4f<3t~FFtiOtgm&J0NX1k=%tG5v#P}S+{~U)$Ll_qP&gKna`Jw;S5Eeu%tGDG3*aa4K zv@Ux+Ui%}uDY(QZbO|tgg%xvIz~@3(zPA$Cmd*;CT7nP2$v=$GzUT&;2stMX`vElQ|XJ+>Rf)#_ok9pN-oWQ`UGnfSOT*PS9k+QcS(;@i=< zQNRGauu(EjVN|~nUns98x-brSH4$?tRom@K^lIXF{FV-f=`}taaPAI+Jf(--lxc-Z zMo@X8Rfg?}bi_>=;VWo5GvzT8P*Ix65+2!sCZ5bW+cc^?qKr?&-qa1-Gver6H z5D}9ELKJpCtFc+7@B_nRv(`Ihv~tT%8Liy669EcQl_FxPK#1yy9d=CB#Knd{P_F=$ zM>=#&sR|5_bS5JsjC9r@qs1Pbjv}L`5{>5jLtKV-*=0{xPEl{eszevY_*Lxj^LN=} z{3x^Q#50JMoMX7M z$X@E(ka%cO3T;Q~A!A}zm^LP27eUg`So*`9!KOq^Fm%TA5Zcc9_x~9UTV-pV!s5gJ zh=f`oK;mJ4gE;-TKsX=!x_Q|DJlISO`^!ZFc1rV5{}4#V&el40d{cS1uRut+6B)Jf z>U8dI#K47KodybdkU+5g6Kq#`b-D^{aU~e`hXz=@e=6#fUd7ugA61Golhj%PmlMt8 z-Mxc%W6;E<02Dm-AW$efnwf;cdlS#%X_}dYNwHjP- zg0bHqt(ugZ&xnLQ=+GATiCAN331cqZgXV(gr*n?0TJkPDiuviA_Q?En5q@BP`WgJd z{PeOthFYPk-y_pU1Eqot5{T&|XMQ?mFLE(3M^`QMwE|IwA0kK0(RF1$r+*4t)XLm| z5!kJ17hled_+nh!tQgm_S-0)QGC9dp3{S;FhVg!I!MzV0FN?3`E#<5C8k8{sXAw&qmS@NvJHDKwu*`c5D2|JpTYH6plrrw1G=3#0c8R56|wyokZ^_eTxKU45u2V z&IYSL5Qxxbmi6O4EcunA9K*DmQ70jwQWEYFh%liBrgz&ftqnCKVM=!(o}D_cct-7q z`%T{gjq0S3*TLm|4`?)Xzd^o&0~&paA0=SHkJ1Ag`5FwhQd1|@rd}Wxv@mkxm70DH z5dK?*7FDHYmsaJw$_uLU9ZPb_K89m#M{$E%s8eO6aPI&ob@3S2#advv)Q)kfwLgGK zB@THy#&zS~1DNOF;yBJee(?Z?yC-xnE$%oTK z#@uumi<1b4Nz7h!m|aVrYH)>VDr28HjF1v%1_YwWshsJ{AqCAJl^ckN-FS8)Lh+;? zfn<&d?eMBOn5I+zRui*zGV%HH_bId)Z)-IhWNInXIO;KQt#v%4tU&|jx zT~U!>M?mg-@sTjYaAmQtiDnq6WY-@>q1rD{n!#2DfMnkY;&C82IIxf|$oG!g4Y5jv zt0yWr{WnJql+(xj(0~VyfKT|j-7$kQ@DUpNjGwPOW>6|V^p8PON70rOWYb*cC_)NI z=tlDx@yszez-hVyB_qOk7Y~T}V^POoe6SZHRTbD|xVW66y{%Bx0(Zy?P_2QN{3nnP zAKW@*SabSjrk}Ry(E(w9q$%|lruIE9jRI0PXQ*R_=Y=5>wU|Ynm!Y4SxEaJ4ZgvHY zb~Dmk}=_407sp7XKF-Nh?< zZ<)N)>n#%~#th^bZktKQQh;z{SEe&+HlS z(E3xT*r&MfRb<6zDf6sqMB82mG>7qe#%7+z2HGLRXeU4Z^n`ZK{jE z=}NvYz1NuhBFk|k{U_d`4=1ogA5MTn&v%=Yg}y9*Z*5?3ghli)-x!(;id>oda00sD zQD9<`U}2#Z0oR|0c<4p9Nvb8eBNi+Qj`ib)_CCe_B6>by*2WY%40^x{1oQndbpZ|5 z^{hzr7Rw4)0h-Ktbx1HNqa+qAx*-xNj?fF7v?ReKPx!Q(GQu<~0edH2PB4i)l|T)( z{9(>!RRY@e`4Hge8w75L3RJ}+wjhCt!lW}Q0&oiy7vOoCNs5#$r12=Tw)Gla?U3?> z^r%hxw?iru(qlHM)FD+0>2aI%mP2X~(gd6Il|xE9t2D&NxLJ*#9a654CfcbyI+Vxx z-clh=LLQfa@)dCU1e9fy#I?d8e;%_}}gG@ez+#r070>Qr;3jaPOyV>M+Z#rQt^Al3pwmpd4zv;e(R{-j-u3i|hG3jvOZDv1dzy=CLzI!2o!*RS|CkO=f1R@*Z_@*J@xB=F2 zIL;ToN`c_lcn!hRMyE8lvf?&3k%{jQ<+@b`_!1%;IwVjdBoQ;03Nsd{n7Ohg^B+yT zD1Il>ov64Jxx>YHeb)BHU`hqPOs++IV*rEiCU2xizE3~ zfM`8m@wLlzf~9wfe57$Ri%3qE=2zNp`bk6Eodt9-P-Mk5?R1byV?nZGnV#c|Sv~xPm!YD1bzozZ z;a61THGZy=TqY2b-(^ks3&*0#EH_d5;GZ-)lVTFj;pa*m<_XOG^%sr@3dv@%XYxC5 zLT43!&gkto`zuxmSH_@ivm#F1=gn98s-}J}mdJ?a>WKeKJZS2}RKxcW^~TePF*o(5 zIvs6(p~oaoo}Pe@mIkdT{lsDQ8n6|>1_QK}8FwLJn0D%>>7k2KQAs%(yost*=COBf zZKG3;U=|_H#siURQu?GeI_0ODs&y{V5qQ9FDVFZU19=d1gxT%pO)}F zDn+eLA_JdZoh(N%HPg)BSPbI0bTzr6C zyvD2Q(Gli5iYLr1S#&fJcN=zXWAeq$&97iAv_dqWH=>fKIw^rNiiguLd?C`rHYUW6 zqF-2#58Bv8wO+!dP9Pkd4+k6l+v>DOu*O1VF{l<6HpjNbsORCfI#sukqOy69OPXtm zn4gG0XgX9x6bXc=5){b+mrhA-(YL{Ea@mFoj*(azz5W-?;g3nW7PrYQ6?A(k6NnuP zvACJAb6b;m!8y-J;DopYFC9B?9s>oNZ^{hSefX4!d}%crz1!AQR}of0C*lj`DkZsd zq1=9^{Z0%;tRl-FwQjLS$NKcb>(;KRqAHp!^paEosBJq5qeuDbmcO*) z(hFGhSQ6^rgYC@U&pc|y`k{AiXOq2Hm7&V+1(n?koZZ*R4$EWQD?p#*g(SS$_aI*t zcCRHN?5=FzGVHip)gXoT5*&{{*wgJ%o~Z0;rYg@Vh$&{yjl(eWdWX4GNK+l=ia3m~ zzC!|?YHzAk7oUR=MizY+DZ}~+LzBH=YVpoR)tSYV5Qnj>gK+y!ZGz%B&T z_2)=Hr6sorsFy)OK;Qc&q4$$KTT`eL5COYeN$)42N$-2EsgCsz6jMtD!em~ZTE0`G;pf87 zIPE=N$nyn)?ISb>V3QjHRtW95K=6DF?}6uON0hci6o!u|FN_=u2 zW>g~ne2eCDl~{KkLLHGtDJsqRvTU5EAtiw_Le?lA67OfmC(bv?lj}*KjOZxK3il31V|KD5%rJ{p=Kg%CN0rVV zZW++)t`N)-V16gw;-)VVO00uzQt4N!u#{<23~tA#7-+}8g=r6-nQ!VO%^< zQyFpad{g20$dVw>`GZLrBm6i!dAsEgj1hDXM#s&gs%4LV0Uat1L9hNyrL8Rc0T7Bu zS`mMtMLQVv22kv3wS2G?&(3$qjx*1$KS;N^PW*Eb?wMhac~g}Sn=mi<=R2ItN@0jfHcT6}u>jJxj z^^S=*m~^n@L76^-DS->%_#>_{UVdHg3$q7 z3yMP)Q6t}YbK^zmMdr(dJR*c6_nBn)CU}uap6b+u_^G+FWDL_@zX-*I*%fM~N?b#C z4{!FxOhWa^77}ihQE&FVI1DrI05cDyGDY_;0udEn498ehnIvrEU@Dtoph<&ZI@p>R z7eh&ClGa)&+099w+YE!`rEwV6?J^{93OuZX_PMoBFUQNJU&ja8#$>rXY9l)clir(> zaVq+ji%s%Wv_{iJTljjJ=@*-nVMQXeCHb~>DYX2z^67s=|9&_1ziFV8OYX|}+$N+* zZWB`QqUL`5Q+(OXZU_(2Q;bjjqX|WxNP!{%K0(30*?;`qKA}GlxG7M?2M$$#!pSyw zRebiZr*MsWJq0yt?jP+M^*U=no;p}tToj-;QV>Qp&;s9to5hdq((Gc%_Nroe1I5B0 zDVrcxRtiKEe1P)&Izgu^(@gT=!)zlUp{Znkb6O%|jljXb7S@OrxmEGjELPSmW^2mQ zFzM>W(5NUtOIhPj)6l!NM$t-s+>Puxt}4ZVrH%oUB!{H}VZbWefYeLGfZmt<{);k- zFIBk#DX6OxFEPo3D|@*}*0TwVE)k11^W$;o z!xrVXQAM)ev1q*bYQ8{Nw8ggQfdO#;KX0+9*NRT!W-?Yc7 z2VY}k6GsaZh(BQRi-@~!W#X5UkcLs`Oku1L*wl67{@GeVcNA-($!T~E{j=8T(m$(g z)6_rXtJiN$H`VYn0v2@C8rsBl)G+*fAzg-_bJF2*9)6~^RdSpj+a{**;HQUZ&|4rh zaVNqwrvqJ>pX<-ZPK%LN2y26O5#g%~s!Mhy5zTP9nlIAM;Lk~&NjE9W?$v<{g?LFv zld_8Z(WW;F&0s?Bj(9_ICxn-JxsX3%QX!M#IyIj%dLq#1k*I~il`c$ef51rlwZ%eK zG52>MK%3jGm_Hb@0=E>8qBO360!OKl} zW(#epK=91R`~Y}5cS0w9O>}#yZHF4u zp`A|ipyL0^KJ1SSce1T+mi=4!#0$||3CP}isi_XM8(B}E3CuqSs_(#!;AG6L(J1YERUSPALgrEz zk*}skqX*zXTvDDX)Gh)uiD=rzq;3^|<5F&$aEzc*a=}`)iDNenZ4+1%9p?T|2Stl4 zE9`ita;;KJdFVUkq3_s3OD{Dk3oF5xnfi{m4xG9a3%?LB5St=)^NQ~y844MW(H=(3 z%P=W37_j_BH%9#V*!w^)^8y)VXoQSlE z_CcS<7DS{~uIA4Sl(kcZ&P&5adOMwZh?2PiAq~sKzU$iQG$+#}U!Qh5ZNiV++v#+M zO~9q^_=*OyA03wvXI+1z8t|mQ%~u-7e+mNFA^6w>9;U@nvC7O2nJh?~ctcZ#M2yW6 z@*IIM0V~3MRHZ_YHW6u+L;hFg%y;@PH~ND`R=^2A+`tb#RT$3JxNV!Qb$Hf8m-wV{ zsgqEx7YKd+f&xO}ZWp9Ygg(-^z?S1ergd)42WkI`w5U6cEr>|_`?f?%LXO&HKPa(E z{Ys^NC96NkXOcZ99Uxbwh8=*^RqB-{VK3=!CbUr@wmj2#k4~^`MZ2~;yPV^RQ^#M@P zp~WRd0iNfjw{%= zVVo$JY`ml6>vd0Dh88>_QWA;UZjKcd&8&G(cXhhd$@e{dKo<`g zvo15KQ<&ao6TRE5~D=^Ry0usEHL z#s71%O!b<`+^CZE5cnI5|6PP+v)Fa&Hy8g=G%X_e){y16+Phh0r&^X870fkU(mk?F z%E}KC)o@7{<1uRaBl$ydVGqmrDu|zPW zaC;f|*RH72=OmE24CW|H4uLTQ^`nA$8*JWl6G&}3KE-Jd0$B~D1l-fS6zaPiod({E z{aoV6Jt>rTxk=>lo`+26l|=uZ6nX?G>5|PBpUxMEQagy|VZ;WF=3Q>8H`*Mb+FbBF zT0h@28#JnAapoKCbjmqT6)!#`Deav_P5jN?rf9H z6YX@WI4^;^6Oo1PTifBt z&|asr;4@emAx{tpHdg`>D|Tf!QEWsvG@^LT%@@9<0>Q6?|MLwRP48x^cXy>wZ7z5T zZN3kiQMwbyAP_@}qm=>zbq4otl;HVA}8e}FzH_iCttx85G1 z+FY>6DfdH4+6V1*Y7{=LR;deV7eLyo_Bx&WXJ`qxUnu160>Q?oQg^k}ssEo%^6hW; z{}}rc@Ft7r|4EY<@}@zn+=5Uo&_jwvQBhF~qJV-{KtV-qXwo*2rZGu56tyURIpipE zDhLI+MJzWeVmVY)kgABFpjALn5z8T z|EhsOorJHeK#RXlLxbMD+N9JA8X9zx`5}LR@I?iJ|8l5-c)O*86zh-<&7rqLCkd~&Uozl{F2vu{{kk;rL^kdZ>8IH&3 z9PsDin-bk(6@RUKtXi)1b!_Rwh}PG^LdNyiU@;9(=oFv6MmI@{N@nAR1|>96RWu8l zUE9#0g~*dhnL_HVN!u+_NJxD(=|_uHDkT2a69O-|7IhwZRS2oSPSxHbQB$SH1Dezq zq-x92g$DwKIk;LSmG`gokvaUWo}kyyuS>Nja1L>G_xWcXTY3S}pLIG-bJNgkO^R*2 z|76c>2DZQ-^cNH{Cq_|+7>9+(Kl!=Yb=rF>jl6o1hh#rZ=C91wU1w7AFq&T%{%_FH zB)ll01$9vX*Wr=BY9b=R=p$d0^oXAAP`LzebAeSZWtvpl~QvFg5N5#F+W|5eJoC zq6%Rm3gM$84r+aaNvV5|IH)_A%x(=0st}%+!4qm|&{H5eL7G{qB5}4r1pE@bePk+q zqo@vnpq6PN^H~UV=qlvC0>SnH3&BBUUcu%I1hoRx*htZ%Xg-cyKJU;mk$q5Up=!%q zCr^OpIv<|qrgv^IDLv|oM2Y6|6(UE0hzyTlkq9_fUpK|ysKa0`e*}eAjUTwb_vsO7K<5uqHnOgU~DJMN7eO_J|Em$6e0 z01@yP_@g8k>V$fyUAc^1nbb+UQl+Bg0)G%M@i44}U%;upu(K?Q)Uo4~7IWc*WW@Mcc4Fq8abEGg#>z&U` zb5oO>@Osl1q%F=H-;Spn@mSyw(pFCSFdkLwv5gTUZ^8>tILlA(GG+lVq2_kpSkQ-u ztex>CY6Uy-i<>yU>NYayoH)UKNSOj>+z4l+H8SXrn@mc&s*yp7H=C4nV`vloH9IpPU`0W0fC~nJsmEm_S%s080_2H+7bGGhU(Q3j|kn zlxl3W7L}tkyE6=Qo@i{)Mxj>-gp?@qYSzS{XOP#GO{%%J#c-|XlEJ%>9fFOO0V=;T zS;Tq$hck1-KA+{o%%dtkl`nr+zWkki`F>~dJ}k^D)luenjP%Ef5YE0SCa@4Q>@@_Y`WDKybm?sV&}d&~>+BOpmrf zlLyphL)-8G*i_9f6Z#r~5H}K;qGmqh4UVCL*Zzy@)$0-U%m6% zF4$NZpz;pTA8s{ODb?%B5rOGcg(TQ55Sc!WOm{ahCi2`!b^7r^U?`iZwm)Nd61$_2tKM7`<=HFifWDhK;Y zw!)l&QWg0NxxqlAb6!$37sVlW5pqXd??Yt4nzpV7X83z4hhx{0KXl>sU@I%hfe z+CIEBzbH7kt0(G7l@?q(F7t<22QF~g(jz%U3tUCh-E_(ASY1X#69Eb%r<)k%8;X#k z1uhJi#@ucyV?$zWhJju+W>FaPgct?5)zkUnAYE9f3UICDzn1fVg)@`<*K+9wd=*{ZtB#{q;!7}C-lVtK2n zLFIzpE)aqHIFx^~sX_0s6tEo=@@au!8>HEef~`jU;I3P2u*L+@mx@D^v!Y?E>~OfQ z3ZY3v^ns0=v=40WO>k3u5A>8kVyU4Wo@(4aOCQ)!Cc}ED-9%pc$hK*^n`Ys8U{Nl8 zVrx9sO`r6LeV34uR$HGuAM2y_$ULsxZex9ZQu_SF`gFd-q;?^){lxkZ-XB(9#bx`6 zl_7UYA{H2Y9xokdb%xzxlDiy8==3w|wB?S!HI=oz0 z?mJCN=ivfP49J?_;4wv;_*3+bVIOi5+a8zth z9!|5Fxob~EBRLIfE4gO~L}B4ba=x%NucxUlY#j&w>cSuB4|>AG)s(6)MoZcT91)8Q zm6O-U*?2XMv)tSh71xW^xj1}odTB2-V?^p{d@sDeEG&$YZ9VHx?ZUoDv`bMg5U@`n zFbTG)B$OQR*^L|=OD; z9NpBQ*;PvZ5`oBNAhrxGLbE~Ufm05uyv)w{JhUF>X$LJpdUMVx2PIyvNPduZo^sHa zOnR-UL0Liyg0#1(K^JG?gTtl-N3v{KTrmt%;JODDCRFX*+3CgHTQ z!p223-)2kq5Y4yk%XQQHSy*5R;ITZ{O~zN;u_d~*0 zB@p~8;a;Ym6Rh);G7i*LpjNvi*B|sp^7$i(M+z)2B>bWhC~q9G*$|k2v7ukU(8MG= z)@6&ivR%|0_5J6j26Yf8b`^;H&Z3`y2RaHembi6BTeD?(ir|u(4>5ygQf~n6K^9TyxgfZnZcI`JqvRN`@(!nguZ4*7i#HQA0ir!|^=G*0d~Z}#emW9QE&J%Ubx+)&T(=&Q?TJJ89}_*CVPVi`NiO{@Hisi| zvZMY8caoHx7owwa_n(TVE`3bWZ;#J1@iE2}_9_hfuuY!MabA1-U>+WzsyH+svAxj`uD}Vi7KEA2h&(Su>a-05JO|_Yz&c^d&)r>tyQfF zp-{%)2H~WVQx1B!uc@{#h=1RzNUV4c^9fUkaAo3o8 z@7sG_Vo>hAqJKxBb`=P&t%w+IEqe-flt575)B2CSSM*- z&VfonY9dCceAv!`0}k4CuSv2OGA4FX^kqW6V5BIFekByO;wSOEa)64|kM+Jpl={hg zvHTpO&+G`KPWPc?19ZTy0;HlfzA_k0m0a$RFqJB$+}+d$;S=%%v3z}u?{;|lKCF%+ z9x~ggcsNiq9v0pw{@gFIeN-SEfOz0;XgkEPW2=Om^1CttY*t|Wc%L}-f_?}L=Zgm% zv_TlR2}FV_1YUJ3osz~ijjOgdRaHC7NBAHS?8;Za!dC&JU+twc+|<4wM)YAHhRr@8 zFD{R#zWsFV{*|A(UY=)n&go}Ta)d@Y;OWi$fHnp08p9V6xch@4lta@Uo`dn!k01Sy z`Dlg%bB0NH4r0pcr6mTAw0#d4^_js$2R<|S3y6q6>I>5{$8R&-bpQRezdJ~IG92@b zy*tFE##MZ*qszf~dh&i$@p7OG^^wD`a{lw~$EGVG3gJON4O&HpsxKxE1wwz`nPlSFd%D`rx!@Fn)UUpD(1?fRdyu(c!S^5;SI2x0lCs}HTONX|_%o1fNp7Nj z4SesQ!%X83cZW&FWdc!i8$bd6aQ9&h?Z#eWP=%0p3j`bAXJVe+>!6aWm5bUT;l=wL zbOjRfjf~|&x)P*sPB>`D!zQJEf5JhV@#EYH2OWCYR9D(k2RolfXkBT$_YpK~xSBCT zNceyu6_*$O{)kCV+)C*DQ+X~yAPzLdrD#OX@h2RVA?S_*LFO+UmY#6X?b)WffVqQE zHCGcNeKn)$z*R*Zlz|zrBI%@q{Mqo#+Di=bO45)(cxE6UGE;epK_!y#A;z?9W>C3c zIzm+UW(JLiD0X9&knRNuZhVbNkdt_gQs6$w;Sw3}m$ExsAo9HjR5R&}gR+Dl$04P5 zIpd(;vQ0|5d?yZBC13nys*74PDb44pcNiieD6_* zMV?xl4Y$kw3N~`a{pcVE**Uh|0-+%;St0YDW(Jjrh5;Bmw3$K82b$!>O_zznpCS-N zR)uaozRE#)19eE0U!$Vs1V{*paS(uTY%2*f1VR8pf_?fK1aRbNa>v?ibE_Pbn1kv= z&f;bUWeY<Y#o^DlV6_()6i);%ZV9K<9i~Am zb4(SaI#IqPhU0J4w}kH&P2ieQ!Z2PSG?@fV;F_~wXpz^X zq`J)wI)NXTH8O3= zY_N5wPi(CMKXH>?CX5>d!f&g&vb=H9L8YSVA*j0lq=V|`nv{Czq=T-_MV1)WZjV!% zkzGMZ_yPRm>l-u@{21RZVE!AL8{`l^w?Od!2>t=h4SEm!-sT4Vkc)xYmkx3Yn`VbI zXJOQgro3Jrio=P+V7LaAxs)Z=qPJ}(e#nP5YE^fR+TA7>pUEki>h>H;?CbAulx_|EqZ z+E7=qZ4?N$xo{m~>se+)Y;70v4uN1R1sh^(iC<#tLyKSY;n*bfvrofny%zMoZ$`t>V(vbv{j~hn3=laZoPy4IAb(H6~O4 zlGjyM9;N@D*OnT2{q}$7rM~dc60_=QE8wZ04Onr~!2HX3Re`Bixb{l!M+JiOm$({N zChZrJX2A+kjg>X5F}WqL<^mz-90D3^OgLu*l~5BQ;qPlqaszPK7b*-#A1Evsh!X|1 z4aEtuwtQKK-z|Mv2gZyjFiGY>U)8~6Y&8&S4)ir6J}0M zZx|U2nv|^e)f}vIad|u~0^%{&gpM=jtDs5sHeRT7*)K3T;0xwO^XW88X;g@AL5-%$ zZ&1PiFxowgrmioP(bQ@2(|Lgy3{_#s6kp|_5Jd0*n{KQ%nwkOus3&xRQosw=YB=Pk;K@(YPu!#=M+YQWdIfu8Wn57LQ`TgC`L1sTWSzooo?DJc?BgEOK}8MEnt&%Nv@%0G3}-l=5?%u&It@ zRZR03*#pR+Zzy(*^4VAOIlBPR{BFLlC!FVt(po+d>jR*pJe{Ax2V2=9wHHIU_sI|s z;H)<_D3hADL11<7!yU?LL3h1KIReFj^WC^Zc{Cy&N_MB}4uucsTL}vKk&;fe`jOoc z1R>9e%O#c7kDO&1zcukneo=wQcP>heslx<2N+77E2omg#x+IDo1TSWG2zjqSu&vW< zH(6{|LOw4LY;Phj_E?p83)QA6^Y+6wQZ4lP0uHEd4f4UI><41H}D*a>NUvcGpz{aHQ{1|wu#I33xw;wz;SGNhq7Ieu|(?~iYD$BCZyEwP!I{H1&=%R zY(z}|7Ddcdr}(FVK&0k_#+=xK-9)gh1cLe#r^QahfMACS1obS68;3V+fN9mGgR^k( zPD1wV>td;tq_){N6Y-_}LjiEt-j{Ik(1As{SJ714jg8!N@sOHtz4ax2p`z&B0|!~6 z@ZBNoR3hivY!Eq@MRp%z5-ye^n#;Q(o*H6Oa&Dj~lJE0UDQ|^X2NZIpd7SCK|IDjC_@M9*`n<@D_nkYV%$12Wa>0_9R;PTn*o9zTw z;gz0=3&BfnR)c`ou~68toq`wf!U=IneTDGc4IW%l?;2qeUsB&=gh@#ew?P5?c*GnIRa@wN`d8_HBgW)*LjC!~1G zgi>9+m4ZR>s%zd4iCI_@fGcX0>aZuIRA=D_N_7)tTIW!2(7bYtWNI4E<6nX$c~}XyHA?r zE&(R!8#<77ajNrAqC%5-<)sEyc2UlH6>-x0QiD1?gLr9th68@$`D`Dy& z5OO}yI;03v6Cpu#z(gP<hN!rWX-Zbr*kwf|6R|G_e!Db_84dZy%o?ddNjy-|$)3RKK@ zNjOHvcIKh5FXRdPZV&r>7$giV%8gi=WG2RDHo;-zc5Z@wu>4YkC~51Z1|^R)$+`1VgLfFCu^A@9Fr9lEJ5ZTgCfE%EK^=*5i#LHs+GTUKG4~2bb zy!-=kMbm748T-&kT^#CJRc36qg;61KEi`HY2UIsGULk1~=oRZcHh+XLjS>hsEzv>p zLdkf+&K3x2qPDw4keUej%5D>GuaIKF3q2*RQW8`NgfzU<<7Jmag4D!nY5(FyX%@5( zD{A=iWrwYGtS=X`E1gqEs!bCM9QU7!rw>P(RIjI`TP`r!PYXG`bPZ7JcXXdJ_P;r=F^y&&^!lL z=bAo)xi-z`Y`o8yqCuw}gFUCEibfA4_aQBxSp(Ffj)F_k8kML5bt9lK61f#2O zk2`4bD3g+YIPRbi@Z(S1J~zrFe#O^U{Lo(@ys-)C@QN?tSt#nnhM<{3Umy^CKk4`0 zC6b|R4nh~QQTVW>50+?t?n16+xt0S`^uAhu&>yvIuV@onLR0GSX4)xrPzv`wYm&Ph zj`!0_zU6rWBszb;)S!Gx77&Pxufh@Ji7oK$fb)tL29*e{Odxo!!))sxbPD@&0v>;X zrA~2LtXkztt#VeY>{*l4r&>hiyl{8H|4d3Q2+>D%l8(pIB0L4KP*C^}?_(=!wvtI^R9ure&ke+FMNMHjHfp8JxKZ;e42tFu2dZdZZR%}&J29L}+rL7Y44ppcMoe|6FY z-#aLiNqE^%CM2AGz>60hl9h`(39Mec;6u$Tg+C9ag7-kO@Q~3>fhf5?;K#zp$75yTqpVx?!Uyl7xM&~wg+VWkGbwda3xmp$j62ao zDas?q1VZf)79DUP$WcLRBHC2+0Hn6rrKGB~{RJW|))x7FlPjx5g=2=lG zIha8+nQ`a@)Kd(-yz~NNRsf@_{|;VIEu5$}2)$ZK+O*V9Pg^W2d$s~8;9cV)6 zRXz=)pajhYW03ORpzyEY>(@7cnsKKcw*;@|Osaq>2PQ&m`Eu=h67puDrMETxpJsUrAvWfkj_{TdM6MCtdg_Yl!9j*%|@={fpt7NH=s{|tL$GS@Pd0whyaW7S;tSY(kd8v}S!GbDT)~i;P zJj;&c_OnuwTkYp1Q`pIvVxN|bu?xUP#0q?Zj-Yx(e6AJ?On|%ew)=mm+UgCPuFI7* z>hy6{r;l@;zG;f?q9a7dIS#Xd!Wb+k;E6C6dqtm_qU-T-j&Q6uoK(O5Eq@v6>fA$D zs!~)f%c;7y*7&~ed~<4ycPBZ~I(`LIba)6XPaTXW+cfRhNqh-AIbUpH&=`?9ULfpAz+S42EevWk z4bk)W(PG5MA7Mo-jmk1&J&s#8@`+}$X5FS)$E(a_y#@inMF19PIsupno-nK~n}&gy zpI&sL<^2>-`5v0(M9XVV$J*XqykI<=)7}D17BGH#i7^AGLunpjJ4G$GKp1xkjKdK7 zS@1DiUm^Dw2)34*Z3)=^!3P}pxQRFNfG=EVd7!d=tS8D;8I`doKAdiOqKq{?4up6@ zVJ@0sQoEqf=Vh6;GCp*L+@-M3I*M{8O1W59A3 z#YlCB{7V+HC!cQfKC_n3!1x854y(le(*og#tF#}^fbHM>&lpG4tbEav!DQhNA^Il;5teNYr-DhFs;`4d4@`pz-+USh))97QqK%@f1P>A-h=T6~?4=wFms81K8G8Zb?6|?aw zrZ8N6&^)c=&!&ff5G))@pY!b)FYqJFpmUt^5HQ#U9-(LBaR6iUm;N>>O1#@n$vL#w z{O|=g-SQH4ycYviIGFxoJqI$bL~ze@&d)Ng0=NJN<0{tR&6iNYv+R`O4;CSs`7#vj zThGkGT`0JS=q<*5^^!^TI$Wb-tgpc2upB*4?=q8Zj!E@0g=jHF3%!N(A=}p~mZ7^a zTKDqoV4iMs5KQ=j7%HeUi@suQ2hG7(t3Elu_FLwj4{kTzW9`468jtDaJ3w>bI<8ha zI|U+e_n?*`aE-Z$?{``l)IgIpB;tE%QV+E-D23V3tEUJVd(#oeVB3#lAu#-DTZ^ds zb6IcdF66!fk@hR3#c-ylAT<$b)o{lAsf9rq_oy-BA)IE>w536J&NV41sii?dOShvU zqDmm-{|x!i?U*1n5%MwC5ZyX9kV? zsfjUZarM!1A#V_fv{&)+MMuN1usdo|ouM-h+9*g(yorb&ggyA^K@+zNQ!^r3g1{#A zlDbmx#{?qZYdBxL;3kPow?I&D!e-nS?>J~OI~-@+HW9L}7GS#_hU4&DO>8Ah`1T6t z-O7o!%%f=;|I9;X3o8Z=mBgk!}87YdGN!@V-9K}UQj6}n!2p&v~%7kr$z?g{5lbhZ#lN(&Q^3Hs@a^)!Nu4XwR$C~BDzYM$np})tnCU^I`o5zMF#EI(e=f?A( zWNthLCGB1|38T=t@fec$@Weg++<5f#v+*3$)6b1ZPyZ>f12OEPx$)>}T?=By3<}j5 zDDLD1F>HbK={(3=$QL)wVw~!Em$Rfz3(ynx!ScHQF|33}BD6NX{F`|C29yveVfkQh zBd2gIgcsappzzR-&c|cWoV*YpyLs|_IDFwev)-Xmy*!b@bUwb|?RY9ca-I_$l~Sw{ zh|=VnpfD#W0h@{K_X&Mf7?&ADY1nB`T!dMI<~ob5^{6J;SD$%<3XZMR3Ki8<2!XyYLp584>i@O6m$(W(pws|Tg2=Y2sQRVjczRs z`V(sKGWt;=9}@^R{!9XIVlP{Siig$q#2b}StptMYG^By8zs1%;$ejd&?Yw521U9p$ z%bm5WVEn(i{F_hNaY&|QT06o^dD z#1Zjyr)V*v!wC;B5qgr z-NJ=Kqxtj_qn`m9=S`II<41T5^6Y0GBTfJ@gudX18yy*7OkBSV<4;e}dsk5b zCI=BZ$M`Rhh|k0=zfT2JGnCs$M;!DANPJZ7UMbe20#Uq|X|}{yOm%ebt*;=`ad>U_ zo0Ty=1%m%B@Xz?sLAhY#9IGV9`#?gDW3{N|;NfA$co8vEAVj6qp&COGOKq8@sV?B!QV6A?6xuDMrgtRp<9{uv?%cWl~=~vw^^Zwq(ugbfc@_sShG1~GQUPUXy zy>OMJvU=f%n8ve`2A!4Dn+rs~uS4e{`yA9nkeY}Q_)Nk-9=&G>xsyPoeH&>p^6wx> zO+;EX^8c4oMDH&P6S@S;Z&S*tR%weW$t_%x#jmQ8+`=Wf9#1?5*uo`w{8e<^s*^rg z4{cWK6;?CyeLXZI-B)0VzdxE0yuP5%>Y)c747QN;z@Kr-i5kT{@MrbV1Fu|xD8M`4 z*<$8$f$+gQb%^-;>9YPR2;`kF#;`|)uXz_`8u&j!IisoG&0^76X)?BLA(p}R8Q9QS zoduhTUZ_$sU=n(vcCSe12(p<O3;Y#5k4P=+@dUpgpJ zsIgoaAaHCy0byj`+tQ$n`<07-g@ngj8dO?lQtC4;4f?#yq@?Fs8g#)*lgyTQKP$Xz zFo*AY)Isf6nv~S@sDtuXvazt@v}Aln%Yj?@M-Ck)h$-HtrytR1AZJtzV(7zzK^F{R-sa1j|gpLWyh_G-58aU79-dn zjQGM)OCa9)B2bml`z^uyS@7m+!TVY8p&EicUT-Ai%kg^xmYmQXv5J0rAz%9Q>4oU+ zo2`bHM4w)WgByC|iC4^k!3bpRYCSypl%FY?P1=1S++V(0?KaqdA$A+M*5K$t)r)_9 zAza@Xn8E&_mnttrO&^A5KhCx|d|~f^n_k1SZDcOzQ%*CqA( z-(0J!#)9t3n&kq)_H|wPmb@*qVTlMYkKx1}F>K$04KIF%td&bqwh2{pp=Cv@%$K6P z3SnfH{oY_#zC|3!+pjWW7mAZ?-gOAb#v@MFgGuQeBlIf=$)XK9;(q0rl!*I{ zBksTk6>(1|AmWl=ua3B96A*C^0~7W|iULs@oq&*@1`NmCxCF%9hSw#+D#fHYr$pG7 zufvgOwko8)>=g)48BU3?KbZ{?Rw?9T0>S14TfsgDb$>%5?2u437d&f4*i&!7dmLd2 z52^@rK>{Ld-5U~N^rl4Eb#F?91@Qxcwct$z79YzX$uB~39Lw+?cyKJkMH}T~H=n42A^M$Q=D!EJPui|P`0yk`%640=%g;_0`ybQEq%Ks$(CZ{-Q! zWV@;TCX?Ld56blhw3Ef( zdT{02h^b!KawVkxx^Q~GEwz3uc-RDN(1PlCJqd`cA4b*~^E#w_G!a=td6F@N$bSU# zF@{(U`4~eSWce_yr-&FJ5c0DjzkHvA@CFo_Y9iz-)2ahUwMf;mqhluP?%3EltYYUd z$Id0?C{6l~%loEs`OJ8uXs|;dG#OO0u*!v`S)dQ>)rG|)r&6(cu(o=1xs04XE=OR% zYNsgIL_;D9Xsg{CDG<5TwmRaA-Wu?Ef<>WQ3xiSLu&8A(UgyAPs?Myu$d(?8hKW{Q zgj&;VGxi%r!+nQ`e88b*^ad^`?DIr@S+@d-`ZE?te5zfIDhDb@W!j@F81H*8p3ZJ& zU#`Arncq$Awy5U&&5O`{`?i>>cc~DMyakV-5tzM2;_TBc@Cd33-mO7xyiWs!M^@GJ z{wVQ>W`SQ~z0aOqAWUlnLe3iP*(HM1L`YDcRaNC7RaM^9RpkuSs}8lizd2nyyW*bM zVB4&nzL|a5bSpl#t#;gIc3jq0-2!c9-$_5X?IN@l-_RfiSO>&nh@BGCa$?u^VLTLzWacAKjIIe)%5WF3Q+*Kgheghju3Lm~FBZW>v)m(@^#BVB^ z`n2uUBZY@m7@mg&6vNoO*On{hSwC<_Zy1DG+(Jgd(tIw;(kUxhPwzYeK{q?duN??}B~Vx^31q04uR*+*D0i zaj~?`D=tRcT>2qmp2{vh_JW%}$3sDg)?AF1^nxAIl2(ZYxQC6sb=wYUNk{B}lJFMZ z93Yc&sdlgcw?D@0h+88h%>p}Oy~Uo`Elh_5LQZ>R0#ED}q$Waw@&s3@j7L-mcQ9q} zJs0X#J1H-xpJ%YIb0E+s8ZHdx-5xG1=;!kcwj4zFsfbdJ+EL%8E-L@1K3`PMW{$sC zvCI|-^>)?_iV`7d7AP5Orn$^8C|gwh2&#T;7_uvp~IA+u6YG!qi_NVZoL-a^*!L|_5w`^(MkFYj@<1^{oh8T3DeS}XfA3+zEONSd` zG~Nrv;;vxaQ2Mc9=4dy~U|eE0_(F7oUGoia^%nG3Sz|2`?oEz_=xoDjv)pvWF1*gj z<}^9{M85uN}dQ{c95J#$^kvdW#ja2)O5qoH) zx{HYZ0wF&ko+xcSe^-hM+EYlH1-YQ~^gX<+XH=L*351*@HREWQkTeVAK-(G(YlY~1 z)J4QPKmM`k{EnsbMiI0_AoBWN#~N1TQDB;gT%aux-wU@}((Dz8w8xNF-#CLV{KO=_ zAnvM9;2$)B`-ShAK=A*ByfA606r?61FIXi0r-91<#~~m7zxNZdWiHEy|656%br1;o zKf`a;{!bB-W$zKynuuIt^gJr$a{`gJKGLEd9TTJ`A}utOdQ>I4HPE_U|C#vkna{Kj zvFZxlGNsFa{D$yDbv^1JB+Y_cpka-A)L)qL1wu|Mt>*wiY9b`W=$QaWLASn`qw0HW zt>;dyXX8Cu&jli4jX=n6Q&Z0+LeebAC05VsK|#=ANlC2}4eGNGE=H4G z_MoczSb2jd;NmD;TwQh7)M9~)QIJ;EMedc7`!JpRkbRQ-3&<3?yS|Xz)4#yv38^xC zO6R9_s;n<0RRqjPbqGJ8bKNg>s!~ZcL8nUlQd0H$QY_tsA4v5LQgNfaZf@DUYZOdkRbrcp_1Hy)lB&v0oSba#3faK&Z94 zX2F*UNwYw?*n(%bx+Rz90wHIsWu7245fYSJW6c}NX7%z6zc+~5Wto(jtqf77ZEq|` ziEL*}usfUIN4(FE$@@*E&j!hIyFlpkA?&Jd+cpYGvuLeqv~5+wgzrJ&U&yI|U1)<( z*CIj!JX7fc;k8$oVK&IM&c#Mxa zssKVw6%?ZGSA6;$ZZkf?95(}@cCr5zhdca=fWXEn^|<^iY&^`xvOZ0;dyd7^uCLID zWdfB$GweuH=WE>73rv`1*)ba%`ZbnioUb2oP|851Z%2Xf*`qjj1KWXK1Rw9r(d5h+ zwlLVRGv_U^nMWELl$d8{3Ahl0Z$YjEi7!h=ua1)579+u8Yg~zng9|7fIZ7Y5Kg7J}=F6;9c9d zhfo#s?YL3@tbN|Wcrp%~xCQPUKV%(7y(|VjQdCgj2@j`Y$Ao*_6am-J{K9~bMl<5x z?;yy}aqt|Um$C9Y1g<+1IGuPV^X>+38Spvuf&-P^e}s7x2iYk(K&71i$)hH@%NbLc zcUv$T_WL4PzA!K5(IN*z?bK0NUzpd+GfZtxT+Haq?-@l>Y0)&y{85^QT|b({;kZ{M>=y`0+)SV; zsB;YJm{%tnLGb(XwXf^OiIm8G-wrm3{5oXd;BO)G^piI7(k(IDv?wdAr%yO z>L*cP-A|@!e=3B<%5>;)&J@Whs4_hV4D{6VTUrLL~_T~BJ7i* zU&WB+Py^kWTVy2&tYOGyzo8gWY1#@oL!iZmN;4g7D#uF6@j=*&Qi%Uua_sWExK3+5 zrnV@wSRee(+FRS1YK%{E{XuUpPblQ`cE(-ky6#ogsDnSKI`{+E!QSW5H-edda9~VX zbY53D^~{HD{OrId-!spfRByJ(LaT zf1y>0+Hs>*&2Ic#rjI8VCC7H-2MuUtv$-gFPzF^o(-1uIkm)a`{)lIuhN$ONaV|?f z{a|2_a<|<@ZW>}IQ5&|MU}0zME+TgZNQ0=DPnl}!aFIJ>D42^1Xk;BYa-PG5%}5j2 zNyi*6e4<-C%udOJsGJjBXt>CoFkBLC=0x2L@ZpRacH`fJeUVVcq zgv3{YY^-n4SSEdTnL#)r9nqSMR8^N5w3|r~nDMyMrxQpJ*wP7s=+;YxbT>$-Vrd%9iu+N;h87pq&0<5dG6!trys3dfU4^f^K=ARw z@_iZ@^e6aO=SoqmFBF5$m${%bEKL}p%wZBN?Fy0;{uvWiN{kl>;mihK<$+C=f=Fl& z;V6Z1E-8gh@vsM?8bp++CITUf9SBh?!N$rIizp^Rnd{w<@GHWgNF)e^1mvg;ER%$M z2MP?_%n6aTL)sCiKqO=~WPJ#1oONZa1Y}*e9wPJK8(cZ$L^o33R?kJU4Ddd;SDtz$1}uwl7@@h+u!P- z;s5mlv^Y=LDY?KijQ-@7>DCKfm|TJJNHjP%I=IF2=1wiv(#5+{2Xl2?F6fGzLWM+wFp`5C}3( zv9P!uZt3RFTq8!mbJVt z`q)2#(BpK4y5=HuMm#iM%{+mNT;wi)l2@zB>Y@z3z6iZX6;OU!Ul-kJql;aX9P-l! zK1A#Ki@}ij6#9i6dW$&*fWxPs7UL=6r%lY?wm$kU{WP>binthqaN5pk&(}xEu?NqI zOU?^~YsyeUSZR8o0op)}#Y4raINt&Z!}Q4@@i4vPW6D41YIH+vu{j#*GSK9n!j6nK zK^lhXH-e2LsZ=tuNMRVWcvDi_ZF*#*%fQv1E;|66WuY5ou+;zOj{c z!sE(aO#~uq7K^Oonn>2Yn!xk0I!m&CU1$9)czBJkiDZrb7FlMQjA z0+IE4h(&?MHPt1h$pd28aJCtj)b(I9@4wuj(pbfwzTBWtQx}=fU2agtNITCu24e2M z@^XWwAd&JU>K*cZANisX*EJPS{?yb(+_N8*e2)nPJDY=^z2hZN^Vj+YWk|j3pdpcB z!(`wYu#xkP%MCg$Vw^F2$lZyl)VW$jOqK%hrP4O*X;tnxZjH75=n~P^XeQ3@Dq_0} ztf9uZW>&dJg+4+c_#igz&j$Fa8l`_>b1D6+nj_gicdP}T@ZjD!`ne#>zeiEt}#@I83 zZMHy*4K*>f6)TUWiW153IC8{N#n@IZwHUEPQf?QB`eiLfEEi-f(OQgH&f>X}?iRj7 z0wDzJ9jSX04cevnu()wf=*^`LK;0_4EN)!V8U>0{D;I4~LR*yDO|8X0(bnQ0Eg>;R zl2vNwAt`3)P~Z#a`TFp3w%-@AYG7rt^0BIk8=bcFND4#)4+3zklsO*0#mOA58QAd(N^-|zQkSc}LRg*H9 zR4>V(gwZP1!yu(38Pq4mMX4Q<3>tzTHzyhN0+|1$iHi)6^uX!P+JlKtYfm;MqHf%z zb;I~;V~R_^pX`RW4|P*r6whj~s1TKDE;*yMN6_mVMkRdbOyeuCwlIe6oWhuaj2UaEgjG*})=GH7dC7bT5C zETp^C8-na{$~i%>;0-}Wx{LV5z+?DPoM_NX>4=*pNd}b&b0L^_BpFl=p1ny1x!SoX z>4zkP(%ZSn#8C(p<5VE62QOZ-X0>xs>f9e46lsS>ic3axbP7bRXZ}4$95fwl_*xjJ zX+5X3EQPRUtqgh{!jfAVWWUlysU2Gx)aFVTCEd}=pdR?~U@L%v zz)@VO^I92n4yvwgWl)z47bU&d${=5ci%b+_sVMdgWTNQCfCWYO30SU9G^k2gtfKoR zLyGQ}_AcV08`vI(qUaLGD-T*l_bhl&bi3M1(VcAXQt!%-iq2I6tztnjHoQuTvEx;` z7|#hGI(^g?iyy`KAlRfBL!!he7=>bd62ef7+wlX%_~TVlj2B$(A}+@ESBqL&ozs#X zN(M!$3PfYIGN4-J*7s_ytJcvSQy$fYud%VEWtHks2q7|2tYu=!E3gDb`6ARsQ69n% z6lL8GE;2cICn(ojR;P7v5nCO_4_G~;gIN6rejs=cAT{byv80Bj9K3W5$`rv{CZuZp z_ls4P0&6%S2?;rPHTh5sn`P$hU{gU@DH&Kn=)XqVkJZ;m`;j*fw$rG40#u;6WrR{&q=+1L64GxZC$RXe!w6 z+UKD4*Q2a(fZPt@)A@kkIza9V@Z$ivKftg2d{h#g76`@GY8NHmATFvBs^+S;F1EAB zs+upDr+Vd7tHakFeeqaT;;O^nXtUuviH)|0r@3j$4OW-Gk?*@(2ZZ$b3iA^%)>f>I zHf+Ueb)z*RcV?;@TNc){mn;=L{td+L8kW&QKNZU$~ni!K76$%4A zRE%O_(`mUvW#1$f>KXh%g<5ozRHzQZ9}tKN@&HN$_2_*#mFrPR$XYzuta@||Qc;gC zycvT-vrU{qp%N7m-$T}bID@VNjX&ntAhOB@Le*nn!^a#?-0V^xbCfba3ba!AP74J8 z&nR3JN2wI9YAXM`_VIi{*Sdsps-RTu`$g5hU%2-Dc(XNQ_{D}P!{xW&wZrg8pQtBF z^b6Ou+iuZy?H8^LloF&f`WJ7)rnBlXIZWqlm?`An;;Nw`Hb&m+$;tPn%6h)msoc`- zY9eZFOV1%{{oxC4dJc-{#hBKZ46Lx!;73dd+mrmR*HPq*5SZ+zG$i^Mip2V=wkCI3 zDilAb-2_#xXH~9;DyL&rxgJs*XKIh|qo|^OC;RCJ%&l(7#H@}l7nyBa8&pwZC**en!lHX?gI>;bQEFCegZ5>*D5-yIgU;iJr?o*XJ0s|7&9TI- zd_$;ZLuf**EqAdkcfpo>JG&@7M06LXTjM*Ut@Be~EU|CxjA`;@o+jT91Aaqp?h0TA zQl95nJoW2>&gPvK2F(^vN~Wb#YGvBFD>7BD5ymV3pBD(j^B@YZ z5r#2etzwi-%lx*hZy^_|Rf<)Ulr$Sh=1`t54@Cja>Z;>#=v5cGiLn2m*oM+{--e<^ zH%23ESZeIY2zm;9Yi@IqyJ8BzXn)p*vi{|EEJS8Z#j$7D<1v}*bCYi9qEL_}Q z5rmmq&rA?>1A%bvOsIjVItjVFJ6V&P#IVf*8wR@#yCam5S#u#{(2YDG?Ip0i-P)jz zVB>(u5G8rS0ec_pE@ol?j)Z+hkT#QugL{;q#|VV{XW5Ogy;#IPha9K3HfTq87n#tj ze42_*7@qoGYlD7CL6A?8*5S7@QZu6C=H{0A~t`!FzpZsQRwp6 z{A+r+RNno2AQCEC8v&cG@s_C%{^R{?k8%2$UTIIpgh#Q z16x|C4G3v>xX4{P-A>7asT~l#?tl%G+89(JDqjx$UTI^{Gk3Tsb#)tq7J=uV^~(wP zJYl7w)~xvXSPi?ghTWmz`*$b}yCdKFcjCP?sfTOtWDT=Gxc^RhL4@1hVPj9|v-EO< zUcS>s{HExqJJF=nYATe!LWQB6ptwvcQZ*%3p+T(BASjg3QzJ%dQGh5s^=!X(2f;_FF%dp)x3v+DFJ2}jkTetor)w; zn#c{?`d-l6T@D66E#V|zAPLRnP1#F1Ny1%ty-XW8$<=rmOt169{JUIKuX3t#Yt}Rc zlLyhqOx=4Iq?5B-LxVa9Q%8X)&`BuJP(y<*xEryDwcw1Is=B-k64rv--z{sweevVw zB!h-p%-O=cUo*c59xNQcg&$z91ar*@sV*37^Rvn#W4h8_mB~?6CP%qU>{+hrtrJJN z?Qa8A#M*WS-Qhf&!mp{m4|xg#6b-tt7Ps>e&D2mEVy7}8-q&3svlt0fi?zB z%5qWa;Wh@X%5qWCFKrC^3_t#CV^I7(@NkU^P}96V1)f0QF3Y%t8L>u=kK@1_ABVte zcTbFw3jfAE=wA_FewxUf zRlsDtV5j7Pewq}AFs?4wesp}_Ug@j zV!XPVz3Krf#;dE@qEUUcSJkt4RUYJfrZvpExDQ-|_C!4&z;iIx&k9d{;b(Z&Pir~l zwZJ62$W?nCyZQdUkVWq>c|>2>&HnL`i6E;XmHv1AHI%Kn{BXSSf-aZ&l zhdAp*R#R-*TPE?m|8GgHRg=c?fibr48uI~FE+h6Jj- z&#Fe~EGK`(%IK_Z{2%!67YWbVrZsibNB6nZDr<_QZz~WMoI^WudY^+%K!Iw@Q!EV% zLRQ9=l29oyQibx!I9S%8pSE;l+`PH4)a!|Q=-D{TXs_!h8q5{}3j{&~d=SMNJkYO( z@2fQ!7_xHCdNJ1hd=DF#3e=Ne$0+#gX{djdptKtw;6?hm(+d0K4Ja__upl?`|cE(*0bW_>` z=;u+7LpijN)AxD+QDW4ijc86%z#_yPNSf z1wPSjAV^Jws_+;o9e@TuH?z=T=K#^+o`*z-Dfj^$);*+k$eyh5W znf+iFOx}lt?EFk2cNGY>`@sh1pSRe$3%RF2usr~_HT4X-`e7F_|9ubZ!pai9Y=Ph( z01x16$-aWrM0h|K);b+T;&ec zqWu+TOU@ad6Yx>7{l>TBX~M&(c0s%>f0hwt591APvO%Td*kU#|*`Om2yC~_iWP@Cf zxG3qHWP{QlaS>mASS*dkc!5xSJ5M-Y`@%sbg553<)E#J)fBM2fS&z8Xb)k_*&^B@u zH4&9l1R}xzYSzByLeebo24cyoeTYU)>>$jLGaBZg7Eg!r2+OuY)m&N>A4a!RX@%0s z6uzDUA#JQKovwn^LA}`8d;oqG1ZPq<{9X75T=pys-6oX1$QVmKu0wpWOpq~c1C}}{7K?yk$?iv!30Z9-JRu69zJfmFe6OZc0t+wrZDPMh(vvR1V(pV@gz^%%yJxIkb<9q_bCwq+E(*51fc18RnHDndOxt z$rejO0^y~YBH1D6-2y@W64Fp4pF#+Vq(Z2g3*NMf#O1S!HjIn&sj`*SUG6yjwdB7^-QqG|kT_)kC# z&G+Zz1B3DBop~-w&I{38$Ni_`X&RmiLR9KNQ}GR+xGk8^soUne)Cr8}oKQlE{3%Lt zib0(gDAl?P1T_g%b8d=3<#SX6pfEU@vq~^G#u4#ZlPmHuAo<=wzvjaiR{NG4>Kpdt z_y&4%24{J~QNJf}+pv%)=(U`Yutepo%6N;zCI)X|m7Durl%5+R+Ty^RVgepRIbDi! zff@ftJY6(Mm$*tm$wAu6Nuz_X6WJ4q-d+^U>Cu(8IX-Gh_2V9gysRrE>aFNqYHKfkhPS60w)9ZA zFqdemQFN!9-Uw)CPBl=!$ayi&Hjy*|fryQ1x+;~;Q&p+8u1W`BD{2Q7NbN`}fNPu& zf8n5zh!`diqNXFv{$2mDFQ+i#y=h>n>5l>zB}eo95ry2y$iyJ*FwvqE&sQb0P{&OtGp6kR(WmId1by_E3bxyl2=Fk zK=KC*5iV%zLbaKn1P}6>haX_x2Il`LfgDdLS`_y6iF$GdTb`#^VoPAVRRY_&1dbHC zD1{rYT?R%W+(dn2powZ9!tzpxzU6Q9`i5MT63U^2{6s0-TB(n8|35X-{kP~-E>JZm zhYVZmP!4^^(w##?E@~YK$z#Hcc+A5i^lI`J9^0@2hm5e_O^KDjuoXwxij*dgx~Mf* zm?H-2OGehCE=nn)!-ivm`@a;9L<_^dK7qoamVSw^#`^D9%YVPJ;4zPKw@-f>XvaUn z6Du~d4!qesj3s_dSt2yH4k~JIU;=mvpq`8H%pKzdroSD=tDTqmRrS*>!;GLp029ig zXZYK{wh_d4y)ny`W@`kdKyIJtU@EB-cRU^~6wbv?SfHZv&p0{6_im)-z9CCUP3+P) zgd@^7Yy?krRYy131@-zibdxH+62&jsE0h+h9B0+3?)qo3MPEm(zEU9?MKQ9%y34FO z=oot8S-Q$ub^d0qzs3(CdZ`YUjXsIG)IilM*+&JUfof^5ad>XNkTeTM zGZ+$C^DGQe8;JU?1VT;*?##!Ww}~J%5fW;Thoa%(y}9M+2v3ye@ za+HtWjJKudh1wIn8IO22i(=On|5cnNq_`~B;qPc+@3648MVNHt3`D>CPCVS31I%#0 zFW@D5C;rl}<7s)3i|U;dmD;|hq+yjhS`?vo<1w{92%X%;3#7%RUGZNXjHfz7RIq;& zk6=$7f^~xz{&3JSk)>tAhwtK3r&WJAC^7{5T2DGCaiN`3h-d>o6-Yegpk)w1=G0V! zvKFc!-Gx-|q#E>%PE{t9=Aa-|lc5NH721nbs&5P(+Mqc+Gz>OdLp5*VKS!Ju z8i}h^p{jU0p+n>Zhscy+)gf|%L*#>DF48CQPjI8*YCqgXT(oN>*tQ9TZBE<`hN2xY zT#B}2QFYN0@x8;lB|(FgN);rmhieqy`;IOCKaG*@LYq=j!=yQGTWeIIId0S-=j!1u zYF(%*eSntXY;_00cF2cGs(ZYJz=jM4rj|~@crl{IOX;Hlg z9&^*oCtTR#_KG676BgUK!+(`cS^K2w@K>%wDb=~F$BaRE^aKL*I&0r?9-i9-sjMEl_RPIcVcL{ID_Pcs z#V%?M+t;z_&Thpna*j3nJt~q9HGnl zZHlM=jl@S_{B5{GEFGy!=`wtggZ+h<tsv!5Osot_@}Y$SRTrs*4Z$POq_|3wb)KbgbT^l2@44#tbxF4D)!!X6l^Dfpx%Oz zU}~mdy9xxgvqkML*q#DwQUihw2?X_4q^)*AN}A<_h@5<>P0!j8>w-UR?1MjT+ZMa& zFE}BhH+%lcn?0|6Cbj{nDHq!b`V{5lSTZwTkCi<(j%AOHgY1H5P~otbjK=c&o04a= zlgIKaAW9xs7^M>az4{qUPzpoxxCf6M7vtiv@37@4jQ3Ct{q#I1=rRf~gJ=m40hcn$ z2b6g%&7jOBs^se1q$n{@gVXT7R29b(amsRm7FCg}1btc{$Z1$j!uGvc$mri(4;LZm zyYVGM;oBZ9@&qh5CB6~su64>?>)2hJMp^D!$L{)Nly;YTme=L$_?;UznXYH6+dk_e zCyZVoMX^R8RK5`^*T2G`yTOH^)?CYDxZsu4yDvBBf6tjSak)?*Uo^E{2(OK_n5uH6uWj~2MAU~@b^Fv^;-*Rn zixBm}?M#iwNX@(t4fI`j8dw;O78WSP|A2^u{6Sw&Uy$wx&r+axxsV=cv&2nZ#^QXB zrC4189}RkFnVZG~S-gzD&m4NICqxBE+Ik#@0luMqnDJ38kv%jHgF61cqI{X1pz8f7 zFP@fke_NT*S0Jdl7+Yal(o>L{SUoK<@#?5bq+x@viM8RxSM1D#$q%Uk5O41v3aNcwK@CI#~zq7<6Ksi&B5P!l0DrWWzysDX6{zksV&45;={w24xF6Um(aVZ3c+X;lskV20a65 z(7`Kgn!OnjHN)Z?-ip5Q9K6k+E@*m4_#u`zsp1QV6W}vn##c(BKemjr+oQriLST&B z;lOg?hd6d%l_11{@Chc(pyPiUe4-rEV7xeFrih*`5Pag0h05I|M?&WRy{1xcobRt&giqj2BT%jU=E^5cgZ4Sy3#N#U9SEU`H5x9TQM~*TTtNPR;Yi8v_g-UNTtizqWsrE zAaaYTbe#mA_ZWK1n4tH5?b%--{zSqv6 zZNj%#AjDQd?3s23O@TBluWT3U4uRm}Z8HZBIB4Bum$lCFk1bW$7r~`o(nb|;Q_-N> zs`IL?I?rv@zR7t1REGTrs=*l>hX&{TWGq(#L*wGm;Iw=m5sK-`mC}-2$v%x3_6yv zLe{K5Mfak-<{ta`log%yPHdEZq^@1~D9-kpn|4moJ?cjsjZ|-@gmjs}OAohl{gM`;rO zymqR5tlC_X_Y??cPJ)Z@u_~z!DqCpz0>Se=9GdhUF>zCUWjn7bOqz z1ZXP1Y1{)>Rq$zbU80t68MQ&PyCHWAItz(*QdJHHm5++A{kJ311dubZH z<3x+SL9D702-{qEM8|o{ilVhxnR73VEl5QgTCb)YnU1; zvI;(@sGul-Z7Udb0B2CR$qe z;_`U9a|YhRc*27_M-XebhKlG_P7u_Du0F3n#}oB=L0iGJ$(nXM77_i0K~Dh0*O<6Q z6MJ|AKA_7O{W;K)9M6#Ij5aahH%;i02ky5SdFf0SIh&q$Pa5Nw%1q}^mmUL)9YfuO!^Q9DYpbQK8dN1(%7SeDxCswGs` zkhI8VGq1hUpoFz{f@D5@q|cQG{SR`g)v6ZM-ja~HQwJGRZnfFcLm{HAHZ&PatJQjk z=QyW;2-3U!(C7uMw&x6`LpI!jpN+>*VT2Cb@cLo~KSsk5dXGP2c;f{Zxii9b{EiYov`#L)%7Y-amrx^F^i||j#6Qol-F>5{xFQEnw;!a0+ZUN4N zTdJryZgPJ&4Fm$;x#K|YVqQ&rR-=01gY@FKg16&o32+4i{m}^hY8&cv(}%OTiF)IN zgG$ybhyIFDj*||`*r2Tb4JngOI_MN~ za3;A7S|dsK3Pj9cyqbEe%b-ScuyA^Iufrnmk6|OUf-viPu$etw24zb|O!B!58ZgI2 zsRb^Bp2CmeE`#Rd$7q*9d*`?)X}Zgx-{-i956M3&!jB1r8E3GBQ*hKlE#{&St3y8} zEmRl?boE7Yu>LnN;E&|@2<8@A;a_Q0gO)_}0;+)*Ft5x_o#$d%7ThoLGJ+S+!)P%- zjGik;qLgp26Vl>~VAa84`5c(Vn4$Bqb&TXPULdGTnflrn4w@?19RfjJf-=CH0ZCJz?9 zTk4|ZVYG_Pyq_QX_#UOz{5+x*JMLb`7iK`d&d9mANtpZgtk;z@R$0b15k0a5!Z<#U z4aRLL72^&=0=ufe@C^_M{tb{;ZFjR)1l$vCNr5T_6xL zzl2ODMmNg^yHOyhUt#`=(Ty6~lnF_*+(yJh8*7&cW-FSwU6>)~Yt9AJgWZBXBoI{8 zc$psXP~)i3&Itt1K_WH6I3`F49t#S+KG2`q8Bd}4dNOdFpD8(H1H3!`@LcTnR6E=t|M-$6GlaFO%7 z{SIm?f-(eR-w~#EsqO;~dT0Uq5f)W0qJD?k5LK*2y{tuLiHN=eYlwQ6Mg6V$;>Dnx z!h!;S)Cz)(Pn3;rwc$i&|GJb&x--Nw6)79eGP8Ttt?z^4+n8vjDdP zd5IP{P%m38a*@=_1rF59Za@t6_@n+p@L*0hXpt_k1^i6OD9weAn?|{5!XgZ8176zV zz&zmdMOZ|9!%itfXdPonRk-tFSf@{wE^t{oT=ql@gMM4&qBPVzN=3Eny4WSDGKITX zAW~)MRC$YC#PxOMVynJ7S2i?glyHv`h*Z}i)mIG-+7036*((i7d{fobn?Y)jVNi=D zE=s*L!=TPfT$GfOVUTx;i_B{>3@R308+iL<7&ICbbe?5GnS|DY$b2Znpi04Y0r$8J zgO)+^^BD%6UgC-kO*P1qZclxDr3VC zYpILe2^;arn6H2~8F&@@EO6Pt`RQ!~rL+W?GGIbpD(8e>ErnvLuc?%HIVuqTTNx*v z;~_z6BKnJ%z9vP~&Jc*SxXpz7nzn+}M5I-Hjd}cHgQ_;F0@(_;CDb>_w#-FoP3of| z=A%0EC4E33gqMREA5gbihNyuYO)iOH+X6Po>{c!FZ*>!kZI2WhL6ubG<}~D2hSLq?=4cNlsX%OEwX85nqv9haY9j za3=i_YL&XmO`k5qNcJr|B}YSa)rdRXbPlHTyY{TN?5xvO|EQCtN5vPh`aEDu_xb{! z;Y1H$X{OmLT8RhhUgXvrSRQ~LoaMupuXa=KSMbW8g21lAZ`DITh36me%-G}2bE+D&gPcj1t5ua73$ zAp8JWGTz3t+)FR>tRUr8evz6-3wRpv$g5aQ14lGOuh`(Ar(fmhCUbL!K~--nANep- z`7y(wg`k?hWEhlKu1I{wtE;_1`l2Z zxZ3|}wmR&fQjxqIncjZbL6@&UxZc&?pi)V;Kp>L83bsIdgKh&G7j1<|z*69k*_TQu+wAE?bfGPM@S{W*!h!iKR4gUDwaQI%S76z7Gxl>(BT%;@ zzv^Hf>KW|2OU+s=ktO?MMYmR>TO;vrBDxJkw|~t=?hGZmEvWathC;CFNyJy+$>H5r zmc+yZTJjY{)Y6uY?@e1`JN9@8%Z4ytI83b|ZY#vOOEfMSxbJ|gnMEYv^Mx$QC11tn zkZwy4Mu^f8^=6rl`gBMlY#9$nsU2vUWmq_(t1viy3BLFu@ zw`(OF91c@A2ufdx*r(e#+STVrnPO&lfiRXg6*=*7(gMLQ7YHg=EhKn1+Zl5^0tCUE zweoLXMWGdiEL#)5jEoR%K5j)%HwUx zqrg9q-m;aUq2l~D@v1@1{IWcGzCQlPd{#b*-^Awkp3d(*C6n{p!TF7{^7~NHIKTH} z^8286enH>R!bm7wm}BKq_HAr#l{Qtpl{S>`;#CrfM>)T*ke??S4bu_ZoHcHuln6ei z;_tSrM1w-SE&j{a=P+O2-rk^)SW_$z;fR;y+dc)($yGc9tpaS>lM z;?J{4Wflt@QG(+Fh)Vcl>_w~b`DqZ37&E-T8lSa!XtF(iZH@1}lteC6OI*g`Sj848 zv5Q#j#cOZ^fhXt<_`MHGaR3_} z!+AMG2Y9+3SmUB}PdIM~5Utj_X%eyx($`E~wZ=uxne7egD|!wP2q#ox;`&;9gFaj1 zqSQCq8+2|BdQI?qg)bx!{0B9Ev$ev1^I9xvZ*8yks8~E-w>QYcq(9mlRJ=vCso&w< zcE_s>D%)bGkBR7ML~@MbAy3esb8EgYXK;JV=)@!1_*O*JQGHv{5ydofEv8nHJfe=L z-ixO<*P;^%(Jg3b|5)oH^W*^sRc^5p5?+t2oXC8tcyo4A^Mctip}e= z6WEs*0pfX5YO{P?e!K8bNp%AYy)68ia&78TMbGjo8k`RWXK{5y- zFM|-JhN37Ly3x&4lS-4K!AO|+J)Uc?eVywJuh0AY$FDzHbM{($?X}ikd+qyuDTPA( z$rP@1Nuk=9L7d`GaAHF6VnU`^FegdVU5` zW6XoUTlB#DXli)u<5bMqe^~Spe)IXfyA0re$M1Za%|H5lfNpth3?=9Hp@%uC@B{eq zEqzTYU8@|XIVLny&o$|}58~8`%&N69d=f;RL-F2mu1U*9g%twfOD%X2?D2C=Cv{yD zZ0ZaBOxpQD9HqY2&!jdh<0$o=ekP4t8AslICk$F8^g17opPUbIy8mWS5y*T0X3&zA zaMg&1vCYtR@eLctT2)a1K0(30R;w^P>G?zztn~Vo%3Yu1HJR_hFXFEy_E1!UlfU!_ zxAphSa?XCv+HzGKCFj$(%ryf)ifyXr#|aAX%eLt!1N(=IS7A83E{2jP;nTg;z&W;q zrYJY0fBz1uY*6LXZ%onoL+Hxa_Da{O`myzm)Qf@h792Nd+)*Xsc!8*mTVS`ljvF)) zWPDQ&2U&o65U9K(2HpFi+>~1d8eYGyJ*HUI3k3aL&7v-d9u$-&!DJf~b-nZ-i>2ZC zPn5K?F}=s>>#QFXuBQh>@$+}AW#$5C?06e>5{R#}w18itJ2a?o!F3j<+po*}1@>f`RhR^!$( z(Q4Cw(xOXOW7qbR7)qW-Tg)rBS@Z>1w(Klf#tTHCG8eA&(Oqkrlo%;a7gJyFE^e>6(Xe2!B$(Qpfs>zpa`Snrxu^FXb_UT?&0k-srr88u<)F8!RIB=} zIeLL@RdM9AVXqL~(_-W*AGQ1lUF^Y~1}zeKD+I#H9>4(-EVpEQ6i2D}4A5AIzDnrU z2?YIvntqz5e-iYODvs!fXXI3y*1FaH;@0tn7Odk7EuiD7kMzXhg%)rlnK-=A0$zj# zN?Ota`ZfO;-++aFykY)!3+$j@^fC5FX>AL1)8UV?r(X+_eEOmVqVYmxivJX0AV6E0 z{|BHVZ&g2&5=E+B0-@$ws9D#~q@S72ao}j}h6i(t{f<^S4Q}lYDeVrib{*EnQSXu| z(|LRdA@hU03WTu z&uO_qn`pilt{?bBJI#Er@{>vVw182Meu6FnJ<278;m{S6CSe}*j2B1Kc15AG!$Df? zO{%i!6w*UQB@X!uOg`e%IEwwLpGn!Ds@U4zAWAyDzezWL8b?XJ`kV9+{y49{NtO6x zNPm;Ie2V`43QkhFx)?;M#r;h>0h($3P3pKlj*{-|Z&D`ynAhK=d)LRQh0(+l$|l`4 zB%;63eW0Z-U5^MCixy4m74xoO4%|lIxZEs3B>{zUe?%!Lx6KAA_doapk|6cbF+`z{QZThblt-s}G{66#>>>U}O#ZgLLZUEQI!EB%MfqyAS+KnzH>koHn zwO(#F-*N%oTR-D=^COoK{|tWhbd^!-<@U1;as)~O*9LQK+aKh{8KOh{QA6M7+?4iv zxheUo*CXsuofpsj-0?XA)l{nU;=P~MpU06eV*_qvO`;=Sy!Z3%=diK2V+)gxiB0Ht zB`qvE5f;6ug-PbdI7%JS!laaqxhYio$+%Qtvu?PZ}Z;GSXq5BO!3qsEjLYKG=51LVTF|I3*tCzv`A=F@%0Kexu{d;`@8MU-mS4N@%g&(rOxbob z&cEeO^COD0E@Q(uDLOw6ad#B31tsB<>^xe;ycTSZBaRgKq%kTDFJh>puDwnv>1=_( z4#KUYxqLq(qSqwYjUCR>s3J>G@KPj~?Oo zB)Wg;uA)d0Qs26O3^H&gOzL}~#UWxbbMQ|15+Ndw7Bl~PIlZ$30-{L=eRx2gnW)Vk$ zEWac#m>Zf*%Pmxql4*u;FBJ%xzW!e_iAl6X6&|-q-m^SCSh5u?n761pj(YUeWxi)2 zOvW!kVu4K?S*nB@{Iv6EBa58a{wwvgg8G><`YT*anL2S|k)J-b?v3%$JzvEsmuVDn zGerc5z2@IshEK4vL2oK{1kQ?a3tneQ=TfKag5lQ@mhXLCT=!-w!vujS?o^~Wp0MQz z&K3xozd#c?tCcS(O@ff)p0GvyAiqS*tYxieu0&97u-K=XuzEn`r>|r>u-FGzPx(5I zcsihd-h`iGMq2EH9iRAG50e)A*cXzE{L}nJ^okG5t6zK_M|ky_USVWCe(HKuNhw?6 zDCLe|MsFOPO-!n?Nc}AG2PhFoZONDM8G5n80^SR#@FDnLwgp3=SNofM`WW^}#H%5( z_otau@tGR8^uv=Cc=Y1ME%N9^4gSER7wxvj5kGn{U@Q21*59OR$(4#+`}>CQT)BFLNh{-~eOQsg(XHN}H9$Q$T zjHiuO=5-tI4+$)Pu)(_QkihZ>2bL`SEDDnR%-f-B?GI(?NO^^!!4pe@;p`y}`|7A- zo|h+zp654pwmFQRZ^_{(!YCyzyD$*&2UB}z7X}7B9+wYh(Skz<9f&z6xR5;u3GJq%I~ua`5o7dsB?9|E(|z|lv=8M4=Z#H zD|8JM8n;Wi>~#?J!Ci6WE8T>X5jI^9@%{_;EM29DfHxRex~~`oFR2yOMC>TTlNz*i zr|)heoh`+Q)@1zE#2I1 z_uzw3x4*Q;N2_*gf4Uu2Mah1;W6%sA?c0q_&hSi|?mSXuQL`ExAwi*23Iq9ckLS)c z7WJq>-0icFs7!=iAP`l?r#-S45{+cCy}ui@OwbJiK{f{lYW&@xDK&AFWc*>!!!>af zyZJ|hO23GqbRv2R%Rt}$XwaJ=`Tj?Pw&IV!el+N}nmBa}K(nqC*0=?*8gq5r0ytw2 zOz!gVkUz{Wp6?i@GTv>VS3I6{xG%lJ-*3Ed51M3Q0Kd^z!h5vQUg6&u0_FzjRlbz+ z+8*US@A78xN&IrIsC>YCJQ;h@I=I#^!F`j}o|RQT8Y`)4NWa!IljV-+&)9z$D*3pdR_bpe#Xs0K@Cy&{IMz4q$>}^Q7o?MP<0IZ=>--MU(E?0h$Anq<}4K} zEE5QCma91w>p9QlL&_-T6O07Bz0NafhmabrRowx*9<%QW=izW~!FeXt{0oVJTeR~) zlJ*4tPkQk@lNKI;HN0<~XHqX=H(nsvR)g&xP&3;MLAgmVZBuMh!S;g#afcn<%>>eHST8(G2*`y&8~`Rt$1nnw%8$hbD8NR)JENR@&CB^vY(uXtlCgyTcY; z2kthlYKD+H3qRB+c|UH3S=zJTp&R=O*Oh1ke-P^w%Jx+vDd3;(Dyy0~BpyaoK}4;U zn#x4OMnt_9C~v|*lZwO$vjoBc@Ew!X%z-A|dN_`}pBynLELb;H5}55dV$d^(@!-S} zEEE!dcpvBK9X^{68&m}fd}yA?ivV~~*c}rH;%u-(Fg(DrAsCXFDn%g3t^yeXWAi!* zjHz{T?_#gSDVq2_&3xw%^skVH-yV`VhRdrCNQPpZF`Qos_i&e8y(Yw ztmFK!9qMor)pMmK9gA~@MD+^GbyzDluwjEezZx|BnABn6F)Xy>3sLyPcfz2L@dv8$ z&@r^}q!R{ZY*8*S6WK30VbD20#8J|vCk(m@fBff!K~wRE|Aay1KZpqzwO5K&2!sjO zYZI>gL7Pycm$=9_unF0Nd}5x?0#WmL@Wtgp@q*GMsDHNy#dfPR$QBi5p^30hjhAeb z1%mVK+NUN+qDF$J@~KG0hNj{eZvF_T3KP#z8haL9K}0vJRk53|`^BQYKj=EUIrOoQ zC>fik*YQKr;qG5ql=34Mu)`&`?a!y_7)ozPN^WRoAa4xKzv1aHKLgA7{$e{khHeG3qxCqEzC}$dry%c~pRnm%kEBAn4RfPce!^sF8XN zLOl{xp?zr!kJEeo>?*b|ZRI1Pc}NWS{rQ6-AZ_zN;%9!w-u`wR?k<^5-+3U)wx1Ch ziTM5FI!!jB;!;0hLC+QlCV19?nGA0b+h^@?+uqe2 zg8tCdV*d!I71VZ8GO5;Z4+0x+%FsQ{&<5^kfV=Eq4}72*T(Ia@)DPCCszhVFgv51O zA5|z@C2^;+iVxr5fH5BVRg6(3&2WQ2=mBG}4c1Ab8|kt^FA+LZAo4mkSpTcoAbaQk zumQK7>>#(szK#VFJG(9MVKWa_0l#X7!0VN&ajsIXRi_M0eqHVpLR`6NAzNC*p`-=bppIaUaD`9NkerkK-p+ z*woLqzv&n@CkE|Z&cG-nzSl(e@wdAA|AtS<*t9YR>-XdFOKp7Q(Mm4nc_amJHf3`R zmg4E8YXN_2i|7^ntt}9)$XB`(8`b&raSWDY9ykeCnL5y`#p}5aDrm=?+^f6 z*JYAxGjic`HzmKzj&*)lB|wqD$c}Yv`9PEJ&Ojz)OnqyhNslp49LlBLN|G<3)PMS# z^p%6k5>&ND{pSxzf?TD7`bwh?F$yOla6boS{fOQ#;%_sq@)wNFo#uC(B}QAecYAB&d!iXdSb=Yh%H=&gM6X&&K_pU8gXrso^Ey48)D3zi>Vi z2?6SZgrdJtA>0#qlN6Ws0>KLldg?ewMNJH+3?x#=IbQfnj&o!SeU3on8w(3Y_R_`) zN|R{a^j?~)3EDUTT2zuZ#nGxdO1ohmoE{#IQQW7H0PSaiP%IG$2`+^Mk@izAC`|$h z+;vFJab`sqmC-|)!n24^uEhT>s(K|oK_CihqN-0&ngj)*8XZ*~*DaVHEDZadvNC$= zGQssauEgt5iRb=}Q7sjsx*o^h1vcG?_O}mWtsH35K`FCdAQ89iBEPbGu4wmjc6)5$$Ur?EzG2%lHBls zeoh`cD(~%__x5GGEV_>@O2G#j?l8{x5qU>FWKcgL9xV{6--&2*^Fs!`Mi!-pA2Mhw zS(G;GAv`NOhyQ;R|2OqI76f7)3$jmJh#7)FOHnl zJf9GA?Hp)QoygEA5awElRvUx!MXA;xlR7o~r|+VCU2c4uyZonB`A_pq>!M~BB~PX2 zSeU8!QAjVbFjdXOSTA~1-9ebyqR9e*5ay+5VGjI@Fq8c=O6+jR5hUYmw?KG3sW6aN zG&SF!F@xmUq~nMBcYN2L}0wsCoNA zrc+X>Q(|h`Ad_Bdle(NyAcIX_aKG76=AoQD5&lX9(8`N|XE}5>xY=2bqpNvU)28J&RIj4>D;L zN`j%|CB3sil=LGa49vX`qBd!5Xd&1W)C|icqY5MS*fAuz@Y&3?C>Dj}h@wAf#y!k` zdVVMzEDRJl{A#24HmHhh;EEh+TB_D6g=U372>&a}Ky}#vRz^`_p5I|zc8<0QGM-dL zoaC#+3s3~?H%@3K3j`%*@@&Q3TEQ|=(6QWKTzJHE#kqw6r@V@Db-9>tKJMW@{|R1y zIkUM%>NZemvyan#G`_h-DU$;9T(d6waQN6iqcA+Oq-076k0HIrOMYb_!RpM_0UlUv z=H(gkRqer&5Z%Rm-fnIY`DkwpCFju!{-Or8u!veN@2g~~5|})LHa6?g!$&zl#}3$Q zP|jX8d3qm%*Pwe^SQPtRjX|}7InaS96%Piz1TNkgdksp-PzCi8 z2-OdQZ1G-$J_8x4)t4+G7%32h*uy06LxW6OAgSvF0=p4a{@fswjD56dUFNlj zVnH9jPYK@)vn0?2yh8H@(_o<_OK9+bmKrS15Npg9h*B=bWXijIkV#7{AaAn2$o2;v!pi<`);~7$ z@M_vgVMBt2qF8v91z$<+ED+dW^p-)qpfm|0oqKTWwNDrhvX>BpPYBDq{f_9Cu$B?(Uz-~3?A1_?t+p~-eiCNs7aSh4b5~YG6uM9 zy)cFsnaX*Y=ek&nyajsISf`R5o+U3cMEi0&~!)g#VQhcoc31Iqmg>m@k3G%^l-E|w4SMXbpJ zLB$yF+Ut<5$`b{U&S`P$hSvl;g@aGOMH9uZObAb-n)?7qpxuOqoCp2qw04*pGCf`S{&1&WlV9q z4_&Qy)hUA-M5{)D(5@@2ap06e6+Vl+57b1cP6`!f(Dx&{Rn{1^mbvk%FS}Mn)*gK2 z7ZuuTkkT_lAo%wLe^lsq$jKE^AsI7)LPf;3l#0k|3Ex1a&pltUDi;V=1EQ~>&J&a- zL3Oyp0$)L`m$C-wE2wpnt3e=gHLb*nx%tKTOtw>TRZ$gKWq8u@UP6`e-C7^zLFSm? z)Ihkf*zaWR>*S$KrHQKib&_`-=bh6uZ_q!f$e$PH!ga>1CjhTA(XC_cVQ+%-I*Z<=$p^dKA!hX5!uSzq#mny}X%v(F zhKFD7$dOcAAS!nvyeMMF(DcG^-sEv5LvU+A-Rai6vM+KAc2W#_(!c;Irj

#eVvo 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 p8__yTjBkM)kRCL@Us?|u6 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%0AcBNT@3yn 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 diff --git a/service/matching/matching_engine_test.go b/service/matching/matching_engine_test.go index aba5f873dd0..612797e1c0c 100644 --- a/service/matching/matching_engine_test.go +++ b/service/matching/matching_engine_test.go @@ -30,7 +30,6 @@ import ( "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" - "go.temporal.io/server/api/adminservice/v1" clockspb "go.temporal.io/server/api/clock/v1" deploymentspb "go.temporal.io/server/api/deployment/v1" enumsspb "go.temporal.io/server/api/enums/v1" @@ -5464,26 +5463,22 @@ func TestGetHistoryForQueryTask(t *testing.T) { runID := "test-run-id" tests := []struct { - name string - isStickyEnabled bool - sendRawHistoryBytesToMatchingService bool - setupMocks func(*historyservicemock.MockHistoryServiceClient, *namespace.MockRegistry) - expectHistory bool - expectRawHistoryBytes bool - expectNextPageToken bool - expectError bool + name string + isStickyEnabled bool + setupMocks func(*historyservicemock.MockHistoryServiceClient, *namespace.MockRegistry) + expectHistory bool + expectNextPageToken bool + expectError bool }{ { - name: "sticky enabled returns empty history", - isStickyEnabled: true, - sendRawHistoryBytesToMatchingService: false, - setupMocks: func(_ *historyservicemock.MockHistoryServiceClient, _ *namespace.MockRegistry) {}, - expectHistory: true, // empty history + name: "sticky enabled returns empty history", + isStickyEnabled: true, + setupMocks: func(_ *historyservicemock.MockHistoryServiceClient, _ *namespace.MockRegistry) {}, + expectHistory: true, // empty history }, { - name: "SendRawHistoryBytesToMatchingService disabled uses GetWorkflowExecutionHistory", - isStickyEnabled: false, - sendRawHistoryBytesToMatchingService: false, + name: "non-sticky uses GetWorkflowExecutionHistory", + isStickyEnabled: false, setupMocks: func(mockHistoryClient *historyservicemock.MockHistoryServiceClient, mockNsRegistry *namespace.MockRegistry) { mockNsRegistry.EXPECT().GetNamespaceName(nsID).Return(nsName, nil).Times(1) mockHistoryClient.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any()).Return( @@ -5502,38 +5497,8 @@ func TestGetHistoryForQueryTask(t *testing.T) { expectNextPageToken: true, }, { - name: "SendRawHistoryBytesToMatchingService enabled uses GetWorkflowExecutionRawHistory", - isStickyEnabled: false, - sendRawHistoryBytesToMatchingService: true, - setupMocks: func(mockHistoryClient *historyservicemock.MockHistoryServiceClient, _ *namespace.MockRegistry) { - mockHistoryClient.EXPECT().GetWorkflowExecutionRawHistory(gomock.Any(), gomock.Any()).Return( - &historyservice.GetWorkflowExecutionRawHistoryResponse{ - Response: &adminservice.GetWorkflowExecutionRawHistoryResponse{ - HistoryBatches: []*commonpb.DataBlob{ - {Data: []byte("raw-history-batch-1")}, - {Data: []byte("raw-history-batch-2")}, - }, - NextPageToken: []byte("raw-next-token"), - }, - }, nil).Times(1) - }, - expectRawHistoryBytes: true, - expectNextPageToken: true, - }, - { - name: "GetWorkflowExecutionRawHistory error is propagated", - isStickyEnabled: false, - sendRawHistoryBytesToMatchingService: true, - setupMocks: func(mockHistoryClient *historyservicemock.MockHistoryServiceClient, _ *namespace.MockRegistry) { - mockHistoryClient.EXPECT().GetWorkflowExecutionRawHistory(gomock.Any(), gomock.Any()).Return( - nil, errors.New("history service error")).Times(1) - }, - expectError: true, - }, - { - name: "GetWorkflowExecutionHistory error is propagated", - isStickyEnabled: false, - sendRawHistoryBytesToMatchingService: false, + name: "GetWorkflowExecutionHistory error is propagated", + isStickyEnabled: false, setupMocks: func(mockHistoryClient *historyservicemock.MockHistoryServiceClient, _ *namespace.MockRegistry) { mockHistoryClient.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any()).Return( nil, errors.New("history service error")).Times(1) @@ -5555,12 +5520,10 @@ func TestGetHistoryForQueryTask(t *testing.T) { mockSAMapperProvider := searchattribute.NewMockMapperProvider(ctrl) mockVisibilityManager := manager.NewMockVisibilityManager(ctrl) - // Set up config config := defaultTestConfig() - config.SendRawHistoryBytesToMatchingService = dynamicconfig.GetBoolPropertyFn(tc.sendRawHistoryBytesToMatchingService) - // Set up mock expectations for ProcessInternalRawHistory when using old path - if !tc.isStickyEnabled && !tc.sendRawHistoryBytesToMatchingService && !tc.expectError { + // Set up mock expectations for ProcessInternalRawHistory + if !tc.isStickyEnabled && !tc.expectError { mockVisibilityManager.EXPECT().GetIndexName().Return("test-index").AnyTimes() mockSAProvider.EXPECT().GetSearchAttributes("test-index", false).Return(searchattribute.NameTypeMap{}, nil).AnyTimes() mockSAMapperProvider.EXPECT().GetMapper(gomock.Any()).Return(nil, nil).AnyTimes() @@ -5592,7 +5555,7 @@ func TestGetHistoryForQueryTask(t *testing.T) { }, } - hist, rawHistoryBytes, nextPageToken, err := engine.getHistoryForQueryTask( + hist, nextPageToken, err := engine.getHistoryForQueryTask( context.Background(), nsID, task, @@ -5607,25 +5570,14 @@ func TestGetHistoryForQueryTask(t *testing.T) { require.NoError(t, err) if tc.isStickyEnabled { - // Sticky enabled should return empty history assert.NotNil(t, hist) assert.Empty(t, hist.Events) - assert.Nil(t, rawHistoryBytes) assert.Nil(t, nextPageToken) return } - if tc.expectRawHistoryBytes { - assert.Nil(t, hist) - assert.NotNil(t, rawHistoryBytes) - assert.Len(t, rawHistoryBytes, 2) - assert.Equal(t, []byte("raw-history-batch-1"), rawHistoryBytes[0]) - assert.Equal(t, []byte("raw-history-batch-2"), rawHistoryBytes[1]) - } - if tc.expectHistory { assert.NotNil(t, hist) - assert.Nil(t, rawHistoryBytes) } if tc.expectNextPageToken { @@ -5649,7 +5601,7 @@ func defaultTestConfig() *Config { config := NewConfig(dynamicconfig.NewNoopCollection()) config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(100 * time.Millisecond) config.MaxTaskDeleteBatchSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(1) - config.AutoEnableV2 = dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(true) + config.AutoEnableV2Sub = trueTaskQueueSub return config } @@ -5691,6 +5643,10 @@ func staticTrueChange(_, _ string, _ enumspb.TaskQueueType, _ func(dynamicconfig return dynamicconfig.StaticGradualChange(true), func() {} } +func trueTaskQueueSub(_, _ string, _ enumspb.TaskQueueType, _ func(bool)) (bool, func()) { + return true, func() {} +} + func staticFalseChange(_, _ string, _ enumspb.TaskQueueType, _ func(dynamicconfig.GradualChange[bool])) (dynamicconfig.GradualChange[bool], func()) { return dynamicconfig.StaticGradualChange(false), func() {} } @@ -5826,3 +5782,209 @@ func TestCancelOutstandingWorkerPolls(t *testing.T) { require.Equal(t, 0, engine.shutdownWorkers.Size()) }) } + +// TestAutoEnableV2ConfigChange tests that switching autoEnable triggers unload when effective config changes +func TestAutoEnableV2ConfigChange(t *testing.T) { + controller := gomock.NewController(t) + + logger := testlogger.NewTestLogger(t, testlogger.FailOnAnyUnexpectedError) + + dcClient := dynamicconfig.NewMemoryClient() + dcCollection := dynamicconfig.NewCollection(dcClient, logger) + dcCollection.Start() + defer dcCollection.Stop() + + matchingClient := matchingservicemock.NewMockMatchingServiceClient(controller) + matchingClient.EXPECT().ForceLoadTaskQueuePartition(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&matchingservice.ForceLoadTaskQueuePartitionResponse{}, nil).AnyTimes() + + _, registry := createMockNamespaceCache(controller, namespace.Name(namespaceName)) + + config := NewConfig(dcCollection) + config.EnableMigration = dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(false) + config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(100 * time.Millisecond) + config.MaxTaskDeleteBatchSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(1) + + engine := createTestMatchingEngine(logger, controller, config, matchingClient, registry) + engine.Start() + defer engine.Stop() + + // autoEnable ON, base configs OFF -> with V2 fairnessState, effective config is NewMatcher=true, EnableFairness=true + cleanupAutoEnable := dcClient.OverrideSetting(dynamicconfig.MatchingAutoEnableV2, true) + cleanupFairness := dcClient.OverrideSetting(dynamicconfig.MatchingEnableFairness, false) + cleanupNewMatcher := dcClient.OverrideSetting(dynamicconfig.MatchingUseNewMatcher, false) + defer cleanupAutoEnable() + defer cleanupFairness() + defer cleanupNewMatcher() + + testNamespaceID := uuid.NewString() + testTaskQueueName := "test-tq-" + uuid.NewString() + + ns := namespace.NewLocalNamespaceForTest( + &persistencespb.NamespaceInfo{Name: "test-namespace", Id: testNamespaceID}, + nil, + "", + ) + + f, err := tqid.NewTaskQueueFamily(testNamespaceID, testTaskQueueName) + require.NoError(t, err) + partition := f.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition() + + tqConfig := newTaskQueueConfig(partition.TaskQueue(), engine.config, ns.Name()) + + userData := &mockUserDataManager{ + data: &persistencespb.VersionedTaskQueueUserData{ + Data: &persistencespb.TaskQueueUserData{ + PerType: map[int32]*persistencespb.TaskQueueTypeUserData{ + int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW): { + FairnessState: enumsspb.FAIRNESS_STATE_V2, + }, + }, + }, + }, + } + + pm, err := newTaskQueuePartitionManager( + engine, + ns, + partition, + tqConfig, + logger, + logger, + metrics.NoopMetricsHandler, + userData, + ) + require.NoError(t, err) + + engine.partitions[partition.Key()] = pm + + pm.Start() + defer pm.Stop(unloadCauseIdle) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + err = pm.WaitUntilInitialized(ctx) + require.NoError(t, err) + + pq, err := pm.defaultQueueFuture.Get(ctx) + require.NoError(t, err) + + // Turn autoEnable OFF -> effective config changes to NewMatcher=false, EnableFairness=false + cleanupAutoEnable() + _ = dcClient.OverrideSetting(dynamicconfig.MatchingAutoEnableV2, false) + + require.Eventually(t, func() bool { + return !pm.config.AutoEnableV2() + }, 2*time.Second, 10*time.Millisecond, "autoEnable should be updated") + + require.Eventually(t, func() bool { + return pq.(*physicalTaskQueueManagerImpl).tqCtx.Err() != nil + }, 2*time.Second, 10*time.Millisecond, "physical queue should be stopped when effective config changes") +} + +func TestAutoEnableV2ConfigChange_NoUnloadWhenEffectiveConfigUnchanged(t *testing.T) { + controller := gomock.NewController(t) + + logger := testlogger.NewTestLogger(t, testlogger.FailOnAnyUnexpectedError) + + dcClient := dynamicconfig.NewMemoryClient() + dcCollection := dynamicconfig.NewCollection(dcClient, logger) + dcCollection.Start() + defer dcCollection.Stop() + + matchingClient := matchingservicemock.NewMockMatchingServiceClient(controller) + matchingClient.EXPECT().ForceLoadTaskQueuePartition(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&matchingservice.ForceLoadTaskQueuePartitionResponse{}, nil).AnyTimes() + + _, registry := createMockNamespaceCache(controller, namespace.Name(namespaceName)) + + config := NewConfig(dcCollection) + config.EnableMigration = dynamicconfig.GetBoolPropertyFnFilteredByTaskQueue(false) + config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(100 * time.Millisecond) + config.MaxTaskDeleteBatchSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(1) + + engine := createTestMatchingEngine(logger, controller, config, matchingClient, registry) + engine.Start() + defer engine.Stop() + + // autoEnable OFF, base configs ON -> with V2 fairnessState, effective config is NewMatcher=true, EnableFairness=true + cleanupAutoEnable := dcClient.OverrideSetting(dynamicconfig.MatchingAutoEnableV2, false) + cleanupFairness := dcClient.OverrideSetting(dynamicconfig.MatchingEnableFairness, true) + cleanupNewMatcher := dcClient.OverrideSetting(dynamicconfig.MatchingUseNewMatcher, true) + defer cleanupAutoEnable() + defer cleanupFairness() + defer cleanupNewMatcher() + + testNamespaceID := uuid.NewString() + testTaskQueueName := "test-tq-" + uuid.NewString() + + ns := namespace.NewLocalNamespaceForTest( + &persistencespb.NamespaceInfo{Name: "test-namespace", Id: testNamespaceID}, + nil, + "", + ) + + f, err := tqid.NewTaskQueueFamily(testNamespaceID, testTaskQueueName) + require.NoError(t, err) + partition := f.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition() + + tqConfig := newTaskQueueConfig(partition.TaskQueue(), engine.config, ns.Name()) + + userData := &mockUserDataManager{ + data: &persistencespb.VersionedTaskQueueUserData{ + Data: &persistencespb.TaskQueueUserData{ + PerType: map[int32]*persistencespb.TaskQueueTypeUserData{ + int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW): { + FairnessState: enumsspb.FAIRNESS_STATE_V2, + }, + }, + }, + }, + } + + pm, err := newTaskQueuePartitionManager( + engine, + ns, + partition, + tqConfig, + logger, + logger, + metrics.NoopMetricsHandler, + userData, + ) + require.NoError(t, err) + + engine.partitions[partition.Key()] = pm + + pm.Start() + defer pm.Stop(unloadCauseIdle) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + err = pm.WaitUntilInitialized(ctx) + require.NoError(t, err) + + require.Eventually(t, func() bool { + return !pm.config.AutoEnableV2() && pm.config.NewMatcher && pm.config.EnableFairness + }, 2*time.Second, 10*time.Millisecond, "config should be initialized") + + pq, err := pm.defaultQueueFuture.Get(ctx) + require.NoError(t, err) + + // Turn autoEnable ON -> effective config stays NewMatcher=true, EnableFairness=true (same as before) + cleanupAutoEnable() + _ = dcClient.OverrideSetting(dynamicconfig.MatchingAutoEnableV2, true) + + require.Eventually(t, func() bool { + return pm.config.AutoEnableV2() + }, 2*time.Second, 10*time.Millisecond, "autoEnable should be updated") + + require.Never(t, func() bool { + select { + case <-pq.(*physicalTaskQueueManagerImpl).tqCtx.Done(): + return true + default: + return false + } + }, 100*time.Millisecond, 10*time.Millisecond, "physical queue should NOT be stopped when effective config does not change") +} diff --git a/service/matching/task_queue_partition_manager.go b/service/matching/task_queue_partition_manager.go index 8823e68ed24..0ea2f895fe0 100644 --- a/service/matching/task_queue_partition_manager.go +++ b/service/matching/task_queue_partition_manager.go @@ -91,6 +91,7 @@ type ( cancelNewMatcherSub func() cancelFairnessSub func() + cancelAutoEnableSub func() // rateLimitManager is used to manage the rate limit for task queues. rateLimitManager *rateLimitManager @@ -163,14 +164,38 @@ func newTaskQueuePartitionManager( return pm, nil } +// computeEffectiveConfig determines the effective NewMatcher and EnableFairness config values +// based on fairnessState, autoEnable, and the base dynamic config values. +func (pm *taskQueuePartitionManagerImpl) computeEffectiveConfig(autoEnable, fairness, newMatcher bool) (effectiveNewMatcher, effectiveEnableFairness bool) { + isSticky := pm.partition.Kind() == enumspb.TASK_QUEUE_KIND_STICKY + effectiveEnableFairness = fairness && !isSticky + effectiveNewMatcher = newMatcher || fairness + if !autoEnable { + return + } + + switch pm.fairnessState { + case enumsspb.FAIRNESS_STATE_UNSPECIFIED: + // use values from config + case enumsspb.FAIRNESS_STATE_V0: + effectiveNewMatcher = false + effectiveEnableFairness = false + case enumsspb.FAIRNESS_STATE_V1: + effectiveNewMatcher = true + effectiveEnableFairness = false + case enumsspb.FAIRNESS_STATE_V2: + effectiveNewMatcher = true + effectiveEnableFairness = !isSticky + default: + pm.logger.Error("unknown fairnessState in user data") + } + return +} + func (pm *taskQueuePartitionManagerImpl) initialize() (retErr error) { defer pm.initCancel() defer func() { pm.defaultQueueFuture.SetIfNotReady(nil, retErr) }() - unload := func(bool) { - pm.unloadFromEngine(unloadCauseConfigChange) - } - err := pm.userDataManager.WaitUntilInitialized(pm.initCtx) if err != nil { return err @@ -181,37 +206,25 @@ func (pm *taskQueuePartitionManagerImpl) initialize() (retErr error) { } pm.fairnessState = data.GetFairnessState() - switch { - case !pm.config.AutoEnableV2() || pm.fairnessState == enumsspb.FAIRNESS_STATE_UNSPECIFIED: - var fairness bool - changeKey := pm.partition.GradualChangeKey() - fairness, pm.cancelFairnessSub = dynamicconfig.SubscribeGradualChange( - pm.config.EnableFairnessSub, changeKey, unload, pm.engine.timeSource) - // Fairness is disabled for sticky queues for now so that we can still use TTLs. - pm.config.EnableFairness = fairness && pm.partition.Kind() != enumspb.TASK_QUEUE_KIND_STICKY - if fairness { - pm.config.NewMatcher = true - } else { - pm.config.NewMatcher, pm.cancelNewMatcherSub = dynamicconfig.SubscribeGradualChange( - pm.config.NewMatcherSub, changeKey, unload, pm.engine.timeSource) - } - case pm.fairnessState == enumsspb.FAIRNESS_STATE_V0: - pm.config.NewMatcher = false - pm.config.EnableFairness = false - case pm.fairnessState == enumsspb.FAIRNESS_STATE_V1: - pm.config.NewMatcher = true - pm.config.EnableFairness = false - case pm.fairnessState == enumsspb.FAIRNESS_STATE_V2: - pm.config.NewMatcher = true - if pm.partition.Kind() == enumspb.TASK_QUEUE_KIND_STICKY { - pm.config.EnableFairness = false - } else { - pm.config.EnableFairness = true + changeKey := pm.partition.GradualChangeKey() + + var autoEnable, fairness, newMatcher bool + autoEnable, pm.cancelAutoEnableSub = pm.config.AutoEnableV2Sub(pm.autoEnableChanged) + + unloadOnBaseConfigChange := func(bool) { + if pm.fairnessState == enumsspb.FAIRNESS_STATE_UNSPECIFIED || !pm.config.AutoEnableV2() { + pm.unloadFromEngine(unloadCauseConfigChange) } - default: - return serviceerror.NewInternal("Unknown FairnessState in UserData") } + newMatcher, pm.cancelNewMatcherSub = dynamicconfig.SubscribeGradualChange( + pm.config.NewMatcherSub, changeKey, unloadOnBaseConfigChange, pm.engine.timeSource) + fairness, pm.cancelFairnessSub = dynamicconfig.SubscribeGradualChange( + pm.config.EnableFairnessSub, changeKey, unloadOnBaseConfigChange, pm.engine.timeSource) + + // Determine initial config values + pm.config.NewMatcher, pm.config.EnableFairness = pm.computeEffectiveConfig(autoEnable, fairness, newMatcher) + defaultQ, err := newPhysicalTaskQueueManager(pm, UnversionedQueueKey(pm.partition)) if err != nil { return err @@ -268,6 +281,9 @@ func (pm *taskQueuePartitionManagerImpl) Stop(unloadCause unloadCause) { if pm.cancelNewMatcherSub != nil { pm.cancelNewMatcherSub() } + if pm.cancelAutoEnableSub != nil { + pm.cancelAutoEnableSub() + } pm.versionedQueuesLock.Lock() for version, vq := range pm.versionedQueues { @@ -314,6 +330,36 @@ func (pm *taskQueuePartitionManagerImpl) WaitUntilInitialized(ctx context.Contex return queue.WaitUntilInitialized(ctx) } +// autoEnableChanged is called when the AutoEnableV2 dynamic config value changes. +// It determines the effective config based on the new autoEnable value and fairnessState, +// and unloads if the effective config differs from the current config. +func (pm *taskQueuePartitionManagerImpl) autoEnableChanged(en bool) { + _, err := pm.defaultQueueFuture.Get(context.Background()) + if err != nil { + return + } + + // When fairnessState is UNSPECIFIED, autoEnable changes don't affect the effective config + if pm.fairnessState == enumsspb.FAIRNESS_STATE_UNSPECIFIED { + return + } + + changeKey := pm.partition.GradualChangeKey() + now := pm.engine.timeSource.Now() + + fairnessGC, _ := pm.config.EnableFairnessSub(nil) + fairness := fairnessGC.Value(changeKey, now) + + newMatcherGC, _ := pm.config.NewMatcherSub(nil) + newMatcher := newMatcherGC.Value(changeKey, now) + + effectiveNewMatcher, effectiveEnableFairness := pm.computeEffectiveConfig(en, fairness, newMatcher) + + if effectiveNewMatcher != pm.config.NewMatcher || effectiveEnableFairness != pm.config.EnableFairness { + pm.unloadFromEngine(unloadCauseConfigChange) + } +} + func (pm *taskQueuePartitionManagerImpl) autoEnableIfNeeded(ctx context.Context, params addTaskParams) { if pm.fairnessState != enumsspb.FAIRNESS_STATE_UNSPECIFIED { return From 8af32b44fdbc0a6c341932b7637971d8fe543309 Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 10 Apr 2026 18:53:04 -0700 Subject: [PATCH 38/40] Remove accidentally committed .claude/worktrees Co-Authored-By: Claude Opus 4.6 --- .claude/worktrees/agent-a02c129e | 1 - 1 file changed, 1 deletion(-) delete mode 160000 .claude/worktrees/agent-a02c129e diff --git a/.claude/worktrees/agent-a02c129e b/.claude/worktrees/agent-a02c129e deleted file mode 160000 index 3133f82c85e..00000000000 --- a/.claude/worktrees/agent-a02c129e +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3133f82c85ee13fd1a06e04cbd5363f522a38f4f From e0d9e855248b255b8bced3efa48ce54ddc0a76ac Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 10 Apr 2026 19:13:00 -0700 Subject: [PATCH 39/40] Fix proto lint: remove stray blank line and fix indentation Co-Authored-By: Claude Opus 4.6 --- .../temporal/server/api/persistence/v1/executions.proto | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proto/internal/temporal/server/api/persistence/v1/executions.proto b/proto/internal/temporal/server/api/persistence/v1/executions.proto index 9098d551c45..ea71998538a 100644 --- a/proto/internal/temporal/server/api/persistence/v1/executions.proto +++ b/proto/internal/temporal/server/api/persistence/v1/executions.proto @@ -14,7 +14,6 @@ import "temporal/api/failure/v1/message.proto"; import "temporal/api/history/v1/message.proto"; import "temporal/api/worker/v1/message.proto"; import "temporal/api/workflow/v1/message.proto"; - import "temporal/server/api/clock/v1/message.proto"; import "temporal/server/api/enums/v1/common.proto"; import "temporal/server/api/enums/v1/nexus.proto"; @@ -518,7 +517,7 @@ message OutboundTaskInfo { // WorkerCommandsTask contains worker commands to dispatch via Nexus. message WorkerCommandsTask { - repeated temporal.api.worker.v1.WorkerCommand commands = 1; + repeated temporal.api.worker.v1.WorkerCommand commands = 1; } message NexusInvocationTaskInfo { From 41608afbd65e81f073660ecec2c7e6b2cc937c0a Mon Sep 17 00:00:00 2001 From: Kannan Rajah Date: Fri, 10 Apr 2026 19:39:11 -0700 Subject: [PATCH 40/40] Fix lint: replace assert.Equal with require.Equal in task generator test Co-Authored-By: Claude Opus 4.6 --- service/history/workflow/task_generator_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/history/workflow/task_generator_test.go b/service/history/workflow/task_generator_test.go index 94d9548ac7d..12eafd98022 100644 --- a/service/history/workflow/task_generator_test.go +++ b/service/history/workflow/task_generator_test.go @@ -1164,9 +1164,9 @@ func TestGenerateWorkerCommandsTasks(t *testing.T) { require.Len(t, capturedTasks, 1) commandTask, ok := capturedTasks[0].(*tasks.WorkerCommandsTask) require.True(t, ok) - assert.Equal(t, tc.commands, commandTask.Commands) - assert.Equal(t, tc.controlQueue, commandTask.Destination) - assert.Equal(t, tests.NamespaceID.String(), commandTask.NamespaceID) + require.Equal(t, tc.commands, commandTask.Commands) + require.Equal(t, tc.controlQueue, commandTask.Destination) + require.Equal(t, tests.NamespaceID.String(), commandTask.NamespaceID) } }) }